diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index e87590b976..addb35ee70 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -40,3 +40,6 @@ f223bc02490902dfcc32892058f13f343d51fbaf # frappe.cache() -> frappe.cache fa6dc03cc87ad74e11609e7373078366fdcb3e1b + +# Bulk refactor with sourcery +c35476256f85271fb57584eb0a26f4d9def3caf4 diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml index 97fa4a1a2c..a43d1343f9 100644 --- a/.github/workflows/labeller.yml +++ b/.github/workflows/labeller.yml @@ -7,6 +7,6 @@ jobs: triage: runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@v5 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 5f2c5cfa29..8b35471d0c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -38,7 +38,7 @@ jobs: steps: - name: 'Setup Environment' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/checkout@v4 @@ -57,7 +57,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' cache: pip @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml index e737c536bf..8979417c10 100644 --- a/.github/workflows/on_release.yml +++ b/.github/workflows/on_release.yml @@ -24,7 +24,7 @@ jobs: with: node-version: 18 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' - name: Set up bench and build assets diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index d249c4a3b3..f48df1367a 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -62,7 +62,7 @@ jobs: fi - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml index b3f2c5b0ca..5a7025acff 100644 --- a/.github/workflows/publish-assets-develop.yml +++ b/.github/workflows/publish-assets-develop.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 18 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Set up bench and build assets diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 351958de07..572253bdfa 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -83,7 +83,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.12' diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index cbc0f74470..0bfde0fc25 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -65,7 +65,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.12' diff --git a/cypress/integration/control_currency.js b/cypress/integration/control_currency.js index 1fb912d9ff..9db5dee2a3 100644 --- a/cypress/integration/control_currency.js +++ b/cypress/integration/control_currency.js @@ -9,6 +9,7 @@ context("Control Currency", () => { function get_dialog_with_currency(df_options = {}) { return cy.dialog({ title: "Currency Check", + animate: false, fields: [ { fieldname: fieldname, @@ -76,6 +77,7 @@ context("Control Currency", () => { }); get_dialog_with_currency(test_case.df_options).as("dialog"); + cy.wait(300); cy.get_field(fieldname, "Currency").clear(); cy.wait(300); cy.fill_field(fieldname, test_case.input, "Currency").blur(); diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js index 08b71eb870..25936066cd 100644 --- a/cypress/integration/control_float.js +++ b/cypress/integration/control_float.js @@ -7,6 +7,7 @@ context("Control Float", () => { function get_dialog_with_float() { return cy.dialog({ title: "Float Check", + animate: false, fields: [ { fieldname: "float_number", @@ -19,6 +20,7 @@ context("Control Float", () => { it("check value changes", () => { get_dialog_with_float().as("dialog"); + cy.wait(300); let data = get_data(); data.forEach((x) => { diff --git a/cypress/integration/control_phone.js b/cypress/integration/control_phone.js index 955678a2d6..103b813013 100644 --- a/cypress/integration/control_phone.js +++ b/cypress/integration/control_phone.js @@ -6,6 +6,10 @@ context("Control Phone", () => { cy.visit("/app/website"); }); + afterEach(() => { + cy.clear_dialogs(); + }); + function get_dialog_with_phone() { return cy.dialog({ title: "Phone", @@ -20,31 +24,37 @@ context("Control Phone", () => { it("should set flag and data", () => { get_dialog_with_phone().as("dialog"); + cy.get(".selected-phone").click(); + cy.wait(100); cy.get(".phone-picker .phone-wrapper[id='afghanistan']").click(); + cy.wait(100); + cy.get(".selected-phone .country").should("have.text", "+93"); + cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/af.svg"); + cy.get(".selected-phone").click(); + cy.wait(100); cy.get(".phone-picker .phone-wrapper[id='india']").click(); + cy.wait(100); cy.get(".selected-phone .country").should("have.text", "+91"); cy.get(".selected-phone > img").should("have.attr", "src").and("include", "/in.svg"); let phone_number = "9312672712"; cy.get(".selected-phone > img").click().first(); - cy.get_field("phone").first().click({ multiple: true }); + cy.get_field("phone").first().click(); cy.get(".frappe-control[data-fieldname=phone]") .findByRole("textbox") .first() - .type(phone_number, { force: true }); + .type(phone_number); cy.get_field("phone").first().should("have.value", phone_number); - cy.get_field("phone").first().blur({ force: true }); + cy.get_field("phone").first().blur(); cy.wait(100); cy.get("@dialog").then((dialog) => { let value = dialog.get_value("phone"); expect(value).to.equal("+91-" + phone_number); }); - }); - it("case insensitive search for country and clear search", () => { let search_text = "india"; cy.get(".selected-phone").click().first(); cy.get(".phone-picker").get(".search-phones").click().type(search_text); diff --git a/frappe/__init__.py b/frappe/__init__.py index a43ecdb2ff..42920a33de 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -109,7 +109,7 @@ class _dict(dict): def _(msg: str, lang: str | None = None, context: str | None = None) -> str: - """Returns translated string in current lang, if exists. + """Return translated string in current lang, if exists. Usage: _('Change') _('Change', context='Coins') @@ -148,8 +148,8 @@ def _(msg: str, lang: str | None = None, context: str | None = None) -> str: return translated_string or non_translated_string -def as_unicode(text: str, encoding: str = "utf-8") -> str: - """Convert to unicode if required""" +def as_unicode(text, encoding: str = "utf-8") -> str: + """Convert to unicode if required.""" if isinstance(text, str): return text elif text is None: @@ -331,7 +331,7 @@ def connect_replica() -> bool: def get_site_config(sites_path: str | None = None, site_path: str | None = None) -> dict[str, Any]: - """Returns `site_config.json` combined with `sites/common_site_config.json`. + """Return `site_config.json` combined with `sites/common_site_config.json`. `site_config` is a set of site wide settings like database name, password, email etc.""" config = _dict() @@ -377,7 +377,7 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None) def get_common_site_config(sites_path: str | None = None) -> dict[str, Any]: - """Returns common site config as dictionary. + """Return common site config as dictionary. This is useful for: - checking configuration which should only be allowed in common site config @@ -436,7 +436,7 @@ def setup_redis_cache_connection(): def get_traceback(with_context: bool = False) -> str: - """Returns error traceback.""" + """Return error traceback.""" from frappe.utils import get_traceback return get_traceback(with_context=with_context) @@ -642,7 +642,7 @@ def get_user(): def get_roles(username=None) -> list[str]: - """Returns roles of current user.""" + """Return roles of current user.""" if not local.session or not local.session.user: return ["Guest"] import frappe.permissions @@ -784,9 +784,9 @@ def sendmail( return builder.process(send_now=now) -whitelisted = [] -guest_methods = [] -xss_safe_methods = [] +whitelisted = set() +guest_methods = set() +xss_safe_methods = set() allowed_http_methods_for_whitelisted_func = {} @@ -825,14 +825,14 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None): else: fn = validate_argument_types(fn, apply_condition=in_request_or_test) - whitelisted.append(fn) + whitelisted.add(fn) allowed_http_methods_for_whitelisted_func[fn] = methods if allow_guest: - guest_methods.append(fn) + guest_methods.add(fn) if xss_safe: - xss_safe_methods.append(fn) + xss_safe_methods.add(fn) return method or fn @@ -1007,8 +1007,9 @@ def has_permission( parent_doctype=None, ): """ - Returns True if the user has permission `ptype` for given `doctype` or `doc` - Raises `frappe.PermissionError` if user isn't permitted and `throw` is truthy + Return True if the user has permission `ptype` for given `doctype` or `doc`. + + Raise `frappe.PermissionError` if user isn't permitted and `throw` is truthy :param doctype: DocType for which permission is to be check. :param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`. @@ -1088,7 +1089,7 @@ def has_website_permission(doc=None, ptype="read", user=None, verbose=False, doc def is_table(doctype: str) -> bool: - """Returns True if `istable` property (indicating child Table) is set for given DocType.""" + """Return True if `istable` property (indicating child Table) is set for given DocType.""" def get_tables(): return db.get_values("DocType", filters={"istable": 1}, order_by=None, pluck=True) @@ -1132,7 +1133,7 @@ def new_doc( as_dict: bool = False, **kwargs, ) -> "Document": - """Returns a new document of the given DocType with defaults set. + """Return a new document of the given DocType with defaults set. :param doctype: DocType of the new document. :param parent_doc: [optional] add to parent document. @@ -1156,6 +1157,7 @@ def set_value(doctype, docname, fieldname, value=None): def get_cached_doc(*args, **kwargs) -> "Document": + """Identical to `frappe.get_doc`, but return from cache if available.""" if (key := can_cache_doc(args)) and (doc := cache.get_value(key)): return doc @@ -1178,7 +1180,7 @@ def _set_document_in_cache(key: str, doc: "Document") -> None: def can_cache_doc(args) -> str | None: """ Determine if document should be cached based on get_doc params. - Returns cache key if doc can be cached, None otherwise. + Return cache key if doc can be cached, None otherwise. """ if not args: @@ -1430,17 +1432,17 @@ def rename_doc( def get_module(modulename): - """Returns a module object for given Python module name using `importlib.import_module`.""" + """Return a module object for given Python module name using `importlib.import_module`.""" return importlib.import_module(modulename) def scrub(txt: str) -> str: - """Returns sluggified string. e.g. `Sales Order` becomes `sales_order`.""" + """Return sluggified string. e.g. `Sales Order` becomes `sales_order`.""" return cstr(txt).replace(" ", "_").replace("-", "_").lower() def unscrub(txt: str) -> str: - """Returns titlified string. e.g. `sales_order` becomes `Sales Order`.""" + """Return titlified string. e.g. `sales_order` becomes `Sales Order`.""" return txt.replace("_", " ").replace("-", " ").title() @@ -1540,7 +1542,7 @@ def get_installed_apps(*, _ensure_on_bench=False) -> list[str]: def get_doc_hooks(): - """Returns hooked methods for given doc. It will expand the dict tuple if required.""" + """Return hooked methods for given doc. Expand the dict tuple if required.""" if not hasattr(local, "doc_events_hooks"): hooks = get_hooks("doc_events", {}) out = {} @@ -1647,7 +1649,7 @@ def setup_module_map(): def get_file_items(path, raise_not_found=False, ignore_empty_lines=True): - """Returns items from text file as a list. Ignores empty lines.""" + """Return items from text file as a list. Ignore empty lines.""" import frappe.utils content = read_file(path, raise_not_found=raise_not_found) @@ -2000,7 +2002,7 @@ def get_all(doctype, *args, **kwargs): def get_value(*args, **kwargs): - """Returns a document property or list of properties. + """Return a document property or list of properties. Alias for `frappe.db.get_value` @@ -2015,6 +2017,7 @@ def get_value(*args, **kwargs): def as_json(obj: dict | list, indent=1, separators=None, ensure_ascii=True) -> str: + """Return the JSON string representation of the given `obj`.""" from frappe.utils.response import json_handler if separators is None: @@ -2047,7 +2050,7 @@ def are_emails_muted(): def get_test_records(doctype): - """Returns list of objects from `test_records.json` in the given doctype's folder.""" + """Return list of objects from `test_records.json` in the given doctype's folder.""" from frappe.modules import get_doctype_module, get_module_path path = os.path.join( @@ -2280,7 +2283,7 @@ log_level = None def logger( module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20 ): - """Returns a python logger that uses StreamHandler""" + """Return a python logger that uses StreamHandler.""" from frappe.utils.logger import get_logger return get_logger( @@ -2300,7 +2303,8 @@ def get_desk_link(doctype, name): return html.format(doctype=doctype, name=name, doctype_local=_(doctype)) -def bold(text): +def bold(text: str) -> str: + """Return `text` wrapped in `` tags.""" return f"{text}" @@ -2323,7 +2327,8 @@ def get_website_settings(key): return local.website_settings.get(key) -def get_system_settings(key): +def get_system_settings(key: str): + """Return the value associated with the given `key` from System Settings DocType.""" if not hasattr(local, "system_settings"): try: local.system_settings = get_cached_doc("System Settings") @@ -2342,7 +2347,7 @@ def get_active_domains(): def get_version(doctype, name, limit=None, head=False, raise_err=True): """ - Returns a list of version information of a given DocType. + Return a list of version information for the given DocType. Note: Applicable only if DocType has changes tracked. diff --git a/frappe/auth.py b/frappe/auth.py index 10bac8261a..56f1bcae26 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -289,7 +289,7 @@ class LoginManager: def check_password(self, user, pwd): """check password""" try: - # returns user in correct case + # return user in correct case return check_password(user, pwd) except frappe.AuthenticationError: self.fail("Incorrect password", user=user) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index c36c009adf..956bb0c9c3 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -150,7 +150,7 @@ class AutoRepeat(Document): def validate_auto_repeat_days(self): auto_repeat_days = self.get_auto_repeat_days() - if not len(set(auto_repeat_days)) == len(auto_repeat_days): + if len(set(auto_repeat_days)) != len(auto_repeat_days): repeated_days = get_repeated(auto_repeat_days) plural = "s" if len(repeated_days) > 1 else "" @@ -297,11 +297,11 @@ class AutoRepeat(Document): def get_next_schedule_date(self, schedule_date, for_full_schedule=False): """ - Returns the next schedule date for auto repeat after a recurring document has been created. - Adds required offset to the schedule_date param and returns the next schedule date. + Return the next schedule date for auto repeat after a recurring document has been created. + Add required offset to the schedule_date param and return the next schedule date. :param schedule_date: The date when the last recurring document was created. - :param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. + :param for_full_schedule: If True, return the immediate next schedule date, else the full schedule. """ if month_map.get(self.frequency): month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 diff --git a/frappe/automation/doctype/milestone/milestone.json b/frappe/automation/doctype/milestone/milestone.json index aa2dd35891..db3d61b2fd 100644 --- a/frappe/automation/doctype/milestone/milestone.json +++ b/frappe/automation/doctype/milestone/milestone.json @@ -53,7 +53,7 @@ ], "in_create": 1, "links": [], - "modified": "2022-08-03 12:20:55.076769", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Automation", "name": "Milestone", @@ -74,7 +74,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "reference_type", "track_changes": 1 diff --git a/frappe/automation/doctype/milestone_tracker/milestone_tracker.json b/frappe/automation/doctype/milestone_tracker/milestone_tracker.json index 8d4ed94dcd..f0dc452c8d 100644 --- a/frappe/automation/doctype/milestone_tracker/milestone_tracker.json +++ b/frappe/automation/doctype/milestone_tracker/milestone_tracker.json @@ -35,7 +35,7 @@ } ], "links": [], - "modified": "2022-08-03 12:20:54.955953", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Automation", "name": "Milestone Tracker", @@ -55,7 +55,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/build.py b/frappe/build.py index 7f111b9a69..03b830f0cb 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -133,10 +133,9 @@ def setup_assets(assets_archive): return directories_created -def download_frappe_assets(verbose=True): - """Downloads and sets up Frappe assets if they exist based on the current - commit HEAD. - Returns True if correctly setup else returns False. +def download_frappe_assets(verbose=True) -> bool: + """Download and set up Frappe assets if they exist based on the current commit HEAD. + Return True if correctly setup else return False. """ frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD") @@ -407,7 +406,7 @@ def link_assets_dir(source, target, hard_link=False): def scrub_html_template(content): - """Returns HTML content with removed whitespace and comments""" + """Return HTML content with removed whitespace and comments.""" # remove whitespace to a single space content = WHITESPACE_PATTERN.sub(" ", content) @@ -418,7 +417,7 @@ def scrub_html_template(content): def html_to_js_template(path, content): - """returns HTML template content as Javascript code, adding it to `frappe.templates`""" + """Return HTML template content as Javascript code, by adding it to `frappe.templates`.""" return """frappe.templates["{key}"] = '{content}';\n""".format( key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content) ) diff --git a/frappe/client.py b/frappe/client.py index 91f531fe1e..028df862c4 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -10,6 +10,7 @@ import frappe.utils from frappe import _ from frappe.desk.reportview import validate_args from frappe.model.db_query import check_parent_permission +from frappe.model.utils import is_virtual_doctype from frappe.utils import get_safe_filters from frappe.utils.deprecations import deprecated @@ -37,7 +38,7 @@ def get_list( as_dict: bool = True, or_filters=None, ): - """Returns a list of records by filters, fields, ordering and limit + """Return a list of records by filters, fields, ordering and limit. :param doctype: DocType of the data to be queried :param fields: fields to be returned. Default is `name` @@ -73,7 +74,7 @@ def get_count(doctype, filters=None, debug=False, cache=False): @frappe.whitelist() def get(doctype, name=None, filters=None, parent=None): - """Returns a document by name or filters + """Return a document by name or filters. :param doctype: DocType of the document to be returned :param name: return document of this `name` @@ -96,7 +97,7 @@ def get(doctype, name=None, filters=None, parent=None): @frappe.whitelist() def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, parent=None): - """Returns a value form a document + """Return a value from a document. :param doctype: DocType to be queried :param fieldname: Field to be returned (default `name`) @@ -295,7 +296,7 @@ def bulk_update(docs): @frappe.whitelist() def has_permission(doctype, docname, perm_type="read"): - """Returns a JSON with data whether the document has the requested permission + """Return a JSON with data whether the document has the requested permission. :param doctype: DocType of the document to be checked :param docname: `name` of the document to be checked @@ -306,7 +307,7 @@ def has_permission(doctype, docname, perm_type="read"): @frappe.whitelist() def get_doc_permissions(doctype, docname): - """Returns an evaluated document permissions dict like `{"read":1, "write":1}` + """Return an evaluated document permissions dict like `{"read":1, "write":1}`. :param doctype: DocType of the document to be evaluated :param docname: `name` of the document to be evaluated @@ -353,7 +354,7 @@ def get_js(items): @frappe.whitelist(allow_guest=True) def get_time_zone(): - """Returns default time zone""" + """Return the default time zone.""" return {"time_zone": frappe.defaults.get_defaults().get("time_zone")} @@ -431,6 +432,18 @@ def validate_link(doctype: str, docname: str, fields=None): ) values = frappe._dict() + + if is_virtual_doctype(doctype): + try: + frappe.get_doc(doctype, docname) + values.name = docname + except frappe.DoesNotExistError: + frappe.clear_last_message() + frappe.msgprint( + _("Document {0} {1} does not exist").format(frappe.bold(doctype), frappe.bold(docname)), + ) + return values + values.name = frappe.db.get_value(doctype, docname, cache=True) fields = frappe.parse_json(fields) @@ -453,8 +466,7 @@ def validate_link(doctype: str, docname: str, fields=None): def insert_doc(doc) -> "Document": - """Inserts document and returns parent document object with appended child document - if `doc` is child document else returns the inserted document object + """Insert document and return parent document object with appended child document if `doc` is child document else return the inserted document object. :param doc: doc to insert (dict)""" diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 65f896eb24..48a4feea57 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -72,13 +72,10 @@ def new_site( setup_db=True, ): "Create a new site" - from frappe.installer import _new_site, extract_sql_from_archive + from frappe.installer import _new_site frappe.init(site=site, new_site=True) - if source_sql: - source_sql = extract_sql_from_archive(source_sql) - _new_site( db_name, site, @@ -180,75 +177,113 @@ def _restore( with_public_files=None, with_private_files=None, ): + from frappe.installer import extract_files + from frappe.utils.backups import decrypt_backup, get_or_generate_backup_encryption_key - from frappe.installer import ( - _new_site, - extract_files, - extract_sql_from_archive, - is_downgrade, - is_partial, - validate_database_sql, - ) - from frappe.utils.backups import Backup, get_or_generate_backup_encryption_key + err, out = frappe.utils.execute_in_shell(f"file {sql_file_path}", check_exit_code=True) + if err: + click.secho("Failed to detect type of backup file", fg="red") + sys.exit(1) - _backup = Backup(sql_file_path) - - try: - decompressed_file_name = extract_sql_from_archive(sql_file_path) - if is_partial(decompressed_file_name): - click.secho( - "Partial Backup file detected. You cannot use a partial file to restore a Frappe site.", - fg="red", - ) - click.secho( - "Use `bench partial-restore` to restore a partial backup to an existing site.", - fg="yellow", - ) - _backup.decryption_rollback() - sys.exit(1) - - except UnicodeDecodeError: - _backup.decryption_rollback() + if "cipher" in out.decode().split(":")[-1].strip(): if encryption_key: click.secho("Encrypted backup file detected. Decrypting using provided key.", fg="yellow") - _backup.backup_decryption(encryption_key) else: click.secho("Encrypted backup file detected. Decrypting using site config.", fg="yellow") encryption_key = get_or_generate_backup_encryption_key() - _backup.backup_decryption(encryption_key) - # Rollback on unsuccessful decryrption - if not os.path.exists(sql_file_path): - click.secho("Decryption failed. Please provide a valid key and try again.", fg="red") + with decrypt_backup(sql_file_path, encryption_key): + # Rollback on unsuccessful decryption + if not os.path.exists(sql_file_path): + click.secho("Decryption failed. Please provide a valid key and try again.", fg="red") + sys.exit(1) - _backup.decryption_rollback() - sys.exit(1) - - decompressed_file_name = extract_sql_from_archive(sql_file_path) - - if is_partial(decompressed_file_name): - click.secho( - "Partial Backup file detected. You cannot use a partial file to restore a Frappe site.", - fg="red", + restore_backup( + sql_file_path, + site, + db_root_username, + db_root_password, + verbose, + install_app, + admin_password, + force, ) - click.secho( - "Use `bench partial-restore` to restore a partial backup to an existing site.", - fg="yellow", - ) - _backup.decryption_rollback() - sys.exit(1) + else: + restore_backup( + sql_file_path, + site, + db_root_username, + db_root_password, + verbose, + install_app, + admin_password, + force, + ) - validate_database_sql(decompressed_file_name, _raise=not force) + # Extract public and/or private files to the restored site, if user has given the path + if with_public_files: + # Decrypt data if there is a Key + if encryption_key: + with decrypt_backup(with_public_files, encryption_key): + public = extract_files(site, with_public_files) + else: + public = extract_files(site, with_public_files) - # dont allow downgrading to older versions of frappe without force - if not force and is_downgrade(decompressed_file_name, verbose=True): + # Removing temporarily created file + os.remove(public) + + if with_private_files: + # Decrypt data if there is a Key + if encryption_key: + with decrypt_backup(with_private_files, encryption_key): + private = extract_files(site, with_private_files) + else: + private = extract_files(site, with_private_files) + + # Removing temporarily created file + os.remove(private) + + success_message = "Site {} has been restored{}".format( + site, " with files" if (with_public_files or with_private_files) else "" + ) + click.secho(success_message, fg="green") + + +def restore_backup( + sql_file_path: str, + site, + db_root_username, + db_root_password, + verbose, + install_app, + admin_password, + force, +): + from frappe.installer import _new_site, is_downgrade, is_partial, validate_database_sql + + if is_partial(sql_file_path): + click.secho( + "Partial Backup file detected. You cannot use a partial file to restore a Frappe site.", + fg="red", + ) + click.secho( + "Use `bench partial-restore` to restore a partial backup to an existing site.", + fg="yellow", + ) + sys.exit(1) + + # Check if the backup is of an older version of frappe and the user hasn't specified force + if is_downgrade(sql_file_path, verbose=True) and not force: warn_message = ( "This is not recommended and may lead to unexpected behaviour. " "Do you want to continue anyway?" ) click.confirm(warn_message, abort=True) + # Validate the sql file + validate_database_sql(sql_file_path, _raise=not force) + try: _new_site( frappe.conf.db_name, @@ -258,53 +293,15 @@ def _restore( admin_password=admin_password, verbose=verbose, install_apps=install_app, - source_sql=decompressed_file_name, + source_sql=sql_file_path, force=True, db_type=frappe.conf.db_type, ) except Exception as err: print(err.args[1]) - _backup.decryption_rollback() sys.exit(1) - # Removing temporarily created file - if decompressed_file_name != sql_file_path: - os.remove(decompressed_file_name) - _backup.decryption_rollback() - - # Extract public and/or private files to the restored site, if user has given the path - if with_public_files: - # Decrypt data if there is a Key - if encryption_key: - _backup = Backup(with_public_files) - _backup.backup_decryption(encryption_key) - if not os.path.exists(with_public_files): - _backup.decryption_rollback() - public = extract_files(site, with_public_files) - - # Removing temporarily created file - os.remove(public) - _backup.decryption_rollback() - - if with_private_files: - # Decrypt data if there is a Key - if encryption_key: - _backup = Backup(with_private_files) - _backup.backup_decryption(encryption_key) - if not os.path.exists(with_private_files): - _backup.decryption_rollback() - private = extract_files(site, with_private_files) - - # Removing temporarily created file - os.remove(private) - _backup.decryption_rollback() - - success_message = "Site {} has been restored{}".format( - site, " with files" if (with_public_files or with_private_files) else "" - ) - click.secho(success_message, fg="green") - @click.command("partial-restore") @click.argument("sql-file-path") @@ -312,38 +309,23 @@ def _restore( @click.option("--encryption-key", help="Backup encryption key") @pass_context def partial_restore(context, sql_file_path, verbose, encryption_key=None): - from frappe.installer import extract_sql_from_archive, partial_restore - from frappe.utils.backups import Backup, get_or_generate_backup_encryption_key + from frappe.installer import is_partial, partial_restore + from frappe.utils.backups import decrypt_backup, get_or_generate_backup_encryption_key if not os.path.exists(sql_file_path): print("Invalid path", sql_file_path) sys.exit(1) site = get_site(context) - frappe.init(site=site) - - _backup = Backup(sql_file_path) - verbose = context.verbose or verbose - + frappe.init(site=site) frappe.connect(site=site) - try: - decompressed_file_name = extract_sql_from_archive(sql_file_path) + err, out = frappe.utils.execute_in_shell(f"file {sql_file_path}", check_exit_code=True) + if err: + click.secho("Failed to detect type of backup file", fg="red") + sys.exit(1) - with open(decompressed_file_name) as f: - header = " ".join(f.readline() for _ in range(5)) - - # Check for full backup file - if "Partial Backup" not in header: - click.secho( - "Full backup file detected.Use `bench restore` to restore a Frappe Site.", - fg="red", - ) - _backup.decryption_rollback() - sys.exit(1) - - except UnicodeDecodeError: - _backup.decryption_rollback() + if "cipher" in out.decode().split(":")[-1].strip(): if encryption_key: click.secho("Encrypted backup file detected. Decrypting using provided key.", fg="yellow") key = encryption_key @@ -352,35 +334,30 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None): click.secho("Encrypted backup file detected. Decrypting using site config.", fg="yellow") key = get_or_generate_backup_encryption_key() - _backup.backup_decryption(key) - - # Rollback on unsuccessful decryrption - if not os.path.exists(sql_file_path): - click.secho("Decryption failed. Please provide a valid key and try again.", fg="red") - _backup.decryption_rollback() - sys.exit(1) - - decompressed_file_name = extract_sql_from_archive(sql_file_path) - - with open(decompressed_file_name) as f: - header = " ".join(f.readline() for _ in range(5)) - - # Check for Full backup file. - if "Partial Backup" not in header: + with decrypt_backup(sql_file_path, key): + if not is_partial(sql_file_path): click.secho( - "Full Backup file detected.Use `bench restore` to restore a Frappe Site.", + "Full backup file detected.Use `bench restore` to restore a Frappe Site.", fg="red", ) - _backup.decryption_rollback() sys.exit(1) - partial_restore(sql_file_path, verbose) + partial_restore(sql_file_path, verbose) - # Removing temporarily created file - _backup.decryption_rollback() - if os.path.exists(sql_file_path.rstrip(".gz")): - os.remove(sql_file_path.rstrip(".gz")) + # Rollback on unsuccessful decryption + if not os.path.exists(sql_file_path): + click.secho("Decryption failed. Please provide a valid key and try again.", fg="red") + sys.exit(1) + else: + if not is_partial(sql_file_path): + click.secho( + "Full backup file detected.Use `bench restore` to restore a Frappe Site.", + fg="red", + ) + sys.exit(1) + + partial_restore(sql_file_path, verbose) frappe.destroy() @@ -524,6 +501,130 @@ def list_apps(context, format): click.echo(frappe.as_json(summary_dict)) +@click.command("add-database-index") +@click.option("--doctype", help="DocType on which index needs to be added") +@click.option( + "--column", + multiple=True, + help="Column to index. Multiple columns will create multi-column index in given order. To create a multiple, single column index, execute the command multiple times.", +) +@pass_context +def add_db_index(context, doctype, column): + "Adds a new DB index and creates a property setter to persist it." + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + + columns = column # correct naming + for site in context.sites: + frappe.connect(site=site) + try: + frappe.db.add_index(doctype, columns) + if len(columns) == 1: + make_property_setter( + doctype, + columns[0], + property="search_index", + value="1", + property_type="Check", + for_doctype=False, # Applied on docfield + ) + frappe.db.commit() + finally: + frappe.destroy() + + if not context.sites: + raise SiteNotSpecifiedError + + +@click.command("describe-database-table") +@click.option("--doctype", help="DocType to describe") +@click.option( + "--column", + multiple=True, + help="Explicitly fetch accurate cardinality from table data. This can be quite slow on large tables.", +) +@pass_context +def describe_database_table(context, doctype, column): + """Describes various statistics about the table. + + This is useful to build integration like + This includes: + 1. Schema + 2. Indexes + 3. stats - total count of records + 4. if column is specified then extra stats are generated for column: + Distinct values count in column + """ + import json + + for site in context.sites: + frappe.connect(site=site) + try: + data = _extract_table_stats(doctype, column) + # NOTE: Do not print anything else in this to avoid clobbering the output. + print(json.dumps(data, indent=2)) + finally: + frappe.destroy() + + if not context.sites: + raise SiteNotSpecifiedError + + +def _extract_table_stats(doctype: str, columns: list[str]) -> dict: + from frappe.utils import cstr, get_table_name + + def sql_bool(val): + return cstr(val).lower() in ("yes", "1", "true") + + table = get_table_name(doctype, wrap_in_backticks=True) + + schema = [] + for field in frappe.db.sql(f"describe {table}", as_dict=True): + schema.append( + { + "column": field["Field"], + "type": field["Type"], + "is_nullable": sql_bool(field["Null"]), + "default": field["Default"], + } + ) + + def update_cardinality(column, value): + for col in schema: + if col["column"] == column: + col["cardinality"] = value + break + + indexes = [] + for idx in frappe.db.sql(f"show index from {table}", as_dict=True): + indexes.append( + { + "unique": not sql_bool(idx["Non_unique"]), + "cardinality": idx["Cardinality"], + "name": idx["Key_name"], + "sequence": idx["Seq_in_index"], + "nullable": sql_bool(idx["Null"]), + "column": idx["Column_name"], + "type": idx["Index_type"], + } + ) + if idx["Seq_in_index"] == 1: + update_cardinality(idx["Column_name"], idx["Cardinality"]) + + total_rows = frappe.db.count(doctype) + + # fetch accurate cardinality for columns by query. WARN: This can take a lot of time. + for column in columns: + cardinality = frappe.db.sql(f"select count(distinct {column}) from {table}")[0][0] + update_cardinality(column, cardinality) + + return { + "table_name": table.strip("`"), + "total_rows": total_rows, + "schema": schema, + "indexes": indexes, + } + + @click.command("add-system-manager") @click.argument("email") @click.option("--first-name") @@ -741,6 +842,9 @@ def use(site, sites_path="."): ) @click.option("--verbose", default=False, is_flag=True, help="Add verbosity") @click.option("--compress", default=False, is_flag=True, help="Compress private and public files") +@click.option( + "--old-backup-metadata", default=False, is_flag=True, help="Use older backup metadata" +) @pass_context def backup( context, @@ -755,6 +859,7 @@ def backup( compress=False, include="", exclude="", + old_backup_metadata=False, ): "Backup" @@ -780,6 +885,7 @@ def backup( compress=compress, verbose=verbose, force=True, + old_backup_metadata=old_backup_metadata, ) except Exception: click.secho( @@ -1289,7 +1395,7 @@ def trim_database(context, dry_run, format, no_backup, yes=False): for table_name in database_tables: if not table_name.startswith("tab"): continue - if not (table_name.replace("tab", "", 1) in doctype_tables or table_name in STANDARD_TABLES): + if table_name.replace("tab", "", 1) not in doctype_tables and table_name not in STANDARD_TABLES: TABLES_TO_DROP.append(table_name) if not TABLES_TO_DROP: @@ -1436,6 +1542,8 @@ def add_new_user( commands = [ add_system_manager, add_user_for_sites, + add_db_index, + describe_database_table, backup, drop_site, install_app, diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 169c9eecb4..a008f4638c 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -143,7 +143,7 @@ def get_preferred_address(doctype, name, preferred_key="is_primary_address"): def get_default_address( doctype: str, name: str | None, sort_key: str = "is_primary_address" ) -> str | None: - """Returns default Address name for the given doctype, name""" + """Return default Address name for the given doctype, name.""" if sort_key not in ["is_shipping_address", "is_primary_address"]: return None @@ -228,7 +228,7 @@ def get_address_list(doctype, txt, filters, limit_start, limit_page_length=20, o def has_website_permission(doc, ptype, user, verbose=False): - """Returns true if there is a related lead or contact related to this document""" + """Return True if there is a related lead or contact related to this document.""" contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user}) if contact_name: diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index 679d8b4c8f..83d1002acb 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -257,7 +257,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-02 12:00:27.299156", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Contacts", "name": "Contact", @@ -392,7 +392,7 @@ ], "show_title_field_in_link": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "full_name" } \ No newline at end of file diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index c1bd2f55ec..b40a27ca13 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -50,14 +50,14 @@ class Contact(Document): def autoname(self): self.name = self._get_full_name() - if frappe.db.exists("Contact", self.name): - self.name = append_number_if_name_exists("Contact", self.name) - # concat party name if reqd for link in self.links: self.name = self.name + "-" + link.link_name.strip() break + if frappe.db.exists("Contact", self.name): + self.name = append_number_if_name_exists("Contact", self.name) + def validate(self): self.full_name = self._get_full_name() self.set_primary_email() @@ -168,7 +168,7 @@ class Contact(Document): def get_default_contact(doctype, name): - """Returns default contact for the given doctype, name""" + """Return default contact for the given doctype, name.""" out = frappe.db.sql( """select parent, IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) diff --git a/frappe/core/api/file.py b/frappe/core/api/file.py index aa8be30707..7756fd274b 100644 --- a/frappe/core/api/file.py +++ b/frappe/core/api/file.py @@ -15,8 +15,7 @@ def unzip_file(name: str): @frappe.whitelist() def get_attached_images(doctype: str, names: list[str] | str) -> frappe._dict: - """get list of image urls attached in form - returns {name: ['image.jpg', 'image.png']}""" + """Return list of image urls attached in form `{name: ['image.jpg', 'image.png']}`.""" if isinstance(names, str): names = json.loads(names) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 32500c9158..de2dfb7702 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -298,7 +298,7 @@ class Communication(Document, CommunicationEmailMixin): @staticmethod def _get_emails_list(emails=None, exclude_displayname=False): - """Returns list of emails from given email string. + """Return list of emails from given email string. * Removes duplicate mailids * Removes display name from email address if exclude_displayname is True @@ -309,15 +309,15 @@ class Communication(Document, CommunicationEmailMixin): return [email.lower() for email in set(emails) if email] def to_list(self, exclude_displayname=True): - """Returns to list.""" + """Return `to` list.""" return self._get_emails_list(self.recipients, exclude_displayname=exclude_displayname) def cc_list(self, exclude_displayname=True): - """Returns cc list.""" + """Return `cc` list.""" return self._get_emails_list(self.cc, exclude_displayname=exclude_displayname) def bcc_list(self, exclude_displayname=True): - """Returns bcc list.""" + """Return `bcc` list.""" return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) def get_attachments(self): @@ -438,7 +438,7 @@ class Communication(Document, CommunicationEmailMixin): frappe.db.commit() def parse_email_for_timeline_links(self): - if not frappe.db.get_value("Email Account", self.email_account, "enable_automatic_linking"): + if not frappe.db.get_value("Email Account", filters={"enable_automatic_linking": 1}): return for doctype, docname in parse_email([self.recipients, self.cc, self.bcc]): @@ -615,9 +615,9 @@ def parse_email(email_strings): def get_email_without_link(email): - """ - returns email address without doctype links - returns admin@example.com for email admin+doctype+docname@example.com + """Return email address without doctype links. + + e.g. 'admin@example.com' is returned for email 'admin+doctype+docname@example.com' """ if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): return email @@ -662,7 +662,10 @@ def update_parent_document_on_communication(doc): def update_first_response_time(parent, communication): if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"): - if is_system_user(communication.sender): + if ( + is_system_user(communication.sender) + or frappe.get_cached_value("User", frappe.session.user, "user_type") == "System User" + ): if communication.sent_or_received == "Sent": first_responded_on = communication.creation if parent.meta.has_field("first_responded_on"): diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index a0c9d35f20..50853f6bec 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -191,7 +191,8 @@ def _make( def validate_email(doc: "Communication") -> None: """Validate Email Addresses of Recipients and CC""" if ( - not (doc.communication_type == "Communication" and doc.communication_medium == "Email") + doc.communication_type != "Communication" + or doc.communication_medium != "Email" or doc.flags.in_receive ): return diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 81b882113b..2c05570cdb 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -1,6 +1,9 @@ import frappe from frappe import _ from frappe.core.utils import get_parent_doc +from frappe.desk.doctype.notification_settings.notification_settings import ( + is_email_notifications_enabled_for_type, +) from frappe.desk.doctype.todo.todo import ToDo from frappe.email.doctype.email_account.email_account import EmailAccount from frappe.utils import get_formatted_email, get_url, parse_addr @@ -26,7 +29,7 @@ class CommunicationEmailMixin: ) def get_email_with_displayname(self, email_address): - """Returns email address after adding displayname.""" + """Return email address after adding displayname.""" display_name, email = parse_addr(email_address) if display_name and display_name != email: return email_address @@ -78,7 +81,12 @@ class CommunicationEmailMixin: if doc_owner := self.get_owner(): cc.append(doc_owner) cc = set(cc) - {self.sender_mailid} - cc.update(self.get_assignees()) + assignees = set(self.get_assignees()) + # Check and remove If user disabled notifications for incoming emails on assigned document. + for assignee in assignees.copy(): + if not is_email_notifications_enabled_for_type(assignee, "threads_on_assigned_document"): + assignees.remove(assignee) + cc.update(assignees) cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc)) cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)) @@ -143,7 +151,7 @@ class CommunicationEmailMixin: return self.content def get_attach_link(self, print_format): - """Returns public link for the attachment via `templates/emails/print_link.html`.""" + """Return public link for the attachment via `templates/emails/print_link.html`.""" return frappe.get_template("templates/emails/print_link.html").render( { "url": get_url(), diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 7db3aa9629..b3fa136eb4 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -136,47 +136,40 @@ frappe.ui.form.on("Data Import", { let total_records = cint(r.message.total_records); if (!total_records) return; + let action, message; + if (frm.doc.import_type === "Insert New Records") { + action = "imported"; + } else { + action = "updated"; + } - let message; if (failed_records === 0) { - let message_args = [successful_records]; - if (frm.doc.import_type === "Insert New Records") { - message = - successful_records > 1 - ? __("Successfully imported {0} records.", message_args) - : __("Successfully imported {0} record.", message_args); + let message_args = [action, successful_records]; + if (successful_records === 1) { + message = __("Successfully {0} 1 record.", message_args); } else { - message = - successful_records > 1 - ? __("Successfully updated {0} records.", message_args) - : __("Successfully updated {0} record.", message_args); + message = __("Successfully {0} {1} records.", message_args); } } else { - let message_args = [successful_records, total_records]; - if (frm.doc.import_type === "Insert New Records") { - message = - successful_records > 1 - ? __( - "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ) - : __( - "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ); + let message_args = [action, successful_records, total_records]; + if (successful_records === 1) { + message = __( + "Successfully {0} {1} record out of {2}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); } else { - message = - successful_records > 1 - ? __( - "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ) - : __( - "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.", - message_args - ); + message = __( + "Successfully {0} {1} records out of {2}. Click on Export Errored Rows, fix the errors and import again.", + message_args + ); } } + + // If the job timed out, display an extra hint + if (r.message.status === "Timed Out") { + message += "
" + __("Import timed out, please re-try."); + } + frm.dashboard.set_headline(message); }, }); diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json index faa9a33bf1..97716219a2 100644 --- a/frappe/core/doctype/data_import/data_import.json +++ b/frappe/core/doctype/data_import/data_import.json @@ -1,198 +1,198 @@ { - "actions": [], - "autoname": "format:{reference_doctype} Import on {creation}", - "beta": 1, - "creation": "2019-08-04 14:16:08.318714", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "reference_doctype", - "import_type", - "download_template", - "import_file", - "payload_count", - "html_5", - "google_sheets_url", - "refresh_google_sheet", - "column_break_5", - "status", - "submit_after_import", - "mute_emails", - "template_options", - "import_warnings_section", - "template_warnings", - "import_warnings", - "section_import_preview", - "import_preview", - "import_log_section", - "show_failed_logs", - "import_log_preview" - ], - "fields": [ - { - "fieldname": "reference_doctype", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Document Type", - "options": "DocType", - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "import_type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Import Type", - "options": "\nInsert New Records\nUpdate Existing Records", - "reqd": 1, - "set_only_once": 1 - }, - { - "depends_on": "eval:!doc.__islocal", - "fieldname": "import_file", - "fieldtype": "Attach", - "in_list_view": 1, - "label": "Import File", - "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" - }, - { - "fieldname": "import_preview", - "fieldtype": "HTML", - "label": "Import Preview" - }, - { - "fieldname": "section_import_preview", - "fieldtype": "Section Break", - "label": "Preview" - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "template_options", - "fieldtype": "Code", - "hidden": 1, - "label": "Template Options", - "options": "JSON", - "read_only": 1 - }, - { - "fieldname": "import_log_section", - "fieldtype": "Section Break", - "label": "Import Log" - }, - { - "fieldname": "import_log_preview", - "fieldtype": "HTML", - "label": "Import Log Preview" - }, - { - "default": "Pending", - "fieldname": "status", - "fieldtype": "Select", - "hidden": 1, - "label": "Status", - "no_copy": 1, - "options": "Pending\nSuccess\nPartial Success\nError", - "read_only": 1 - }, - { - "fieldname": "template_warnings", - "fieldtype": "Code", - "hidden": 1, - "label": "Template Warnings", - "options": "JSON" - }, - { - "default": "0", - "fieldname": "submit_after_import", - "fieldtype": "Check", - "label": "Submit After Import", - "set_only_once": 1 - }, - { - "fieldname": "import_warnings_section", - "fieldtype": "Section Break", - "label": "Import File Errors and Warnings" - }, - { - "fieldname": "import_warnings", - "fieldtype": "HTML", - "label": "Import Warnings" - }, - { - "depends_on": "eval:!doc.__islocal", - "fieldname": "download_template", - "fieldtype": "Button", - "label": "Download Template" - }, - { - "default": "1", - "fieldname": "mute_emails", - "fieldtype": "Check", - "label": "Don't Send Emails", - "set_only_once": 1 - }, - { - "default": "0", - "fieldname": "show_failed_logs", - "fieldtype": "Check", - "label": "Show Failed Logs" - }, - { - "depends_on": "eval:!doc.__islocal && !doc.import_file", - "fieldname": "html_5", - "fieldtype": "HTML", - "options": "
Or
" - }, - { - "depends_on": "eval:!doc.__islocal && !doc.import_file\n", - "description": "Must be a publicly accessible Google Sheets URL", - "fieldname": "google_sheets_url", - "fieldtype": "Data", - "label": "Import from Google Sheets", - "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" - }, - { - "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)", - "fieldname": "refresh_google_sheet", - "fieldtype": "Button", - "label": "Refresh Google Sheet" - }, - { - "fieldname": "payload_count", - "fieldtype": "Int", - "hidden": 1, - "label": "Payload Count", - "read_only": 1 - } - ], - "hide_toolbar": 1, - "links": [], - "modified": "2022-02-14 10:08:37.624914", - "modified_by": "Administrator", - "module": "Core", - "name": "Data Import", - "naming_rule": "Expression", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 1 -} + "actions": [], + "autoname": "format:{reference_doctype} Import on {creation}", + "beta": 1, + "creation": "2019-08-04 14:16:08.318714", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "import_type", + "download_template", + "import_file", + "payload_count", + "html_5", + "google_sheets_url", + "refresh_google_sheet", + "column_break_5", + "status", + "submit_after_import", + "mute_emails", + "template_options", + "import_warnings_section", + "template_warnings", + "import_warnings", + "section_import_preview", + "import_preview", + "import_log_section", + "show_failed_logs", + "import_log_preview" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "import_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Import Type", + "options": "\nInsert New Records\nUpdate Existing Records", + "reqd": 1, + "set_only_once": 1 + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "import_file", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "Import File", + "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" + }, + { + "fieldname": "import_preview", + "fieldtype": "HTML", + "label": "Import Preview" + }, + { + "fieldname": "section_import_preview", + "fieldtype": "Section Break", + "label": "Preview" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "template_options", + "fieldtype": "Code", + "hidden": 1, + "label": "Template Options", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "import_log_section", + "fieldtype": "Section Break", + "label": "Import Log" + }, + { + "fieldname": "import_log_preview", + "fieldtype": "HTML", + "label": "Import Log Preview" + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "label": "Status", + "no_copy": 1, + "options": "Pending\nSuccess\nPartial Success\nError\nTimed Out", + "read_only": 1 + }, + { + "fieldname": "template_warnings", + "fieldtype": "Code", + "hidden": 1, + "label": "Template Warnings", + "options": "JSON" + }, + { + "default": "0", + "fieldname": "submit_after_import", + "fieldtype": "Check", + "label": "Submit After Import", + "set_only_once": 1 + }, + { + "fieldname": "import_warnings_section", + "fieldtype": "Section Break", + "label": "Import File Errors and Warnings" + }, + { + "fieldname": "import_warnings", + "fieldtype": "HTML", + "label": "Import Warnings" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "download_template", + "fieldtype": "Button", + "label": "Download Template" + }, + { + "default": "1", + "fieldname": "mute_emails", + "fieldtype": "Check", + "label": "Don't Send Emails", + "set_only_once": 1 + }, + { + "default": "0", + "fieldname": "show_failed_logs", + "fieldtype": "Check", + "label": "Show Failed Logs" + }, + { + "depends_on": "eval:!doc.__islocal && !doc.import_file", + "fieldname": "html_5", + "fieldtype": "HTML", + "options": "
Or
" + }, + { + "depends_on": "eval:!doc.__islocal && !doc.import_file\n", + "description": "Must be a publicly accessible Google Sheets URL", + "fieldname": "google_sheets_url", + "fieldtype": "Data", + "label": "Import from Google Sheets", + "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" + }, + { + "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)", + "fieldname": "refresh_google_sheet", + "fieldtype": "Button", + "label": "Refresh Google Sheet" + }, + { + "fieldname": "payload_count", + "fieldtype": "Int", + "hidden": 1, + "label": "Payload Count", + "read_only": 1 + } + ], + "hide_toolbar": 1, + "links": [], + "modified": "2023-12-15 12:45:49.452834", + "modified_by": "Administrator", + "module": "Core", + "name": "Data Import", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index bd6c6efe4f..f3dca2d5af 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -3,6 +3,8 @@ import os +from rq.timeouts import JobTimeoutException + import frappe from frappe import _ from frappe.core.doctype.data_import.exporter import Exporter @@ -32,11 +34,13 @@ class DataImport(Document): payload_count: DF.Int reference_doctype: DF.Link show_failed_logs: DF.Check - status: DF.Literal["Pending", "Success", "Partial Success", "Error"] + status: DF.Literal["Pending", "Success", "Partial Success", "Error", "Timed Out"] submit_after_import: DF.Check template_options: DF.Code | None template_warnings: DF.Code | None + # end: auto-generated types + def validate(self): doc_before_save = self.get_doc_before_save() if ( @@ -136,6 +140,9 @@ def start_import(data_import): try: i = Importer(data_import.reference_doctype, data_import=data_import) i.import_data() + except JobTimeoutException: + frappe.db.rollback() + data_import.db_set("status", "Timed Out") except Exception: frappe.db.rollback() data_import.db_set("status", "Error") @@ -190,6 +197,9 @@ def download_import_log(data_import_name): def get_import_status(data_import_name): import_status = {} + data_import = frappe.get_doc("Data Import", data_import_name) + import_status["status"] = data_import.status + logs = frappe.get_all( "Data Import Log", fields=["count(*) as count", "success"], diff --git a/frappe/core/doctype/data_import/data_import_list.js b/frappe/core/doctype/data_import/data_import_list.js index c054655e62..a16478cdb1 100644 --- a/frappe/core/doctype/data_import/data_import_list.js +++ b/frappe/core/doctype/data_import/data_import_list.js @@ -20,13 +20,14 @@ frappe.listview_settings["Data Import"] = { Success: "green", "In Progress": "orange", Error: "red", + "Timed Out": "orange", }; let status = doc.status; if (imports_in_progress.includes(doc.name)) { status = "In Progress"; } - if (status == "Pending") { + if (status === "Pending") { status = "Not Started"; } diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index fbbf12a978..84f6acf8af 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -179,7 +179,7 @@ class Importer: log_index += 1 - if not self.data_import.status == "Partial Success": + if self.data_import.status != "Partial Success": self.data_import.db_set("status", "Partial Success") # commit after every successful import @@ -514,8 +514,8 @@ class ImportFile: def parse_next_row_for_import(self, data): """ - Parses rows that make up a doc. A doc maybe built from a single row or multiple rows. - Returns the doc, rows, and data without the rows. + Parse rows that make up a doc. A doc maybe built from a single row or multiple rows. + Return the doc, rows, and data without the rows. """ doctypes = self.header.doctypes diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py index aa6239c279..c99b6ad507 100644 --- a/frappe/core/doctype/deleted_document/deleted_document.py +++ b/frappe/core/doctype/deleted_document/deleted_document.py @@ -27,6 +27,14 @@ class DeletedDocument(Document): # end: auto-generated types pass + @staticmethod + def clear_old_logs(days=180): + from frappe.query_builder import Interval + from frappe.query_builder.functions import Now + + table = frappe.qb.DocType("Deleted Document") + frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) + @frappe.whitelist() def restore(name, alert=True): diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index dc26c1f96f..01fa56a9ce 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -118,9 +118,10 @@ class DocField(Document): width: DF.Data | None # end: auto-generated types def get_link_doctype(self): - """Returns the Link doctype for the docfield (if applicable) - if fieldtype is Link: Returns "options" - if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table + """Return the Link doctype for the `docfield` (if applicable). + + * If fieldtype is Link: Return "options". + * If fieldtype is Table MultiSelect: Return "options" of the Link field in the Child Table. """ if self.fieldtype == "Link": return self.options diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index c21654a109..ebc4d85d0b 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -3,7 +3,7 @@ frappe.ui.form.on("DocType", { onload: function (frm) { - if (frm.is_new()) { + if (frm.is_new() && !frm.doc?.fields) { frappe.listview_settings["DocType"].new_doctype_dialog(); } }, diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 082471da7d..25d4bd78f2 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -68,6 +68,7 @@ "column_break_51", "email_append_to", "sender_field", + "sender_name_field", "subject_field", "sb2", "permissions", @@ -520,7 +521,7 @@ "depends_on": "email_append_to", "fieldname": "sender_field", "fieldtype": "Data", - "label": "Sender Field", + "label": "Sender Email Field", "mandatory_depends_on": "email_append_to" }, { @@ -661,6 +662,12 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_name_field", + "fieldtype": "Data", + "label": "Sender Name Field" } ], "icon": "fa fa-bolt", @@ -743,7 +750,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2023-11-01 16:45:14.960949", + "modified": "2023-12-01 18:37:16.799471", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index bbdcfd1817..f0f7f96bfc 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -162,6 +162,7 @@ class DocType(Document): route: DF.Data | None search_fields: DF.Data | None sender_field: DF.Data | None + sender_name_field: DF.Data | None show_name_in_global_search: DF.Check show_preview_popup: DF.Check show_title_field_in_link: DF.Check @@ -177,6 +178,7 @@ class DocType(Document): translated_doctype: DF.Check website_search_field: DF.Data | None # end: auto-generated types + def validate(self): """Validate DocType before saving. @@ -290,7 +292,7 @@ class DocType(Document): if not [d.fieldname for d in self.fields if d.in_list_view]: cnt = 0 for d in self.fields: - if d.reqd and not d.hidden and not d.fieldtype in not_allowed_in_list_view: + if d.reqd and not d.hidden and d.fieldtype not in not_allowed_in_list_view: d.in_list_view = 1 cnt += 1 if cnt == 4: @@ -305,7 +307,7 @@ class DocType(Document): def check_indexing_for_dashboard_links(self): """Enable indexing for outgoing links used in dashboard""" for d in self.fields: - if d.fieldtype == "Link" and not (d.unique or d.search_index): + if d.fieldtype == "Link" and not d.unique and not d.search_index: referred_as_link = frappe.db.exists( "DocType Link", {"parent": d.options, "link_doctype": self.name, "link_fieldname": d.fieldname}, @@ -412,7 +414,7 @@ class DocType(Document): if self.has_web_view: # route field must be present - if not "route" in [d.fieldname for d in self.fields]: + if "route" not in [d.fieldname for d in self.fields]: frappe.throw(_('Field "route" is mandatory for Web Views'), title="Missing Field") # clear website cache @@ -984,7 +986,7 @@ class DocType(Document): add_column(self.name, "parentfield", "Data") def get_max_idx(self): - """Returns the highest `idx`""" + """Return the highest `idx`.""" max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""", self.name) return max_idx and max_idx[0][0] or 0 @@ -1265,7 +1267,7 @@ def validate_fields(meta): ), WrongOptionsDoctypeLinkError, ) - elif not (options == d.options): + elif options != d.options: frappe.throw( _("{0}: Options {1} must be the same as doctype name {2} for the field {3}").format( docname, d.options, options, d.label @@ -1513,7 +1515,7 @@ def validate_fields(meta): def check_table_multiselect_option(docfield): """check if the doctype provided in Option has atleast 1 Link field""" - if not docfield.fieldtype == "Table MultiSelect": + if docfield.fieldtype != "Table MultiSelect": return doctype = docfield.options @@ -1579,7 +1581,7 @@ def validate_fields(meta): title=_("Invalid Option"), ) - if not (meta.is_virtual == child_doctype_meta.is_virtual): + if meta.is_virtual != child_doctype_meta.is_virtual: error_msg = " should be virtual." if meta.is_virtual else " cannot be virtual." frappe.throw( _("Child Table {0} for field {1}" + error_msg).format( @@ -1666,22 +1668,12 @@ def validate_permissions_for_doctype(doctype, for_remove=False, alert=False): def clear_permissions_cache(doctype): + from frappe.cache_manager import clear_user_cache + frappe.clear_cache(doctype=doctype) delete_notification_count_for(doctype) - for user in frappe.db.sql_list( - """ - SELECT - DISTINCT `tabHas Role`.`parent` - FROM - `tabHas Role`, - `tabDocPerm` - WHERE `tabDocPerm`.`parent` = %s - AND `tabDocPerm`.`role` = `tabHas Role`.`role` - AND `tabHas Role`.`parenttype` = 'User' - """, - doctype, - ): - frappe.clear_cache(user=user) + + clear_user_cache() def validate_permissions(doctype, for_remove=False, alert=False): @@ -1891,7 +1883,7 @@ def check_email_append_to(doc): if doc.sender_field and not sender_field: frappe.throw(_("Select a valid Sender Field for creating documents from Email")) - if not sender_field.options == "Email": + if sender_field.options != "Email": frappe.throw(_("Sender Field should have Email in options")) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index c1c7589564..a5657f590a 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -786,6 +786,7 @@ def new_doctype( depends_on: str = "", fields: list[dict] | None = None, custom: bool = True, + default: str | None = None, **kwargs, ): if not name: @@ -803,6 +804,7 @@ def new_doctype( "fieldname": "some_fieldname", "fieldtype": "Data", "unique": unique, + "default": default, "depends_on": depends_on, } ], diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py index 4a1a63329b..d56475f2cd 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.py +++ b/frappe/core/doctype/domain_settings/domain_settings.py @@ -21,7 +21,7 @@ class DomainSettings(Document): active_domains = [d.domain for d in self.active_domains] added = False for d in domains: - if not d in active_domains: + if d not in active_domains: self.append("active_domains", dict(domain=d)) added = True diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.py b/frappe/core/doctype/dynamic_link/dynamic_link.py index 0ff01eb438..faf78cb425 100644 --- a/frappe/core/doctype/dynamic_link/dynamic_link.py +++ b/frappe/core/doctype/dynamic_link/dynamic_link.py @@ -32,7 +32,7 @@ def deduplicate_dynamic_links(doc): links, duplicate = [], False for l in doc.links or []: t = (l.link_doctype, l.link_name) - if not t in links: + if t not in links: links.append(t) else: duplicate = True diff --git a/frappe/core/doctype/error_log/error_log.json b/frappe/core/doctype/error_log/error_log.json index a8bb7a57d0..813fb5f3c2 100644 --- a/frappe/core/doctype/error_log/error_log.json +++ b/frappe/core/doctype/error_log/error_log.json @@ -70,7 +70,7 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2023-08-23 14:20:15.343339", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Core", "name": "Error Log", @@ -89,7 +89,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "method" } \ No newline at end of file diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index 0477d82383..215178d8ec 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -189,7 +189,7 @@ "icon": "fa fa-file", "idx": 1, "links": [], - "modified": "2023-08-02 09:43:51.178012", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Core", "name": "File", @@ -217,7 +217,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "file_name", "track_changes": 1 diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index f1a96bffd0..f6c0b1defa 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -544,7 +544,7 @@ class File(Document): return self._content def get_full_path(self): - """Returns file path from given file name""" + """Return file path using the set file name.""" file_path = self.file_url or self.file_name @@ -705,7 +705,7 @@ class File(Document): return has_permission(self, "read") def get_extension(self): - """returns split filename and extension""" + """Split and return filename and extension for the set `file_name`.""" return os.path.splitext(self.file_name) def create_attachment_record(self): diff --git a/frappe/core/doctype/module_def/module_def.json b/frappe/core/doctype/module_def/module_def.json index 12830c8b4f..e9a03bce39 100644 --- a/frappe/core/doctype/module_def/module_def.json +++ b/frappe/core/doctype/module_def/module_def.json @@ -123,7 +123,7 @@ "link_fieldname": "module" } ], - "modified": "2022-01-03 13:56:52.817954", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Core", "name": "Module Def", @@ -160,7 +160,7 @@ ], "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 28f7b4f11a..6cb407adbb 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -47,7 +47,7 @@ class ModuleDef(Document): if not frappe.local.module_app.get(frappe.scrub(self.name)): with open(frappe.get_app_path(self.app_name, "modules.txt")) as f: content = f.read() - if not self.name in content.splitlines(): + if self.name not in content.splitlines(): modules = list(filter(None, content.splitlines())) modules.append(self.name) diff --git a/frappe/core/doctype/page/page.json b/frappe/core/doctype/page/page.json index b5e9941a6d..83117d802d 100644 --- a/frappe/core/doctype/page/page.json +++ b/frappe/core/doctype/page/page.json @@ -102,7 +102,7 @@ "icon": "fa fa-file", "idx": 1, "links": [], - "modified": "2023-10-22 22:41:25.568952", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Core", "name": "Page", @@ -129,7 +129,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 270ece6fa5..ce72220953 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -118,7 +118,7 @@ class Page(Document): shutil.rmtree(dir_path, ignore_errors=True) def is_permitted(self): - """Returns true if Has Role is not set or the user is allowed.""" + """Return True if `Has Role` is not set or the user is allowed.""" from frappe.utils import has_common allowed = [ diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index f3e9450034..1761b0d574 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -62,6 +62,8 @@ "reqd": 1 }, { + "fetch_from": "ref_doctype.module", + "fetch_if_empty": 1, "fieldname": "module", "fieldtype": "Link", "label": "Module", diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 2d78892f14..5fb0feeca2 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -87,7 +87,8 @@ class Report(Document): if ( self.is_standard == "Yes" and not cint(getattr(frappe.local.conf, "developer_mode", 0)) - and not (frappe.flags.in_migrate or frappe.flags.in_patch) + and not frappe.flags.in_migrate + and not frappe.flags.in_patch ): frappe.throw(_("You are not allowed to delete Standard Report")) delete_custom_role("report", self.name) @@ -104,7 +105,7 @@ class Report(Document): self.set("roles", roles) def is_permitted(self): - """Returns true if Has Role is not set or the user is allowed.""" + """Return True if `Has Role` is not set or the user is allowed.""" from frappe.utils import has_common allowed = [ @@ -183,7 +184,7 @@ class Report(Document): def execute_script(self, filters): # server script loc = {"filters": frappe._dict(filters), "data": None, "result": None} - safe_exec(self.report_script, None, loc) + safe_exec(self.report_script, None, loc, script_filename=f"Report {self.name}") if loc["data"]: return loc["data"] else: diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 2039d3889d..1b147e3ddb 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -148,7 +148,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-05 18:33:27.694065", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Core", "name": "Role", @@ -169,7 +169,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1, "translated_doctype": 1 diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index b3ec48d946..87ff615e0f 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -80,12 +80,23 @@ class Role(Document): if frappe.flags.in_install: return if self.has_value_changed("desk_access"): - for user_name in get_users(self.name): - user = frappe.get_doc("User", user_name) - user_type = user.user_type - user.set_system_user() - if user_type != user.user_type: - user.save() + self.update_user_type_on_change() + + def update_user_type_on_change(self): + """When desk access changes, all the users that have this role need to be re-evaluated""" + + users_with_role = get_users(self.name) + + # perf: Do not re-evaluate users who already have same desk access that this role permits. + role_user_type = "System User" if self.desk_access else "Website User" + users_with_same_user_type = frappe.get_all("User", {"user_type": role_user_type}, pluck="name") + + for user_name in set(users_with_role) - set(users_with_same_user_type): + user = frappe.get_doc("User", user_name) + user_type = user.user_type + user.set_system_user() + if user_type != user.user_type: + user.save() def get_info_based_on_role(role, field="email", ignore_permissions=False): diff --git a/frappe/core/doctype/role_profile/role_profile.py b/frappe/core/doctype/role_profile/role_profile.py index b005a695a4..74c34e3993 100644 --- a/frappe/core/doctype/role_profile/role_profile.py +++ b/frappe/core/doctype/role_profile/role_profile.py @@ -1,6 +1,8 @@ # Copyright (c) 2017, Frappe Technologies and contributors # License: MIT. See LICENSE +from collections import defaultdict + import frappe from frappe.model.document import Document @@ -24,9 +26,24 @@ class RoleProfile(Document): def on_update(self): """Changes in role_profile reflected across all its user""" - users = frappe.get_all("User", filters={"role_profile_name": self.name}) - roles = [role.role for role in self.roles] - for d in users: - user = frappe.get_doc("User", d) - user.set("roles", []) - user.add_roles(*roles) + has_role = frappe.qb.DocType("Has Role") + user = frappe.qb.DocType("User") + + all_current_roles = ( + frappe.qb.from_(user) + .join(has_role) + .on(user.name == has_role.parent) + .where(user.role_profile_name == self.name) + .select(user.name, has_role.role) + ).run() + + user_roles = defaultdict(set) + for user, role in all_current_roles: + user_roles[user].add(role) + + role_profile_roles = {role.role for role in self.roles} + for user, roles in user_roles.items(): + if roles != role_profile_roles: + user = frappe.get_doc("User", user) + user.roles = [] + user.add_roles(*role_profile_roles) diff --git a/frappe/core/doctype/rq_job/rq_job.py b/frappe/core/doctype/rq_job/rq_job.py index 81fa3fdf3e..69027af76f 100644 --- a/frappe/core/doctype/rq_job/rq_job.py +++ b/frappe/core/doctype/rq_job/rq_job.py @@ -152,6 +152,12 @@ def serialize_job(job: Job) -> frappe._dict: if matches := re.match(r".*) at 0x.*>", job_name): job_name = matches.group("func_name") + exc_info = None + + # Get exc_string from the job result if it exists + if job_result := job.latest_result(): + exc_info = job_result.exc_string + return frappe._dict( name=job.id, job_id=job.id, @@ -161,7 +167,7 @@ def serialize_job(job: Job) -> frappe._dict: started_at=convert_utc_to_system_timezone(job.started_at) if job.started_at else "", ended_at=convert_utc_to_system_timezone(job.ended_at) if job.ended_at else "", time_taken=(job.ended_at - job.started_at).total_seconds() if job.ended_at else "", - exc_info=job.exc_info, + exc_info=exc_info, arguments=frappe.as_json(job.kwargs), timeout=job.timeout, creation=convert_utc_to_system_timezone(job.created_at), diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index a58e50dddc..a9e047d9b2 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -128,14 +128,14 @@ class ServerScript(Document): frappe.msgprint(str(e), title=_("Compilation warning")) def execute_method(self) -> dict: - """Specific to API endpoint Server Scripts + """Specific to API endpoint Server Scripts. - Raises: - frappe.DoesNotExistError: If self.script_type is not API - frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user + Raise: + frappe.DoesNotExistError: If self.script_type is not API. + frappe.PermissionError: If self.allow_guest is unset for API accessed by Guest user. - Returns: - dict: Evaluates self.script with frappe.utils.safe_exec.safe_exec and returns the flags set in it's safe globals + Return: + dict: Evaluate self.script with frappe.utils.safe_exec.safe_exec and return the flags set in its safe globals. """ if self.enable_rate_limit: @@ -155,7 +155,12 @@ class ServerScript(Document): Args: doc (Document): Executes script with for a certain document's events """ - safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True) + safe_exec( + self.script, + _locals={"doc": doc}, + restrict_commit_rollback=True, + script_filename=self.name, + ) def execute_scheduled_method(self): """Specific to Scheduled Jobs via Server Scripts @@ -166,30 +171,28 @@ class ServerScript(Document): if self.script_type != "Scheduler Event": raise frappe.DoesNotExistError - safe_exec(self.script) + safe_exec(self.script, script_filename=self.name) def get_permission_query_conditions(self, user: str) -> list[str]: - """Specific to Permission Query Server Scripts + """Specific to Permission Query Server Scripts. Args: - user (str): Takes user email to execute script and return list of conditions + user (str): Take user email to execute script and return list of conditions. - Returns: - list: Returns list of conditions defined by rules in self.script + Return: + list: Return list of conditions defined by rules in self.script. """ locals = {"user": user, "conditions": ""} - safe_exec(self.script, None, locals) + safe_exec(self.script, None, locals, script_filename=self.name) if locals["conditions"]: return locals["conditions"] @frappe.whitelist() def get_autocompletion_items(self): - """Generates a list of a autocompletion strings from the context dict + """Generate a list of autocompletion strings from the context dict that is used while executing a Server Script. - Returns: - list: Returns list of autocompletion items. - For e.g., ["frappe.utils.cint", "frappe.get_all", ...] + e.g., ["frappe.utils.cint", "frappe.get_all", ...] """ def get_keys(obj): @@ -278,7 +281,7 @@ def execute_api_server_script(script=None, *args, **kwargs): raise frappe.PermissionError # output can be stored in flags - _globals, _locals = safe_exec(script.script) + _globals, _locals = safe_exec(script.script, script_filename=script.name) return _globals.frappe.flags diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 6ba65e7353..d0ae253d29 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -23,7 +23,7 @@ EVENT_MAP = { def run_server_script_for_doc_event(doc, event): # run document event method - if not event in EVENT_MAP: + if event not in EVENT_MAP: return if frappe.flags.in_install: diff --git a/frappe/core/doctype/sms_log/README.md b/frappe/core/doctype/sms_log/README.md new file mode 100644 index 0000000000..9ee2b79ef0 --- /dev/null +++ b/frappe/core/doctype/sms_log/README.md @@ -0,0 +1 @@ +Log of SMS sent via SMS Center. \ No newline at end of file diff --git a/frappe/core/doctype/sms_log/__init__.py b/frappe/core/doctype/sms_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/sms_log/sms_log.js b/frappe/core/doctype/sms_log/sms_log.js new file mode 100644 index 0000000000..ce036f234d --- /dev/null +++ b/frappe/core/doctype/sms_log/sms_log.js @@ -0,0 +1,6 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("SMS Log", { + refresh: function (frm) {}, +}); diff --git a/frappe/core/doctype/sms_log/sms_log.json b/frappe/core/doctype/sms_log/sms_log.json new file mode 100644 index 0000000000..1bdcec13ee --- /dev/null +++ b/frappe/core/doctype/sms_log/sms_log.json @@ -0,0 +1,371 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "SYS-SMS-.#####", + "beta": 0, + "creation": "2012-03-27 14:36:47", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "editable_grid": 0, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sender_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sender Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sent_on", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sent On", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0, + "width": "50%" + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "message", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sec_break1", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "options": "Simple", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "no_of_requested_sms", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "No of Requested SMS", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "requested_numbers", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Requested Numbers", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break1", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0, + "width": "50%" + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "no_of_sent_sms", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "No of Sent SMS", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sent_to", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sent To", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-mobile-phone", + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2018-08-21 16:15:40.898889", + "modified_by": "Administrator", + "module": "Core", + "name": "SMS Log", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/sms_log/sms_log.py b/frappe/core/doctype/sms_log/sms_log.py new file mode 100644 index 0000000000..8e4c248fd6 --- /dev/null +++ b/frappe/core/doctype/sms_log/sms_log.py @@ -0,0 +1,26 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +from frappe.model.document import Document + + +class SMSLog(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + message: DF.SmallText | None + no_of_requested_sms: DF.Int + no_of_sent_sms: DF.Int + requested_numbers: DF.Code | None + sender_name: DF.Data | None + sent_on: DF.Date | None + sent_to: DF.Code | None + # end: auto-generated types + + pass diff --git a/frappe/core/doctype/sms_log/test_sms_log.py b/frappe/core/doctype/sms_log/test_sms_log.py new file mode 100644 index 0000000000..3ff0202388 --- /dev/null +++ b/frappe/core/doctype/sms_log/test_sms_log.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest + +# test_records = frappe.get_test_records('SMS Log') + + +class TestSMSLog(unittest.TestCase): + pass diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py index 1a68368ba0..f2609ad05b 100644 --- a/frappe/core/doctype/sms_settings/sms_settings.py +++ b/frappe/core/doctype/sms_settings/sms_settings.py @@ -46,7 +46,7 @@ def validate_receiver_nos(receiver_list): @frappe.whitelist() def get_contact_number(contact_name, ref_doctype, ref_name): - "returns mobile number of the contact" + "Return mobile number of the given contact." number = frappe.db.sql( """select mobile_no, phone from tabContact where name=%s diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js index bf8988d64c..d018d9443e 100644 --- a/frappe/core/doctype/system_settings/system_settings.js +++ b/frappe/core/doctype/system_settings/system_settings.js @@ -32,10 +32,25 @@ frappe.ui.form.on("System Settings", { frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0); } }, - on_update: function (frm) { - if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) { - // Clear cache after saving to refresh the values of boot. - frappe.ui.toolbar.clear_cache(); + after_save: function (frm) { + /** + * Checks whether the effective value has changed. + * + * @param {Array.} - Tuple with new fallback, previous fallback and + * optionally an override value. + * @returns {boolean} - Whether the resulting value has effectively changed + */ + const has_effectively_changed = ([new_fallback, prev_fallback, override = undefined]) => + !override && prev_fallback !== new_fallback; + + const attr_tuples = [ + [frm.doc.language, frappe.boot.sysdefaults.language, frappe.boot.user.language], + [frm.doc.rounding_method, frappe.boot.sysdefaults.rounding_method], // no user override. + ]; + + if (attr_tuples.some(has_effectively_changed)) { + frappe.msgprint(__("Refreshing...")); + window.location.reload(); } }, first_day_of_the_week(frm) { diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 45fa621cec..8f6ee3ca94 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -639,7 +639,7 @@ "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-11-27 14:08:01.927794", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -655,7 +655,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index a12dda661e..24bc610df3 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -114,22 +114,6 @@ frappe.ui.form.on("User", { return; } - const hasChanged = (doc_attr, boot_attr) => { - return doc_attr && boot_attr && doc_attr !== boot_attr; - }; - - if ( - doc.name === frappe.session.user && - !doc.__unsaved && - frappe.all_timezones && - (hasChanged(doc.language, frappe.boot.user.language) || - hasChanged(doc.time_zone, frappe.boot.time_zone.user) || - hasChanged(doc.desk_theme, frappe.boot.user.desk_theme)) - ) { - frappe.msgprint(__("Refreshing...")); - window.location.reload(); - } - frm.toggle_display(["sb1", "sb3", "modules_access"], false); if (!frm.is_new()) { @@ -335,10 +319,31 @@ frappe.ui.form.on("User", { }, }); }, - on_update: function (frm) { - if (frappe.boot.time_zone && frappe.boot.time_zone.user !== frm.doc.time_zone) { - // Clear cache after saving to refresh the values of boot. - frappe.ui.toolbar.clear_cache(); + after_save: function (frm) { + /** + * Checks whether the effective value has changed. + * + * @param {Array.} - Tuple with new override, previous override, + * and optionally fallback. + * @returns {boolean} - Whether the resulting value has effectively changed + */ + const has_effectively_changed = ([new_override, prev_override, fallback = undefined]) => { + const prev_effective = prev_override || fallback; + const new_effective = new_override || fallback; + return new_override !== undefined && prev_effective !== new_effective; + }; + + const doc = frm.doc; + const boot = frappe.boot; + const attr_tuples = [ + [doc.language, boot.user.language, boot.sysdefaults.language], + [doc.time_zone, boot.time_zone.user, boot.time_zone.system], + [doc.desk_theme, boot.user.desk_theme], // No system default. + ]; + + if (doc.name === frappe.session.user && attr_tuples.some(has_effectively_changed)) { + frappe.msgprint(__("Refreshing...")); + window.location.reload(); } }, }); diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 028af756df..1a13a20e4e 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -230,7 +230,7 @@ class User(Document): frappe.cache.delete_key("users_for_mentions") def has_website_permission(self, ptype, user, verbose=False): - """Returns true if current user is the session user""" + """Return True if current user is the session user.""" return self.name == frappe.session.user def set_full_name(self): @@ -686,7 +686,7 @@ class User(Document): ) def get_blocked_modules(self): - """Returns list of modules blocked for that user""" + """Return list of modules blocked for that user.""" return [d.module for d in self.block_modules] if self.block_modules else [] def validate_user_email_inbox(self): @@ -1083,7 +1083,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters): def get_total_users(): - """Returns total no. of system users""" + """Return total number of system users.""" return flt( frappe.db.sql( """SELECT SUM(`simultaneous_sessions`) @@ -1118,7 +1118,7 @@ def get_system_users(exclude_users: Iterable[str] | str | None = None, limit: in def get_active_users(): - """Returns No. of system users who logged in, in the last 3 days""" + """Return number of system users who logged in, in the last 3 days.""" return frappe.db.sql( """select count(*) from `tabUser` where enabled = 1 and user_type != 'Website User' @@ -1131,12 +1131,12 @@ def get_active_users(): def get_website_users(): - """Returns total no. of website users""" + """Return total number of website users.""" return frappe.db.count("User", filters={"enabled": True, "user_type": "Website User"}) def get_active_website_users(): - """Returns No. of website users who logged in, in the last 3 days""" + """Return number of website users who logged in, in the last 3 days.""" return frappe.db.sql( """select count(*) from `tabUser` where enabled = 1 and user_type = 'Website User' diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index ea00b604c1..ae43eb2d9d 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -173,7 +173,7 @@ def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, def get_permitted_documents(doctype): - """Returns permitted documents from the given doctype for the session user""" + """Return permitted documents from the given doctype for the session user.""" # sort permissions in a way to make the first permission in the list to be default user_perm_list = sorted( get_user_permissions().get(doctype, []), key=lambda x: x.get("is_default"), reverse=True diff --git a/frappe/core/doctype/version/version.json b/frappe/core/doctype/version/version.json index 13c82fa2b2..570b53623c 100644 --- a/frappe/core/doctype/version/version.json +++ b/frappe/core/doctype/version/version.json @@ -54,7 +54,7 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2022-08-03 12:20:53.929691", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Core", "name": "Version", @@ -74,7 +74,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "docname", "track_changes": 1 diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 26e920bca9..928874912f 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -17,7 +17,7 @@ def get_notification_config(): def get_things_todo(as_list=False): - """Returns a count of incomplete todos""" + """Return a count of incomplete ToDos.""" data = frappe.get_list( "ToDo", fields=["name", "description"] if as_list else "count(*)", @@ -35,7 +35,7 @@ def get_things_todo(as_list=False): def get_todays_events(as_list: bool = False): - """Returns a count of todays events in calendar""" + """Return a count of today's events in calendar.""" from frappe.desk.doctype.event.event import get_events from frappe.utils import nowdate diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 1368ced6eb..71d6a4a002 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -109,8 +109,10 @@ def add(parent, role, permlevel): @frappe.whitelist() -def update(doctype, role, permlevel, ptype, value=None, if_owner=0): - """Update role permission params +def update( + doctype: str, role: str, permlevel: int, ptype: str, value=None, if_owner=0 +) -> str | None: + """Update role permission params. Args: doctype (str): Name of the DocType to update params for @@ -119,8 +121,8 @@ def update(doctype, role, permlevel, ptype, value=None, if_owner=0): ptype (str): permission type, example "read", "delete", etc. value (None, optional): value for ptype, None indicates False - Returns: - str: Refresh flag is permission is updated successfully + Return: + str: Refresh flag if permission is updated successfully """ def clear_cache(): diff --git a/frappe/core/utils.py b/frappe/core/utils.py index 5f388f5458..3e7fb8f350 100644 --- a/frappe/core/utils.py +++ b/frappe/core/utils.py @@ -7,7 +7,7 @@ import frappe def get_parent_doc(doc): - """Returns document of `reference_doctype`, `reference_doctype`""" + """Return document of `reference_doctype`, `reference_doctype`.""" if not hasattr(doc, "parent_doc"): if doc.reference_doctype and doc.reference_name: doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name) @@ -38,8 +38,7 @@ def set_timeline_doc(doc): def find(list_of_dict, match_function): - """Returns a dict in a list of dicts on matching the conditions - provided in match function + """Return a dict in a list of dicts on matching the conditions provided in match function. Usage: list_of_dict = [{'name': 'Suraj'}, {'name': 'Aditya'}] @@ -54,8 +53,7 @@ def find(list_of_dict, match_function): def find_all(list_of_dict, match_function): - """Returns all matching dicts in a list of dicts. - Uses matching function to filter out the dicts + """Return all matching dicts in a list of dicts. Uses matching function to filter out the dicts. Usage: colored_shapes = [ @@ -86,6 +84,7 @@ def ljust_list(_list, length, fill_word=None): return _list -def html2text(html, strip_links=False, wrap=True): +def html2text(html: str, strip_links=False, wrap=True) -> str: + """Return the given `html` as markdown text.""" strip = ["a"] if strip_links else None return md(html, heading_style="ATX", strip=strip, wrap=wrap) diff --git a/frappe/custom/doctype/client_script/client_script.json b/frappe/custom/doctype/client_script/client_script.json index 1db4dfe160..dddf0c0a04 100644 --- a/frappe/custom/doctype/client_script/client_script.json +++ b/frappe/custom/doctype/client_script/client_script.json @@ -77,7 +77,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-04-12 12:48:15.717985", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Custom", "name": "Client Script", @@ -108,7 +108,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 332969b036..3f51636d9b 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -457,7 +457,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-25 06:55:10.713382", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", @@ -488,7 +488,7 @@ ], "search_fields": "dt,label,fieldtype,options", "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index c77d2f4bb2..e84e0dd712 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -357,7 +357,7 @@ def rename_fieldname(custom_field: str, fieldname: str): if field.is_system_generated: frappe.throw(_("System Generated Fields can not be renamed")) if frappe.db.has_column(parent_doctype, fieldname): - frappe.throw(_("Can not rename as fieldname {0} is already present on DocType.")) + frappe.throw(_("Can not rename as column {0} is already present on DocType.").format(fieldname)) if old_fieldname == new_fieldname: frappe.msgprint(_("Old and new fieldnames are same."), alert=True) return diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index aad7a59b37..273cab894a 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -46,6 +46,7 @@ "column_break_26", "email_append_to", "sender_field", + "sender_name_field", "subject_field", "section_break_8", "sort_field", @@ -219,7 +220,7 @@ "depends_on": "email_append_to", "fieldname": "sender_field", "fieldtype": "Data", - "label": "Sender Field", + "label": "Sender Email Field", "mandatory_depends_on": "email_append_to" }, { @@ -392,6 +393,12 @@ "fieldname": "details_tab", "fieldtype": "Tab Break", "label": "Details" + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_name_field", + "fieldtype": "Data", + "label": "Sender Name Field" } ], "hide_toolbar": 1, @@ -400,7 +407,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-11-16 11:23:06.427432", + "modified": "2023-12-01 18:18:23.086134", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 34933978a6..5cf05978e1 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -72,6 +72,7 @@ class CustomizeForm(Document): quick_entry: DF.Check search_fields: DF.Data | None sender_field: DF.Data | None + sender_name_field: DF.Data | None show_preview_popup: DF.Check show_title_field_in_link: DF.Check sort_field: DF.Literal @@ -83,6 +84,7 @@ class CustomizeForm(Document): track_views: DF.Check translated_doctype: DF.Check # end: auto-generated types + def on_update(self): frappe.db.delete("Singles", {"doctype": "Customize Form"}) frappe.db.delete("Customize Form Field") diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 8a62d331be..7354c55efa 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -240,8 +240,9 @@ class TestCustomizeForm(FrappeTestCase): # Using Notification Log doctype as it doesn't have any other custom fields d = self.get_customize_form("Notification Log") + new_document_length = 255 document_name = d.get("fields", {"fieldname": "document_name"})[0] - document_name.length = 255 + document_name.length = new_document_length d.run_method("save_customization") self.assertEqual( @@ -250,11 +251,9 @@ class TestCustomizeForm(FrappeTestCase): {"doc_type": "Notification Log", "property": "length", "field_name": "document_name"}, "value", ), - "255", + str(new_document_length), ) - self.assertTrue(d.flags.update_db) - length = frappe.db.sql( """SELECT character_maximum_length FROM information_schema.columns @@ -262,7 +261,7 @@ class TestCustomizeForm(FrappeTestCase): AND column_name = 'document_name'""" )[0][0] - self.assertEqual(length, 255) + self.assertEqual(length, new_document_length) def test_custom_link(self): try: diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index a3aec328bd..67c6c8ba95 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -483,7 +483,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-07 13:17:21.373626", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", @@ -491,6 +491,6 @@ "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [] } \ No newline at end of file diff --git a/frappe/database/database.py b/frappe/database/database.py index d04135e827..ba25b41841 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -123,7 +123,7 @@ class Database: self._conn.select_db(db_name) def get_connection(self): - """Returns a Database connection object that conforms with https://peps.python.org/pep-0249/#connection-objects""" + """Return a Database connection object that conforms with https://peps.python.org/pep-0249/#connection-objects.""" raise NotImplementedError def get_database_size(self): @@ -160,7 +160,7 @@ class Database: :param ignore_ddl: Catch exception if table, column missing. :param auto_commit: Commit after executing the query. :param update: Update this dict to all rows (if returned `as_dict`). - :param run: Returns query without executing it if False. + :param run: Return query without executing it if False. :param pluck: Get the plucked field only. :param explain: Print `EXPLAIN` in error log. Examples: @@ -397,7 +397,7 @@ class Database: raise ImplicitCommitError("This statement can cause implicit commit", query) def fetch_as_dict(self) -> list[frappe._dict]: - """Internal. Converts results to dict.""" + """Internal. Convert results to dict.""" result = self.last_result if result: keys = [column[0] for column in self._cursor.description] @@ -410,7 +410,7 @@ class Database: frappe.cache.delete_key("db_tables") def get_description(self): - """Returns result metadata.""" + """Return result metadata.""" return self._cursor.description @staticmethod @@ -419,7 +419,7 @@ class Database: return [[value for value in row] for row in res] def get(self, doctype, filters=None, as_dict=True, cache=False): - """Returns `get_value` with fieldname='*'""" + """Return `get_value` with fieldname='*'.""" return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache) def get_value( @@ -438,7 +438,7 @@ class Database: pluck=False, distinct=False, ): - """Returns a document property or list of properties. + """Return a document property or list of properties. :param doctype: DocType name. :param filters: Filters like `{"x":"y"}` or name of the document. `None` if Single DocType. @@ -510,7 +510,7 @@ class Database: distinct=False, limit=None, ): - """Returns multiple document properties. + """Return multiple document properties. :param doctype: DocType name. :param filters: Filters like `{"x":"y"}` or name of the document. @@ -926,11 +926,11 @@ class Database: self.set_default(key, val, user) def get_global(self, key, user="__global"): - """Returns a global key value.""" + """Return a global key value.""" return self.get_default(key, user) def get_default(self, key, parent="__default"): - """Returns default value as a list if multiple or single""" + """Return default value as a list if multiple or single.""" d = self.get_defaults(key, parent) return isinstance(d, list) and d[0] or d @@ -1006,7 +1006,7 @@ class Database: return self.exists("DocField", {"fieldname": fn, "parent": dt}) def table_exists(self, doctype, cached=True): - """Returns True if table for given doctype exists.""" + """Return True if table for given doctype exists.""" return f"tab{doctype}" in self.get_tables(cached=cached) def has_table(self, doctype): @@ -1016,7 +1016,7 @@ class Database: raise NotImplementedError def a_row_exists(self, doctype): - """Returns True if atleast one row exists.""" + """Return True if at least one row exists.""" return frappe.get_all(doctype, limit=1, order_by=None, as_list=True) def exists(self, dt, dn=None, cache=False): @@ -1055,7 +1055,7 @@ class Database: return self.get_value(dt, dn, ignore=True, cache=cache, order_by=None) def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True): - """Returns `COUNT(*)` for given DocType and filters.""" + """Return `COUNT(*)` for given DocType and filters.""" if cache and not filters: cache_count = frappe.cache.get_value(f"doctype:count:{dt}") if cache_count is not None: @@ -1098,7 +1098,7 @@ class Database: ) def get_db_table_columns(self, table) -> list[str]: - """Returns list of column names from given table.""" + """Return list of column names from given table.""" columns = frappe.cache.hget("table_columns", table) if columns is None: information_schema = frappe.qb.Schema("information_schema") @@ -1116,14 +1116,14 @@ class Database: return columns def get_table_columns(self, doctype): - """Returns list of column names from given doctype.""" + """Return list of column names from given doctype.""" columns = self.get_db_table_columns("tab" + doctype) if not columns: raise self.TableMissingError("DocType", doctype) return columns def has_column(self, doctype, column): - """Returns True if column exists in database.""" + """Return True if column exists in database.""" return column in self.get_table_columns(doctype) def has_index(self, table_name, index_name): diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 68cd39f2f5..01c18d69c4 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -57,14 +57,15 @@ class DbManager: from frappe.database import get_command from frappe.utils import execute_in_shell - pv = which("pv") - command = [] - if pv: - command.extend([pv, source, "|"]) - source = [] - print("Restoring Database file...") + if source.endswith(".gz"): + if gzip := which("gzip"): + command.extend([gzip, "-cd", source, "|"]) + source = [] + else: + raise Exception("`gzip` not installed") + else: source = ["<", source] diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 1f087a243a..00cbd1c332 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -191,7 +191,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): } def get_database_size(self): - """'Returns database size in MB""" + """Return database size in MB.""" db_size = self.sql( """ SELECT `table_schema` as `database_name`, @@ -281,7 +281,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): ) def create_global_search_table(self): - if not "__global_search" in self.get_tables(): + if "__global_search" not in self.get_tables(): self.sql( """create table __global_search( doctype varchar(100), @@ -314,7 +314,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): return "ON DUPLICATE key UPDATE " def get_table_columns_description(self, table_name): - """Returns list of column and its description""" + """Return list of columns with descriptions.""" return self.sql( """select column_name as 'name', @@ -339,7 +339,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): ) def get_column_type(self, doctype, column): - """Returns column type from database.""" + """Return column type from database.""" information_schema = frappe.qb.Schema("information_schema") table = get_table_name(doctype) @@ -440,13 +440,13 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): self.commit() db_table.sync() - self.begin() + self.commit() def get_database_list(self): return self.sql("SHOW DATABASES", pluck=True) def get_tables(self, cached=True): - """Returns list of tables""" + """Return list of tables.""" to_query = not cached if cached: diff --git a/frappe/database/operator_map.py b/frappe/database/operator_map.py index d98f46d758..72ed8b4a75 100644 --- a/frappe/database/operator_map.py +++ b/frappe/database/operator_map.py @@ -17,21 +17,21 @@ def like(key: Field, value: str) -> frappe.qb: key (str): field value (str): criterion - Returns: - frappe.qb: `frappe.qb object with `LIKE` + Return: + frappe.qb: `frappe.qb` object with `LIKE` """ return key.like(value) def func_in(key: Field, value: list | tuple) -> frappe.qb: - """Wrapper method for `IN` + """Wrapper method for `IN`. Args: key (str): field value (Union[int, str]): criterion - Returns: - frappe.qb: `frappe.qb object with `IN` + Return: + frappe.qb: `frappe.qb` object with `IN` """ if isinstance(value, str): value = value.split(",") @@ -39,27 +39,27 @@ def func_in(key: Field, value: list | tuple) -> frappe.qb: def not_like(key: Field, value: str) -> frappe.qb: - """Wrapper method for `NOT LIKE` + """Wrapper method for `NOT LIKE`. Args: key (str): field value (str): criterion - Returns: - frappe.qb: `frappe.qb object with `NOT LIKE` + Return: + frappe.qb: `frappe.qb` object with `NOT LIKE` """ return key.not_like(value) def func_not_in(key: Field, value: list | tuple | str): - """Wrapper method for `NOT IN` + """Wrapper method for `NOT IN`. Args: key (str): field value (Union[int, str]): criterion - Returns: - frappe.qb: `frappe.qb object with `NOT IN` + Return: + frappe.qb: `frappe.qb` object with `NOT IN` """ if isinstance(value, str): value = value.split(",") @@ -73,21 +73,21 @@ def func_regex(key: Field, value: str) -> frappe.qb: key (str): field value (str): criterion - Returns: - frappe.qb: `frappe.qb object with `REGEX` + Return: + frappe.qb: `frappe.qb` object with `REGEX` """ return key.regex(value) def func_between(key: Field, value: list | tuple) -> frappe.qb: - """Wrapper method for `BETWEEN` + """Wrapper method for `BETWEEN`. Args: key (str): field value (Union[int, str]): criterion - Returns: - frappe.qb: `frappe.qb object with `BETWEEN` + Return: + frappe.qb: `frappe.qb` object with `BETWEEN` """ return key[slice(*value)] @@ -98,14 +98,14 @@ def func_is(key, value): def func_timespan(key: Field, value: str) -> frappe.qb: - """Wrapper method for `TIMESPAN` + """Wrapper method for `TIMESPAN`. Args: key (str): field value (str): criterion - Returns: - frappe.qb: `frappe.qb object with `TIMESPAN` + Return: + frappe.qb: `frappe.qb` object with `TIMESPAN` """ return func_between(key, get_timespan_date_range(value)) diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 37fc9601f2..48dd55381a 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -197,7 +197,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database): return str(psycopg2.extensions.QuotedString(s)) def get_database_size(self): - """'Returns database size in MB""" + """Return database size in MB""" db_size = self.sql( "SELECT (pg_database_size(%s) / 1024 / 1024) as database_size", self.db_name, as_dict=True ) @@ -288,7 +288,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database): ) def create_global_search_table(self): - if not "__global_search" in self.get_tables(): + if "__global_search" not in self.get_tables(): self.sql( """create table "__global_search"( doctype varchar(100), @@ -329,7 +329,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database): self.commit() db_table.sync() - self.begin() + self.commit() @staticmethod def get_on_duplicate_update(key="name"): @@ -380,7 +380,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database): ) def get_table_columns_description(self, table_name): - """Returns list of column and its description""" + """Return list of columns with description.""" # pylint: disable=W1401 return self.sql( """ @@ -411,7 +411,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database): ) def get_column_type(self, doctype, column): - """Returns column type from database.""" + """Return column type from database.""" information_schema = frappe.qb.Schema("information_schema") table = get_table_name(doctype) diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 8de3e532b9..f5f3b14006 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -1,7 +1,7 @@ import os import frappe -from frappe import _ +from frappe.database.db_manager import DbManager def setup_database(): @@ -36,45 +36,16 @@ def bootstrap_database(db_name, verbose, source_sql=None): def import_db_from_sql(source_sql=None, verbose=False): - import shlex - from shutil import which - - from frappe.database import get_command - from frappe.utils import execute_in_shell - - # bootstrap db + if verbose: + print("Starting database import...") + db_name = frappe.conf.db_name if not source_sql: source_sql = os.path.join(os.path.dirname(__file__), "framework_postgres.sql") - - pv = which("pv") - - command = [] - - if pv: - command.extend([pv, source_sql, "|"]) - source = [] - print("Restoring Database file...") - else: - source = ["-f", source_sql] - - bin, args, bin_name = get_command( - host=frappe.conf.db_host, - port=frappe.conf.db_port, - user=frappe.conf.db_name, - password=frappe.conf.db_password, - db_name=frappe.conf.db_name, + DbManager(frappe.local.db).restore_database( + verbose, db_name, source_sql, db_name, frappe.conf.db_password ) - - if not bin: - frappe.throw( - _("{} not found in PATH! This is required to restore the database.").format(bin_name), - exc=frappe.ExecutableNotFound, - ) - command.append(bin) - command.append(shlex.join(args)) - command.extend(source) - execute_in_shell(" ".join(command), check_exit_code=True, verbose=verbose) - frappe.cache.delete_keys("") # Delete all keys associated with this site. + if verbose: + print("Imported from database %s" % source_sql) def get_root_connection(root_login=None, root_password=None): diff --git a/frappe/database/query.py b/frappe/database/query.py index 06295d33a6..a2cb2486f4 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -218,7 +218,7 @@ class Engine: self.query = self.query.where(operator_fn(_field, _value)) def get_function_object(self, field: str) -> "Function": - """Expects field to look like 'SUM(*)' or 'name' or something similar. Returns PyPika Function object""" + """Return PyPika Function object. Expect field to look like 'SUM(*)' or 'name' or something similar.""" func = field.split("(", maxsplit=1)[0].capitalize() args_start, args_end = len(func) + 1, field.index(")") args = field[args_start:args_end].split(",") diff --git a/frappe/defaults.py b/frappe/defaults.py index 65b145f338..d8ba0dc93b 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -79,7 +79,7 @@ def is_a_user_permission_key(key): def not_in_user_permission(key, value, user=None): - # returns true or false based on if value exist in user permission + # return true or false based on if value exist in user permission user = user or frappe.session.user user_permission = get_user_permissions(user).get(frappe.unscrub(key)) or [] diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py index d8c058536d..478c53c395 100644 --- a/frappe/desk/calendar.py +++ b/frappe/desk/calendar.py @@ -19,7 +19,7 @@ def update_event(args, field_map): def get_event_conditions(doctype, filters=None): - """Returns SQL conditions with user permissions and filters for event queries""" + """Return SQL conditions with user permissions and filters for event queries.""" from frappe.desk.reportview import get_filters_cond if not frappe.has_permission(doctype): diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 8ae20f7bb0..a7c9a5ef0c 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -68,7 +68,7 @@ class Workspace: ) def is_permitted(self): - """Returns true if Has Role is not set or the user is allowed.""" + """Return true if `Has Role` is not set or the user is allowed.""" from frappe.utils import has_common allowed = [d.role for d in self.doc.roles] @@ -383,13 +383,12 @@ class Workspace: @frappe.whitelist() @frappe.read_only() def get_desktop_page(page): - """Applies permissions, customizations and returns the configruration for a page - on desk. + """Apply permissions, customizations and return the configuration for a page on desk. Args: page (json): page data - Returns: + Return: dict: dictionary of cards, charts and shortcuts to be displayed on website """ try: @@ -503,7 +502,7 @@ def get_custom_doctype_list(module): def get_custom_report_list(module): - """Returns list on new style reports for modules.""" + """Return list on new style reports for modules.""" reports = frappe.get_all( "Report", fields=["name", "ref_doctype", "report_type"], @@ -617,14 +616,14 @@ def new_widget(config, doctype, parentfield): def prepare_widget(config, doctype, parentfield): - """Create widget child table entries with parent details + """Create widget child table entries with parent details. Args: config (dict): Dictionary containing widget config doctype (string): Doctype name of the child table parentfield (string): Parent field for the child table - Returns: + Return: TYPE: List of Document objects """ if not config: diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index 27ffb4ffb8..a0f5a45326 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -6,6 +6,7 @@ from frappe import _ from frappe.core.doctype.submission_queue.submission_queue import queue_submission from frappe.model.document import Document from frappe.utils import cint +from frappe.utils.deprecations import deprecated from frappe.utils.scheduler import is_scheduler_inactive @@ -24,6 +25,7 @@ class BulkUpdate(Document): limit: DF.Int update_value: DF.SmallText # end: auto-generated types + @frappe.whitelist() def bulk_update(self): self.check_permission("write") @@ -45,12 +47,12 @@ class BulkUpdate(Document): @frappe.whitelist() -def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): +def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None, task_id=None): if isinstance(docnames, str): docnames = frappe.parse_json(docnames) if len(docnames) < 20: - return _bulk_action(doctype, docnames, action, data) + return _bulk_action(doctype, docnames, action, data, task_id) elif len(docnames) <= 500: frappe.msgprint(_("Bulk operation is enqueued in background."), alert=True) frappe.enqueue( @@ -59,6 +61,7 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): docnames=docnames, action=action, data=data, + task_id=task_id, queue="short", timeout=1000, ) @@ -68,14 +71,15 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): ) -def _bulk_action(doctype, docnames, action, data): +def _bulk_action(doctype, docnames, action, data, task_id=None): if data: data = frappe.parse_json(data) failed = [] + num_documents = len(docnames) - for i, d in enumerate(docnames, 1): - doc = frappe.get_doc(doctype, d) + for idx, docname in enumerate(docnames, 1): + doc = frappe.get_doc(doctype, docname) try: message = "" if action == "submit" and doc.docstatus.is_draft(): @@ -93,17 +97,23 @@ def _bulk_action(doctype, docnames, action, data): doc.save() message = _("Updating {0}").format(doctype) else: - failed.append(d) + failed.append(docname) frappe.db.commit() - show_progress(docnames, message, i, d) + frappe.publish_progress( + percent=idx / num_documents * 100, + title=message, + description=docname, + task_id=task_id, + ) except Exception: - failed.append(d) + failed.append(docname) frappe.db.rollback() return failed +@deprecated def show_progress(docnames, message, i, description): n = len(docnames) frappe.publish_progress(float(i) * 100 / n, title=message, description=description) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 059624d28f..2385860115 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -317,7 +317,7 @@ def get_result(data, timegrain, from_date, to_date, chart_type): d[1] += data[data_index][1] count += data[data_index][2] data_index += 1 - if chart_type == "Average" and not count == 0: + if chart_type == "Average" and count != 0: d[1] = d[1] / count if chart_type == "Count": d[1] = count diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py index 387c73f954..b88745a757 100644 --- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py @@ -51,7 +51,7 @@ def save_chart_config(reset, config, chart_name): chart_config[chart_name] = {} else: config = frappe.parse_json(config) - if not chart_name in chart_config: + if chart_name not in chart_config: chart_config[chart_name] = {} chart_config[chart_name].update(config) diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py index 4c1bee8d20..70ee6db623 100644 --- a/frappe/desk/doctype/list_view_settings/list_view_settings.py +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py @@ -96,7 +96,7 @@ def get_default_listview_fields(doctype): fields = [f.get("fieldname") for f in doctype_json.get("fields") if f.get("in_list_view")] if meta.title_field: - if not meta.title_field.strip() in fields: + if meta.title_field.strip() not in fields: fields.append(meta.title_field.strip()) return fields diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json index 16b70171f5..4d60393d3f 100644 --- a/frappe/desk/doctype/note/note.json +++ b/frappe/desk/doctype/note/note.json @@ -86,7 +86,7 @@ "icon": "fa fa-file-text", "idx": 1, "links": [], - "modified": "2023-08-28 20:23:59.424943", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Desk", "name": "Note", @@ -141,7 +141,7 @@ ], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "title", "track_changes": 1 diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js index ba72369273..35c94025a3 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.js +++ b/frappe/desk/doctype/notification_settings/notification_settings.js @@ -3,11 +3,6 @@ frappe.ui.form.on("Notification Settings", { onload: (frm) => { - frappe.breadcrumbs.add({ - label: __("Settings"), - route: "#modules/Settings", - type: "Custom", - }); frm.set_query("subscribed_documents", () => { return { filters: { diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index 1a6efd5a0d..b4ea0fd1b9 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -12,6 +12,7 @@ "enable_email_notifications", "enable_email_mention", "enable_email_assignment", + "enable_email_threads_on_assigned_document", "enable_email_energy_point", "enable_email_share", "enable_email_event_reminders", @@ -105,12 +106,20 @@ "fieldname": "enable_email_event_reminders", "fieldtype": "Check", "label": "Event Reminders" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "description": "Get notified when an email is received on any of the documents assigned to you.", + "fieldname": "enable_email_threads_on_assigned_document", + "fieldtype": "Check", + "label": "Email Threads on Assigned Document" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-24 14:45:31.931154", + "modified": "2023-12-01 12:46:15.490640", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", @@ -132,5 +141,6 @@ "read_only": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 431c94f53d..dcdf430c4e 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -23,6 +23,7 @@ class NotificationSettings(Document): enable_email_mention: DF.Check enable_email_notifications: DF.Check enable_email_share: DF.Check + enable_email_threads_on_assigned_document: DF.Check enabled: DF.Check energy_points_system_notifications: DF.Check seen: DF.Check diff --git a/frappe/desk/doctype/route_history/route_history.json b/frappe/desk/doctype/route_history/route_history.json index a5d73fc360..0b96277431 100644 --- a/frappe/desk/doctype/route_history/route_history.json +++ b/frappe/desk/doctype/route_history/route_history.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_copy": 1, "creation": "2018-10-05 11:26:04.601113", "doctype": "DocType", "editable_grid": 1, @@ -13,7 +14,9 @@ "fieldname": "route", "fieldtype": "Data", "in_list_view": 1, - "label": "Route" + "label": "Route", + "no_copy": 1, + "read_only": 1 }, { "fieldname": "user", @@ -21,30 +24,29 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "User", - "options": "User" + "no_copy": 1, + "options": "User", + "read_only": 1 } ], + "in_create": 1, "links": [], - "modified": "2022-06-13 05:48:56.967244", + "modified": "2023-12-04 04:41:32.448331", "modified_by": "Administrator", "module": "Desk", "name": "Route History", "owner": "Administrator", "permissions": [ { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "share": 1, - "write": 1 + "share": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py index 9aba975c3a..5c0c37d4a7 100644 --- a/frappe/desk/doctype/route_history/route_history.py +++ b/frappe/desk/doctype/route_history/route_history.py @@ -18,6 +18,7 @@ class RouteHistory(Document): route: DF.Data | None user: DF.Link | None # end: auto-generated types + @staticmethod def clear_old_logs(days=30): from frappe.query_builder import Interval diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py index 4969f5a04a..f34f750f9c 100644 --- a/frappe/desk/doctype/system_console/system_console.py +++ b/frappe/desk/doctype/system_console/system_console.py @@ -28,7 +28,7 @@ class SystemConsole(Document): try: frappe.local.debug_log = [] if self.type == "Python": - safe_exec(self.console) + safe_exec(self.console, script_filename="System Console") self.output = "\n".join(frappe.debug_log) elif self.type == "SQL": self.output = frappe.as_json(read_sql(self.console, as_dict=1)) diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index 8ee18fa74b..f71afef6da 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -77,33 +77,33 @@ class DocTags: self.dt = dt def get_tag_fields(self): - """returns tag_fields property""" + """Return `tag_fields` property.""" return frappe.db.get_value("DocType", self.dt, "tag_fields") def get_tags(self, dn): - """returns tag for a particular item""" + """Return tag for a particular item.""" return (frappe.db.get_value(self.dt, dn, "_user_tags", ignore=1) or "").strip() def add(self, dn, tag): - """add a new user tag""" + """Add a new user tag.""" tl = self.get_tags(dn).split(",") - if not tag in tl: + if tag not in tl: tl.append(tag) if not frappe.db.exists("Tag", tag): frappe.get_doc({"doctype": "Tag", "name": tag}).insert(ignore_permissions=True) self.update(dn, tl) def remove(self, dn, tag): - """remove a user tag""" + """Remove a user tag.""" tl = self.get_tags(dn).split(",") self.update(dn, filter(lambda x: x.lower() != tag.lower(), tl)) def remove_all(self, dn): - """remove all user tags (call before delete)""" + """Remove all user tags (call before delete).""" self.update(dn, []) def update(self, dn, tl): - """updates the _user_tag column in the table""" + """Update the `_user_tag` column in the table.""" if not tl: tags = "" @@ -128,16 +128,15 @@ class DocTags: raise def setup(self): - """adds the _user_tags column if not exists""" + """Add the `_user_tags` column if not exists.""" from frappe.database.schema import add_column add_column(self.dt, "_user_tags", "Data") def delete_tags_for_document(doc): - """ - Delete the Tag Link entry of a document that has - been deleted + """Delete the Tag Link entry of a document that has been deleted. + :param doc: Deleted document """ if not frappe.db.table_exists("Tag Link"): @@ -147,7 +146,7 @@ def delete_tags_for_document(doc): def update_tags(doc, tags): - """Adds tags for documents + """Add tags for documents. :param doc: Document to be added to global tags """ @@ -181,8 +180,8 @@ def update_tags(doc, tags): @frappe.whitelist() def get_documents_for_tag(tag): - """ - Search for given text in Tag Link + """Search for given text in Tag Link. + :param tag: tag to be searched """ # remove hastag `#` from tag diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 858ec2c5b8..d6427f9388 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -136,7 +136,7 @@ class ToDo(Document): @classmethod def get_owners(cls, filters=None): - """Returns list of owners after applying filters on todo's.""" + """Return list of owners after applying filters on ToDos.""" rows = frappe.get_all(cls.DocType, filters=filters or {}, fields=["allocated_to"]) return [parse_addr(row.allocated_to)[1] for row in rows if row.allocated_to] diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index cbe6dd7acc..45bf42fd8f 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -131,7 +131,7 @@ class SubmittableDocumentTree: return self._references_across_doctypes.get(doctype, []) def get_document_sources(self): - """Returns list of doctypes from where we access submittable documents.""" + """Return list of doctypes from where we access submittable documents.""" return list(set(self.get_link_sources() + [self.root_doctype])) def get_link_sources(self): @@ -139,7 +139,7 @@ class SubmittableDocumentTree: return list(set(self.get_submittable_doctypes()) - set(get_exempted_doctypes() or [])) def get_submittable_doctypes(self) -> list[str]: - """Returns list of submittable doctypes.""" + """Return list of submittable doctypes.""" if not self._submittable_doctypes: self._submittable_doctypes = frappe.get_all( "DocType", {"is_submittable": 1}, pluck="name", order_by=None @@ -148,7 +148,7 @@ class SubmittableDocumentTree: def get_child_tables_of_doctypes(doctypes: list[str] = None): - """Returns child tables by doctype.""" + """Return child tables by doctype.""" filters = [["fieldtype", "=", "Table"]] filters_for_docfield = filters filters_for_customfield = filters @@ -387,7 +387,7 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None): docinfo (dict): The document to check for submitted and non-exempt from auto-cancel ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling. - Returns: + Return: bool: True if linked document passes all validations, else False """ # ignore doctype to cancel diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 77767f589c..cc515c0ff1 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -274,7 +274,7 @@ def _get_communications(doctype, name, start=0, limit=20): def get_communication_data( doctype, name, start=0, limit=20, after=None, fields=None, group_by=None, as_dict=True ): - """Returns list of communications for a given document""" + """Return list of communications for a given document.""" if not fields: fields = """ C.name, C.communication_type, C.communication_medium, @@ -437,7 +437,7 @@ def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None): doctype = field.options if field.fieldtype == "Link" else doc.get(field.options) meta = frappe.get_meta(doctype) - if not meta or not (meta.title_field and meta.show_title_field_in_link): + if not meta or not meta.title_field or not meta.show_title_field_in_link: continue link_title = frappe.db.get_value( diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py index ff41019aa1..91aa2084cc 100644 --- a/frappe/desk/leaderboard.py +++ b/frappe/desk/leaderboard.py @@ -15,18 +15,17 @@ def get_leaderboards(): @frappe.whitelist() def get_energy_point_leaderboard(date_range, company=None, field=None, limit=None): - all_users = frappe.get_all( + users = frappe.get_list( "User", filters={ "name": ["not in", ["Administrator", "Guest"]], "enabled": 1, "user_type": ["!=", "Website User"], }, - order_by="name ASC", + pluck="name", ) - all_users_list = list(map(lambda x: x["name"], all_users)) - filters = [["type", "!=", "Review"], ["user", "in", all_users_list]] + filters = [["type", "!=", "Review"], ["user", "in", users]] if date_range: date_range = frappe.parse_json(date_range) filters.append(["creation", "between", [date_range[0], date_range[1]]]) @@ -39,7 +38,7 @@ def get_energy_point_leaderboard(date_range, company=None, field=None, limit=Non ) energy_point_users_list = list(map(lambda x: x["name"], energy_point_users)) - for user in all_users_list: + for user in users: if user not in energy_point_users_list: energy_point_users.append({"name": user, "value": 0}) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index c9f7929b28..75865f6ae4 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -349,12 +349,16 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide { setup_telemetry_events() { let me = this; this.fields.filter(frappe.model.is_value_type).forEach((field) => { - me.get_input(field.fieldname).on("change", function () { - frappe.telemetry.capture(`${field.fieldname}_set`, "setup"); - if (field.fieldname == "enable_telemetry" && !me.get_value("enable_telemetry")) { - frappe.telemetry.disable(); - } - }); + field.fieldname && + me.get_input(field.fieldname)?.on("change", function () { + frappe.telemetry.capture(`${field.fieldname}_set`, "setup"); + if ( + field.fieldname == "enable_telemetry" && + !me.get_value("enable_telemetry") + ) { + frappe.telemetry.disable(); + } + }); }); } }; diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 88a53df7b6..608f894be1 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -4,6 +4,7 @@ import json import frappe +from frappe import _ from frappe.geo.country_info import get_country_info from frappe.permissions import AUTOMATIC_ROLES from frappe.translate import send_translations, set_default_language @@ -19,8 +20,8 @@ def get_setup_stages(args): # That is done by frappe after successful completion of all stages stages = [ { - "status": "Updating global settings", - "fail_msg": "Failed to update global settings", + "status": _("Updating global settings"), + "fail_msg": _("Failed to update global settings"), "tasks": [ {"fn": update_global_settings, "args": args, "fail_msg": "Failed to update global settings"} ], diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 7ca483d806..952d30a274 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -123,7 +123,7 @@ def generate_report_result( def normalize_result(result, columns): - # Converts to list of dicts from list of lists/tuples + # Convert to list of dicts from list of lists/tuples data = [] column_names = [column["fieldname"] for column in columns] if result and isinstance(result[0], (list, tuple)): @@ -318,6 +318,7 @@ def export_query(): file_format_type = form_params.file_format_type custom_columns = frappe.parse_json(form_params.custom_columns or "[]") include_indentation = form_params.include_indentation + include_filters = form_params.include_filters visible_idx = form_params.visible_idx if isinstance(visible_idx, str): @@ -327,6 +328,8 @@ def export_query(): report_name, form_params.filters, custom_columns=custom_columns, are_default_filters=False ) data = frappe._dict(data) + data.filters = form_params.applied_filters + if not data.columns: frappe.respond_as_web_page( _("No data to export"), @@ -335,7 +338,9 @@ def export_query(): return format_duration_fields(data) - xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation) + xlsx_data, column_widths = build_xlsx_data( + data, visible_idx, include_indentation, include_filters=include_filters + ) if file_format_type == "CSV": content = get_csv_bytes(xlsx_data, csv_params) @@ -360,7 +365,9 @@ def format_duration_fields(data: frappe._dict) -> None: row[index] = format_duration(row[index]) -def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False): +def build_xlsx_data( + data, visible_idx, include_indentation, include_filters=False, ignore_visible_idx=False +): EXCEL_TYPES = ( str, bool, @@ -380,17 +387,34 @@ def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=F # Note: converted for faster lookups visible_idx = set(visible_idx) - result = [[]] + result = [] column_widths = [] + if cint(include_filters): + filter_data = [] + filters = data.filters + for filter_name, filter_value in filters.items(): + if not filter_value: + continue + filter_value = ( + ", ".join([cstr(x) for x in filter_value]) + if isinstance(filter_value, list) + else cstr(filter_value) + ) + filter_data.append([cstr(filter_name), filter_value]) + filter_data.append([]) + result += filter_data + + column_data = [] for column in data.columns: if column.get("hidden"): continue - result[0].append(_(column.get("label"))) + column_data.append(_(column.get("label"))) column_width = cint(column.get("width", 0)) # to convert into scale accepted by openpyxl column_width /= 10 column_widths.append(column_width) + result.append(column_data) # build table from result for row_idx, row in enumerate(data.result): @@ -603,11 +627,11 @@ def has_match( columns_dict, user, ): - """Returns True if after evaluating permissions for each linked doctype - - There is an owner match for the ref_doctype - - `and` There is a user permission match for all linked doctypes + """Return True if after evaluating permissions for each linked doctype: + - There is an owner match for the ref_doctype + - `and` There is a user permission match for all linked doctypes - Returns True if the row is empty + Return True if the row is empty. Note: Each doctype could have multiple conflicting user permission doctypes. @@ -705,9 +729,10 @@ def get_linked_doctypes(columns, data): def get_columns_dict(columns): - """Returns a dict with column docfield values as dict + """Return a dict with column docfield values as dict. + The keys for the dict are both idx and fieldname, - so either index or fieldname can be used to search for a column's docfield properties + so either index or fieldname can be used to search for a column's docfield properties. """ columns_dict = frappe._dict() for idx, col in enumerate(columns): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 8f2f7f8dca..746c6b299f 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -217,6 +217,8 @@ def clean_params(data): def parse_json(data): if (filters := data.get("filters")) and isinstance(filters, str): data["filters"] = json.loads(filters) + if (applied_filters := data.get("applied_filters")) and isinstance(applied_filters, str): + data["applied_filters"] = json.loads(applied_filters) if (or_filters := data.get("or_filters")) and isinstance(or_filters, str): data["or_filters"] = json.loads(or_filters) if (fields := data.get("fields")) and isinstance(fields, str): @@ -584,7 +586,7 @@ def get_filter_dashboard_data(stats, doctype, filters=None): columns = frappe.db.get_table_columns(doctype) for tag in tags: - if not tag["name"] in columns: + if tag["name"] not in columns: continue tagcount = [] if tag["type"] not in ["Date", "Datetime"]: diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index f8b2a67c82..dcce6f3850 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -15,8 +15,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters): tree_method = frappe.get_attr(tree_method) - if tree_method not in frappe.whitelisted: - frappe.throw(_("Not Permitted"), frappe.PermissionError) + frappe.is_whitelisted(tree_method) data = tree_method(doctype, parent, **filters) out = [dict(parent=label, data=data)] diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 38e627539d..6fe2596d7f 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -128,7 +128,7 @@ class AutoEmailReport(Document): ) def get_report_content(self): - """Returns file in for the report in given format""" + """Return file for the report in given format.""" report = frappe.get_doc("Report", self.report) self.filters = frappe.parse_json(self.filters) if self.filters else {} @@ -235,7 +235,7 @@ class AutoEmailReport(Document): else: message = self.get_html_table() - if not self.format == "HTML": + if self.format != "HTML": attachments = [{"fname": self.get_file_name(), "fcontent": data}] frappe.sendmail( diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index d7c75e03a1..2f4c23eac6 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -237,10 +237,7 @@ class EmailAccount(Document): return frappe.db.get_value("Email Domain", domain, EMAIL_DOMAIN_FIELDS, as_dict=True) def get_incoming_server(self, in_receive=False, email_sync_rule="UNSEEN"): - """Returns logged in POP3/IMAP connection object.""" - if frappe.cache.get_value("workers:no-internet") == True: - return None - + """Return logged in POP3/IMAP connection object.""" oauth_token = self.get_oauth_token() args = frappe._dict( { @@ -309,16 +306,13 @@ class EmailAccount(Document): except OSError: if in_receive: # timeout while connecting, see receive.py connect method - description = frappe.clear_last_message() if frappe.message_log else "Socket Error" - if test_internet(): - self.db_set("no_failed", self.no_failed + 1) - if self.no_failed > 2: - self.handle_incoming_connect_error(description=description) - else: - frappe.cache.set_value("workers:no-internet", True) - return None - else: - raise + description = frappe.message_log.pop() if frappe.message_log else "Socket Error" + self.db_set("no_failed", self.no_failed + 1) + if self.no_failed > 2: + self.handle_incoming_connect_error(description=description) + return + + raise @property def _password(self): @@ -495,29 +489,25 @@ class EmailAccount(Document): state.pop("_smtp_server_instance", None) def handle_incoming_connect_error(self, description): - if test_internet(): - if self.get_failed_attempts_count() > 2: - self.db_set("enable_incoming", 0) + if self.get_failed_attempts_count() > 2: + self.db_set("enable_incoming", 0) - for user in get_system_managers(only_name=True): - try: - assign_to.add( - { - "assign_to": user, - "doctype": self.doctype, - "name": self.name, - "description": description, - "priority": "High", - "notify": 1, - } - ) - except assign_to.DuplicateToDoError: - frappe.clear_last_message() - pass - else: - self.set_failed_attempts_count(self.get_failed_attempts_count() + 1) + for user in get_system_managers(only_name=True): + try: + assign_to.add( + { + "assign_to": user, + "doctype": self.doctype, + "name": self.name, + "description": description, + "priority": "High", + "notify": 1, + } + ) + except assign_to.DuplicateToDoError: + frappe.clear_last_message() else: - frappe.cache.set_value("workers:no-internet", True) + self.set_failed_attempts_count(self.get_failed_attempts_count() + 1) def set_failed_attempts_count(self, value): frappe.cache.set(f"{self.name}:email-account-failed-attempts", value) @@ -719,22 +709,6 @@ def get_append_to( return [[d] for d in set(email_append_to_list) if txt in d] -def test_internet(host="8.8.8.8", port=53, timeout=3): - """Returns True if internet is connected - - Host: 8.8.8.8 (google-public-dns-a.google.com) - OpenPort: 53/tcp - Service: domain (DNS/TCP) - """ - try: - socket.setdefaulttimeout(timeout) - socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) - return True - except Exception as ex: - print(ex.message) - return False - - def notify_unreplied(): """Sends email notifications if there are unreplied Communications and `notify_if_unreplied` is set as true.""" @@ -792,11 +766,6 @@ def pull(now=False): """Will be called via scheduler, pull emails from all enabled Email accounts.""" from frappe.integrations.doctype.connected_app.connected_app import has_token - if frappe.cache.get_value("workers:no-internet") == True: - if test_internet(): - frappe.cache.set_value("workers:no-internet", False) - return - doctype = frappe.qb.DocType("Email Account") email_accounts = ( frappe.qb.from_(doctype) diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index b9ba3a2bc1..cd9eac08d4 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -626,7 +626,6 @@ class TestInboundMail(FrappeTestCase): email_account = frappe.get_doc("Email Account", "_Test Email Account 1") inbound_mail = InboundMail(mail_content, email_account, 12345, 1) communication = inbound_mail.process() - self.assertTrue(communication.is_first) self.assertTrue(communication._attachments) diff --git a/frappe/email/doctype/email_group/email_group.js b/frappe/email/doctype/email_group/email_group.js index 5ad4a39dd9..bfe1577594 100644 --- a/frappe/email/doctype/email_group/email_group.js +++ b/frappe/email/doctype/email_group/email_group.js @@ -1,74 +1,97 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on("Email Group", "refresh", function (frm) { - if (!frm.is_new()) { - frm.add_custom_button( - __("Import Subscribers"), - function () { - frappe.prompt( - { - fieldtype: "Select", - options: frm.doc.__onload.import_types, - label: __("Import Email From"), - fieldname: "doctype", - reqd: 1, - }, - function (data) { - frappe.call({ - method: "frappe.email.doctype.email_group.email_group.import_from", - args: { - name: frm.doc.name, - doctype: data.doctype, - }, - callback: function (r) { - frm.set_value("total_subscribers", r.message); - }, - }); - }, - __("Import Subscribers"), - __("Import") - ); - }, - __("Action") - ); +frappe.ui.form.on("Email Group", { + refresh: function (frm) { + if (!frm.is_new()) { + frm.add_custom_button( + __("Import Subscribers"), + function () { + frappe.prompt( + { + fieldtype: "Select", + options: frm.doc.__onload.import_types, + label: __("Import Email From"), + fieldname: "doctype", + reqd: 1, + }, + function (data) { + frappe.call({ + method: "frappe.email.doctype.email_group.email_group.import_from", + args: { + name: frm.doc.name, + doctype: data.doctype, + }, + callback: function (r) { + frm.set_value("total_subscribers", r.message); + }, + }); + }, + __("Import Subscribers"), + __("Import") + ); + }, + __("Action") + ); - frm.add_custom_button( - __("Add Subscribers"), - function () { - frappe.prompt( - { - fieldtype: "Text", - label: __("Email Addresses"), - fieldname: "email_list", - reqd: 1, - }, - function (data) { - frappe.call({ - method: "frappe.email.doctype.email_group.email_group.add_subscribers", - args: { - name: frm.doc.name, - email_list: data.email_list, - }, - callback: function (r) { - frm.set_value("total_subscribers", r.message); - }, - }); - }, - __("Add Subscribers"), - __("Add") - ); - }, - __("Action") - ); + frm.add_custom_button( + __("Add Subscribers"), + function () { + frappe.prompt( + { + fieldtype: "Text", + label: __("Email Addresses"), + fieldname: "email_list", + reqd: 1, + }, + function (data) { + frappe.call({ + method: "frappe.email.doctype.email_group.email_group.add_subscribers", + args: { + name: frm.doc.name, + email_list: data.email_list, + }, + callback: function (r) { + frm.set_value("total_subscribers", r.message); + }, + }); + }, + __("Add Subscribers"), + __("Add") + ); + }, + __("Action") + ); - frm.add_custom_button( - __("New Newsletter"), - function () { - frappe.route_options = { email_group: frm.doc.name }; - frappe.new_doc("Newsletter"); - }, - __("Action") - ); - } + frm.add_custom_button( + __("New Newsletter"), + function () { + frappe.route_options = { email_group: frm.doc.name }; + frappe.new_doc("Newsletter"); + }, + __("Action") + ); + } + + frm.trigger("preview_welcome_url"); + }, + welcome_url(frm) { + frm.trigger("preview_welcome_url"); + }, + add_query_parameters: function (frm) { + frm.trigger("preview_welcome_url"); + }, + preview_welcome_url: function (frm) { + if (frm.doc.add_query_parameters && frm.doc.welcome_url) { + frm.call("preview_welcome_url", { email: "mail@example.org" }).then((r) => { + frm.set_df_property( + "add_query_parameters", + "description", + `${__("Preview:")} ${r.message}` + ); + }); + } else { + frm.set_df_property("add_query_parameters", "description", ""); + } + }, }); diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json index cb74249143..1e90beaecb 100644 --- a/frappe/email/doctype/email_group/email_group.json +++ b/frappe/email/doctype/email_group/email_group.json @@ -9,9 +9,13 @@ "engine": "InnoDB", "field_order": [ "title", + "column_break_oyyj", "total_subscribers", + "sign_up_and_confirmation_section", "confirmation_email_template", - "welcome_email_template" + "welcome_email_template", + "welcome_url", + "add_query_parameters" ], "fields": [ { @@ -41,6 +45,29 @@ "fieldtype": "Link", "label": "Welcome Email Template", "options": "Email Template" + }, + { + "fieldname": "column_break_oyyj", + "fieldtype": "Column Break" + }, + { + "fieldname": "sign_up_and_confirmation_section", + "fieldtype": "Section Break", + "label": "Sign Up and Confirmation" + }, + { + "description": "Redirect to this URL after successful confirmation.", + "fieldname": "welcome_url", + "fieldtype": "Data", + "label": "Welcome URL", + "options": "URL" + }, + { + "default": "0", + "depends_on": "welcome_url", + "fieldname": "add_query_parameters", + "fieldtype": "Check", + "label": "Add Query Parameters" } ], "index_web_pages_for_search": 1, @@ -51,10 +78,11 @@ "link_fieldname": "email_group" } ], - "modified": "2021-06-15 11:25:13.556201", + "modified": "2023-11-24 18:35:17.268492", "modified_by": "Administrator", "module": "Email", "name": "Email Group", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -75,5 +103,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py index f365fa2fb6..9619802edb 100755 --- a/frappe/email/doctype/email_group/email_group.py +++ b/frappe/email/doctype/email_group/email_group.py @@ -18,10 +18,12 @@ class EmailGroup(Document): if TYPE_CHECKING: from frappe.types import DF + add_query_parameters: DF.Check confirmation_email_template: DF.Link | None title: DF.Data total_subscribers: DF.Int welcome_email_template: DF.Link | None + welcome_url: DF.Data | None # end: auto-generated types def onload(self): singles = [d.name for d in frappe.get_all("DocType", "name", {"issingle": 1})] @@ -72,6 +74,22 @@ class EmailGroup(Document): self.name, )[0][0] + @frappe.whitelist() + def preview_welcome_url(self, email: str | None = None) -> str | None: + """Get Welcome URL for the email group.""" + return self.get_welcome_url(email) + + def get_welcome_url(self, email: str | None = None) -> str | None: + """Get Welcome URL for the email group.""" + if not self.welcome_url: + return None + + return ( + add_query_params(self.welcome_url, {"email": email, "email_group": self.name}) + if self.add_query_parameters + else self.welcome_url + ) + def on_trash(self): for d in frappe.get_all("Email Group Member", "name", {"email_group": self.name}): frappe.delete_doc("Email Group Member", d.name) @@ -124,3 +142,19 @@ def send_welcome_email(welcome_email, email, email_group): args = dict(email=email, email_group=email_group) message = frappe.render_template(welcome_email.response_, args) frappe.sendmail(email, subject=welcome_email.subject, message=message) + + +def add_query_params(url: str, params: dict) -> str: + from urllib.parse import urlencode, urlparse, urlunparse + + if not params: + return url + + query_string = urlencode(params) + parsed = list(urlparse(url)) + if parsed[4]: + parsed[4] += f"&{query_string}" + else: + parsed[4] = query_string + + return urlunparse(parsed) diff --git a/frappe/email/doctype/email_group/test_email_group.py b/frappe/email/doctype/email_group/test_email_group.py index ffc325a6bd..fdb36825f5 100644 --- a/frappe/email/doctype/email_group/test_email_group.py +++ b/frappe/email/doctype/email_group/test_email_group.py @@ -1,9 +1,32 @@ # Copyright (c) 2015, Frappe Technologies and Contributors # License: MIT. See LICENSE +import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import validate_url # test_records = frappe.get_test_records('Email Group') class TestEmailGroup(FrappeTestCase): - pass + def test_welcome_url(self): + email_group = frappe.new_doc("Email Group") + email_group.title = "Test" + email_group.welcome_url = "http://example.com/welcome?hello=world" + email_group.add_query_parameters = 1 + email_group.insert() + + welcome_url = email_group.get_welcome_url("mail@example.org") + self.assertTrue(validate_url(welcome_url)) + self.assertIn(email_group.welcome_url, welcome_url) + self.assertIn("email_group=Test", welcome_url) + self.assertIn("email=mail%40example.org", welcome_url) + + email_group.add_query_parameters = 0 + welcome_url = email_group.get_welcome_url("mail@example.org") + self.assertTrue(validate_url(welcome_url)) + self.assertIn(email_group.welcome_url, welcome_url) + self.assertNotIn("email_group=Test", welcome_url) + self.assertNotIn("email=mail%40example.org", welcome_url) + + email_group.welcome_url = "" + self.assertEqual(email_group.get_welcome_url(), None) diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json index 0362a820e5..6bf08cd534 100644 --- a/frappe/email/doctype/email_template/email_template.json +++ b/frappe/email/doctype/email_template/email_template.json @@ -52,12 +52,12 @@ "fieldname": "response_html", "fieldtype": "Code", "label": "Response ", - "options": "HTML" + "options": "Jinja" } ], "icon": "fa fa-comment", "links": [], - "modified": "2023-08-28 22:29:04.457992", + "modified": "2023-12-12 20:01:07.080625", "modified_by": "Administrator", "module": "Email", "name": "Email Template", diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py index 214879dac4..b37598e051 100644 --- a/frappe/email/doctype/email_template/email_template.py +++ b/frappe/email/doctype/email_template/email_template.py @@ -22,6 +22,7 @@ class EmailTemplate(Document): subject: DF.Data use_html: DF.Check # end: auto-generated types + @property def response_(self): return self.response_html if self.use_html else self.response @@ -48,7 +49,7 @@ class EmailTemplate(Document): @frappe.whitelist() def get_email_template(template_name, doc): - """Returns the processed HTML of a email template with the given doc""" + """Return the processed HTML of a email template with the given doc""" email_template = frappe.get_doc("Email Template", template_name) return email_template.get_formatted_email(doc) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 2c79f13541..ea33937d49 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -359,19 +359,29 @@ def confirm_subscription(email, email_group=None): if email_group is None: email_group = get_default_email_group() - if not frappe.db.exists("Email Group", email_group): - frappe.get_doc({"doctype": "Email Group", "title": email_group}).insert(ignore_permissions=True) + try: + group = frappe.get_doc("Email Group", email_group) + except frappe.DoesNotExistError: + group = frappe.get_doc({"doctype": "Email Group", "title": email_group}).insert( + ignore_permissions=True + ) frappe.flags.ignore_permissions = True add_subscribers(email_group, email) frappe.db.commit() - frappe.respond_as_web_page( - _("Confirmed"), - _("{0} has been successfully added to the Email Group.").format(email), - indicator_color="green", - ) + welcome_url = group.get_welcome_url(email) + + if welcome_url: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = welcome_url + else: + frappe.respond_as_web_page( + _("Confirmed"), + _("{0} has been successfully added to the Email Group.").format(email), + indicator_color="green", + ) def get_list_context(context=None): diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 268de161b3..449c0b5b15 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -394,7 +394,7 @@ def get_email_html(template, args, subject, header=None, with_container=False): def inline_style_in_html(html): - """Convert email.css and html to inline-styled html""" + """Convert email.css and html to inline-styled html.""" from premailer import Premailer from frappe.utils.jinja_globals import bundled_asset @@ -460,7 +460,7 @@ def add_attachment(fname, fcontent, content_type=None, parent=None, content_id=N def get_message_id(): - """Returns Message ID created from doctype and name""" + """Return Message ID created from doctype and name.""" return email.utils.make_msgid(domain=frappe.local.site) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 19798851fe..72a2dfce82 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -162,7 +162,7 @@ class EmailServer: return def get_messages(self, folder="INBOX"): - """Returns new email messages.""" + """Return new email messages.""" self.latest_messages = [] self.seen_status = {} @@ -642,13 +642,11 @@ class InboundMail(Email): if self.reference_document(): data["reference_doctype"] = self.reference_document().doctype data["reference_name"] = self.reference_document().name - else: - if append_to and append_to != "Communication": - reference_doc = self._create_reference_document(append_to) - if reference_doc: - data["reference_doctype"] = reference_doc.doctype - data["reference_name"] = reference_doc.name - data["is_first"] = True + elif append_to and append_to != "Communication": + reference_name = self._create_reference_document(append_to) + if reference_name: + data["reference_doctype"] = append_to + data["reference_name"] = reference_name if self.is_notification(): # Disable notifications for notification. @@ -815,28 +813,25 @@ class InboundMail(Email): def _create_reference_document(self, doctype): """Create reference document if it does not exist in the system.""" parent = frappe.new_doc(doctype) - email_fileds = self.get_email_fields(doctype) + email_fields = self.get_email_fields(doctype) - if email_fileds.subject_field: - parent.set(email_fileds.subject_field, frappe.as_unicode(self.subject)[:140]) + if email_fields.subject_field: + parent.set(email_fields.subject_field, frappe.as_unicode(self.subject)[:140]) - if email_fileds.sender_field: - parent.set(email_fileds.sender_field, frappe.as_unicode(self.from_email)) + if email_fields.sender_field: + parent.set(email_fields.sender_field, frappe.as_unicode(self.from_email)) + + if email_fields.sender_name_field: + parent.set(email_fields.sender_name_field, frappe.as_unicode(self.from_real_name)) parent.flags.ignore_mandatory = True try: parent.insert(ignore_permissions=True) + return parent.name except frappe.DuplicateEntryError: # try and find matching parent - parent_name = frappe.db.get_value( - self.email_account.append_to, {email_fileds.sender_field: self.from_email} - ) - if parent_name: - parent.name = parent_name - else: - parent = None - return parent + return frappe.db.get_value(doctype, {email_fields.sender_field: self.from_email}) @staticmethod def get_doc(doctype, docname, ignore_error=False): @@ -869,10 +864,10 @@ class InboundMail(Email): @staticmethod def get_email_fields(doctype): - """Returns Email related fields of a doctype.""" + """Return Email related fields of a doctype.""" fields = frappe._dict() - email_fields = ["subject_field", "sender_field"] + email_fields = ["subject_field", "sender_field", "sender_name_field"] meta = frappe.get_meta(doctype) for field in email_fields: diff --git a/frappe/exceptions.py b/frappe/exceptions.py index f4bcb661f1..2258c6e5ae 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -64,7 +64,8 @@ class RequestToken(Exception): class Redirect(Exception): - http_status_code = 301 + def __init__(self, http_status_code: int = 301): + self.http_status_code = http_status_code class CSRFTokenError(Exception): diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py index 7ad016828a..0e18fbf483 100644 --- a/frappe/frappeclient.py +++ b/frappe/frappeclient.py @@ -109,7 +109,7 @@ class FrappeClient: def get_list( self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=None ): - """Returns list of records of a particular type""" + """Return list of records of a particular type.""" if not isinstance(fields, str): fields = json.dumps(fields) params = { @@ -173,7 +173,7 @@ class FrappeClient: return self.post_request({"cmd": "frappe.client.submit", "doc": frappe.as_json(doc)}) def get_value(self, doctype, fieldname=None, filters=None): - """Returns a value form a document + """Return a value from a document. :param doctype: DocType to be queried :param fieldname: Field to be returned (default `name`) @@ -212,7 +212,7 @@ class FrappeClient: return self.post_request({"cmd": "frappe.client.cancel", "doctype": doctype, "name": name}) def get_doc(self, doctype, name="", filters=None, fields=None): - """Returns a single remote document + """Return a single remote document. :param doctype: DocType of the document to be returned :param name: (optional) `name` of the document to be returned diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index f308dc63e3..eba166d8fc 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -1926,7 +1926,7 @@ "currency_fraction_units": 100, "currency_name": "Nepalese Rupee", "currency_symbol": "\u20a8", - "number_format": "#,###.##", + "number_format": "#,##,###.##", "timezones": [ "Asia/Kathmandu" ], diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 662e058d68..9b4a42179c 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -19,7 +19,7 @@ def get_coords(doctype, filters, type): def convert_to_geojson(type, coords): - """Converts GPS coordinates to geoJSON string.""" + """Convert GPS coordinates to geoJSON string.""" geojson = {"type": "FeatureCollection", "features": None} if type == "location_field": @@ -90,7 +90,7 @@ def return_coordinates(doctype, filters_sql): def get_coords_conditions(doctype, filters=None): - """Returns SQL conditions with user permissions and filters for event queries.""" + """Return SQL conditions with user permissions and filters for event queries.""" from frappe.desk.reportview import get_filters_cond if not frappe.has_permission(doctype): diff --git a/frappe/installer.py b/frappe/installer.py index 89b52e1d4e..d96f1167f1 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -1,6 +1,7 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE - +import configparser +import gzip import json import os import re @@ -52,13 +53,13 @@ def _new_site( ): """Install a new Frappe site""" - from frappe.utils import get_site_path, scheduler, touch_file + from frappe.utils import scheduler if not force and os.path.exists(site): print(f"Site {site} already exists") sys.exit(1) - if no_mariadb_socket and not db_type == "mariadb": + if no_mariadb_socket and db_type != "mariadb": print("--no-mariadb-socket requires db_type to be set to mariadb.") sys.exit(1) @@ -419,7 +420,7 @@ def _delete_modules(modules: list[str], dry_run: bool) -> list[str]: Note: All record linked linked to Module Def are also deleted. - Returns: list of deleted doctypes.""" + Return: list of deleted doctypes.""" drop_doctypes = [] doctype_link_field_map = _get_module_linked_doctype_field_map() @@ -449,7 +450,6 @@ def _delete_modules(modules: list[str], dry_run: bool) -> list[str]: def _delete_linked_documents( module_name: str, doctype_linkfield_map: dict[str, str], dry_run: bool ) -> None: - """Deleted all records linked with module def""" for doctype, fieldname in doctype_linkfield_map.items(): for record in frappe.get_all(doctype, filters={fieldname: module_name}, pluck="name"): @@ -461,7 +461,7 @@ def _delete_linked_documents( def _get_module_linked_doctype_field_map() -> dict[str, str]: """Get all the doctypes which have module linked with them. - returns ordered dictionary with doctype->link field mapping.""" + Return ordered dictionary with doctype->link field mapping.""" # Hardcoded to change order of deletion ordered_doctypes = [ @@ -664,32 +664,6 @@ def remove_missing_apps(): frappe.db.set_global("installed_apps", json.dumps(installed_apps)) -def extract_sql_from_archive(sql_file_path): - """Return the path of an SQL file if the passed argument is the path of a gzipped - SQL file or an SQL file path. The path may be absolute or relative from the bench - root directory or the sites sub-directory. - - Args: - sql_file_path (str): Path of the SQL file - - Returns: - str: Path of the decompressed SQL file - """ - from frappe.utils import get_bench_relative_path - - sql_file_path = get_bench_relative_path(sql_file_path) - # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file - if sql_file_path.endswith("sql.gz"): - decompressed_file_name = extract_sql_gzip(sql_file_path) - else: - decompressed_file_name = sql_file_path - - # convert archive sql to latest compatible - convert_archive_content(decompressed_file_name) - - return decompressed_file_name - - def convert_archive_content(sql_file_path): if frappe.conf.db_type == "mariadb": # ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed @@ -723,20 +697,6 @@ def convert_archive_content(sql_file_path): old_sql_file_path.unlink() -def extract_sql_gzip(sql_gz_path): - import subprocess - - try: - original_file = sql_gz_path - decompressed_file = original_file.rstrip(".gz") - cmd = f"gzip --decompress --force < {original_file} > {decompressed_file}" - subprocess.check_call(cmd, shell=True) - except Exception: - raise - - return decompressed_file - - def _guess_mariadb_version() -> tuple[int] | None: # Using command-line because we *might* not have a connection yet and this command is required # in non-interactive mode. @@ -793,53 +753,58 @@ def is_downgrade(sql_file_path, verbose=False): from semantic_version import Version - head = "INSERT INTO `tabInstalled Application` VALUES" + backup_version = extract_version_from_dump(sql_file_path) + if backup_version is None: + # This is likely an older backup, so try to extract another way + header = get_db_dump_header(sql_file_path).split("\n") + if "Version" in header[0]: + backup_version = header[0].split(":")[-1].strip() - with open(sql_file_path) as f: - for line in f: - if head in line: - # 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master') - line = line.strip().lstrip(head).rstrip(";").strip() - app_rows = frappe.safe_eval(line) - # check if iterable consists of tuples before trying to transform - apps_list = ( - app_rows - if all(isinstance(app_row, (tuple, list, set)) for app_row in app_rows) - else (app_rows,) - ) - # 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')] - all_apps = [x[-3:] for x in apps_list] + # Assume it's not a downgrade if we can't determine backup version + if backup_version is None: + return False - for app in all_apps: - app_name = app[0] - app_version = app[1].split(" ", 1)[0] + current_version = Version(frappe.__version__) + downgrade = Version(backup_version) < current_version - if app_name == "frappe": - try: - current_version = Version(frappe.__version__) - backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version) - except ValueError: - return False + if verbose and downgrade: + print(f"Your site will be downgraded from Frappe {current_version} to {backup_version}") - downgrade = backup_version > current_version - - if verbose and downgrade: - print(f"Your site will be downgraded from Frappe {backup_version} to {current_version}") - - return downgrade + return downgrade -def is_partial(sql_file_path): - with open(sql_file_path) as f: - header = " ".join(f.readline() for _ in range(5)) - if "Partial Backup" in header: - return True - return False +def extract_version_from_dump(sql_file_path: str) -> str | None: + """ + Extract frappe version from DB dump + + :param sql_file_path: The path to the dump file + :return: The frappe version used to create the backup + """ + header = get_db_dump_header(sql_file_path).split("\n") + metadata = "" + if "begin frappe metadata" in header[0]: + for line in header[1:]: + if "end frappe metadata" in line: + break + metadata += line.replace("--", "").strip() + "\n" + parser = configparser.ConfigParser() + parser.read_string(metadata) + return parser["frappe"]["version"] + return None + + +def is_partial(sql_file_path: str) -> bool: + """ + Function to return whether the database dump is a partial backup or not + + :param sql_file_path: path to the database dump file + :return: True if the database dump is a partial backup, False otherwise + """ + header = get_db_dump_header(sql_file_path) + return "Partial Backup" in header def partial_restore(sql_file_path, verbose=False): - sql_file = extract_sql_from_archive(sql_file_path) - if frappe.conf.db_type == "mariadb": from frappe.database.mariadb.setup_db import import_db_from_sql elif frappe.conf.db_type == "postgres": @@ -853,43 +818,60 @@ def partial_restore(sql_file_path, verbose=False): fg="yellow", ) warnings.warn(warn) + else: + click.secho("Unsupported database type", fg="red") + return - import_db_from_sql(source_sql=sql_file, verbose=verbose) - - # Removing temporarily created file - if sql_file != sql_file_path: - os.remove(sql_file) + import_db_from_sql(source_sql=sql_file_path, verbose=verbose) -def validate_database_sql(path, _raise=True): - """Check if file has contents and if DefaultValue table exists +def validate_database_sql(path: str, _raise: bool = True) -> None: + """Check if file has contents and if `__Auth` table exists Args: path (str): Path of the decompressed SQL file _raise (bool, optional): Raise exception if invalid file. Defaults to True. """ - empty_file = False - missing_table = True - error_message = "" + if path.endswith(".gz"): + executable_name = "zgrep" + else: + executable_name = "grep" - if not os.path.getsize(path): + if os.path.getsize(path): + if (executable := which(executable_name)) is None: + frappe.throw( + f"`{executable_name}` not found in PATH! This is required to take a backup.", + exc=frappe.ExecutableNotFound, + ) + try: + frappe.utils.execute_in_shell(f"{executable} -m1 __Auth {path}", check_exit_code=True) + return + except Exception: + error_message = "Table `__Auth` not found in file." + else: error_message = f"{path} is an empty file!" - empty_file = True - - # dont bother checking if empty file - if not empty_file: - with open(path) as f: - for line in f: - if "tabDefaultValue" in line: - missing_table = False - break - - if missing_table: - error_message = "Table `tabDefaultValue` not found in file." if error_message: click.secho(error_message, fg="red") - if _raise and (missing_table or empty_file): + if _raise: raise frappe.InvalidDatabaseFile + + +def get_db_dump_header(file_path: str, file_bytes: int = 256) -> str: + """ + Get the header of a database dump file + + :param file_path: path to the database dump file + :param file_bytes: number of bytes to read from the file + :return: The first few bytes of the file as requested + """ + + # Use `gzip` to open the file if the extension is `.gz` + if file_path.endswith(".gz"): + with gzip.open(file_path, "rb") as f: + return f.read(file_bytes).decode() + + with open(file_path, "rb") as f: + return f.read(file_bytes).decode() diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index d6b173d040..d571b2ba00 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -48,8 +48,7 @@ class ConnectedApp(Document): def validate(self): base_url = frappe.utils.get_url() callback_path = ( - "/api/method/frappe.integrations.doctype.connected_app.connected_app.callback" - + f"?app={self.name}" + "/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/" + self.name ) self.redirect_uri = urljoin(base_url, callback_path) @@ -149,7 +148,7 @@ class ConnectedApp(Document): @frappe.whitelist(methods=["GET"], allow_guest=True) -def callback(code=None, state=None, app=None): +def callback(code=None, state=None): """Handle client's code. Called during the oauthorization flow by the remote oAuth2 server to @@ -162,7 +161,11 @@ def callback(code=None, state=None, app=None): frappe.local.response["location"] = "/login?" + urlencode({"redirect-to": frappe.request.url}) return - connected_app = frappe.get_doc("Connected App", app) + path = frappe.request.path[1:].split("/") + if len(path) != 4 or not path[3]: + frappe.throw(_("Invalid Parameters.")) + + connected_app = frappe.get_doc("Connected App", path[3]) token_cache = frappe.get_doc("Token Cache", connected_app.name + "-" + frappe.session.user) if state != token_cache.state: diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 3f412efc90..8430e5c80c 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -202,9 +202,7 @@ def sync(g_calendar=None): def get_google_calendar_object(g_calendar): - """ - Returns an object of Google Calendar along with Google Calendar doc. - """ + """Return an object of Google Calendar along with Google Calendar doc.""" google_settings = frappe.get_doc("Google Settings") account = frappe.get_doc("Google Calendar", g_calendar) @@ -257,8 +255,8 @@ def check_google_calendar(account, google_calendar): def sync_events_from_google_calendar(g_calendar, method=None): - """ - Syncs Events from Google Calendar in Framework Calendar. + """Sync Events from Google Calendar in Framework Calendar. + Google Calendar returns nextSyncToken when all the events in Google Calendar are fetched. nextSyncToken is returned at the very last page https://developers.google.com/calendar/v3/sync @@ -685,12 +683,10 @@ def format_date_according_to_google_calendar(all_day, starts_on, ends_on=None): def parse_google_calendar_recurrence_rule(repeat_day_week_number, repeat_day_name): - """ - Returns (repeat_on) exact date for combination eg 4TH viz. 4th thursday of a month - """ + """Return (repeat_on) exact date for combination eg 4TH viz. 4th thursday of a month.""" if repeat_day_week_number < 0: # Consider a month with 5 weeks and event is to be repeated in last week of every month, google caledar considers - # a month has 4 weeks and hence itll return -1 for a month with 5 weeks. + # a month has 4 weeks and hence it'll return -1 for a month with 5 weeks. repeat_day_week_number = 4 weekdays = get_weekdays() @@ -714,9 +710,7 @@ def parse_google_calendar_recurrence_rule(repeat_day_week_number, repeat_day_nam def repeat_on_to_google_calendar_recurrence_rule(doc): - """ - Returns event (repeat_on) in Google Calendar format ie RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH - """ + """Return event (repeat_on) in Google Calendar format ie RRULE:FREQ=WEEKLY;BYDAY=MO,TU,TH.""" recurrence = framework_frequencies.get(doc.repeat_on) weekdays = get_weekdays() @@ -732,8 +726,8 @@ def repeat_on_to_google_calendar_recurrence_rule(doc): def get_week_number(dt): - """ - Returns the week number of the month for the specified date. + """Return the week number of the month for the specified date. + https://stackoverflow.com/questions/3806473/python-week-number-of-the-month/16804556 """ from math import ceil @@ -771,9 +765,7 @@ def get_conference_data(doc): def get_attendees(doc): - """ - Returns a list of dicts with attendee emails, if available in event_participants table - """ + """Return a list of dicts with attendee emails, if available in event_participants table.""" attendees, email_not_found = [], [] for participant in doc.event_participants: diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.json b/frappe/integrations/doctype/google_contacts/google_contacts.json index 2143cc6ca8..3858217a65 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.json +++ b/frappe/integrations/doctype/google_contacts/google_contacts.json @@ -99,7 +99,7 @@ } ], "links": [], - "modified": "2023-08-28 20:22:58.267442", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Integrations", "name": "Google Contacts", @@ -126,7 +126,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 65bd2bf1d3..fa316de026 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -74,9 +74,7 @@ def authorize_access(g_contact, reauthorize=False, code=None): def get_google_contacts_object(g_contact): - """ - Returns an object of Google Calendar along with Google Calendar doc. - """ + """Return an object of Google Calendar along with Google Calendar doc.""" account = frappe.get_doc("Google Contacts", g_contact) oauth_obj = GoogleOAuth("contacts") diff --git a/frappe/integrations/doctype/google_drive/google_drive.json b/frappe/integrations/doctype/google_drive/google_drive.json index 592281be68..7bc967a6f8 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.json +++ b/frappe/integrations/doctype/google_drive/google_drive.json @@ -102,7 +102,7 @@ ], "issingle": 1, "links": [], - "modified": "2022-12-04 15:53:58.702389", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Integrations", "name": "Google Drive", @@ -120,7 +120,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index aa85bac06d..a8c44796ef 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -88,9 +88,7 @@ def authorize_access(reauthorize=False, code=None): def get_google_drive_object(): - """ - Returns an object of Google Drive. - """ + """Return an object of Google Drive.""" account = frappe.get_doc("Google Drive") oauth_obj = GoogleOAuth("drive") diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py index 63eadd7f4a..b66f7e9479 100644 --- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py @@ -21,7 +21,7 @@ class OAuthProviderSettings(Document): def get_oauth_settings(): - """Returns oauth settings""" + """Return OAuth settings.""" return frappe._dict( { "skip_authorization": frappe.db.get_single_value( diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py index 3445bb92e3..54f2c3ae1b 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/social_login_key.py @@ -223,5 +223,5 @@ def provider_allows_signup(provider: str) -> bool: sign_up_config = frappe.db.get_value("Social Login Key", provider, "sign_ups") if not sign_up_config: # fallback to global settings - return is_signup_disabled() + return not is_signup_disabled() return sign_up_config == "Allow" diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index 961cd41b65..2a32ab8aa1 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -90,7 +90,7 @@ class TestSocialLoginKey(FrappeTestCase): def make_social_login_key(**kwargs): kwargs["doctype"] = "Social Login Key" - if not "provider_name" in kwargs: + if "provider_name" not in kwargs: kwargs["provider_name"] = "Test OAuth2 Provider" return frappe.get_doc(kwargs) diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.json b/frappe/integrations/doctype/webhook_header/webhook_header.json index 4aea5d02ed..6a7e8f9999 100644 --- a/frappe/integrations/doctype/webhook_header/webhook_header.json +++ b/frappe/integrations/doctype/webhook_header/webhook_header.json @@ -11,20 +11,20 @@ "fields": [ { "fieldname": "key", - "fieldtype": "Data", + "fieldtype": "Small Text", "in_list_view": 1, "label": "Key" }, { "fieldname": "value", - "fieldtype": "Data", + "fieldtype": "Small Text", "in_list_view": 1, "label": "Value" } ], "istable": 1, "links": [], - "modified": "2022-08-03 12:20:51.949422", + "modified": "2023-12-11 12:20:51.949422", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook Header", diff --git a/frappe/integrations/google_oauth.py b/frappe/integrations/google_oauth.py index 8bc54e0b1d..7f24c611bf 100644 --- a/frappe/integrations/google_oauth.py +++ b/frappe/integrations/google_oauth.py @@ -56,7 +56,7 @@ class GoogleOAuth: frappe.throw(frappe._("Please update {} before continuing.").format(google_settings)) def authorize(self, oauth_code: str) -> dict[str, str | int]: - """Returns a dict with access and refresh token. + """Return a dict with access and refresh token. :param oauth_code: code got back from google upon successful auhtorization """ @@ -99,7 +99,7 @@ class GoogleOAuth: ) def get_authentication_url(self, state: dict[str, str]) -> dict[str, str]: - """Returns google authentication url. + """Return Google authentication url. :param state: dict of values which you need on callback (for calling methods, redirection back to the form, doc name, etc) """ @@ -117,7 +117,7 @@ class GoogleOAuth: } def get_google_service_object(self, access_token: str, refresh_token: str): - """Returns google service object""" + """Return Google service object.""" credentials_dict = { "token": access_token, diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 86f8b0b1ef..14ae944192 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -10,7 +10,9 @@ from frappe import _ from frappe.utils import get_request_session -def make_request(method, url, auth=None, headers=None, data=None, json=None, params=None): +def make_request( + method: str, url: str, auth=None, headers=None, data=None, json=None, params=None +): auth = auth or "" data = data or {} headers = headers or {} @@ -31,23 +33,71 @@ def make_request(method, url, auth=None, headers=None, data=None, json=None, par raise exc -def make_get_request(url, **kwargs): +def make_get_request(url: str, **kwargs): + """Make a 'GET' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ return make_request("GET", url, **kwargs) -def make_post_request(url, **kwargs): +def make_post_request(url: str, **kwargs): + """Make a 'POST' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ return make_request("POST", url, **kwargs) -def make_put_request(url, **kwargs): +def make_put_request(url: str, **kwargs): + """Make a 'PUT' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ return make_request("PUT", url, **kwargs) -def make_patch_request(url, **kwargs): +def make_patch_request(url: str, **kwargs): + """Make a 'PATCH' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ return make_request("PATCH", url, **kwargs) -def make_delete_request(url, **kwargs): +def make_delete_request(url: str, **kwargs): + """Make a 'DELETE' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ return make_request("DELETE", url, **kwargs) diff --git a/frappe/migrate.py b/frappe/migrate.py index 33c930e9da..cad55d5d24 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -151,7 +151,7 @@ class SiteMigration: frappe.get_attr(fn)() def required_services_running(self) -> bool: - """Returns True if all required services are running. Returns False and prints + """Return True if all required services are running. Return False and print instructions to stdout when required services are not available. """ service_status = check_connection(redis_services=["redis_cache"]) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 9c0282a24d..ad29e31ee4 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -194,6 +194,8 @@ def get_permitted_fields( parenttype: str | None = None, user: str | None = None, permission_type: str | None = None, + *, + ignore_virtual=False, ) -> list[str]: meta = frappe.get_meta(doctype) valid_columns = meta.get_valid_columns() @@ -209,7 +211,10 @@ def get_permitted_fields( permission_type = "select" if frappe.only_has_select_perm(doctype, user=user) else "read" if permitted_fields := meta.get_permitted_fieldnames( - parenttype=parenttype, user=user, permission_type=permission_type + parenttype=parenttype, + user=user, + permission_type=permission_type, + with_virtual_fields=not ignore_virtual, ): if permission_type == "select": return permitted_fields diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 42c575371e..241bfdee31 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -55,9 +55,9 @@ DOCTYPES_FOR_DOCTYPE = {"DocType", *TABLE_DOCTYPES_FOR_DOCTYPE.values()} def get_controller(doctype): - """ - Returns the locally cached **class** object of the given DocType. - For `custom` type, returns `frappe.model.document.Document`. + """Return the locally cached **class** object of the given DocType. + + For `custom` type, return `frappe.model.document.Document`. :param doctype: DocType name as string. """ @@ -146,9 +146,9 @@ class BaseDocument: return get_permitted_fields(doctype=self.doctype, parenttype=getattr(self, "parenttype", None)) def __getstate__(self): - """ + """Return a copy of `__dict__` excluding unpicklable values like `meta`. + Called when pickling. - Returns a copy of `__dict__` excluding unpicklable values like `meta`. More info: https://docs.python.org/3/library/pickle.html#handling-stateful-objects """ @@ -633,7 +633,7 @@ class BaseDocument: def get_field_name_by_key_name(self, key_name): """MariaDB stores a mapping between `key_name` and `column_name`. - This function returns the `column_name` associated with the `key_name` passed + Return the `column_name` associated with the `key_name` passed. Args: key_name (str): The name of the database index. @@ -641,7 +641,7 @@ class BaseDocument: Raises: IndexError: If the key is not found in the table. - Returns: + Return: str: The column name associated with the key. """ return frappe.db.sql( @@ -660,12 +660,12 @@ class BaseDocument: )[0].get("Column_name") def get_label_from_fieldname(self, fieldname): - """Returns the associated label for fieldname + """Return the associated label for fieldname. Args: fieldname (str): The fieldname in the DocType to use to pull the label. - Returns: + Return: str: The label associated with the fieldname, if found, otherwise `None`. """ df = self.meta.get_field(fieldname) @@ -743,7 +743,7 @@ class BaseDocument: return missing def get_invalid_links(self, is_submittable=False): - """Returns list of invalid links and also updates fetch values if not set""" + """Return list of invalid links and also update fetch values if not set.""" def get_msg(df, docname): # check if parentfield exists (only applicable for child table doctype) @@ -850,7 +850,7 @@ class BaseDocument: return for df in self.meta.get_select_fields(): - if df.fieldname == "naming_series" or not (self.get(df.fieldname) and df.options): + if df.fieldname == "naming_series" or not self.get(df.fieldname) or not df.options: continue options = (df.options or "").split("\n") @@ -1112,7 +1112,7 @@ class BaseDocument: return "".join(set(pwd)) == "*" def precision(self, fieldname, parentfield=None) -> int | None: - """Returns float precision for a particular field (or get global default). + """Return float precision for a particular field (or get global default). :param fieldname: Fieldname for which precision is required. :param parentfield: If fieldname is in child table.""" @@ -1174,7 +1174,7 @@ class BaseDocument: return format_value(val, df=df, doc=doc, currency=currency, format=format) def is_print_hide(self, fieldname, df=None, for_print=True): - """Returns true if fieldname is to be hidden for print. + """Return True if fieldname is to be hidden for print. Print Hide can be set via the Print Format Builder or in the controller as a list of hidden fields. Example @@ -1203,7 +1203,8 @@ class BaseDocument: return print_hide def in_format_data(self, fieldname): - """Returns True if shown via Print Format::`format_data` property. + """Return True if shown via Print Format::`format_data` property. + Called from within standard print format.""" doc = getattr(self, "parent_doc", self) diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index f8b7a73a3b..d836fa481d 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -67,7 +67,7 @@ def set_user_and_static_default_values(doc): ) if user_default_value is not None: # if fieldtype is link check if doc exists - if not df.fieldtype == "Link" or frappe.db.exists(df.options, user_default_value): + if df.fieldtype != "Link" or frappe.db.exists(df.options, user_default_value): doc.set(df.fieldname, user_default_value) else: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 76a03f5a76..80e78a3f6d 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -179,6 +179,9 @@ class DatabaseQuery: from frappe.model.base_document import get_controller controller = get_controller(self.doctype) + if not hasattr(controller, "get_list"): + return [] + self.parse_args() kwargs = { "as_list": as_list, @@ -332,7 +335,7 @@ class DatabaseQuery: return args def parse_args(self): - """Convert fields and filters from strings to list, dicts""" + """Convert fields and filters from strings to list, dicts.""" if isinstance(self.fields, str): if self.fields == "*": self.fields = ["*"] @@ -465,7 +468,7 @@ class DatabaseQuery: # add tables from fields if self.fields: for field in self.fields: - if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field): + if "tab" not in field or "." not in field or any(x for x in sql_functions if x in field): continue table_name = field.split(".", 1)[0] @@ -478,7 +481,7 @@ class DatabaseQuery: if table_name.lower().startswith("group_concat("): table_name = table_name[13:] - if not table_name[0] == "`": + if table_name[0] != "`": table_name = f"`{table_name}`" if ( table_name not in self.query_tables and table_name not in self.linked_table_aliases.values() @@ -632,6 +635,7 @@ class DatabaseQuery: doctype=self.doctype, parenttype=self.parent_doctype, permission_type=self.permission_map.get(self.doctype), + ignore_virtual=True, ) for i, field in enumerate(self.fields): @@ -712,7 +716,8 @@ class DatabaseQuery: j = j + len(permitted_fields) - 1 def prepare_filter_condition(self, f): - """Returns a filter condition in the format: + """Return a filter condition in the format: + ifnull(`tabDocType`.`fieldname`, fallback) operator "value" """ @@ -736,7 +741,7 @@ class DatabaseQuery: df = meta.get("fields", {"fieldname": f.fieldname}) df = df[0] if df else None - can_be_null = True + can_be_null = f.fieldname != "name" # primary key is never nullable value = None @@ -791,7 +796,7 @@ class DatabaseQuery: # if values contain '' or falsy values then only coalesce column # for `in` query this is only required if values contain '' or values are empty. # for `not in` queries we can't be sure as column values might contain null. - can_be_null = not getattr(df, "not_nullable", False) + can_be_null &= not getattr(df, "not_nullable", False) if f.operator.lower() == "in": can_be_null &= not f.value or any(v is None or v == "" for v in f.value) @@ -927,7 +932,8 @@ class DatabaseQuery: role_permissions = frappe.permissions.get_role_permissions(self.doctype_meta, user=self.user) if ( not self.doctype_meta.istable - and not (role_permissions.get("select") or role_permissions.get("read")) + and not role_permissions.get("select") + and not role_permissions.get("read") and not self.flags.ignore_permissions and not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype) ): @@ -1075,6 +1081,8 @@ class DatabaseQuery: self.fields[0].lower().startswith("count(") or self.fields[0].lower().startswith("min(") or self.fields[0].lower().startswith("max(") + or self.fields[0].lower().startswith("sum(") + or self.fields[0].lower().startswith("avg(") ) and not self.group_by ) @@ -1335,7 +1343,7 @@ def get_date_range(operator: str, value: str): def requires_owner_constraint(role_permissions): - """Returns True if "select" or "read" isn't available without being creator.""" + """Return True if "select" or "read" isn't available without being creator.""" if not role_permissions.get("has_if_owner_enabled"): return diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index e2202882b1..b6ab2546bf 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -254,7 +254,7 @@ def check_if_doc_is_linked(doc, method="Delete"): for lf in link_fields: link_dt, link_field, issingle = lf["parent"], lf["fieldname"], lf["issingle"] - if link_dt in ignored_doctypes or link_field == "amended_from": + if link_dt in ignored_doctypes or (link_field == "amended_from" and method == "Cancel"): continue try: diff --git a/frappe/model/document.py b/frappe/model/document.py index 9ed4e2a3f2..1201d3755b 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: def get_doc(*args, **kwargs): - """returns a frappe.model.Document object. + """Return a `frappe.model.Document` object. :param arg1: Document dict or DocType name. :param arg2: [optional] document name. @@ -356,6 +356,7 @@ class Document(BaseDocument): return self.insert() self.check_if_locked() + self._set_defaults() self.check_permission("write", "save") self.set_user_and_timestamp() @@ -454,7 +455,7 @@ class Document(BaseDocument): return getattr(self, "_doc_before_save", None) def has_value_changed(self, fieldname): - """Returns true if value is changed before and after saving""" + """Return True if value has changed before and after saving.""" previous = self.get_doc_before_save() return previous.get(fieldname) != self.get(fieldname) if previous else True @@ -464,8 +465,10 @@ class Document(BaseDocument): if self.flags.name_set and not force: return + autoname = self.meta.autoname or "" + # If autoname has set as Prompt (name) - if self.get("__newname"): + if self.get("__newname") and autoname.lower() == "prompt": self.name = validate_name(self.doctype, self.get("__newname")) self.flags.name_set = True return @@ -620,7 +623,7 @@ class Document(BaseDocument): workflow = self.meta.get_workflow() if workflow: validate_workflow(self) - if not self._action == "save": + if self._action != "save": set_workflow_state_on_action(self, workflow, self._action) def validate_set_only_once(self): @@ -769,16 +772,18 @@ class Document(BaseDocument): if frappe.flags.in_import: return - new_doc = frappe.new_doc(self.doctype, as_dict=True) - self.update_if_missing(new_doc) + if self.is_new(): + new_doc = frappe.new_doc(self.doctype, as_dict=True) + self.update_if_missing(new_doc) # children for df in self.meta.get_table_fields(): - new_doc = frappe.new_doc(df.options, as_dict=True) + new_doc = frappe.new_doc(df.options, parent_doc=self, parentfield=df.fieldname, as_dict=True) value = self.get(df.fieldname) if isinstance(value, list): for d in value: - d.update_if_missing(new_doc) + if d.is_new(): + d.update_if_missing(new_doc) def check_if_latest(self): """Checks if `modified` timestamp provided by document being updated is same as the @@ -922,7 +927,7 @@ class Document(BaseDocument): frappe.throw(_("Cannot link cancelled document: {0}").format(msg), frappe.CancelledLinkError) def get_all_children(self, parenttype=None) -> list["Document"]: - """Returns all children documents from **Table** type fields in a list.""" + """Return all children documents from **Table** type fields in a list.""" children = [] @@ -975,7 +980,7 @@ class Document(BaseDocument): if self.flags.notifications is None: def _get_notifications(): - """returns enabled notifications for the current doctype""" + """Return enabled notifications for the current doctype.""" return frappe.get_all( "Notification", @@ -1377,7 +1382,7 @@ class Document(BaseDocument): doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.get("parentfield")))) def get_url(self): - """Returns Desk URL for this document.""" + """Return Desk URL for this document.""" return get_absolute_url(self.doctype, self.name) def add_comment( @@ -1451,7 +1456,7 @@ class Document(BaseDocument): ) def get_signature(self): - """Returns signature (hash) for private URL.""" + """Return signature (hash) for private URL.""" return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() def get_document_share_key(self, expires_on=None, no_expiry=False): diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py index 7363bf4583..78f329ee83 100644 --- a/frappe/model/mapper.py +++ b/frappe/model/mapper.py @@ -10,20 +10,19 @@ from frappe.utils import cstr @frappe.whitelist() def make_mapped_doc(method, source_name, selected_children=None, args=None): - """Returns the mapped document calling the given mapper method. - Sets selected_children as flags for the `get_mapped_doc` method. + """Return the mapped document calling the given mapper method. + Set `selected_children` as flags for the `get_mapped_doc` method. Called from `open_mapped_doc` from create_new.js""" for hook in reversed(frappe.get_hooks("override_whitelisted_methods", {}).get(method, [])): - # override using the first hook + # override using the last hook method = hook break method = frappe.get_attr(method) - if method not in frappe.whitelisted: - raise frappe.PermissionError + frappe.is_whitelisted(method) if selected_children: selected_children = json.loads(selected_children) @@ -38,15 +37,15 @@ def make_mapped_doc(method, source_name, selected_children=None, args=None): @frappe.whitelist() def map_docs(method, source_names, target_doc, args=None): - '''Returns the mapped document calling the given mapper method - with each of the given source docs on the target doc + """Return the mapped document calling the given mapper method with each of the given source docs on the target doc. :param args: Args as string to pass to the mapper method - E.g. args: "{ 'supplier': 'XYZ' }"''' + + e.g. args: "{ 'supplier': 'XYZ' }" + """ method = frappe.get_attr(method) - if method not in frappe.whitelisted: - raise frappe.PermissionError + frappe.is_whitelisted(method) for src in json.loads(source_names): _args = (src, target_doc, json.loads(args)) if args else (src, target_doc) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index df04dc1eda..0e80a0957c 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -208,7 +208,7 @@ class Meta(Document): return self._table_fields def get_global_search_fields(self): - """Returns list of fields with `in_global_search` set and `name` if set""" + """Return list of fields with `in_global_search` set and `name` if set.""" fields = self.get("fields", {"in_global_search": 1, "fieldtype": ["not in", no_value_fields]}) if getattr(self, "show_name_in_global_search", None): fields.append(frappe._dict(fieldtype="Data", fieldname="name", label="Name")) @@ -233,17 +233,17 @@ class Meta(Document): return TABLE_DOCTYPES_FOR_DOCTYPE.get(fieldname) def get_field(self, fieldname): - """Return docfield from meta""" + """Return docfield from meta.""" return self._fields.get(fieldname) def has_field(self, fieldname): - """Returns True if fieldname exists""" + """Return True if fieldname exists.""" return fieldname in self._fields def get_label(self, fieldname): - """Get label of the given fieldname""" + """Return label of the given fieldname.""" if df := self.get_field(fieldname): return df.get("label") @@ -273,8 +273,8 @@ class Meta(Document): return search_fields def get_fields_to_fetch(self, link_fieldname=None): - """Returns a list of docfield objects for fields whose values - are to be fetched and updated for a particular link field + """Return a list of docfield objects for fields whose values + are to be fetched and updated for a particular link field. These fields are of type Data, Link, Text, Readonly and their fetch_from property is set as `link_fieldname`.`source_fieldname`""" @@ -565,7 +565,14 @@ class Meta(Document): self.high_permlevel_fields = [df for df in self.fields if df.permlevel > 0] return self.high_permlevel_fields - def get_permitted_fieldnames(self, parenttype=None, *, user=None, permission_type="read"): + def get_permitted_fieldnames( + self, + parenttype=None, + *, + user=None, + permission_type="read", + with_virtual_fields=True, + ): """Build list of `fieldname` with read perm level and all the higher perm levels defined. Note: If permissions are not defined for DocType, return all the fields with value. @@ -590,7 +597,9 @@ class Meta(Document): permitted_fieldnames.extend( df.fieldname - for df in self.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=True) + for df in self.get_fieldnames_with_value( + with_field_meta=True, with_virtual_fields=with_virtual_fields + ) if df.permlevel in permlevel_access ) return permitted_fieldnames @@ -615,7 +624,7 @@ class Meta(Document): return permissions def get_dashboard_data(self): - """Returns dashboard setup related to this doctype. + """Return dashboard setup related to this doctype. This method will return the `data` property in the `[doctype]_dashboard.py` file in the doctype's folder, along with any overrides or extensions @@ -677,15 +686,12 @@ class Meta(Document): dict(label=link.group, items=[link.parent_doctype or link.link_doctype]) ) + if not data.fieldname and link.link_fieldname: + data.fieldname = link.link_fieldname + if not link.is_child_table: - if link.link_fieldname != data.fieldname: - if data.fieldname: - data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname - else: - data.fieldname = link.link_fieldname + data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname elif link.is_child_table: - if not data.fieldname: - data.fieldname = link.link_fieldname data.internal_links[link.parent_doctype] = [link.table_fieldname, link.link_fieldname] def get_row_template(self): @@ -695,7 +701,7 @@ class Meta(Document): return self.get_web_template(suffix="_list") def get_web_template(self, suffix=""): - """Returns the relative path of the row template for this doctype""" + """Return the relative path of the row template for this doctype.""" module_name = frappe.scrub(self.module) doctype = frappe.scrub(self.name) template_path = frappe.get_module_path( diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 7a06e4d248..e775e0573b 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -334,7 +334,7 @@ def parse_naming_series( def has_custom_parser(e): - """Returns true if the naming series part has a custom parser""" + """Return True if the naming series part has a custom parser.""" return frappe.get_hooks("naming_series_variables", {}).get(e) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 9bad8f1028..ed7e529e4a 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -30,8 +30,7 @@ def update_document_title( **kwargs, ) -> str: """ - Update the name or title of a document. Returns `name` if document was renamed, - `docname` if renaming operation was queued. + Update the name or title of a document. Return `name` if document was renamed, `docname` if renaming operation was queued. :param doctype: DocType of the document :param docname: Name of the document @@ -384,7 +383,7 @@ def validate_rename( ): frappe.throw(_("You need write permission to rename")) - if not (force or ignore_permissions) and not meta.allow_rename: + if not force and not ignore_permissions and not meta.allow_rename: frappe.throw(_("{0} not allowed to be renamed").format(_(doctype))) # validate naming like it's done in doc.py diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index f8f5b21de4..153a42ec12 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -84,7 +84,7 @@ def render_include(content): def get_fetch_values(doctype, fieldname, value): - """Returns fetch value dict for the given object + """Return fetch value dict for the given object. :param doctype: Target doctype :param fieldname: Link fieldname selected diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py index ee8b4cd014..b58bdf235f 100644 --- a/frappe/modules/import_file.py +++ b/frappe/modules/import_file.py @@ -12,13 +12,10 @@ from frappe.utils import get_datetime, now def calculate_hash(path: str) -> str: - """Calculate md5 hash of the file in binary mode + """Calculate and return md5 hash of the file in binary mode. Args: path (str): Path to the file to be hashed - - Returns: - str: The calculated hash """ hash_md5 = hashlib.md5(usedforsecurity=False) with open(path, "rb") as f: @@ -82,8 +79,8 @@ def import_file_by_path( pre_process=None, ignore_version: bool = None, reset_permissions: bool = False, -): - """Import file from the given path +) -> bool: + """Import file from the given path. Some conditions decide if a file should be imported or not. Evaluation takes place in the order they are mentioned below. @@ -107,8 +104,7 @@ def import_file_by_path( ignore_version (bool, optional): ignore current version. Defaults to None. reset_permissions (bool, optional): reset permissions for the file. Defaults to False. - Returns: - [bool]: True if import takes place. False if it wasn't imported. + Return True if import takes place, False if it wasn't imported. """ try: docs = read_doc_from_file(path) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 40e3b32690..5d4cd6deac 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -22,10 +22,9 @@ doctype_python_modules = {} def export_module_json(doc: "Document", is_standard: bool, module: str) -> str | None: - """Make a folder for the given doc and add its json file (make it a standard - object that will be synced) + """Make a folder for the given doc and add its json file (make it a standard object that will be synced). - Returns the absolute file_path without the extension. + Return the absolute file_path without the extension. Eg: For exporting a Print Format "_Test Print Format 1", the return value will be `/home/gavin/frappe-bench/apps/frappe/frappe/core/print_format/_test_print_format_1/_test_print_format_1` """ @@ -181,12 +180,12 @@ def sync_customizations_for_doctype(data: dict, folder: str, filename: str = "") def scrub_dt_dn(dt: str, dn: str) -> tuple[str, str]: - """Returns in lowercase and code friendly names of doctype and name for certain types""" + """Return in lowercase and code friendly names of doctype and name for certain types.""" return scrub(dt), scrub(dn) def get_doc_path(module: str, doctype: str, name: str) -> str: - """Returns path of a doc in a module""" + """Return path of a doc in a module.""" return os.path.join(get_module_path(module), *scrub_dt_dn(doctype, name)) @@ -213,7 +212,7 @@ def export_doc(doctype, name, module=None): def get_doctype_module(doctype: str) -> str: - """Returns **Module Def** name of given doctype.""" + """Return **Module Def** name of given doctype.""" doctype_module_map = frappe.cache.get_value( "doctype_modules", generator=lambda: dict(frappe.qb.from_("DocType").select("name", "module").run()), @@ -226,7 +225,7 @@ def get_doctype_module(doctype: str) -> str: def load_doctype_module(doctype, module=None, prefix="", suffix=""): - """Returns the module object for given doctype. + """Return the module object for given doctype. Note: This will return the standard defined module object for the doctype irrespective of the `override_doctype_class` hook. diff --git a/frappe/oauth.py b/frappe/oauth.py index bf7abeb424..119e0d1771 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -395,9 +395,9 @@ class OAuthWebRequestValidator(RequestValidator): - OpenIDConnectHybrid """ if request.prompt == "login": - False + return False else: - True + return True def validate_silent_login(self, request): """Ensure session user has authorized silent OpenID login. diff --git a/frappe/patches/v11_0/replicate_old_user_permissions.py b/frappe/patches/v11_0/replicate_old_user_permissions.py index b66818d252..98ae220ff9 100644 --- a/frappe/patches/v11_0/replicate_old_user_permissions.py +++ b/frappe/patches/v11_0/replicate_old_user_permissions.py @@ -37,7 +37,7 @@ def execute(): def get_doctypes_to_skip(doctype, user): - """Returns doctypes to be skipped from user permission check""" + """Return doctypes to be skipped from user permission check.""" doctypes_to_skip = [] valid_perms = get_user_valid_perms(user) or [] for perm in valid_perms: diff --git a/frappe/patches/v13_0/queryreport_columns.py b/frappe/patches/v13_0/queryreport_columns.py index e9176952d4..61689c46de 100644 --- a/frappe/patches/v13_0/queryreport_columns.py +++ b/frappe/patches/v13_0/queryreport_columns.py @@ -7,7 +7,7 @@ import frappe def execute(): - """Convert Query Report json to support other content""" + """Convert Query Report json to support other content.""" records = frappe.get_all("Report", filters={"json": ["!=", ""]}, fields=["name", "json"]) for record in records: jstr = record["json"] diff --git a/frappe/permissions.py b/frappe/permissions.py index 1f0d182b07..a334cc5722 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -63,8 +63,8 @@ def has_permission( *, parent_doctype=None, ): - """Returns True if user has permission `ptype` for given `doctype`. - If `doc` is passed, it also checks user, share and owner permissions. + """Return True if user has permission `ptype` for given `doctype`. + If `doc` is passed, also check user, share and owner permissions. :param doctype: DocType to check permission for :param ptype: Permission Type to check @@ -159,7 +159,7 @@ def has_permission( def get_doc_permissions(doc, user=None, ptype=None): - """Returns a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`""" + """Return a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`""" if not user: user = frappe.session.user @@ -204,7 +204,7 @@ def get_doc_permissions(doc, user=None, ptype=None): def get_role_permissions(doctype_meta, user=None, is_owner=None): """ - Returns dict of evaluated role permissions like + Return dict of evaluated role permissions like: { "read": 1, "write": 0, @@ -272,7 +272,7 @@ def get_user_permissions(user): def has_user_permission(doc, user=None): - """Returns True if User is allowed to view considering User Permissions""" + """Return True if User is allowed to view considering User Permissions.""" from frappe.core.doctype.user_permission.user_permission import get_user_permissions user_permissions = get_user_permissions(user) @@ -374,7 +374,7 @@ def has_user_permission(doc, user=None): def has_controller_permissions(doc, ptype, user=None): - """Returns controller permissions if defined. None if not defined""" + """Return controller permissions if defined, None if not defined.""" if not user: user = frappe.session.user @@ -405,7 +405,7 @@ def get_valid_perms(doctype=None, user=None): doctypes_with_custom_perms = get_doctypes_with_custom_docperms() for p in perms: - if not p.parent in doctypes_with_custom_perms: + if p.parent not in doctypes_with_custom_perms: custom_perms.append(p) if doctype: @@ -415,7 +415,7 @@ def get_valid_perms(doctype=None, user=None): def get_all_perms(role): - """Returns valid permissions for a given role""" + """Return valid permissions for a given role.""" perms = frappe.get_all("DocPerm", fields="*", filters=dict(role=role)) custom_perms = frappe.get_all("Custom DocPerm", fields="*", filters=dict(role=role)) doctypes_with_custom_perms = frappe.get_all("Custom DocPerm", pluck="parent", distinct=True) @@ -462,7 +462,7 @@ def get_roles(user=None, with_standard=True): def get_doctype_roles(doctype, access_type="read"): - """Returns a list of roles that are allowed to access passed doctype.""" + """Return a list of roles that are allowed to access the given `doctype`.""" meta = frappe.get_meta(doctype) return [d.role for d in meta.get("permissions") if d.get(access_type)] @@ -474,7 +474,7 @@ def get_perms_for(roles, perm_doctype="DocPerm"): def get_doctypes_with_custom_docperms(): - """Returns all the doctypes with Custom Docperms""" + """Return all the doctypes with Custom Docperms.""" doctypes = frappe.get_all("Custom DocPerm", fields=["parent"], distinct=1) return [d.parent for d in doctypes] @@ -655,22 +655,18 @@ def get_doc_name(doc): def allow_everything(): - """ - returns a dict with access to everything - eg. {"read": 1, "write": 1, ...} - """ + """Return a dict with access to everything, eg. {"read": 1, "write": 1, ...}.""" return {ptype: 1 for ptype in rights} def get_allowed_docs_for_doctype(user_permissions, doctype): - """Returns all the docs from the passed user_permissions that are - allowed under provided doctype""" + """Return all the docs from the passed `user_permissions` that are allowed under provided doctype.""" return filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=False) def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=True): - """Returns all the docs from the passed user_permissions that are - allowed under provided doctype along with default doc value if with_default_doc is set""" + """Return all the docs from the passed `user_permissions` that are + allowed under provided doctype along with default doc value if `with_default_doc` is set.""" allowed_doc = [] default_doc = None for doc in user_permissions: diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index daf1a20221..021f79ca93 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -168,7 +168,7 @@ "idx": 1, "links": [], "max_attachments": 3, - "modified": "2023-08-28 22:19:23.720332", + "modified": "2023-12-08 15:52:37.525003", "modified_by": "Administrator", "module": "Printing", "name": "Letter Head", @@ -192,7 +192,7 @@ } ], "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 8058700dbf..a58694f46d 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -112,14 +112,15 @@ "label": "HTML", "oldfieldname": "html", "oldfieldtype": "Text Editor", - "options": "HTML" + "options": "Jinja" }, { "depends_on": "raw_printing", "description": "Any string-based printer languages can be used. Writing raw commands requires knowledge of the printer's native language provided by the printer manufacturer. Please refer to the developer manual provided by the printer manufacturer on how to write their native commands. These commands are rendered on the server side using the Jinja Templating Language.", "fieldname": "raw_commands", "fieldtype": "Code", - "label": "Raw Commands" + "label": "Raw Commands", + "options": "Jinja" }, { "depends_on": "eval:!doc.custom_format", @@ -259,7 +260,7 @@ "icon": "fa fa-print", "idx": 1, "links": [], - "modified": "2023-08-28 20:25:09.660073", + "modified": "2023-12-12 19:59:37.133301", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py index eda084968a..4110f849ec 100644 --- a/frappe/printing/doctype/print_format/print_format.py +++ b/frappe/printing/doctype/print_format/print_format.py @@ -48,6 +48,7 @@ class PrintFormat(Document): show_section_headings: DF.Check standard: DF.Literal["No", "Yes"] # end: auto-generated types + def onload(self): templates = frappe.get_all( "Print Format Field Template", @@ -66,7 +67,8 @@ class PrintFormat(Document): if ( self.standard == "Yes" and not frappe.local.conf.get("developer_mode") - and not (frappe.flags.in_migrate or frappe.flags.in_test) + and not frappe.flags.in_migrate + and not frappe.flags.in_test ): frappe.throw(frappe._("Standard Print Format cannot be updated")) diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.py b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py index e83850a8c4..4f3f5d53b8 100644 --- a/frappe/printing/doctype/print_format_field_template/print_format_field_template.py +++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py @@ -23,7 +23,7 @@ class PrintFormatFieldTemplate(Document): template_file: DF.Data | None # end: auto-generated types def validate(self): - if self.standard and not (frappe.conf.developer_mode or frappe.flags.in_patch): + if self.standard and not frappe.conf.developer_mode and not frappe.flags.in_patch: frappe.throw(_("Enable developer mode to create a standard Print Template")) def before_insert(self): diff --git a/frappe/printing/doctype/print_style/print_style.py b/frappe/printing/doctype/print_style/print_style.py index e3141e7472..c1ca66242c 100644 --- a/frappe/printing/doctype/print_style/print_style.py +++ b/frappe/printing/doctype/print_style/print_style.py @@ -24,7 +24,8 @@ class PrintStyle(Document): if ( self.standard == 1 and not frappe.local.conf.get("developer_mode") - and not (frappe.flags.in_import or frappe.flags.in_test) + and not frappe.flags.in_import + and not frappe.flags.in_test ): frappe.throw(frappe._("Standard Print Style cannot be changed. Please duplicate to edit.")) diff --git a/frappe/public/js/form_builder/components/Field.vue b/frappe/public/js/form_builder/components/Field.vue index 26d488cdf8..9514bbea8e 100644 --- a/frappe/public/js/form_builder/components/Field.vue +++ b/frappe/public/js/form_builder/components/Field.vue @@ -30,7 +30,7 @@ const label_input = ref(null); const hovered = ref(false); const selected = computed(() => store.selected(props.field.df.name)); const component = computed(() => { - return props.field.df.fieldtype.replace(" ", "") + "Control"; + return props.field.df.fieldtype.replaceAll(" ", "") + "Control"; }); function remove_field() { diff --git a/frappe/public/js/form_builder/components/FieldProperties.vue b/frappe/public/js/form_builder/components/FieldProperties.vue index 5f903ed36c..159d8eb43f 100644 --- a/frappe/public/js/form_builder/components/FieldProperties.vue +++ b/frappe/public/js/form_builder/components/FieldProperties.vue @@ -86,7 +86,7 @@ let docfield_df = computed(() => {
{ let aspect_ratio_buttons = computed(() => { return [ { - label: __("1:1"), + label: __("1:1", null, "Image Cropper"), value: 1, }, { - label: __("4:3"), + label: __("4:3", null, "Image Cropper"), value: 4 / 3, }, { - label: __("16:9"), + label: __("16:9", null, "Image Cropper"), value: 16 / 9, }, { - label: __("Free"), + label: __("Free", null, "Image Cropper"), value: NaN, }, ]; diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index e7949ad9e2..992805025a 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -63,7 +63,6 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro on_attach_doc_image() { this.set_upload_options(); this.upload_options.restrictions.allowed_file_types = ["image/*"]; - this.upload_options.restrictions.crop_image_aspect_ratio = 1; this.file_uploader = new frappe.ui.FileUploader(this.upload_options); } set_upload_options() { diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 42832ee6a1..db040d7c58 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -160,6 +160,7 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex JSON: "ace/mode/json", Golang: "ace/mode/golang", Go: "ace/mode/golang", + Jinja: "ace/mode/django", }; const language = this.df.options; diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 4d6e1bc426..827a9be315 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -13,30 +13,41 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f if (!this.disp_area) { return; } + if (!this.map_id) { + this.map_id = frappe.dom.get_unique_id(); + this.map_area = $( + `
+
+
` + ); - this.map_id = frappe.dom.get_unique_id(); - this.map_area = $( - `
-
-
` - ); + $(this.disp_area).html(this.map_area); + } - $(this.disp_area).html(this.map_area); + // show again on idempotent invocations $(this.disp_area).removeClass("like-disabled-input"); $(this.disp_area).css("display", "block"); if (this.frm) { - this.make_map(value); + this.make_map(); + if (value) { + this.bind_leaflet_data(value); + } } else { $(document).on("frappe.ui.Dialog:shown", () => { this.make_map(); + if (value) { + this.bind_leaflet_data(value); + } }); } } make_map(value) { - this.customize_draw_controls(); - this.bind_leaflet_map(); + if (!this.map) { + this.customize_draw_controls(); + this.bind_leaflet_map(); + } if (this.disabled) { this.map.dragging.disable(); this.map.touchZoom.disable(); @@ -47,17 +58,17 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f this.map.zoomControl.remove(); } else { this.bind_leaflet_draw_control(); - this.bind_leaflet_event_listeners(); - this.bind_leaflet_locate_control(); - this.bind_leaflet_data(value); + if (!this.bound_event_listeners) { + this.bind_leaflet_event_listeners(); + } + if (!this.locate_control) { + this.bind_leaflet_locate_control(); + } } } bind_leaflet_data(value) { /* render raw value from db into map */ - if (!this.map || !value) { - return; - } this.clear_editable_layers(); const data_layers = new L.FeatureGroup().addLayer( @@ -159,15 +170,14 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f } bind_leaflet_draw_control() { - if ( - !frappe.perm.has_perm(this.doctype, this.df.permlevel, "write", this.doc) || - this.df.read_only - ) { - return; + if (!this.draw_control) { + this.draw_control = this.get_leaflet_controls(); + } + if (this.disp_status == "Write") { + this.draw_control.addTo(this.map); + } else { + this.draw_control.remove(); } - - this.draw_control = this.get_leaflet_controls(); - this.map.addControl(this.draw_control); } get_leaflet_controls() { @@ -205,6 +215,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f } bind_leaflet_event_listeners() { + this.bound_event_listeners = true; this.map.on("draw:created", (e) => { var type = e.layerType, layer = e.layer; diff --git a/frappe/public/js/frappe/form/controls/int.js b/frappe/public/js/frappe/form/controls/int.js index ffd3fe5fc3..122b43b498 100644 --- a/frappe/public/js/frappe/form/controls/int.js +++ b/frappe/public/js/frappe/form/controls/int.js @@ -2,21 +2,13 @@ frappe.ui.form.ControlInt = class ControlInt extends frappe.ui.form.ControlData static trigger_change_on_input_event = false; make() { super.make(); - // $(this.label_area).addClass('pull-right'); - // $(this.disp_area).addClass('text-right'); } make_input() { - var me = this; super.make_input(); - this.$input - // .addClass("text-right") - .on("focus", function () { - setTimeout(function () { - if (!document.activeElement) return; - document.activeElement.select(); - }, 100); - return false; - }); + this.$input.on("focus", () => { + document.activeElement?.select?.(); + return false; + }); } validate(value) { return this.parse(value); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 46c17343f8..4f42b3ecaa 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -346,13 +346,20 @@ frappe.ui.form.Form = class FrappeForm { // using $.each to preserve df via closure $.each(table_fields, function (i, df) { - frappe.model.on(df.options, "*", function (fieldname, value, doc) { - if (doc.parent == me.docname && doc.parentfield === df.fieldname) { - me.dirty(); - me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc); - return me.script_manager.trigger(fieldname, doc.doctype, doc.name); + frappe.model.on( + df.options, + "*", + function (fieldname, value, doc, skip_dirty_trigger = false) { + if (doc.parent == me.docname && doc.parentfield === df.fieldname) { + if (!skip_dirty_trigger) { + me.dirty(); + } + + me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc); + return me.script_manager.trigger(fieldname, doc.doctype, doc.name); + } } - }); + ); }); } @@ -1276,10 +1283,6 @@ frappe.ui.form.Form = class FrappeForm { frappe.set_route("print", this.doctype, this.doc.name); } - show_audit_trail() { - frappe.set_route("audit-trail"); - } - navigate_records(prev) { let filters, sort_field, sort_order; let list_view = frappe.get_list_view(this.doctype); diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index aee19cd2f5..8e98c1042c 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -149,6 +149,10 @@ frappe.form.formatters = { var original_value = value; let link_title = frappe.utils.get_link_title(doctype, value); + if (link_title === value) { + link_title = null; + } + if (value && value.match && value.match(/^['"].*['"]$/)) { value.replace(/^.(.*).$/, "$1"); } diff --git a/frappe/public/js/frappe/form/sidebar/user_image.js b/frappe/public/js/frappe/form/sidebar/user_image.js index ae6167e184..b022b157b9 100644 --- a/frappe/public/js/frappe/form/sidebar/user_image.js +++ b/frappe/public/js/frappe/form/sidebar/user_image.js @@ -74,7 +74,7 @@ frappe.ui.form.setup_user_image_event = function (frm) { } field.$input.trigger("attach_doc_image"); // close sidebar - frm.page.close_sidebar(); + frm.page.close_sidebar?.(); } else { /// on remove event for a sidebar image wrapper remove attach file. frm.attachments.remove_attachment_by_filename( diff --git a/frappe/public/js/frappe/form/tab.js b/frappe/public/js/frappe/form/tab.js index c81985b13a..4132808871 100644 --- a/frappe/public/js/frappe/form/tab.js +++ b/frappe/public/js/frappe/form/tab.js @@ -21,7 +21,7 @@ export default class Tab { data-fieldname="${this.df.fieldname}" href="#${id}" role="tab" - aria-controls="${this.label}"> + aria-controls="${id}"> ${__(this.label)} diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 443b608839..a1a4893061 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -506,7 +506,7 @@ frappe.ui.form.Toolbar = class Toolbar { this.page.add_menu_item( __("View Audit Trail"), function () { - me.frm.show_audit_trail(); + frappe.set_route("audit-trail"); }, true ); diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index e7c0cffda8..0aaa1f1e31 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -44,7 +44,7 @@ frappe.views.BaseList = class BaseList { this.user_settings = frappe.get_user_settings(this.doctype); this.start = 0; - this.page_length = 20; + this.page_length = frappe.is_large_screen() ? 100 : 20; this.data = []; this.method = "frappe.desk.reportview.get"; @@ -831,7 +831,7 @@ class FilterArea { value = "%" + value + "%"; } filters.push([ - this.list_view.doctype, + field.df.doctype || this.list_view.doctype, field.df.fieldname, field.df.condition || "=", value, diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index a0271967b4..22cf51166d 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -209,28 +209,36 @@ export default class BulkOperations { submit_or_cancel(docnames, action = "submit", done = null) { action = action.toLowerCase(); - frappe - .call({ - method: "frappe.desk.doctype.bulk_update.bulk_update.submit_cancel_or_update_docs", - args: { - doctype: this.doctype, - action: action, - docnames: docnames, - }, + const task_id = Math.random().toString(36).slice(-5); + frappe.realtime.task_subscribe(task_id); + return frappe + .xcall("frappe.desk.doctype.bulk_update.bulk_update.submit_cancel_or_update_docs", { + doctype: this.doctype, + action: action, + docnames: docnames, + task_id: task_id, }) - .then((r) => { - let failed = r.message; - if (!failed) failed = []; - - if (failed.length && !r._server_messages) { - frappe.throw( - __("Cannot {0} {1}", [action, failed.map((f) => f.bold()).join(", ")]) - ); + .then((failed_docnames) => { + if (failed_docnames?.length) { + const comma_separated_records = frappe.utils.comma_and(failed_docnames); + switch (action) { + case "submit": + frappe.throw(__("Cannot submit {0}.", [comma_separated_records])); + break; + case "cancel": + frappe.throw(__("Cannot cancel {0}.", [comma_separated_records])); + break; + default: + frappe.throw(__("Cannot {0} {1}.", [action, comma_separated_records])); + } } - if (failed.length < docnames.length) { + if (failed_docnames?.length < docnames.length) { frappe.utils.play_sound(action); if (done) done(); } + }) + .finally(() => { + frappe.realtime.task_unsubscribe(task_id); }); } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 58f8a7606c..929c7b9812 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -358,11 +358,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); } - this.columns.push({ - type: "Tag", - }); - - // 2nd column: Status indicator + // 3rd column: Status indicator if (frappe.has_indicator(this.doctype)) { // indicator this.columns.push({ @@ -407,6 +403,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.columns = this.columns.slice(0, this.list_view_settings.total_fields || total_fields); + // 2nd column: tag - normally hidden doesn't count towards total_fields + this.columns.splice(1, 0, { + type: "Tag", + }); + if ( !this.settings.hide_name_column && this.meta.title_field && @@ -426,10 +427,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { let fields_order = []; let fields = JSON.parse(this.list_view_settings.fields); - //title and tags field is fixed + // title field is fixed fields_order.push(this.columns[0]); - fields_order.push(this.columns[1]); - this.columns.splice(0, 2); + this.columns.splice(0, 1); for (let fld in fields) { for (let col in this.columns) { diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index 4797756baa..ea5d64334d 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -214,5 +214,5 @@ class RealTimeClient { frappe.realtime = new RealTimeClient(); -// backward compatbility +// backward compatibility frappe.socketio = frappe.realtime; diff --git a/frappe/public/js/frappe/ui/capture.js b/frappe/public/js/frappe/ui/capture.js index 63095c2b7f..1d40f50485 100644 --- a/frappe/public/js/frappe/ui/capture.js +++ b/frappe/public/js/frappe/ui/capture.js @@ -234,7 +234,7 @@ frappe.ui.Capture = class { setup_remove_action() { let me = this; - let elements = this.$template[0].getElementsByClassName("capture-remove-btn"); + let elements = Array.from(this.$template[0].getElementsByClassName("capture-remove-btn")); elements.forEach((el) => { el.onclick = () => { diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index b2ac9a7769..515b693482 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -180,11 +180,9 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.footer.removeClass("hide"); this.has_primary_action = true; var me = this; - return this.get_primary_btn() - .removeClass("hide") - .html(label) - .off("click") - .on("click", function () { + const primary_btn = this.get_primary_btn().removeClass("hide").html(label); + if (typeof click == "function") { + primary_btn.off("click").on("click", function () { me.primary_action_fulfilled = true; // get values and send it // as first parameter to click callback @@ -193,6 +191,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { if (!values) return; click && click.apply(me, [values]); }); + } + return primary_btn; } set_secondary_action(click) { diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 20bb7aadbd..e80dff942a 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -17,7 +17,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } make() { - var me = this; + let me = this; if (this.fields) { super.make(); this.refresh(); @@ -63,7 +63,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } catch_enter_as_submit() { - var me = this; + let me = this; $(this.body) .find('input[type="text"], input[type="password"], select') .keypress(function (e) { @@ -77,7 +77,8 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } get_input(fieldname) { - var field = this.fields_dict[fieldname]; + let field = this.fields_dict[fieldname]; + if (!field) return ""; return $(field.txt ? field.txt : field.input); } @@ -86,14 +87,14 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } get_values(ignore_errors, check_invalid) { - var ret = {}; - var errors = []; + let ret = {}; + let errors = []; let invalid = []; - for (var key in this.fields_dict) { - var f = this.fields_dict[key]; + for (let key in this.fields_dict) { + let f = this.fields_dict[key]; if (f.get_value) { - var v = f.get_value(); + let v = f.get_value(); if (f.df.reqd && is_null(typeof v === "string" ? strip_html(v) : v)) errors.push(__(f.df.label)); @@ -141,13 +142,13 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } get_value(key) { - var f = this.fields_dict[key]; + let f = this.fields_dict[key]; return f && (f.get_value ? f.get_value() : null); } set_value(key, val) { return new Promise((resolve) => { - var f = this.fields_dict[key]; + let f = this.fields_dict[key]; if (f) { f.set_value(val).then(() => { f.set_input?.(val); @@ -170,7 +171,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { set_values(dict) { let promises = []; - for (var key in dict) { + for (let key in dict) { if (this.fields_dict[key]) { promises.push(this.set_value(key, dict[key])); } @@ -180,8 +181,8 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { } clear() { - for (var key in this.fields_dict) { - var f = this.fields_dict[key]; + for (let key in this.fields_dict) { + let f = this.fields_dict[key]; if (f && f.set_input) { f.set_input(f.df["default"] || ""); } diff --git a/frappe/public/js/frappe/ui/page.html b/frappe/public/js/frappe/ui/page.html index 19b0424bf4..55083e1b9a 100644 --- a/frappe/public/js/frappe/ui/page.html +++ b/frappe/public/js/frappe/ui/page.html @@ -4,7 +4,7 @@
- +
@@ -37,7 +37,7 @@