diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e783fbbeb3..0000000000 --- a/.flake8 +++ /dev/null @@ -1,75 +0,0 @@ -[flake8] -ignore = - B001, - B007, - B009, - B010, - B950, - E101, - E111, - E114, - E116, - E117, - E121, - E122, - E123, - E124, - E125, - E126, - E127, - E128, - E131, - E201, - E202, - E203, - E211, - E221, - E222, - E223, - E224, - E225, - E226, - E228, - E231, - E241, - E242, - E251, - E261, - E262, - E265, - E266, - E271, - E272, - E273, - E274, - E301, - E302, - E303, - E305, - E306, - E402, - E501, - E502, - E701, - E702, - E703, - E741, - F401, - F403, - F405, - W191, - W291, - W292, - W293, - W391, - W503, - W504, - E711, - E129, - F841, - E713, - E712, - B028, - -max-line-length = 200 -exclude=,test_*.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index addb35ee70..11ecbd391b 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -43,3 +43,9 @@ fa6dc03cc87ad74e11609e7373078366fdcb3e1b # Bulk refactor with sourcery c35476256f85271fb57584eb0a26f4d9def3caf4 + +# black+isort -> ruff +de9ac897482013f5464a05f3c171da0072619c3a + +# flake8 -> ruff + ruff config update +26ae0f3460f29116e0c083d57eee9f33763237ea diff --git a/.github/helper/ci.py b/.github/helper/ci.py index 1f35d0b18d..9c4e1379bb 100644 --- a/.github/helper/ci.py +++ b/.github/helper/ci.py @@ -1,5 +1,6 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See LICENSE +import json import os from pathlib import Path @@ -85,6 +86,7 @@ if __name__ == "__main__": app = "frappe" site = os.environ.get("SITE") or "test_site" use_orchestrator = bool(os.environ.get("ORCHESTRATOR_URL")) + with_coverage = json.loads(os.environ.get("WITH_COVERAGE", "true").lower()) build_number = 1 total_builds = 1 @@ -98,7 +100,7 @@ if __name__ == "__main__": except Exception: pass - with CodeCoverage(with_coverage=True, app=app): + with CodeCoverage(with_coverage=with_coverage, app=app): if use_orchestrator: from frappe.parallel_test_runner import ParallelTestWithOrchestrator diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index b541583fd6..7eb209cbde 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -1,7 +1,7 @@ import sys -import requests from urllib.parse import urlparse +import requests WEBSITE_REPOS = [ "erpnext_com", @@ -36,11 +36,7 @@ def is_documentation_link(word: str) -> bool: def contains_documentation_link(body: str) -> bool: - return any( - is_documentation_link(word) - for line in body.splitlines() - for word in line.split() - ) + return any(is_documentation_link(word) for line in body.splitlines() for word in line.split()) def check_pull_request(number: str) -> "tuple[int, str]": @@ -53,12 +49,7 @@ def check_pull_request(number: str) -> "tuple[int, str]": head_sha = (payload.get("head") or {}).get("sha") body = (payload.get("body") or "").lower() - if ( - not title.startswith("feat") - or not head_sha - or "no-docs" in body - or "backport" in body - ): + if not title.startswith("feat") or not head_sha or "no-docs" in body or "backport" in body: return 0, "Skipping documentation checks... 🏃" if contains_documentation_link(body): diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py index ebca901c95..121b9ff0d6 100644 --- a/.github/helper/roulette.py +++ b/.github/helper/roulette.py @@ -6,11 +6,11 @@ import subprocess import sys import time import urllib.request -from functools import lru_cache +from functools import cache from urllib.error import HTTPError -@lru_cache(maxsize=None) +@cache def fetch_pr_data(pr_number, repo, endpoint=""): api_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}" @@ -83,9 +83,7 @@ def is_ci(file): def is_frontend_code(file): - return file.lower().endswith( - (".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue", ".html") - ) + return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue", ".html")) def is_docs(file): diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 72f661d3e1..96a319f97e 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,9 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +pattern = re.compile( + r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)" +) words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") @@ -10,44 +12,50 @@ starts_with_f_pattern = re.compile(r"_\(f") # skip first argument files = sys.argv[1:] -files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))] +files_to_scan = [_file for _file in files if _file.endswith((".py", ".js"))] for _file in files_to_scan: - with open(_file, 'r') as f: - print(f'Checking: {_file}') + with open(_file) as f: + print(f"Checking: {_file}") file_lines = f.readlines() for line_number, line in enumerate(file_lines, 1): - if 'frappe-lint: disable-translate' in line: + if "frappe-lint: disable-translate" in line: continue if start_matches := start_pattern.search(line): if starts_with_f := starts_with_f_pattern.search(line): if has_f_string := f_string_pattern.search(line): errors_encounter += 1 - print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}') + print( + f"\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}" + ) continue match = pattern.search(line) error_found = False - if not match and line.endswith((',\n', '[\n')): + if not match and line.endswith((",\n", "[\n")): # concat remaining text to validate multiline pattern - line = "".join(file_lines[line_number - 1:]) - line = line[start_matches.start() + 1:] + line = "".join(file_lines[line_number - 1 :]) + line = line[start_matches.start() + 1 :] match = pattern.match(line) if not match: error_found = True - print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}') + print(f"\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}") if not error_found and not words_pattern.search(line): error_found = True - print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}') + print( + f"\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}" + ) if error_found: errors_encounter += 1 if errors_encounter > 0: - print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.') + print( + '\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.' + ) sys.exit(1) else: - print('\nGood To Go!') + print("\nGood To Go!") diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 32ececc78a..bfc29812e4 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -23,4 +23,4 @@ jobs: with: python-version: '3.10' cache: pip - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 39dc99374e..f1225de962 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -43,6 +43,8 @@ jobs: needs: checkrun if: ${{ needs.checkrun.outputs.build == 'strawberry' }} timeout-minutes: 60 + env: + NODE_ENV: "production" strategy: fail-fast: false @@ -142,6 +144,7 @@ jobs: SITE: test_site CI_BUILD_ID: ${{ github.run_id }} BUILD_NUMBER: ${{ matrix.container }} + WITH_COVERAGE: ${{ github.event_name != 'pull_request' }} FRAPPE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }} TOTAL_BUILDS: 2 COVERAGE_RCFILE: /home/runner/frappe-bench/apps/frappe/.coveragerc @@ -159,6 +162,7 @@ jobs: - name: Upload coverage data uses: actions/upload-artifact@v3 + if: github.event_name != 'pull_request' with: name: coverage-${{ matrix.db }}-${{ matrix.container }} path: /home/runner/frappe-bench/sites/coverage.xml @@ -183,7 +187,7 @@ jobs: name: Coverage Wrap Up needs: [test, checkrun] runs-on: ubuntu-latest - if: ${{ needs.checkrun.outputs.build == 'strawberry' }} + if: ${{ needs.checkrun.outputs.build == 'strawberry' && github.event_name != 'pull_request' }} steps: - name: Clone uses: actions/checkout@v4 @@ -192,9 +196,10 @@ jobs: uses: actions/download-artifact@v3 - name: Upload coverage data - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: name: Server + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true flags: server diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 33a7cde8f4..629f47443a 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -42,6 +42,8 @@ jobs: needs: checkrun if: ${{ needs.checkrun.outputs.build == 'strawberry' && github.repository_owner == 'frappe' }} timeout-minutes: 60 + env: + NODE_ENV: "production" strategy: fail-fast: false @@ -147,6 +149,7 @@ jobs: CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb - name: Stop server and wait for coverage file + if: github.event_name != 'pull_request' run: | ps -ef | grep "[f]rappe serve" | awk '{print $2}' | xargs kill -s SIGINT sleep 5 @@ -154,12 +157,14 @@ jobs: - name: Upload JS coverage data uses: actions/upload-artifact@v3 + if: github.event_name != 'pull_request' with: name: coverage-js-${{ matrix.container }} path: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/clover.xml - name: Upload python coverage data uses: actions/upload-artifact@v3 + if: github.event_name != 'pull_request' with: name: coverage-py-${{ matrix.container }} path: /home/runner/frappe-bench/sites/coverage.xml @@ -191,7 +196,7 @@ jobs: coverage: name: Coverage Wrap Up needs: [test, checkrun] - if: ${{ needs.checkrun.outputs.build == 'strawberry' }} + if: ${{ needs.checkrun.outputs.build == 'strawberry' && github.event_name != 'pull_request' }} runs-on: ubuntu-latest steps: - name: Clone @@ -201,16 +206,17 @@ jobs: uses: actions/download-artifact@v3 - name: Upload python coverage data - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: name: UIBackend + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true files: ./coverage-py-1/coverage.xml,./coverage-py-2/coverage.xml,./coverage-py-3/coverage.xml flags: server-ui - name: Upload JS coverage data - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: name: Cypress token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.mergify.yml b/.mergify.yml index 7c524c5e93..66160a79e4 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -21,27 +21,6 @@ pull_request_rules: @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch. https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch - - name: Automatic merge on CI success and review - conditions: - - label!=dont-merge - - label!=squash - - "#approved-reviews-by>=1" - actions: - merge: - method: merge - - name: Automatic squash on CI success and review - conditions: - - label!=dont-merge - - label=squash - - "#approved-reviews-by>=1" - actions: - merge: - method: squash - commit_message_template: | - {{ title }} (#{{ number }}) - - {{ body }} - - name: backport to develop conditions: - label="backport develop" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7161bb90ae..bc20e86eba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,16 +20,15 @@ repos: - id: check-yaml - id: debug-statements - - repo: https://github.com/asottile/pyupgrade - rev: v3.9.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.0 hooks: - - id: pyupgrade - args: ['--py310-plus'] + - id: ruff + name: "Run ruff linter and apply fixes" + args: ["--fix"] - - repo: https://github.com/frappe/black - rev: 951ccf4d5bb0d692b457a5ebc4215d755618eb68 - hooks: - - id: black + - id: ruff-format + name: "Format Python code" - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.7.1 @@ -67,17 +66,6 @@ repos: frappe/public/js/lib/.* )$ - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - additional_dependencies: ['flake8-bugbear',] - ci: autoupdate_schedule: weekly skip: [] diff --git a/babel_extractors.csv b/babel_extractors.csv index 133a13e736..37858338ba 100644 --- a/babel_extractors.csv +++ b/babel_extractors.csv @@ -6,4 +6,4 @@ hooks.py,frappe.gettext.extractors.navbar.extract **/report/*/*.json,frappe.gettext.extractors.report.extract **.py,frappe.gettext.extractors.python.extract **.js,frappe.gettext.extractors.javascript.extract -**.html,frappe.gettext.extractors.jinja2.extract \ No newline at end of file +**.html,frappe.gettext.extractors.html_template.extract \ No newline at end of file diff --git a/cypress/integration/form_builder.js b/cypress/integration/form_builder.js index 4f7a1c5b1f..04f698a46a 100644 --- a/cypress/integration/form_builder.js +++ b/cypress/integration/form_builder.js @@ -314,7 +314,7 @@ context("Form Builder", () => { .should("contain", "cannot be hidden and mandatory without any default value"); }); - it("Undo/Redo", () => { + it.skip("Undo/Redo", () => { cy.visit(`/app/doctype/${doctype_name}`); cy.findByRole("tab", { name: "Form" }).click(); diff --git a/frappe/__init__.py b/frappe/__init__.py index ec55af2081..ff953cce26 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -17,6 +17,7 @@ import inspect import json import os import re +import traceback import warnings from collections.abc import Callable from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, overload @@ -24,6 +25,7 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, overload import click from werkzeug.local import Local, release_local +import frappe from frappe.query_builder import ( get_query, get_query_builder, @@ -130,9 +132,45 @@ def _lt(msg: str, lang: str | None = None, context: str | None = None): Note: Result is not guaranteed to equivalent to pure strings for all operations. """ - from frappe.translate import LazyTranslate + return _LazyTranslate(msg, lang, context) - return LazyTranslate(msg, lang, context) + +@functools.total_ordering +class _LazyTranslate: + __slots__ = ("msg", "lang", "context") + + def __init__(self, msg: str, lang: str | None = None, context: str | None = None) -> None: + self.msg = msg + self.lang = lang + self.context = context + + @property + def value(self) -> str: + return _(str(self.msg), self.lang, self.context) + + def __str__(self): + return self.value + + def __add__(self, other): + if isinstance(other, str | _LazyTranslate): + return self.value + str(other) + raise NotImplementedError + + def __radd__(self, other): + if isinstance(other, str | _LazyTranslate): + return str(other) + self.value + return NotImplementedError + + def __repr__(self) -> str: + return f"'{self.value}'" + + # NOTE: it's required to override these methods and raise error as default behaviour will + # return `False` in all cases. + def __eq__(self, other): + raise NotImplementedError + + def __lt__(self, other): + raise NotImplementedError def as_unicode(text, encoding: str = "utf-8") -> str: @@ -202,19 +240,8 @@ if TYPE_CHECKING: # pragma: no cover # end: static analysis hack -def init( - site: str, sites_path: str = ".", new_site: bool = False, force=False, site_ready: bool = True -) -> None: - """ - Initialize frappe for the current site. Reset thread locals `frappe.local` - - :param site: Site name. - :param sites_path: Path to sites directory. - :param new_site: Sets a flag to indicate a new site. - :param force: Force initialization if already previously run. - :param site_ready: Any init during site installation should set this to False. - - """ +def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) -> None: + """Initialize frappe for the current site. Reset thread locals `frappe.local`""" if getattr(local, "initialised", None) and not force: return @@ -272,28 +299,20 @@ def init( local.qb = get_query_builder(local.conf.db_type) local.qb.get_query = get_query setup_redis_cache_connection() + setup_module_map(include_all_apps=not (frappe.request or frappe.job or frappe.flags.in_migrate)) if not _qb_patched.get(local.conf.db_type): patch_query_execute() patch_query_aggregation() - if site: - setup_module_map(site_ready) - local.initialised = True - # Set the user as database name if not set in config - if local.conf and local.conf.db_name is not None and local.conf.db_user is None: - local.conf.db_user = local.conf.db_name - -def connect( - site: str | None = None, db_name: str | None = None, set_admin_as_user: bool = True -) -> None: +def connect(site: str | None = None, db_name: str | None = None, set_admin_as_user: bool = True) -> None: """Connect to site database instance. :param site: (Deprecated) If site is given, calls `frappe.init`. - :param db_name: Optional. Will use from `site_config.json`. + :param db_name: (Deprecated) Optional. Will use from `site_config.json`. :param set_admin_as_user: Set Administrator as current user. """ from frappe.database import get_db @@ -306,13 +325,24 @@ def connect( "Instead, explicitly invoke frappe.init(site) prior to calling frappe.connect(), if initializing the site is necessary." ) init(site) + if db_name: + from frappe.utils.deprecations import deprecation_warning + + deprecation_warning( + "Calling frappe.connect with the db_name argument is deprecated and will be removed in next major version. " + "Instead, explicitly invoke frappe.init(site) with the right config prior to calling frappe.connect(), if necessary." + ) + + assert db_name or local.conf.db_user, "site must be fully initialized, db_user missing" + assert db_name or local.conf.db_name, "site must be fully initialized, db_name missing" + assert local.conf.db_password, "site must be fully initialized, db_password missing" local.db = get_db( host=local.conf.db_host, port=local.conf.db_port, - user=local.conf.db_user or db_name or local.conf.db_name, + user=local.conf.db_user or db_name, password=local.conf.db_password, - cur_db_name=db_name or local.conf.db_name, + cur_db_name=local.conf.db_name or db_name, ) if set_admin_as_user: set_user("Administrator") @@ -400,6 +430,21 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None) os.environ.get("FRAPPE_DB_PORT") or config.get("db_port") or db_default_ports(config["db_type"]) ) + # Set the user as database name if not set in config + config["db_user"] = os.environ.get("FRAPPE_DB_USER") or config.get("db_user") or config.get("db_name") + + # Allow externally extending the config with hooks + if extra_config := config.get("extra_config"): + if isinstance(extra_config, str): + extra_config = [extra_config] + for hook in extra_config: + try: + module, method = hook.rsplit(".", 1) + config |= getattr(importlib.import_module(module), method)() + except Exception: + print(f"Config hook {hook} failed") + traceback.print_exc() + return config @@ -615,11 +660,18 @@ def throw( is_minimizable: bool = False, wide: bool = False, as_list: bool = False, + primary_action=None, ) -> None: """Throw execption and show message (`msgprint`). :param msg: Message. - :param exc: Exception class. Default `frappe.ValidationError`""" + :param exc: Exception class. Default `frappe.ValidationError` + :param title: [optional] Message title. Default: "Message". + :param is_minimizable: [optional] Allow users to minimize the modal + :param wide: [optional] Show wide modal + :param as_list: [optional] If `msg` is a list, render as un-ordered list. + :param primary_action: [optional] Bind a primary server/client side action. + """ msgprint( msg, raise_exception=exc, @@ -628,6 +680,7 @@ def throw( is_minimizable=is_minimizable, wide=wide, as_list=as_list, + primary_action=primary_action, ) @@ -872,9 +925,7 @@ def is_whitelisted(method): is_guest = session["user"] == "Guest" if method not in whitelisted or is_guest and method not in guest_methods: summary = _("You are not permitted to access this resource.") - detail = _("Function {0} is not whitelisted.").format( - bold(f"{method.__module__}.{method.__name__}") - ) + detail = _("Function {0} is not whitelisted.").format(bold(f"{method.__module__}.{method.__name__}")) msg = f"
{summary}{detail}
" throw(msg, PermissionError, title="Method Not Allowed") @@ -889,7 +940,6 @@ def is_whitelisted(method): def read_only(): def innfn(fn): def wrapper_fn(*args, **kwargs): - # frappe.read_only could be called from nested functions, in such cases don't swap the # connection again. switched_connection = False @@ -1061,9 +1111,7 @@ def has_permission( ) if throw and not out: - document_label = ( - f"{_(doctype)} {doc if isinstance(doc, str) else doc.name}" if doc else _(doctype) - ) + document_label = f"{_(doctype)} {doc if isinstance(doc, str) else doc.name}" if doc else _(doctype) frappe.flags.error_message = _("No permission for {0}").format(document_label) raise frappe.PermissionError @@ -1238,9 +1286,7 @@ def clear_document_cache(doctype: str, name: str | None = None) -> None: delattr(local, "website_settings") -def get_cached_value( - doctype: str, name: str, fieldname: str = "name", as_dict: bool = False -) -> Any: +def get_cached_value(doctype: str, name: str, fieldname: str = "name", as_dict: bool = False) -> Any: try: doc = get_cached_doc(doctype, name) except DoesNotExistError: @@ -1254,7 +1300,7 @@ def get_cached_value( values = [doc.get(f) for f in fieldname] if as_dict: - return _dict(zip(fieldname, values)) + return _dict(zip(fieldname, values, strict=False)) return values @@ -1598,7 +1644,7 @@ def _load_app_hooks(app_name: str | None = None): raise def _is_valid_hook(obj): - return not isinstance(obj, (types.ModuleType, types.FunctionType, type)) + return not isinstance(obj, types.ModuleType | types.FunctionType | type) for key, value in inspect.getmembers(app_hooks, predicate=_is_valid_hook): if not key.startswith("_"): @@ -1606,9 +1652,7 @@ def _load_app_hooks(app_name: str | None = None): return hooks -def get_hooks( - hook: str = None, default: Any | None = "_KEEP_DEFAULT_LIST", app_name: str = None -) -> _dict: +def get_hooks(hook: str = None, default: Any | None = "_KEEP_DEFAULT_LIST", app_name: str = None) -> _dict: """Get hooks via `app/hooks.py` :param hook: Name of the hook. Will gather all hooks for this name and return as a list. @@ -1649,32 +1693,26 @@ def append_hook(target, key, value): target[key].extend(value) -def setup_module_map(site_ready: bool = True): - """ - Rebuild map of all modules (internal). - - :param site_ready: If the site isn't fully ready yet - install is still going on, we can't - fetch apps from site DB. Fallback to fetching all apps on bench for module map temporarily. - """ +def setup_module_map(include_all_apps=True): + """Rebuild map of all modules (internal).""" if conf.db_name: local.app_modules = cache.get_value("app_modules") local.module_app = cache.get_value("module_app") if not (local.app_modules and local.module_app): local.module_app, local.app_modules = {}, {} - - if site_ready: - apps = get_installed_apps(_ensure_on_bench=True) + if include_all_apps: + apps = get_all_apps(with_internal_apps=True) else: - apps = get_all_apps() - + apps = get_installed_apps(_ensure_on_bench=True) for app in apps: local.app_modules.setdefault(app, []) for module in get_module_list(app): module = scrub(module) if module in local.module_app: - print(f"WARNING: module `{module}` found in apps `{local.module_app[module]}` and `{app}`") - + print( + f"WARNING: module `{module}` found in apps `{local.module_app[module]}` and `{app}`" + ) local.module_app[module] = app local.app_modules[app].append(module) @@ -1723,11 +1761,7 @@ def read_file(path, raise_not_found=False): def get_attr(method_string: str) -> Any: """Get python method object from its name.""" app_name = method_string.split(".", 1)[0] - if ( - not local.flags.in_uninstall - and not local.flags.in_install - and app_name not in get_installed_apps() - ): + if not local.flags.in_uninstall and not local.flags.in_install and app_name not in get_installed_apps(): throw(_("App {0} is not installed").format(app_name), AppNotInstalledError) modulename = ".".join(method_string.split(".")[:-1]) @@ -1749,7 +1783,8 @@ def get_newargs(fn: Callable, kwargs: dict[str, Any]) -> dict[str, Any]: """Remove any kwargs that are not supported by the function. Example: - >>> def fn(a=1, b=2): pass + >>> def fn(a=1, b=2): + ... pass >>> get_newargs(fn, {"a": 2, "c": 1}) {"a": 2} @@ -1869,7 +1904,7 @@ def copy_doc(doc: "Document", ignore_no_copy: bool = True) -> "Document": if not ignore_no_copy: remove_no_copy_fields(newdoc) - for i, d in enumerate(newdoc.get_all_children()): + for d in newdoc.get_all_children(): d.set("__islocal", 1) for fieldname in fields_to_clear: @@ -2312,9 +2347,7 @@ loggers = {} log_level = None -def logger( - module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20 -): +def logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20): """Return a python logger that uses StreamHandler.""" from frappe.utils.logger import get_logger @@ -2329,9 +2362,7 @@ def logger( def get_desk_link(doctype, name): - html = ( - '{doctype_local} {name}' - ) + html = '{doctype_local} {name}' return html.format(doctype=doctype, name=name, doctype_local=_(doctype)) @@ -2384,7 +2415,7 @@ def get_version(doctype, name, limit=None, head=False, raise_err=True): Note: Applicable only if DocType has changes tracked. Example - >>> frappe.get_version('User', 'foobar@gmail.com') + >>> frappe.get_version("User", "foobar@gmail.com") >>> [ { @@ -2462,7 +2493,7 @@ def mock(type, size=1, locale="en"): if type not in dir(fake): raise ValueError("Not a valid mock type.") else: - for i in range(size): + for _ in range(size): data = getattr(fake, type)() results.append(data) @@ -2476,7 +2507,7 @@ def validate_and_sanitize_search_inputs(fn): def wrapper(*args, **kwargs): from frappe.desk.search import sanitize_searchfield - kwargs.update(dict(zip(fn.__code__.co_varnames, args))) + kwargs.update(dict(zip(fn.__code__.co_varnames, args, strict=False))) sanitize_searchfield(kwargs["searchfield"]) kwargs["start"] = cint(kwargs["start"]) kwargs["page_len"] = cint(kwargs["page_len"]) @@ -2489,7 +2520,7 @@ def validate_and_sanitize_search_inputs(fn): return wrapper -from frappe.utils.error import log_error # noqa: backward compatibility +from frappe.utils.error import log_error # noqa if _tune_gc: # generational GC gets triggered after certain allocs (g0) which is 700 by default. diff --git a/frappe/app.py b/frappe/app.py index 6d6a747119..4ce356ecc8 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -272,9 +272,7 @@ def set_cors_headers(response): # only required for preflight requests if request.method == "OPTIONS": - cors_headers["Access-Control-Allow-Methods"] = request.headers.get( - "Access-Control-Request-Method" - ) + cors_headers["Access-Control-Allow-Methods"] = request.headers.get("Access-Control-Request-Method") if allowed_headers := request.headers.get("Access-Control-Request-Headers"): cors_headers["Access-Control-Allow-Headers"] = allowed_headers @@ -513,9 +511,7 @@ def serve( def application_with_statics(): global application, _sites_path - application = SharedDataMiddleware( - application, {"/assets": str(os.path.join(_sites_path, "assets"))} - ) + application = SharedDataMiddleware(application, {"/assets": str(os.path.join(_sites_path, "assets"))}) application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(_sites_path))}) diff --git a/frappe/auth.py b/frappe/auth.py index 1e64e17ebb..ee8ec24104 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -61,9 +61,7 @@ class HTTPRequest: def set_request_ip(self): if frappe.get_request_header("X-Forwarded-For"): - frappe.local.request_ip = ( - frappe.get_request_header("X-Forwarded-For").split(",", 1)[0] - ).strip() + frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",", 1)[0]).strip() elif frappe.get_request_header("REMOTE_ADDR"): frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR") @@ -107,9 +105,7 @@ class LoginManager: self.full_name = None self.user_type = None - if ( - frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login" - ): + if frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login": if self.login() is False: return self.resume = False @@ -138,9 +134,7 @@ class LoginManager: self.authenticate(user=user, pwd=pwd) if self.force_user_to_reset_password(): doc = frappe.get_doc("User", self.user) - frappe.local.response["redirect_to"] = doc.reset_password( - send_email=False, password_expired=True - ) + frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True) frappe.local.response["message"] = "Password Reset" return False @@ -274,9 +268,7 @@ class LoginManager: if self.user in frappe.STANDARD_USERS: return False - reset_pwd_after_days = cint( - frappe.db.get_single_value("System Settings", "force_user_to_reset_password") - ) + reset_pwd_after_days = cint(frappe.get_system_settings("force_user_to_reset_password")) if reset_pwd_after_days: last_password_reset_date = ( @@ -384,7 +376,7 @@ class CookieManager: } def delete_cookie(self, to_delete): - if not isinstance(to_delete, (list, tuple)): + if not isinstance(to_delete, list | tuple): to_delete = [to_delete] self.to_delete.extend(to_delete) @@ -415,9 +407,7 @@ def get_logged_user(): def clear_cookies(): if hasattr(frappe.local, "session"): frappe.session.sid = "" - frappe.local.cookie_manager.delete_cookie( - ["full_name", "user_id", "sid", "user_image", "system_user"] - ) + frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"]) def validate_ip_address(user): @@ -615,9 +605,7 @@ def validate_oauth(authorization_header): req = frappe.request parsed_url = urlparse(req.url) access_token = {"access_token": token} - uri = ( - parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) - ) + uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) http_method = req.method headers = req.headers body = req.get_data() diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index 82f1c7d799..4a70c3a21a 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -39,6 +39,7 @@ class AssignmentRule(Document): unassign_condition: DF.Code | None users: DF.TableMultiSelect[AssignmentRuleUser] # end: auto-generated types + def validate(self): self.validate_document_types() self.validate_assignment_days() @@ -50,9 +51,7 @@ class AssignmentRule(Document): def validate_document_types(self): if self.document_type == "ToDo": - frappe.throw( - _("Assignment Rule is not allowed on {0} document type").format(frappe.bold("ToDo")) - ) + frappe.throw(_("Assignment Rule is not allowed on {0} document type").format(frappe.bold("ToDo"))) def validate_assignment_days(self): assignment_days = self.get_assignment_days() @@ -357,9 +356,7 @@ def update_due_date(doc, state=None): rule_doc = frappe.get_cached_doc("Assignment Rule", rule.get("name")) due_date_field = rule_doc.due_date_based_on field_updated = ( - doc.meta.has_field(due_date_field) - and doc.has_value_changed(due_date_field) - and rule.get("name") + doc.meta.has_field(due_date_field) and doc.has_value_changed(due_date_field) and rule.get("name") ) if field_updated: diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py index 7ae034f896..530d45163d 100644 --- a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py @@ -104,7 +104,7 @@ class TestAutoAssign(FrappeTestCase): frappe.db.delete("ToDo", {"name": d.name}) # add 5 more assignments - for i in range(5): + for _ in range(5): _make_test_record(public=1) # check if each user still has 10 assignments @@ -138,7 +138,9 @@ class TestAutoAssign(FrappeTestCase): # check if auto assigned to doc owner, test1@example.com self.assertEqual( frappe.db.get_value( - "ToDo", dict(reference_type=TEST_DOCTYPE, reference_name=note.name, status="Open"), "owner" + "ToDo", + dict(reference_type=TEST_DOCTYPE, reference_name=note.name, status="Open"), + "owner", ), test_user, ) @@ -247,17 +249,15 @@ class TestAutoAssign(FrappeTestCase): frappe.db.delete("Assignment Rule") assignment_rule = frappe.get_doc( - dict( - name="Assignment with Due Date", - doctype="Assignment Rule", - document_type=TEST_DOCTYPE, - assign_condition="public == 0", - due_date_based_on="expiry_date", - assignment_days=self.days, - users=[ - dict(user="test@example.com"), - ], - ) + name="Assignment with Due Date", + doctype="Assignment Rule", + document_type=TEST_DOCTYPE, + assign_condition="public == 0", + due_date_based_on="expiry_date", + assignment_days=self.days, + users=[ + dict(user="test@example.com"), + ], ).insert() expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) @@ -349,39 +349,35 @@ def get_assignment_rule(days, assign=None): assign = ["public == 1", "notify_on_login == 1"] assignment_rule = frappe.get_doc( - dict( - name=f"For {TEST_DOCTYPE} 1", - doctype="Assignment Rule", - priority=0, - document_type=TEST_DOCTYPE, - assign_condition=assign[0], - unassign_condition="public == 0 or notify_on_login == 1", - close_condition='"Closed" in content', - rule="Round Robin", - assignment_days=days[0], - users=[ - dict(user="test@example.com"), - dict(user="test1@example.com"), - dict(user="test2@example.com"), - ], - ) + name=f"For {TEST_DOCTYPE} 1", + doctype="Assignment Rule", + priority=0, + document_type=TEST_DOCTYPE, + assign_condition=assign[0], + unassign_condition="public == 0 or notify_on_login == 1", + close_condition='"Closed" in content', + rule="Round Robin", + assignment_days=days[0], + users=[ + dict(user="test@example.com"), + dict(user="test1@example.com"), + dict(user="test2@example.com"), + ], ).insert() frappe.delete_doc_if_exists("Assignment Rule", f"For {TEST_DOCTYPE} 2") # 2nd rule frappe.get_doc( - dict( - name=f"For {TEST_DOCTYPE} 2", - doctype="Assignment Rule", - priority=1, - document_type=TEST_DOCTYPE, - assign_condition=assign[1], - unassign_condition="notify_on_login == 0", - rule="Round Robin", - assignment_days=days[1], - users=[dict(user="test3@example.com")], - ) + name=f"For {TEST_DOCTYPE} 2", + doctype="Assignment Rule", + priority=1, + document_type=TEST_DOCTYPE, + assign_condition=assign[1], + unassign_condition="notify_on_login == 0", + rule="Round Robin", + assignment_days=days[1], + users=[dict(user="test3@example.com")], ).insert() return assignment_rule diff --git a/frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py b/frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py index 1fe9af4e21..4b387fb400 100644 --- a/frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py +++ b/frappe/automation/doctype/assignment_rule_day/assignment_rule_day.py @@ -19,4 +19,5 @@ class AssignmentRuleDay(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py index 68ceb6b018..54edd74747 100644 --- a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py +++ b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.py @@ -19,4 +19,5 @@ class AssignmentRuleUser(Document): parenttype: DF.Data user: DF.Link # end: auto-generated types + pass diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 956bb0c9c3..72a750cd59 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -70,6 +70,7 @@ class AutoRepeat(Document): submit_on_creation: DF.Check template: DF.Link | None # end: auto-generated types + def validate(self): self.update_status() self.validate_reference_doctype() @@ -550,7 +551,7 @@ def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters docs += [r.name for r in res] docs = set(list(docs)) - return [[d] for d in docs] + return [[d] for d in docs if txt in d] @frappe.whitelist() diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 740b329851..b6ef8301fb 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -41,7 +41,7 @@ class TestAutoRepeat(FrappeTestCase): def test_daily_auto_repeat(self): todo = frappe.get_doc( - dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") + doctype="ToDo", description="test recurring todo", assigned_by="Administrator" ).insert() doc = make_auto_repeat(reference_document=todo.name) @@ -53,9 +53,7 @@ class TestAutoRepeat(FrappeTestCase): todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) self.assertEqual(todo.auto_repeat, doc.name) - new_todo = frappe.db.get_value( - "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" - ) + new_todo = frappe.db.get_value("ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name") new_todo = frappe.get_doc("ToDo", new_todo) @@ -63,7 +61,7 @@ class TestAutoRepeat(FrappeTestCase): def test_weekly_auto_repeat(self): todo = frappe.get_doc( - dict(doctype="ToDo", description="test weekly todo", assigned_by="Administrator") + doctype="ToDo", description="test weekly todo", assigned_by="Administrator" ).insert() doc = make_auto_repeat( @@ -81,9 +79,7 @@ class TestAutoRepeat(FrappeTestCase): todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) self.assertEqual(todo.auto_repeat, doc.name) - new_todo = frappe.db.get_value( - "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" - ) + new_todo = frappe.db.get_value("ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name") new_todo = frappe.get_doc("ToDo", new_todo) @@ -91,7 +87,7 @@ class TestAutoRepeat(FrappeTestCase): def test_weekly_auto_repeat_with_weekdays(self): todo = frappe.get_doc( - dict(doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator") + doctype="ToDo", description="test auto repeat with weekdays", assigned_by="Administrator" ).insert() weekdays = list(week_map.keys()) @@ -121,15 +117,13 @@ class TestAutoRepeat(FrappeTestCase): end_date = add_months(start_date, 12) todo = frappe.get_doc( - dict(doctype="ToDo", description="test recurring todo", assigned_by="Administrator") + doctype="ToDo", description="test recurring todo", assigned_by="Administrator" ).insert() self.monthly_auto_repeat("ToDo", todo.name, start_date, end_date) # test without end_date todo = frappe.get_doc( - dict( - doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator" - ) + doctype="ToDo", description="test recurring todo without end_date", assigned_by="Administrator" ).insert() self.monthly_auto_repeat("ToDo", todo.name, start_date) @@ -165,11 +159,7 @@ class TestAutoRepeat(FrappeTestCase): def test_email_notification(self): todo = frappe.get_doc( - dict( - doctype="ToDo", - description="Test recurring notification attachment", - assigned_by="Administrator", - ) + doctype="ToDo", description="Test recurring notification attachment", assigned_by="Administrator" ).insert() doc = make_auto_repeat( @@ -183,21 +173,15 @@ class TestAutoRepeat(FrappeTestCase): create_repeated_entries(data) frappe.db.commit() - new_todo = frappe.db.get_value( - "ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name" - ) + new_todo = frappe.db.get_value("ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name") - email_queue = frappe.db.exists( - "Email Queue", dict(reference_doctype="ToDo", reference_name=new_todo) - ) + email_queue = frappe.db.exists("Email Queue", dict(reference_doctype="ToDo", reference_name=new_todo)) self.assertTrue(email_queue) def test_next_schedule_date(self): current_date = getdate(today()) todo = frappe.get_doc( - dict( - doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator" - ) + doctype="ToDo", description="test next schedule date for monthly", assigned_by="Administrator" ).insert() doc = make_auto_repeat( frequency="Monthly", reference_document=todo.name, start_date=add_months(today(), -2) @@ -208,9 +192,7 @@ class TestAutoRepeat(FrappeTestCase): self.assertTrue(doc.next_schedule_date >= current_date) todo = frappe.get_doc( - dict( - doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator" - ) + doctype="ToDo", description="test next schedule date for daily", assigned_by="Administrator" ).insert() doc = make_auto_repeat( frequency="Daily", reference_document=todo.name, start_date=add_days(today(), -2) @@ -222,7 +204,7 @@ class TestAutoRepeat(FrappeTestCase): create_submittable_doctype(doctype) current_date = getdate() - submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test submit on creation")).insert() + submittable_doc = frappe.get_doc(doctype=doctype, test="test submit on creation").insert() submittable_doc.submit() doc = make_auto_repeat( frequency="Daily", diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py index b237f94610..b51d1f78ae 100644 --- a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py @@ -19,4 +19,5 @@ class AutoRepeatDay(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/automation/doctype/milestone/milestone.py b/frappe/automation/doctype/milestone/milestone.py index e980fb17b5..28db183f52 100644 --- a/frappe/automation/doctype/milestone/milestone.py +++ b/frappe/automation/doctype/milestone/milestone.py @@ -20,6 +20,7 @@ class Milestone(Document): track_field: DF.Data value: DF.Data # end: auto-generated types + pass diff --git a/frappe/automation/doctype/milestone_tracker/milestone_tracker.py b/frappe/automation/doctype/milestone_tracker/milestone_tracker.py index 414c2319d5..fcda060c58 100644 --- a/frappe/automation/doctype/milestone_tracker/milestone_tracker.py +++ b/frappe/automation/doctype/milestone_tracker/milestone_tracker.py @@ -20,6 +20,7 @@ class MilestoneTracker(Document): document_type: DF.Link track_field: DF.Literal # end: auto-generated types + def on_update(self): frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) @@ -31,15 +32,13 @@ class MilestoneTracker(Document): from_value = before_save and before_save.get(self.track_field) or None if from_value != doc.get(self.track_field): frappe.get_doc( - dict( - doctype="Milestone", - reference_type=doc.doctype, - reference_name=doc.name, - track_field=self.track_field, - from_value=from_value, - value=doc.get(self.track_field), - milestone_tracker=self.name, - ) + doctype="Milestone", + reference_type=doc.doctype, + reference_name=doc.name, + track_field=self.track_field, + from_value=from_value, + value=doc.get(self.track_field), + milestone_tracker=self.name, ).insert(ignore_permissions=True) diff --git a/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py index 4316edd1ca..2fba6bf4ad 100644 --- a/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py +++ b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py @@ -12,10 +12,10 @@ class TestMilestoneTracker(FrappeTestCase): frappe.cache.delete_key("milestone_tracker_map") milestone_tracker = frappe.get_doc( - dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status") + doctype="Milestone Tracker", document_type="ToDo", track_field="status" ).insert() - todo = frappe.get_doc(dict(doctype="ToDo", description="test milestone", status="Open")).insert() + todo = frappe.get_doc(doctype="ToDo", description="test milestone", status="Open").insert() milestones = frappe.get_all( "Milestone", diff --git a/frappe/automation/doctype/reminder/reminder.py b/frappe/automation/doctype/reminder/reminder.py index fb6f1f222e..c7631d18fe 100644 --- a/frappe/automation/doctype/reminder/reminder.py +++ b/frappe/automation/doctype/reminder/reminder.py @@ -24,6 +24,7 @@ class Reminder(Document): reminder_doctype: DF.Link | None user: DF.Link # end: auto-generated types + @staticmethod def clear_old_logs(days=30): from frappe.query_builder import Interval diff --git a/frappe/automation/doctype/reminder/test_reminder.py b/frappe/automation/doctype/reminder/test_reminder.py index 84cc258701..8085cf876c 100644 --- a/frappe/automation/doctype/reminder/test_reminder.py +++ b/frappe/automation/doctype/reminder/test_reminder.py @@ -10,7 +10,6 @@ from frappe.utils import add_to_date, now_datetime class TestReminder(FrappeTestCase): def test_reminder(self): - description = "TEST_REMINDER" create_new_reminder( diff --git a/frappe/boot.py b/frappe/boot.py index 8caa264443..45a7cfa21c 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -176,9 +176,7 @@ def get_user_pages_or_reports(parent, cache=False): frappe.qb.from_(customRole) .from_(hasRole) .from_(parentTable) - .select( - customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns - ) + .select(customRole[parent.lower()].as_("name"), customRole.modified, customRole.ref_doctype, *columns) .where( (hasRole.parent == customRole.name) & (parentTable.name == customRole[parent.lower()]) @@ -201,9 +199,7 @@ def get_user_pages_or_reports(parent, cache=False): .from_(parentTable) .select(parentTable.name.as_("name"), parentTable.modified, *columns) .where( - (hasRole.role.isin(roles)) - & (hasRole.parent == parentTable.name) - & (parentTable.name.notin(subq)) + (hasRole.role.isin(roles)) & (hasRole.parent == parentTable.name) & (parentTable.name.notin(subq)) ) .distinct() ) @@ -225,7 +221,6 @@ def get_user_pages_or_reports(parent, cache=False): # pages with no role are allowed if parent == "Page": - pages_with_no_roles = ( frappe.qb.from_(parentTable) .select(parentTable.name, parentTable.modified, *columns) diff --git a/frappe/build.py b/frappe/build.py index 49021ae6bb..40d9c98ed5 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -177,9 +177,6 @@ def symlink(target, link_name, overwrite=False): if not overwrite: return os.symlink(target, link_name) - # os.replace() may fail if files are on different filesystems - link_dir = os.path.dirname(link_name) - # Create link to target with temporary filename while True: temp_link_name = f"tmp{frappe.generate_hash()}" @@ -378,9 +375,7 @@ def make_asset_dirs(hard_link=False): symlinks = generate_assets_map() for source, target in symlinks.items(): - start_message = unstrip( - f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}" - ) + start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}") fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}") # Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 11302820c5..960c72f042 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -39,7 +39,6 @@ global_cache_keys = ( "domain_restricted_doctypes", "domain_restricted_pages", "information_schema:counts", - "sitemap_routes", "db_tables", "server_script_autocompletion_items", ) + doctype_map_keys @@ -198,9 +197,7 @@ def build_table_count_cache(): table_rows = frappe.qb.Field("table_rows").as_("count") information_schema = frappe.qb.Schema("information_schema") - data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run( - as_dict=True - ) + data = (frappe.qb.from_(information_schema.tables).select(table_name, table_rows)).run(as_dict=True) counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data} frappe.cache.set_value("information_schema:counts", counts) diff --git a/frappe/commands/gettext.py b/frappe/commands/gettext.py index c3e54f2bf6..27f67a65a6 100644 --- a/frappe/commands/gettext.py +++ b/frappe/commands/gettext.py @@ -35,9 +35,7 @@ def compile_translations(context, app: str | None = None, locale: str = None, fo _compile_translations(app, locale, force=force) -@click.command( - "migrate-csv-to-po", help="Translation: migrate from CSV files (old) to PO files (new)" -) +@click.command("migrate-csv-to-po", help="Translation: migrate from CSV files (old) to PO files (new)") @click.option("--app", help="Only migrate for this app. eg: frappe") @click.option("--locale", help="Compile translations only for this locale. eg: de") @pass_context diff --git a/frappe/commands/redis_utils.py b/frappe/commands/redis_utils.py index 07ad8715c4..5b558de126 100644 --- a/frappe/commands/redis_utils.py +++ b/frappe/commands/redis_utils.py @@ -13,9 +13,7 @@ from frappe.utils.redis_queue import RedisQueue default=False, help="Set new Redis admin(default user) password", ) -@click.option( - "--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites" -) +@click.option("--use-rq-auth", is_flag=True, default=False, help="Enable Redis authentication for sites") def create_rq_users(set_admin_password=False, use_rq_auth=False): """Create Redis Queue users and add to acl and app configs. @@ -46,9 +44,7 @@ def create_rq_users(set_admin_password=False, use_rq_auth=False): validate=False, site_config_path=common_site_config_path, ) - update_site_config( - "use_rq_auth", use_rq_auth, validate=False, site_config_path=common_site_config_path - ) + update_site_config("use_rq_auth", use_rq_auth, validate=False, site_config_path=common_site_config_path) click.secho( "* ACL and site configs are updated with new user credentials. " @@ -65,8 +61,7 @@ def create_rq_users(set_admin_password=False, use_rq_auth=False): ) click.secho(f"`export {env_key}={user_credentials['default'][1]}`") click.secho( - "NOTE: Please save the admin password as you " - "can not access redis server without the password", + "NOTE: Please save the admin password as you " "can not access redis server without the password", fg="yellow", ) diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py index cf760cf4f0..49acc8d7a7 100755 --- a/frappe/commands/scheduler.py +++ b/frappe/commands/scheduler.py @@ -74,9 +74,7 @@ def disable_scheduler(context): @click.command("scheduler") @click.option("--site", help="site name") @click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable", "status"])) -@click.option( - "--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format" -) +@click.option("--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format") @click.option("--verbose", "-v", is_flag=True, help="Verbose output") @pass_context def scheduler(context, state: str, format: str, verbose: bool = False, site: str | None = None): @@ -128,9 +126,7 @@ def set_maintenance_mode(context, state, site=None): frappe.destroy() -@click.command( - "doctor" -) # Passing context always gets a site and if there is no use site it breaks +@click.command("doctor") # Passing context always gets a site and if there is no use site it breaks @click.option("--site", help="site name") @pass_context def doctor(context, site=None): @@ -199,9 +195,7 @@ def start_scheduler(): type=click.Choice(["round_robin", "random"]), help="Dequeuing strategy to use", ) -def start_worker( - queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None -): +def start_worker(queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None): """Start a background worker""" from frappe.utils.background_jobs import start_worker diff --git a/frappe/commands/site.py b/frappe/commands/site.py index da6509e219..832364ab92 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -29,9 +29,7 @@ from frappe.exceptions import SiteNotSpecifiedError "--mariadb-root-username", help='Root username for MariaDB or PostgreSQL, Default is "root"', ) -@click.option( - "--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL" -) +@click.option("--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL") @click.option( "--no-mariadb-socket", is_flag=True, @@ -40,14 +38,10 @@ from frappe.exceptions import SiteNotSpecifiedError ) @click.option("--admin-password", help="Administrator password for new site", default=None) @click.option("--verbose", is_flag=True, default=False, help="Verbose") -@click.option( - "--force", help="Force restore if site/database already exists", is_flag=True, default=False -) +@click.option("--force", help="Force restore if site/database already exists", is_flag=True, default=False) @click.option("--source-sql", "--source_sql", help="Initiate database with a SQL file") @click.option("--install-app", multiple=True, help="Install app after installation") -@click.option( - "--set-default", is_flag=True, default=False, help="Set the new site as default site" -) +@click.option("--set-default", is_flag=True, default=False, help="Set the new site as default site") @click.option( "--setup-db/--no-setup-db", default=True, @@ -76,7 +70,7 @@ def new_site( "Create a new site" from frappe.installer import _new_site - frappe.init(site=site, new_site=True, site_ready=False) + frappe.init(site=site, new_site=True) _new_site( db_name, @@ -108,15 +102,11 @@ def new_site( "--mariadb-root-username", help='Root username for MariaDB or PostgreSQL, Default is "root"', ) -@click.option( - "--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL" -) +@click.option("--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL") @click.option("--db-name", help="Database name for site in case it is a new one") @click.option("--admin-password", help="Administrator password for new site") @click.option("--install-app", multiple=True, help="Install app after installation") -@click.option( - "--with-public-files", help="Restores the public files of the site, given path to its tar file" -) +@click.option("--with-public-files", help="Restores the public files of the site, given path to its tar file") @click.option( "--with-private-files", help="Restores the private files of the site, given path to its tar file", @@ -299,8 +289,7 @@ def restore_backup( # 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?" + "This is not recommended and may lead to unexpected behaviour. " "Do you want to continue anyway?" ) click.confirm(warn_message, abort=True) @@ -391,14 +380,10 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None): "--mariadb-root-username", help='Root username for MariaDB or PostgreSQL, Default is "root"', ) -@click.option( - "--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL" -) +@click.option("--db-root-password", "--mariadb-root-password", help="Root password for MariaDB or PostgreSQL") @click.option("--yes", is_flag=True, default=False, help="Pass --yes to skip confirmation") @pass_context -def reinstall( - context, admin_password=None, db_root_username=None, db_root_password=None, yes=False -): +def reinstall(context, admin_password=None, db_root_username=None, db_root_password=None, yes=False): "Reinstall site ie. wipe all data and start over" site = get_site(context) _reinstall(site, admin_password, db_root_username, db_root_password, yes, verbose=context.verbose) @@ -417,7 +402,7 @@ def _reinstall( if not yes: click.confirm("This will wipe your database. Are you sure you want to reinstall?", abort=True) try: - frappe.init(site=site, site_ready=False) + frappe.init(site=site) frappe.connect() frappe.clear_cache() installed = frappe.get_installed_apps() @@ -429,7 +414,7 @@ def _reinstall( frappe.db.close() frappe.destroy() - frappe.init(site=site, site_ready=False) + frappe.init(site=site) _new_site( frappe.conf.db_name, @@ -726,6 +711,7 @@ def disable_user(context, email): @pass_context def migrate(context, skip_failing=False, skip_search_index=False): "Run patches, sync schema and rebuild files/translations" + from traceback_with_variables import activate_by_import from frappe.migrate import SiteMigration @@ -859,9 +845,7 @@ def use(site, sites_path="."): type=str, help="Specify the DocTypes to not backup seperated by commas", ) -@click.option( - "--backup-path", default=None, help="Set path for saving all the files in this operation" -) +@click.option("--backup-path", default=None, help="Set path for saving all the files in this operation") @click.option("--backup-path-db", default=None, help="Set path for saving database file") @click.option("--backup-path-files", default=None, help="Set path for saving public file") @click.option("--backup-path-private-files", default=None, help="Set path for saving private file") @@ -874,9 +858,7 @@ 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" -) +@click.option("--old-backup-metadata", default=False, is_flag=True, help="Use older backup metadata") @pass_context def backup( context, @@ -976,9 +958,7 @@ def remove_from_installed_apps(context, app): is_flag=True, default=False, ) -@click.option( - "--dry-run", help="List all doctypes that will be deleted", is_flag=True, default=False -) +@click.option("--dry-run", help="List all doctypes that will be deleted", is_flag=True, default=False) @click.option("--no-backup", help="Do not backup the site", is_flag=True, default=False) @click.option("--force", help="Force remove app from site", is_flag=True, default=False) @pass_context @@ -1015,9 +995,7 @@ def uninstall(context, app, dry_run, yes, no_backup, force): ) @click.option("--archived-sites-path") @click.option("--no-backup", is_flag=True, default=False) -@click.option( - "--force", help="Force drop-site even if an error is encountered", is_flag=True, default=False -) +@click.option("--force", help="Force drop-site even if an error is encountered", is_flag=True, default=False) def drop_site( site, db_root_username="root", @@ -1058,7 +1036,7 @@ def _drop_site( f"Error: The operation has stopped because backup of {site}'s database failed.", f"Reason: {str(err)}\n", "Fix the issue and try again.", - "Hint: Use 'bench drop-site {0} --force' to force the removal of {0}".format(site), + f"Hint: Use 'bench drop-site {site} --force' to force the removal of {site}", ] click.echo("\n".join(messages)) sys.exit(1) @@ -1104,9 +1082,7 @@ def move(dest_dir, site): @click.command("set-password") @click.argument("user") @click.argument("password", required=False) -@click.option( - "--logout-all-sessions", help="Log out from all sessions", is_flag=True, default=False -) +@click.option("--logout-all-sessions", help="Log out from all sessions", is_flag=True, default=False) @pass_context def set_password(context, user, password=None, logout_all_sessions=False): "Set password for a user on a site" @@ -1119,9 +1095,7 @@ def set_password(context, user, password=None, logout_all_sessions=False): @click.command("set-admin-password") @click.argument("admin-password", required=False) -@click.option( - "--logout-all-sessions", help="Log out from all sessions", is_flag=True, default=False -) +@click.option("--logout-all-sessions", help="Log out from all sessions", is_flag=True, default=False) @pass_context def set_admin_password(context, admin_password=None, logout_all_sessions=False): "Set Administrator password for a site" @@ -1278,9 +1252,7 @@ def stop_recording(context): @click.command("ngrok") -@click.option( - "--bind-tls", is_flag=True, default=False, help="Returns a reference to the https tunnel." -) +@click.option("--bind-tls", is_flag=True, default=False, help="Returns a reference to the https tunnel.") @click.option( "--use-default-authtoken", is_flag=True, @@ -1388,9 +1360,7 @@ def clear_log_table(context, doctype, days, no_backup): @click.command("trim-database") @click.option("--dry-run", is_flag=True, default=False, help="Show what would be deleted") -@click.option( - "--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format" -) +@click.option("--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format") @click.option("--no-backup", is_flag=True, default=False, help="Do not backup the site") @click.option( "--yes", @@ -1500,9 +1470,7 @@ def get_standard_tables(): @click.command("trim-tables") @click.option("--dry-run", is_flag=True, default=False, help="Show what would be deleted") -@click.option( - "--format", "-f", default="table", type=click.Choice(["json", "table"]), help="Output format" -) +@click.option("--format", "-f", default="table", type=click.Choice(["json", "table"]), help="Output format") @click.option("--no-backup", is_flag=True, default=False, help="Do not backup the site") @pass_context def trim_tables(context, dry_run, format, no_backup): diff --git a/frappe/commands/translate.py b/frappe/commands/translate.py index 247d4a77d5..361a19aee2 100644 --- a/frappe/commands/translate.py +++ b/frappe/commands/translate.py @@ -38,14 +38,8 @@ def new_language(context, lang_code, app): frappe.connect() frappe.translate.write_translations_file(app, lang_code) - print( - "File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format( - app=app, lang_code=lang_code - ) - ) - print( - "You will need to add the language in frappe/geo/languages.json, if you haven't done it already." - ) + print(f"File created at ./apps/{app}/{app}/translations/{lang_code}.csv") + print("You will need to add the language in frappe/geo/languages.json, if you haven't done it already.") @click.command("get-untranslated") diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index a839566130..e4b4118c37 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -437,14 +437,10 @@ def import_doc(context, path, force=False): default="Insert", help="Insert New Records or Update Existing Records", ) -@click.option( - "--submit-after-import", default=False, is_flag=True, help="Submit document after importing it" -) +@click.option("--submit-after-import", default=False, is_flag=True, help="Submit document after importing it") @click.option("--mute-emails", default=True, is_flag=True, help="Mute emails during import") @pass_context -def data_import( - context, file_path, doctype, import_type=None, submit_after_import=False, mute_emails=True -): +def data_import(context, file_path, doctype, import_type=None, submit_after_import=False, mute_emails=True): "Import documents in bulk from CSV or XLSX using data import" from frappe.core.doctype.data_import.data_import import import_file @@ -560,7 +556,7 @@ def jupyter(context): os.mkdir(jupyter_notebooks_path) bin_path = os.path.abspath("../env/bin") print( - """ + f""" Starting Jupyter notebook Run the following in your first cell to connect notebook to frappe ``` @@ -570,9 +566,7 @@ frappe.connect() frappe.local.lang = frappe.db.get_default('lang') frappe.db.connect() ``` - """.format( - site=site, sites_path=sites_path - ) + """ ) os.execv( f"{bin_path}/jupyter", @@ -630,9 +624,7 @@ def console(context, autoreload=False): terminal() -@click.command( - "transform-database", help="Change tables' internal settings changing engine and row formats" -) +@click.command("transform-database", help="Change tables' internal settings changing engine and row formats") @click.option( "--table", required=True, @@ -731,9 +723,7 @@ def transform_database(context, table, engine, row_format, failfast): @click.option("--profile", is_flag=True, default=False) @click.option("--coverage", is_flag=True, default=False) @click.option("--skip-test-records", is_flag=True, default=False, help="Don't create test records") -@click.option( - "--skip-before-tests", is_flag=True, default=False, help="Don't run before tests hook" -) +@click.option("--skip-before-tests", is_flag=True, default=False, help="Don't run before tests hook") @click.option("--junit-xml-output", help="Destination file path for junit xml report") @click.option( "--failfast", is_flag=True, default=False, help="Stop the test run on the first error or failure" @@ -772,12 +762,8 @@ def run_tests( click.secho(f"bench --site {site} set-config allow_tests true", fg="green") return - frappe.init(site=site) - - frappe.flags.skip_before_tests = skip_before_tests - frappe.flags.skip_test_records = skip_test_records - ret = frappe.test_runner.main( + site, app, module, doctype, @@ -790,6 +776,8 @@ def run_tests( doctype_list_path=doctype_list_path, failfast=failfast, case=case, + skip_test_records=skip_test_records, + skip_before_tests=skip_before_tests, ) if len(ret.failures) == 0 and len(ret.errors) == 0: @@ -803,7 +791,12 @@ def run_tests( @click.option("--app", help="For App", default="frappe") @click.option("--build-number", help="Build number", default=1) @click.option("--total-builds", help="Total number of builds", default=1) -@click.option("--with-coverage", is_flag=True, help="Build coverage file") +@click.option( + "--with-coverage", + is_flag=True, + help="Build coverage file", + envvar="CAPTURE_COVERAGE", +) @click.option("--use-orchestrator", is_flag=True, help="Use orchestrator to run parallel tests") @click.option("--dry-run", is_flag=True, default=False, help="Dont actually run tests") @pass_context @@ -985,7 +978,9 @@ def request(context, args=None, path=None): frappe.connect() if args: if "?" in args: - frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")]) + frappe.local.form_dict = frappe._dict( + [a.split("=") for a in args.split("?")[-1].split("&")] + ) else: frappe.local.form_dict = frappe._dict() @@ -1009,9 +1004,7 @@ def request(context, args=None, path=None): @click.command("make-app") @click.argument("destination") @click.argument("app_name") -@click.option( - "--no-git", is_flag=True, default=False, help="Do not initialize git repository for the app" -) +@click.option("--no-git", is_flag=True, default=False, help="Do not initialize git repository for the app") def make_app(destination, app_name, no_git=False): "Creates a boilerplate app" from frappe.utils.boilerplate import make_boilerplate @@ -1032,9 +1025,7 @@ def create_patch(): @click.command("set-config") @click.argument("key") @click.argument("value") -@click.option( - "-g", "--global", "global_", is_flag=True, default=False, help="Set value in bench config" -) +@click.option("-g", "--global", "global_", is_flag=True, default=False, help="Set value in bench config") @click.option("-p", "--parse", is_flag=True, default=False, help="Evaluate as Python Object") @pass_context def set_config(context, key, value, global_=False, parse=False): @@ -1111,9 +1102,7 @@ def get_version(output): @click.command("rebuild-global-search") -@click.option( - "--static-pages", is_flag=True, default=False, help="Rebuild global search for static pages" -) +@click.option("--static-pages", is_flag=True, default=False, help="Rebuild global search for static pages") @pass_context def rebuild_global_search(context, static_pages=False): """Setup help table in the current site (called after migrate)""" diff --git a/frappe/config/__init__.py b/frappe/config/__init__.py index 02626aedf5..6f97efbfdf 100644 --- a/frappe/config/__init__.py +++ b/frappe/config/__init__.py @@ -30,9 +30,7 @@ def get_modules_from_all_apps(): def get_modules_from_app(app): - return frappe.get_all( - "Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"] - ) + return frappe.get_all("Module Def", filters={"app_name": app}, fields=["module_name", "app_name as app"]) def get_all_empty_tables_by_module(): diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index a008f4638c..efb53dd590 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -52,6 +52,7 @@ class Address(Document): pincode: DF.Data | None state: DF.Data | None # end: auto-generated types + def __setup__(self): self.flags.linked = False @@ -124,11 +125,10 @@ def get_preferred_address(doctype, name, preferred_key="is_primary_address"): FROM `tabAddress` addr, `tabDynamic Link` dl WHERE - dl.parent = addr.name and dl.link_doctype = %s and - dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and - %s = %s - """ - % ("%s", "%s", preferred_key, "%s"), + dl.parent = addr.name and dl.link_doctype = {} and + dl.link_name = {} and ifnull(addr.disabled, 0) = 0 and + {} = {} + """.format("%s", "%s", preferred_key, "%s"), (doctype, name, 1), as_dict=1, ) @@ -140,9 +140,7 @@ def get_preferred_address(doctype, name, preferred_key="is_primary_address"): @frappe.whitelist() -def get_default_address( - doctype: str, name: str | None, sort_key: str = "is_primary_address" -) -> str | None: +def get_default_address(doctype: str, name: str | None, sort_key: str = "is_primary_address") -> str | None: """Return default Address name for the given doctype, name.""" if sort_key not in ["is_shipping_address", "is_primary_address"]: return None diff --git a/frappe/contacts/doctype/address_template/address_template.py b/frappe/contacts/doctype/address_template/address_template.py index 601fb54302..d993a54af1 100644 --- a/frappe/contacts/doctype/address_template/address_template.py +++ b/frappe/contacts/doctype/address_template/address_template.py @@ -20,6 +20,7 @@ class AddressTemplate(Document): is_default: DF.Check template: DF.Code | None # end: auto-generated types + def validate(self): validate_template(self.template) @@ -28,7 +29,7 @@ class AddressTemplate(Document): if not self.is_default and not self._get_previous_default(): self.is_default = 1 - if frappe.db.get_single_value("System Settings", "setup_complete"): + if frappe.get_system_settings("setup_complete"): frappe.msgprint(_("Setting this Address Template as default as there is no other default")) def on_update(self): diff --git a/frappe/contacts/doctype/address_template/test_address_template.py b/frappe/contacts/doctype/address_template/test_address_template.py index c3c5b544d6..80c4ee73ac 100644 --- a/frappe/contacts/doctype/address_template/test_address_template.py +++ b/frappe/contacts/doctype/address_template/test_address_template.py @@ -25,9 +25,7 @@ class TestAddressTemplate(FrappeTestCase): self.assertEqual(frappe.db.get_value("Address Template", "Brazil", "is_default"), 1) def test_delete_address_template(self): - india = frappe.get_doc( - {"doctype": "Address Template", "country": "India", "is_default": 0} - ).insert() + india = frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 0}).insert() brazil = frappe.get_doc( {"doctype": "Address Template", "country": "Brazil", "is_default": 1} diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index b40a27ca13..242b08561e 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -47,6 +47,7 @@ class Contact(Document): unsubscribed: DF.Check user: DF.Link | None # end: auto-generated types + def autoname(self): self.name = self._get_full_name() @@ -248,17 +249,14 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond doctype = "Contact" - if ( - not frappe.get_meta(doctype).get_field(searchfield) - and searchfield not in frappe.db.DEFAULT_COLUMNS - ): + if not frappe.get_meta(doctype).get_field(searchfield) and searchfield not in frappe.db.DEFAULT_COLUMNS: return [] link_doctype = filters.pop("link_doctype") link_name = filters.pop("link_name") return frappe.db.sql( - """select + f"""select `tabContact`.name, `tabContact`.full_name, `tabContact`.company_name from `tabContact`, `tabDynamic Link` @@ -267,14 +265,12 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): `tabDynamic Link`.parenttype = 'Contact' and `tabDynamic Link`.link_doctype = %(link_doctype)s and `tabDynamic Link`.link_name = %(link_name)s and - `tabContact`.`{key}` like %(txt)s - {mcond} + `tabContact`.`{searchfield}` like %(txt)s + {get_match_cond(doctype)} order by if(locate(%(_txt)s, `tabContact`.full_name), locate(%(_txt)s, `tabContact`.company_name), 99999), `tabContact`.idx desc, `tabContact`.full_name - limit %(start)s, %(page_len)s """.format( - mcond=get_match_cond(doctype), key=searchfield - ), + limit %(start)s, %(page_len)s """, { "txt": "%" + txt + "%", "_txt": txt.replace("%", ""), @@ -291,8 +287,7 @@ def address_query(links): import json links = [ - {"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} - for d in json.loads(links) + {"link_doctype": d.get("link_doctype"), "link_name": d.get("link_name")} for d in json.loads(links) ] result = [] @@ -335,9 +330,7 @@ def get_contact_with_phone_number(number): def get_contact_name(email_id): - contact = frappe.get_all( - "Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1 - ) + contact = frappe.get_all("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) return contact[0].parent if contact else None diff --git a/frappe/contacts/doctype/contact_email/contact_email.py b/frappe/contacts/doctype/contact_email/contact_email.py index 2667263f39..5d9458ab8d 100644 --- a/frappe/contacts/doctype/contact_email/contact_email.py +++ b/frappe/contacts/doctype/contact_email/contact_email.py @@ -20,4 +20,5 @@ class ContactEmail(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/contacts/doctype/contact_phone/contact_phone.py b/frappe/contacts/doctype/contact_phone/contact_phone.py index 45f164e4c1..f1b1608151 100644 --- a/frappe/contacts/doctype/contact_phone/contact_phone.py +++ b/frappe/contacts/doctype/contact_phone/contact_phone.py @@ -21,4 +21,5 @@ class ContactPhone(Document): parenttype: DF.Data phone: DF.Data # end: auto-generated types + pass diff --git a/frappe/contacts/doctype/gender/gender.py b/frappe/contacts/doctype/gender/gender.py index 69e4e8f75c..812b07d2b7 100644 --- a/frappe/contacts/doctype/gender/gender.py +++ b/frappe/contacts/doctype/gender/gender.py @@ -15,4 +15,5 @@ class Gender(Document): gender: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/contacts/doctype/salutation/salutation.py b/frappe/contacts/doctype/salutation/salutation.py index 2d10dc18b5..161f19dfbb 100644 --- a/frappe/contacts/doctype/salutation/salutation.py +++ b/frappe/contacts/doctype/salutation/salutation.py @@ -15,4 +15,5 @@ class Salutation(Document): salutation: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py index 70ac273e57..12d31b158f 100644 --- a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py @@ -52,7 +52,6 @@ def get_columns(filters): def get_data(filters): - data = [] reference_doctype = filters.get("reference_doctype") reference_name = filters.get("reference_name") @@ -76,12 +75,8 @@ def get_reference_addresses_and_contact(reference_doctype, reference_name): for d in reference_list: reference_details.setdefault(d, frappe._dict()) - reference_details = get_reference_details( - reference_doctype, "Address", reference_list, reference_details - ) - reference_details = get_reference_details( - reference_doctype, "Contact", reference_list, reference_details - ) + reference_details = get_reference_details(reference_doctype, "Address", reference_list, reference_details) + reference_details = get_reference_details(reference_doctype, "Contact", reference_list, reference_details) for reference_name, details in reference_details.items(): addresses = details.get("address", []) diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py index f62343ea66..15723ac1a7 100644 --- a/frappe/core/doctype/access_log/access_log.py +++ b/frappe/core/doctype/access_log/access_log.py @@ -27,6 +27,7 @@ class AccessLog(Document): timestamp: DF.Datetime | 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/core/doctype/activity_log/activity_log.py b/frappe/core/doctype/activity_log/activity_log.py index d58899f6cd..68372ea7af 100644 --- a/frappe/core/doctype/activity_log/activity_log.py +++ b/frappe/core/doctype/activity_log/activity_log.py @@ -34,6 +34,7 @@ class ActivityLog(Document): timeline_name: DF.DynamicLink | None user: DF.Link | None # end: auto-generated types + def before_insert(self): self.full_name = get_fullname(self.user) self.date = now() @@ -52,7 +53,7 @@ class ActivityLog(Document): def set_ip_address(self): if self.operation in ("Login", "Logout"): - self.ip_address = getattr(frappe.local, "request_ip") + self.ip_address = frappe.local.request_ip @staticmethod def clear_old_logs(days=None): diff --git a/frappe/core/doctype/activity_log/test_activity_log.py b/frappe/core/doctype/activity_log/test_activity_log.py index 0017d8f870..f2f029f220 100644 --- a/frappe/core/doctype/activity_log/test_activity_log.py +++ b/frappe/core/doctype/activity_log/test_activity_log.py @@ -9,7 +9,6 @@ from frappe.tests.utils import FrappeTestCase class TestActivityLog(FrappeTestCase): def test_activity_log(self): - # test user login log frappe.local.form_dict = frappe._dict( { diff --git a/frappe/core/doctype/amended_document_naming_settings/amended_document_naming_settings.py b/frappe/core/doctype/amended_document_naming_settings/amended_document_naming_settings.py index 3a25d9fe6c..cc55a66495 100644 --- a/frappe/core/doctype/amended_document_naming_settings/amended_document_naming_settings.py +++ b/frappe/core/doctype/amended_document_naming_settings/amended_document_naming_settings.py @@ -20,4 +20,5 @@ class AmendedDocumentNamingSettings(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/audit_trail/audit_trail.py b/frappe/core/doctype/audit_trail/audit_trail.py index 2b2e843f46..e0959b92cc 100644 --- a/frappe/core/doctype/audit_trail/audit_trail.py +++ b/frappe/core/doctype/audit_trail/audit_trail.py @@ -24,6 +24,7 @@ class AuditTrail(Document): end_date: DF.Date | None start_date: DF.Date | None # end: auto-generated types + pass def validate(self): diff --git a/frappe/core/doctype/block_module/block_module.py b/frappe/core/doctype/block_module/block_module.py index 17dd153d87..cea51f3d81 100644 --- a/frappe/core/doctype/block_module/block_module.py +++ b/frappe/core/doctype/block_module/block_module.py @@ -18,4 +18,5 @@ class BlockModule(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index b33b7e6f63..3534297a73 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -52,6 +52,7 @@ class Comment(Document): seen: DF.Check subject: DF.Text | None # end: auto-generated types + def after_insert(self): notify_mentions(self.reference_doctype, self.reference_name, self.content) self.notify_change("add") diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py index 9ee0e4dd00..12251718e8 100644 --- a/frappe/core/doctype/comment/test_comment.py +++ b/frappe/core/doctype/comment/test_comment.py @@ -11,7 +11,7 @@ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog class TestComment(FrappeTestCase): def test_comment_creation(self): - test_doc = frappe.get_doc(dict(doctype="ToDo", description="test")) + test_doc = frappe.get_doc(doctype="ToDo", description="test") test_doc.insert() comment = test_doc.add_comment("Comment", "test comment") @@ -57,9 +57,7 @@ class TestComment(FrappeTestCase): frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) - add_comment_args.update( - comment="pleez vizits my site http://mysite.com", comment_by="bad commentor" - ) + add_comment_args.update(comment="pleez vizits my site http://mysite.com", comment_by="bad commentor") add_comment(**add_comment_args) self.assertEqual( diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index da985d2772..468f7ea6ea 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -119,6 +119,7 @@ class Communication(Document, CommunicationEmailMixin): unread_notification_sent: DF.Check user: DF.Link | None # end: auto-generated types + """Communication represents an external communication like Email.""" no_feed_on_delete = True @@ -133,7 +134,6 @@ class Communication(Document, CommunicationEmailMixin): and self.uid and self.uid != -1 ): - email_flag_queue = frappe.db.get_value( "Email Flag Queue", {"communication": self.name, "is_completed": 0} ) @@ -556,6 +556,7 @@ def get_contacts(email_strings: list[str], auto_create_contact=False) -> list[st contact.insert(ignore_permissions=True) contact_name = contact.name except Exception: + contact_name = None contact.log_error("Unable to add contact") if contact_name: diff --git a/frappe/core/doctype/communication/communication_list.js b/frappe/core/doctype/communication/communication_list.js index 4ef3a384ff..8f2623ca09 100644 --- a/frappe/core/doctype/communication/communication_list.js +++ b/frappe/core/doctype/communication/communication_list.js @@ -13,8 +13,6 @@ frappe.listview_settings["Communication"] = { "communication_date", ], - filters: [["status", "=", "Open"]], - onload: function (list_view) { let method = "frappe.email.inbox.create_email_flag_queue"; diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 2c05570cdb..b48385ce49 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -146,7 +146,7 @@ class CommunicationEmailMixin: return get_formatted_email(self.mail_sender_fullname(), mail=self.mail_sender()) def get_content(self, print_format=None): - if print_format and frappe.db.get_single_value("System Settings", "attach_view_link"): + if print_format and frappe.get_system_settings("attach_view_link"): return self.content + self.get_attach_link(print_format) return self.content @@ -239,9 +239,7 @@ class CommunicationEmailMixin: if not emails: return [] - return frappe.get_all( - "User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0} - ) + return frappe.get_all("User", pluck="email", filters={"email": ["in", emails], "thread_notify": 0}) @staticmethod def filter_disabled_users(emails): @@ -259,7 +257,6 @@ class CommunicationEmailMixin: print_letterhead=None, is_inbound_mail_communcation=None, ) -> dict: - outgoing_email_account = self.get_outgoing_email_account() if not outgoing_email_account: return {} @@ -270,9 +267,7 @@ class CommunicationEmailMixin: cc = self.get_mail_cc_with_displayname( is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=send_me_a_copy ) - bcc = self.get_mail_bcc_with_displayname( - is_inbound_mail_communcation=is_inbound_mail_communcation - ) + bcc = self.get_mail_bcc_with_displayname(is_inbound_mail_communcation=is_inbound_mail_communcation) if not (recipients or cc): return {} diff --git a/frappe/core/doctype/communication_link/communication_link.py b/frappe/core/doctype/communication_link/communication_link.py index b79ebca236..f6e2a52659 100644 --- a/frappe/core/doctype/communication_link/communication_link.py +++ b/frappe/core/doctype/communication_link/communication_link.py @@ -21,6 +21,7 @@ class CommunicationLink(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/custom_docperm/custom_docperm.py b/frappe/core/doctype/custom_docperm/custom_docperm.py index 97360cf35f..bf3a9b06da 100644 --- a/frappe/core/doctype/custom_docperm/custom_docperm.py +++ b/frappe/core/doctype/custom_docperm/custom_docperm.py @@ -32,5 +32,6 @@ class CustomDocPerm(Document): submit: DF.Check write: DF.Check # end: auto-generated types + def on_update(self): frappe.clear_cache(doctype=self.parent) diff --git a/frappe/core/doctype/custom_role/custom_role.py b/frappe/core/doctype/custom_role/custom_role.py index 9911f2fad2..34879fcbba 100644 --- a/frappe/core/doctype/custom_role/custom_role.py +++ b/frappe/core/doctype/custom_role/custom_role.py @@ -20,6 +20,7 @@ class CustomRole(Document): report: DF.Link | None roles: DF.Table[HasRole] # end: auto-generated types + def validate(self): if self.report and not self.ref_doctype: self.ref_doctype = frappe.db.get_value("Report", self.report, "ref_doctype") diff --git a/frappe/core/doctype/data_export/data_export.js b/frappe/core/doctype/data_export/data_export.js index a819ed7fe3..1808578d31 100644 --- a/frappe/core/doctype/data_export/data_export.js +++ b/frappe/core/doctype/data_export/data_export.js @@ -153,7 +153,7 @@ const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => { const options = fields.map((df) => { return { - label: df.label, + label: __(df.label), value: df.fieldname, danger: df.reqd, checked: 1, @@ -163,7 +163,7 @@ const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => { const multicheck_control = frappe.ui.form.make_control({ parent: parent_wrapper, df: { - label: doctype, + label: __(doctype), fieldname: doctype + "_fields", fieldtype: "MultiCheck", options: options, diff --git a/frappe/core/doctype/data_export/data_export.py b/frappe/core/doctype/data_export/data_export.py index c8d25dfa38..df2abd4702 100644 --- a/frappe/core/doctype/data_export/data_export.py +++ b/frappe/core/doctype/data_export/data_export.py @@ -17,4 +17,5 @@ class DataExport(Document): file_type: DF.Literal["Excel", "CSV"] reference_doctype: DF.Link # end: auto-generated types + pass diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index 1e8df913b7..2cbfc632ec 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -183,9 +183,7 @@ class DataExporter: self.writer.writerow([_("Notes:")]) self.writer.writerow([_("Please do not change the template headings.")]) self.writer.writerow([_("First data column must be blank.")]) - self.writer.writerow( - [_('If you are uploading new records, leave the "name" (ID) column blank.')] - ) + self.writer.writerow([_('If you are uploading new records, leave the "name" (ID) column blank.')]) self.writer.writerow( [_('If you are uploading new records, "Naming Series" becomes mandatory, if present.')] ) @@ -252,7 +250,9 @@ class DataExporter: "label": "Parent", "fieldtype": "Data", "reqd": 1, - "info": _("Parent is the name of the document to which the data will get added to."), + "info": _( + "Parent is the name of the document to which the data will get added to." + ), } ), True, diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 9f0e11a7b7..c27ea9a062 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -135,34 +135,29 @@ frappe.ui.form.on("Data Import", { let failed_records = cint(r.message.failed); 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"; + if (!total_records) { + return; } - if (failed_records === 0) { - let message_args = [action, successful_records]; - if (successful_records === 1) { - message = __("Successfully {0} 1 record.", message_args); - } else { - message = __("Successfully {0} {1} records.", message_args); - } + let message; + if (frm.doc.import_type === "Insert New Records") { + message = __("Successfully imported {0} out of {1} records.", [ + successful_records, + total_records, + ]); } else { - 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 + message = __("Successfully updated {0} out of {1} records.", [ + successful_records, + total_records, + ]); + } + + if (failed_records > 0) { + message += + "
" + + __( + "Please click on 'Export Errored Rows', fix the errors and import again." ); - } else { - 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 @@ -506,13 +501,7 @@ frappe.ui.form.on("Data Import", { }, show_import_log(frm) { - if (!frm.doc.show_failed_logs) { - frm.toggle_display("import_log_preview", false); - return; - } - frm.toggle_display("import_log_section", false); - frm.toggle_display("import_log_preview", true); if (frm.import_in_progress) { return; diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json index 97716219a2..02d65c8004 100644 --- a/frappe/core/doctype/data_import/data_import.json +++ b/frappe/core/doctype/data_import/data_import.json @@ -139,7 +139,7 @@ "default": "0", "fieldname": "show_failed_logs", "fieldtype": "Check", - "label": "Show Failed Logs" + "label": "Show Only Failed Logs" }, { "depends_on": "eval:!doc.__islocal && !doc.import_file", @@ -171,7 +171,7 @@ ], "hide_toolbar": 1, "links": [], - "modified": "2023-12-15 12:45:49.452834", + "modified": "2024-01-30 17:08:05.566686", "modified_by": "Administrator", "module": "Core", "name": "Data Import", diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index ac28f9091f..ab9dfa9372 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -38,7 +38,6 @@ class DataImport(Document): submit_after_import: DF.Check template_options: DF.Code | None template_warnings: DF.Code | None - # end: auto-generated types def validate(self): @@ -93,7 +92,8 @@ class DataImport(Document): def start_import(self): from frappe.utils.scheduler import is_scheduler_inactive - if is_scheduler_inactive() and not frappe.flags.in_test: + run_now = frappe.flags.in_test or frappe.conf.developer_mode + if is_scheduler_inactive() and not run_now: frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) job_id = f"data_import::{self.name}" @@ -106,7 +106,7 @@ class DataImport(Document): event="data_import", job_id=job_id, data_import=self.name, - now=frappe.conf.developer_mode or frappe.flags.in_test, + now=run_now, ) return True @@ -154,9 +154,7 @@ def start_import(data_import): @frappe.whitelist() -def download_template( - doctype, export_fields=None, export_records=None, export_filters=None, file_type="CSV" -): +def download_template(doctype, export_fields=None, export_records=None, export_filters=None, file_type="CSV"): """ Download template from Exporter :param doctype: Document Type @@ -273,7 +271,7 @@ def export_json(doctype, path, filters=None, or_filters=None, name=None, order_b for key in del_keys: if key in doc: del doc[key] - for k, v in doc.items(): + for v in doc.values(): if isinstance(v, list): for child in v: for key in del_keys + ("docstatus", "doctype", "modified", "name"): diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 691474c3d3..3129ff5447 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -50,9 +50,7 @@ class Exporter: self.add_data() def get_all_exportable_fields(self): - child_table_fields = [ - df.fieldname for df in self.meta.fields if df.fieldtype in table_fieldtypes - ] + child_table_fields = [df.fieldname for df in self.meta.fields if df.fieldtype in table_fieldtypes] meta = frappe.get_meta(self.doctype) exportable_fields = frappe._dict({}) @@ -206,9 +204,7 @@ class Exporter: if is_parent: label = _(df.label or df.fieldname) else: - label = ( - f"{_(df.label or df.fieldname)} ({_(df.child_table_df.label or df.child_table_df.fieldname)})" - ) + label = f"{_(df.label or df.fieldname)} ({_(df.child_table_df.label or df.child_table_df.fieldname)})" if label in header: # this label is already in the header, diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 84f6acf8af..fe4b1bc169 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -1,7 +1,6 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import io import json import os import re @@ -103,10 +102,7 @@ class Importer: log_index = 0 # Do not remove rows in case of retry after an error or pending data import - if ( - self.data_import.status == "Partial Success" - and len(import_log) >= self.data_import.payload_count - ): + if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count: # remove previous failures from import log only in case of retry after partial success import_log = [log for log in import_log if log.get("success")] @@ -152,8 +148,8 @@ class Importer: if self.console: update_progress_bar( - f"Importing {total_payload_count} records", - current_index, + f"Importing {self.doctype}: {total_payload_count} records", + current_index - 1, total_payload_count, ) elif total_payload_count > 5: @@ -528,7 +524,6 @@ class ImportFile: # subsequent rows that have blank values in parent columns # are considered as child rows parent_column_indexes = self.header.get_column_indexes(self.doctype) - parent_row_values = first_row.get_values(parent_column_indexes) data_without_first_row = data[1:] for row in data_without_first_row: @@ -622,7 +617,9 @@ class Row: if len_row != len_columns: less_than_columns = len_row < len_columns message = ( - "Row has less values than columns" if less_than_columns else "Row has more values than columns" + "Row has less values than columns" + if less_than_columns + else "Row has more values than columns" ) self.warnings.append( { @@ -657,7 +654,7 @@ class Row: for key in frappe.model.default_fields + frappe.model.child_table_fields + ("__islocal",): doc.pop(key, None) - for col, value in zip(columns, values): + for col, value in zip(columns, values, strict=False): df = col.df if value in INVALID_VALUES: value = None @@ -752,7 +749,7 @@ class Row: def parse_value(self, value, col): df = col.df - if isinstance(value, (datetime, date)) and df.fieldtype in ["Date", "Datetime"]: + if isinstance(value, datetime | date) and df.fieldtype in ["Date", "Datetime"]: return value value = cstr(value) @@ -775,7 +772,7 @@ class Row: return value def get_date(self, value, column): - if isinstance(value, (datetime, date)): + if isinstance(value, datetime | date): return value date_format = column.date_format @@ -939,7 +936,7 @@ class Column: """ def guess_date_format(d): - if isinstance(d, (datetime, date, time)): + if isinstance(d, datetime | date | time): if self.df.fieldtype == "Date": return "%Y-%m-%d" if self.df.fieldtype == "Datetime": @@ -989,9 +986,7 @@ class Column: if self.df.fieldtype == "Link": # find all values that dont exist values = list({cstr(v) for v in self.column_values if v}) - exists = [ - cstr(d.name) for d in frappe.get_all(self.df.options, filters={"name": ("in", values)}) - ] + exists = [cstr(d.name) for d in frappe.get_all(self.df.options, filters={"name": ("in", values)})] not_exists = list(set(values) - set(exists)) if not_exists: missing_values = ", ".join(not_exists) @@ -1140,7 +1135,6 @@ def build_fields_dict_for_column_matching(parent_doctype): label = (df.label or "").strip() translated_label = _(label) - parent = df.parent or parent_doctype if parent_doctype == doctype: # for parent doctypes keys will be @@ -1236,9 +1230,7 @@ def get_item_at_index(_list, i, default=None): def get_user_format(date_format): - return ( - date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd") - ) + return date_format.replace("%Y", "yyyy").replace("%y", "yy").replace("%m", "mm").replace("%d", "dd") def df_as_json(df): diff --git a/frappe/core/doctype/data_import_log/data_import_log.py b/frappe/core/doctype/data_import_log/data_import_log.py index 2135057fea..3abc8d798c 100644 --- a/frappe/core/doctype/data_import_log/data_import_log.py +++ b/frappe/core/doctype/data_import_log/data_import_log.py @@ -22,4 +22,5 @@ class DataImportLog(Document): row_indexes: DF.Code | None success: DF.Check # end: auto-generated types + pass diff --git a/frappe/core/doctype/defaultvalue/defaultvalue.py b/frappe/core/doctype/defaultvalue/defaultvalue.py index 0458ef8452..91b52a3a4d 100644 --- a/frappe/core/doctype/defaultvalue/defaultvalue.py +++ b/frappe/core/doctype/defaultvalue/defaultvalue.py @@ -20,6 +20,7 @@ class DefaultValue(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/deleted_document/deleted_document.py b/frappe/core/doctype/deleted_document/deleted_document.py index c99b6ad507..fb5fc6f5cb 100644 --- a/frappe/core/doctype/deleted_document/deleted_document.py +++ b/frappe/core/doctype/deleted_document/deleted_document.py @@ -25,6 +25,7 @@ class DeletedDocument(Document): new_name: DF.ReadOnly | None restored: DF.Check # end: auto-generated types + pass @staticmethod diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 22b84d1cbc..7b188569d7 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -157,6 +157,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.is_virtual", "fieldname": "in_list_view", "fieldtype": "Check", "label": "In List View", @@ -580,7 +581,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-16 11:26:56.364594", + "modified": "2024-02-01 15:55:44.007917", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index 01fa56a9ce..ed663c3bba 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -117,6 +117,7 @@ class DocField(Document): unique: DF.Check width: DF.Data | None # end: auto-generated types + def get_link_doctype(self): """Return the Link doctype for the `docfield` (if applicable). diff --git a/frappe/core/doctype/docperm/docperm.py b/frappe/core/doctype/docperm/docperm.py index c508b77ce1..d014d7dae1 100644 --- a/frappe/core/doctype/docperm/docperm.py +++ b/frappe/core/doctype/docperm/docperm.py @@ -33,4 +33,5 @@ class DocPerm(Document): submit: DF.Check write: DF.Check # end: auto-generated types + pass diff --git a/frappe/core/doctype/docshare/docshare.py b/frappe/core/doctype/docshare/docshare.py index ae10d161e0..9d25629328 100644 --- a/frappe/core/doctype/docshare/docshare.py +++ b/frappe/core/doctype/docshare/docshare.py @@ -28,6 +28,7 @@ class DocShare(Document): user: DF.Link | None write: DF.Check # end: auto-generated types + no_feed_on_delete = True def validate(self): @@ -58,13 +59,10 @@ class DocShare(Document): if not self.flags.ignore_share_permission and not frappe.has_permission( self.share_doctype, "share", self.get_doc() ): - frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError) def check_is_submittable(self): - if self.submit and not cint( - frappe.db.get_value("DocType", self.share_doctype, "is_submittable") - ): + if self.submit and not cint(frappe.db.get_value("DocType", self.share_doctype, "is_submittable")): frappe.throw( _("Cannot share {0} with submit permission as the doctype {1} is not submittable").format( frappe.bold(self.share_name), frappe.bold(self.share_doctype) diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py index 125e829d9b..9edf78405f 100644 --- a/frappe/core/doctype/docshare/test_docshare.py +++ b/frappe/core/doctype/docshare/test_docshare.py @@ -56,6 +56,24 @@ class TestDocShare(FrappeTestCase): with self.assertRowsRead(1): self.assertTrue(self.event.has_permission()) + def test_list_permission(self): + frappe.set_user(self.user) + with self.assertRaises(frappe.PermissionError): + frappe.get_list("Web Page") + + frappe.set_user("Administrator") + doc = frappe.new_doc("Web Page") + doc.update({"title": "test document for docshare permissions"}) + doc.insert() + frappe.share.add("Web Page", doc.name, self.user) + + frappe.set_user(self.user) + self.assertEqual(len(frappe.get_list("Web Page")), 1) + + doc.delete(ignore_permissions=True) + with self.assertRaises(frappe.PermissionError): + frappe.get_list("Web Page") + def test_share_permission(self): frappe.share.add("Event", self.event.name, self.user, write=1, share=1) @@ -118,9 +136,7 @@ class TestDocShare(FrappeTestCase): doctype = "Test DocShare with Submit" create_submittable_doctype(doctype, submit_perms=0) - submittable_doc = frappe.get_doc( - dict(doctype=doctype, test="test docshare with submit") - ).insert() + submittable_doc = frappe.get_doc(doctype=doctype, test="test docshare with submit").insert() frappe.set_user(self.user) self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user)) @@ -129,15 +145,11 @@ class TestDocShare(FrappeTestCase): frappe.share.add(doctype, submittable_doc.name, self.user, submit=1) frappe.set_user(self.user) - self.assertTrue( - frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user) - ) + self.assertTrue(frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user)) # test cascade self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user)) - self.assertTrue( - frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user) - ) + self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user)) frappe.share.remove(doctype, submittable_doc.name, self.user) diff --git a/frappe/core/doctype/doctype/boilerplate/templates/controller.html b/frappe/core/doctype/doctype/boilerplate/templates/controller.html index 412368d9b7..51237e95f9 100644 --- a/frappe/core/doctype/doctype/boilerplate/templates/controller.html +++ b/frappe/core/doctype/doctype/boilerplate/templates/controller.html @@ -1,7 +1,7 @@ {{% extends "templates/web.html" %}} {{% block page_content %}} -

{{{{ title }}}}

+

{{{{ title |e }}}}

{{% endblock %}} \ No newline at end of file diff --git a/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html b/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html index 66fe744830..e634c28674 100644 --- a/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html +++ b/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html @@ -1,4 +1,4 @@ diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index f71ed048cb..f079e506ff 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -133,7 +133,6 @@ class DocType(Document): is_virtual: DF.Check issingle: DF.Check istable: DF.Check - link_filters: DF.JSON links: DF.Table[DocTypeLink] make_attachments_public: DF.Check max_attachments: DF.Int @@ -313,7 +312,9 @@ class DocType(Document): continue frappe.msgprint( - _("{0} should be indexed because it's referred in dashboard connections").format(_(d.label)), + _("{0} should be indexed because it's referred in dashboard connections").format( + _(d.label) + ), alert=True, indicator="orange", ) @@ -330,9 +331,7 @@ class DocType(Document): ) if self.is_virtual and self.custom: - frappe.throw( - _("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError - ) + frappe.throw(_("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError) if frappe.conf.get("developer_mode"): self.owner = "Administrator" @@ -485,10 +484,14 @@ class DocType(Document): elif d.fieldtype in ("Section Break", "Column Break", "Tab Break"): d.fieldname = d.fieldtype.lower().replace(" ", "_") + "_" + str(random_string(4)) else: - frappe.throw(_("Row #{}: Fieldname is required").format(d.idx), title="Missing Fieldname") + frappe.throw( + _("Row #{}: Fieldname is required").format(d.idx), title="Missing Fieldname" + ) else: if d.fieldname in restricted: - frappe.throw(_("Fieldname {0} is restricted").format(d.fieldname), InvalidFieldNameError) + frappe.throw( + _("Fieldname {0} is restricted").format(d.fieldname), InvalidFieldNameError + ) d.fieldname = ILLEGAL_FIELDNAME_PATTERN.sub("", d.fieldname) # fieldnames should be lowercase @@ -886,9 +889,7 @@ class DocType(Document): if self.allow_auto_repeat: if not frappe.db.exists( "Custom Field", {"fieldname": "auto_repeat", "dt": self.name} - ) and not frappe.db.exists( - "DocField", {"fieldname": "auto_repeat", "parent": self.name} - ): + ) and not frappe.db.exists("DocField", {"fieldname": "auto_repeat", "parent": self.name}): insert_after = self.fields[len(self.fields) - 1].fieldname df = dict( fieldname="auto_repeat", @@ -997,7 +998,8 @@ class DocType(Document): if len(name) > max_length: # length(tab + ) should be equal to 64 characters hence doctype should be 61 characters frappe.throw( - _("Doctype name is limited to {0} characters ({1})").format(max_length, name), frappe.NameError + _("Doctype name is limited to {0} characters ({1})").format(max_length, name), + frappe.NameError, ) # a DocType name should not start or end with an empty space @@ -1056,7 +1058,6 @@ def validate_series(dt, autoname=None, name=None): and (not autoname.startswith("naming_series:")) and (not autoname.startswith("format:")) ): - prefix = autoname.split(".", 1)[0] doctype = frappe.qb.DocType("DocType") used_in = ( @@ -1095,7 +1096,6 @@ def validate_autoincrement_autoname(dt: Union[DocType, "CustomizeForm"]) -> bool and autoname_before_save != "autoincrement" or (not is_autoname_autoincrement and autoname_before_save == "autoincrement") ): - if dt.doctype == "Customize Form": frappe.throw(_("Cannot change to/from autoincrement autoname in Customize Form")) @@ -1333,7 +1333,9 @@ def validate_fields(meta): ) elif d.default not in d.options.split("\n"): frappe.throw( - _("Default value for {0} must be in the list of options.").format(frappe.bold(d.fieldname)) + _("Default value for {0} must be in the list of options.").format( + frappe.bold(d.fieldname) + ) ) def check_precision(d): @@ -1353,7 +1355,7 @@ def validate_fields(meta): d.search_index = 0 if getattr(d, "unique", False): - if d.fieldtype not in ("Data", "Link", "Read Only"): + if d.fieldtype not in ("Data", "Link", "Read Only", "Int"): frappe.throw( _("{0}: Fieldtype {1} for {2} cannot be unique").format(docname, d.fieldtype, d.label), NonUniqueError, @@ -1361,11 +1363,9 @@ def validate_fields(meta): if not d.get("__islocal") and frappe.db.has_column(d.parent, d.fieldname): has_non_unique_values = frappe.db.sql( - """select `{fieldname}`, count(*) - from `tab{doctype}` where ifnull(`{fieldname}`, '') != '' - group by `{fieldname}` having count(*) > 1 limit 1""".format( - doctype=d.parent, fieldname=d.fieldname - ) + f"""select `{d.fieldname}`, count(*) + from `tab{d.parent}` where ifnull(`{d.fieldname}`, '') != '' + group by `{d.fieldname}` having count(*) > 1 limit 1""" ) if has_non_unique_values and has_non_unique_values[0][0]: @@ -1539,16 +1539,14 @@ def validate_fields(meta): field.options = "\n".join(options_list) def scrub_fetch_from(field): - if hasattr(field, "fetch_from") and getattr(field, "fetch_from"): + if hasattr(field, "fetch_from") and field.fetch_from: field.fetch_from = field.fetch_from.strip("\n").strip() def validate_data_field_type(docfield): if docfield.get("is_virtual"): return - if docfield.fieldtype == "Data" and not ( - docfield.oldfieldtype and docfield.oldfieldtype != "Data" - ): + if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"): if docfield.options and (docfield.options not in data_field_options): df_str = frappe.bold(_(docfield.label)) text_str = ( @@ -1688,9 +1686,7 @@ def validate_permissions(doctype, for_remove=False, alert=False): return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx) def check_atleast_one_set(d): - if ( - not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create - ): + if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create: frappe.throw(_("{0}: No basic permissions set").format(get_txt(d))) def check_double(d): @@ -1720,7 +1716,9 @@ def validate_permissions(doctype, for_remove=False, alert=False): if not has_zero_perm: frappe.throw( - _("{0}: Permission at level 0 must be set before higher levels are set").format(get_txt(d)) + _("{0}: Permission at level 0 must be set before higher levels are set").format( + get_txt(d) + ) ) for invalid in ("create", "submit", "cancel", "amend"): @@ -1765,9 +1763,9 @@ def validate_permissions(doctype, for_remove=False, alert=False): if doctype.custom: if d.role in AUTOMATIC_ROLES: frappe.throw( - _("Row # {0}: Non administrator user can not set the role {1} to the custom doctype").format( - d.idx, frappe.bold(_(d.role)) - ), + _( + "Row # {0}: Non administrator user can not set the role {1} to the custom doctype" + ).format(d.idx, frappe.bold(_(d.role))), title=_("Permissions Error"), ) @@ -1775,9 +1773,9 @@ def validate_permissions(doctype, for_remove=False, alert=False): if d.role in roles: frappe.throw( - _("Row # {0}: Non administrator user can not set the role {1} to the custom doctype").format( - d.idx, frappe.bold(_(d.role)) - ), + _( + "Row # {0}: Non administrator user can not set the role {1} to the custom doctype" + ).format(d.idx, frappe.bold(_(d.role))), title=_("Permissions Error"), ) @@ -1803,7 +1801,7 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): and doc.restrict_to_domain and not frappe.db.exists("Domain", doc.restrict_to_domain) ): - frappe.get_doc(dict(doctype="Domain", domain=doc.restrict_to_domain)).insert() + frappe.get_doc(doctype="Domain", domain=doc.restrict_to_domain).insert() if "tabModule Def" in frappe.db.get_tables() and not frappe.db.exists("Module Def", doc.module): m = frappe.get_doc({"doctype": "Module Def", "module_name": doc.module}) @@ -1826,7 +1824,7 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): r.desk_access = 1 r.flags.ignore_mandatory = r.flags.ignore_permissions = True r.insert() - except frappe.DoesNotExistError as e: + except frappe.DoesNotExistError: pass except frappe.db.ProgrammingError as e: if frappe.db.is_table_missing(e): @@ -1840,9 +1838,7 @@ def check_fieldname_conflicts(docfield): doc = frappe.get_doc({"doctype": docfield.dt}) available_objects = [x for x in dir(doc) if isinstance(x, str)] property_list = [x for x in available_objects if is_a_property(getattr(type(doc), x, None))] - method_list = [ - x for x in available_objects if x not in property_list and callable(getattr(doc, x)) - ] + method_list = [x for x in available_objects if x not in property_list and callable(getattr(doc, x))] msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname) if docfield.fieldname in method_list + property_list: diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index a5657f590a..464996d3e9 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -220,9 +220,7 @@ class TestDocType(FrappeTestCase): self.assertListEqual( [f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] ) - self.assertListEqual( - [f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order - ) + self.assertListEqual([f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order) self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) # remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order @@ -246,9 +244,7 @@ class TestDocType(FrappeTestCase): self.assertListEqual( [f["fieldname"] for f in test_doctype_json["fields"]], test_doctype_json["field_order"] ) - self.assertListEqual( - [f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order - ) + self.assertListEqual([f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order) self.assertListEqual(test_doctype_json["field_order"], initial_fields_order) # reorder fields: swap row 1 and 3 @@ -259,9 +255,7 @@ class TestDocType(FrappeTestCase): # assert that reordering fields only affects `field_order` rather than `fields` attr test_doctype.save() test_doctype_json = frappe.get_file_json(path) - self.assertListEqual( - [f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order - ) + self.assertListEqual([f["fieldname"] for f in test_doctype_json["fields"]], initial_fields_order) self.assertListEqual( test_doctype_json["field_order"], ["field_3", "field_2", "field_1", "field_4"] ) diff --git a/frappe/core/doctype/doctype_action/doctype_action.py b/frappe/core/doctype/doctype_action/doctype_action.py index 5ce72814e0..3547bdc6ed 100644 --- a/frappe/core/doctype/doctype_action/doctype_action.py +++ b/frappe/core/doctype/doctype_action/doctype_action.py @@ -24,4 +24,5 @@ class DocTypeAction(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/doctype_link/doctype_link.py b/frappe/core/doctype/doctype_link/doctype_link.py index 338642f567..9458799c3d 100644 --- a/frappe/core/doctype/doctype_link/doctype_link.py +++ b/frappe/core/doctype/doctype_link/doctype_link.py @@ -26,4 +26,5 @@ class DocTypeLink(Document): parenttype: DF.Data table_fieldname: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/core/doctype/doctype_state/doctype_state.py b/frappe/core/doctype/doctype_state/doctype_state.py index 9d2f183eed..a5ee1e9099 100644 --- a/frappe/core/doctype/doctype_state/doctype_state.py +++ b/frappe/core/doctype/doctype_state/doctype_state.py @@ -23,4 +23,5 @@ class DocTypeState(Document): parenttype: DF.Data title: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 545929eaa1..b6460797f6 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -28,6 +28,7 @@ class DocumentNamingRule(Document): prefix_digits: DF.Int priority: DF.Int # end: auto-generated types + def validate(self): self.validate_fields_in_conditions() diff --git a/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py index 60e6803a1a..0747b97025 100644 --- a/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py @@ -7,11 +7,11 @@ from frappe.tests.utils import FrappeTestCase class TestDocumentNamingRule(FrappeTestCase): def test_naming_rule_by_series(self): naming_rule = frappe.get_doc( - dict(doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5) + doctype="Document Naming Rule", document_type="ToDo", prefix="test-todo-", prefix_digits=5 ).insert() todo = frappe.get_doc( - dict(doctype="ToDo", description="Is this my name " + frappe.generate_hash()) + doctype="ToDo", description="Is this my name " + frappe.generate_hash() ).insert() self.assertEqual(todo.name, "test-todo-00001") @@ -21,14 +21,12 @@ class TestDocumentNamingRule(FrappeTestCase): def test_naming_rule_by_condition(self): naming_rule = frappe.get_doc( - dict( - doctype="Document Naming Rule", - document_type="ToDo", - prefix="test-high-", - prefix_digits=5, - priority=10, - conditions=[dict(field="priority", condition="=", value="High")], - ) + doctype="Document Naming Rule", + document_type="ToDo", + prefix="test-high-", + prefix_digits=5, + priority=10, + conditions=[dict(field="priority", condition="=", value="High")], ).insert() # another rule @@ -46,15 +44,15 @@ class TestDocumentNamingRule(FrappeTestCase): naming_rule_2.insert() todo = frappe.get_doc( - dict(doctype="ToDo", priority="High", description="Is this my name " + frappe.generate_hash()) + doctype="ToDo", priority="High", description="Is this my name " + frappe.generate_hash() ).insert() todo_1 = frappe.get_doc( - dict(doctype="ToDo", priority="Medium", description="Is this my name " + frappe.generate_hash()) + doctype="ToDo", priority="Medium", description="Is this my name " + frappe.generate_hash() ).insert() todo_2 = frappe.get_doc( - dict(doctype="ToDo", priority="Low", description="Is this my name " + frappe.generate_hash()) + doctype="ToDo", priority="Low", description="Is this my name " + frappe.generate_hash() ).insert() try: diff --git a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py index 0b8d540448..bb836401c1 100644 --- a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py +++ b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py @@ -21,4 +21,5 @@ class DocumentNamingRuleCondition(Document): parenttype: DF.Data value: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/document_naming_settings/document_naming_settings.py b/frappe/core/doctype/document_naming_settings/document_naming_settings.py index ddb25dd262..84b20469ce 100644 --- a/frappe/core/doctype/document_naming_settings/document_naming_settings.py +++ b/frappe/core/doctype/document_naming_settings/document_naming_settings.py @@ -36,16 +36,15 @@ class DocumentNamingSettings(Document): try_naming_series: DF.Data | None user_must_always_select: DF.Check # end: auto-generated types + @frappe.whitelist() def get_transactions_and_prefixes(self): - transactions = self._get_transactions() prefixes = self._get_prefixes(transactions) return {"transactions": transactions, "prefixes": prefixes} def _get_transactions(self) -> list[str]: - readable_doctypes = set(get_doctypes_with_read()) standard = frappe.get_all("DocField", {"fieldname": "naming_series"}, "parent", pluck="parent") @@ -218,9 +217,7 @@ class DocumentNamingSettings(Document): previous_value = naming_series.get_current_value() naming_series.update_counter(self.current_value) - self.create_version_log_for_change( - naming_series.get_prefix(), previous_value, self.current_value - ) + self.create_version_log_for_change(naming_series.get_prefix(), previous_value, self.current_value) frappe.msgprint( _("Series counter for {} updated to {} successfully").format(self.prefix, self.current_value), diff --git a/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py b/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py index 8fc1584aa3..237d8108c9 100644 --- a/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py +++ b/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py @@ -54,7 +54,6 @@ class TestNamingSeries(FrappeTestCase): serieses = self.dns.preview_series().split("\n") def test_get_transactions(self): - naming_info = self.dns.get_transactions_and_prefixes() self.assertIn(self.ns_doctype, naming_info["transactions"]) @@ -90,16 +89,12 @@ class TestNamingSeries(FrappeTestCase): self.dns.update_amendment_rule() submittable_doc = frappe.get_doc( - dict(doctype=self.ns_doctype, some_fieldname="test doc with submit") + doctype=self.ns_doctype, some_fieldname="test doc with submit" ).submit() submittable_doc.cancel() amended_doc = frappe.get_doc( - dict( - doctype=self.ns_doctype, - some_fieldname="test doc with submit", - amended_from=submittable_doc.name, - ) + doctype=self.ns_doctype, some_fieldname="test doc with submit", amended_from=submittable_doc.name ).insert() self.assertIn(submittable_doc.name, amended_doc.name) @@ -109,10 +104,6 @@ class TestNamingSeries(FrappeTestCase): self.dns.update_amendment_rule() new_amended_doc = frappe.get_doc( - dict( - doctype=self.ns_doctype, - some_fieldname="test doc with submit", - amended_from=submittable_doc.name, - ) + doctype=self.ns_doctype, some_fieldname="test doc with submit", amended_from=submittable_doc.name ).insert() self.assertNotIn(submittable_doc.name, new_amended_doc.name) diff --git a/frappe/core/doctype/document_share_key/document_share_key.py b/frappe/core/doctype/document_share_key/document_share_key.py index ffff1e1387..5ec9ad254f 100644 --- a/frappe/core/doctype/document_share_key/document_share_key.py +++ b/frappe/core/doctype/document_share_key/document_share_key.py @@ -21,6 +21,7 @@ class DocumentShareKey(Document): reference_docname: DF.DynamicLink | None reference_doctype: DF.Link | None # end: auto-generated types + def before_insert(self): self.key = frappe.generate_hash(length=randrange(25, 35)) if not self.expires_on and not self.flags.no_expiry: diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py index 50e187968e..aa4b43a9ee 100644 --- a/frappe/core/doctype/domain/domain.py +++ b/frappe/core/doctype/domain/domain.py @@ -18,6 +18,7 @@ class Domain(Document): domain: DF.Data # end: auto-generated types + """Domain documents are created automatically when DocTypes with "Restricted" domains are imported during installation or migration""" @@ -78,7 +79,7 @@ class Domain(Document): for role_name in self.data.restricted_roles: user.append("roles", {"role": role_name}) if not frappe.db.get_value("Role", role_name): - frappe.get_doc(dict(doctype="Role", role_name=role_name)).insert() + frappe.get_doc(doctype="Role", role_name=role_name).insert() continue role = frappe.get_doc("Role", role_name) @@ -123,9 +124,7 @@ class Domain(Document): # enable frappe.db.sql( """update `tabPortal Menu Item` set enabled=1 - where route in ({})""".format( - ", ".join(f'"{d}"' for d in self.data.allow_sidebar_items) - ) + where route in ({})""".format(", ".join(f'"{d}"' for d in self.data.allow_sidebar_items)) ) if self.data.remove_sidebar_items: @@ -135,7 +134,5 @@ class Domain(Document): # enable frappe.db.sql( """update `tabPortal Menu Item` set enabled=0 - where route in ({})""".format( - ", ".join(f'"{d}"' for d in self.data.remove_sidebar_items) - ) + where route in ({})""".format(", ".join(f'"{d}"' for d in self.data.remove_sidebar_items)) ) diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py index d56475f2cd..e2464854e4 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.py +++ b/frappe/core/doctype/domain_settings/domain_settings.py @@ -17,6 +17,7 @@ class DomainSettings(Document): active_domains: DF.Table[HasDomain] # end: auto-generated types + def set_active_domains(self, domains): active_domains = [d.domain for d in self.active_domains] added = False @@ -51,7 +52,7 @@ class DomainSettings(Document): for domain in all_domains: data = frappe.get_domain_data(domain) if not frappe.db.get_value("Domain", domain): - frappe.get_doc(dict(doctype="Domain", domain=domain)).insert() + frappe.get_doc(doctype="Domain", domain=domain).insert() if "modules" in data: for module in data.get("modules"): frappe.db.set_value("Module Def", module, "restrict_to_domain", domain) @@ -59,7 +60,7 @@ class DomainSettings(Document): if "restricted_roles" in data: for role in data["restricted_roles"]: if not frappe.db.get_value("Role", role): - frappe.get_doc(dict(doctype="Role", role_name=role)).insert() + frappe.get_doc(doctype="Role", role_name=role).insert() frappe.db.set_value("Role", role, "restrict_to_domain", domain) if domain not in active_domains: diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.py b/frappe/core/doctype/dynamic_link/dynamic_link.py index faf78cb425..774c465301 100644 --- a/frappe/core/doctype/dynamic_link/dynamic_link.py +++ b/frappe/core/doctype/dynamic_link/dynamic_link.py @@ -21,6 +21,7 @@ class DynamicLink(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py index 09a671638c..e9d8b63495 100644 --- a/frappe/core/doctype/error_log/error_log.py +++ b/frappe/core/doctype/error_log/error_log.py @@ -23,6 +23,7 @@ class ErrorLog(Document): seen: DF.Check trace_id: DF.Data | None # end: auto-generated types + def onload(self): if not self.seen and not frappe.flags.read_only: self.db_set("seen", 1, update_modified=0) diff --git a/frappe/core/doctype/error_log/test_error_log.py b/frappe/core/doctype/error_log/test_error_log.py index 22eeea329e..98c87dda52 100644 --- a/frappe/core/doctype/error_log/test_error_log.py +++ b/frappe/core/doctype/error_log/test_error_log.py @@ -52,15 +52,21 @@ _THROW_EXC = """ frappe.exceptions.ValidationError: what """ -TEST_EXCEPTIONS = { - "erpnext (app)": _RAW_EXC, - "erpnext (app)": _THROW_EXC, -} +TEST_EXCEPTIONS = ( + ( + "erpnext (app)", + _RAW_EXC, + ), + ( + "erpnext (app)", + _THROW_EXC, + ), +) class TestExceptionSourceGuessing(FrappeTestCase): @patch.object(frappe, "get_installed_apps", return_value=["frappe", "erpnext", "3pa"]) def test_exc_source_guessing(self, _installed_apps): - for source, exc in TEST_EXCEPTIONS.items(): + for source, exc in TEST_EXCEPTIONS: result = guess_exception_source(exc) self.assertEqual(result, source) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index de4375ae6c..e207e24a2a 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -61,6 +61,7 @@ class File(Document): uploaded_to_dropbox: DF.Check uploaded_to_google_drive: DF.Check # end: auto-generated types + no_feed_on_delete = True def __init__(self, *args, **kwargs): @@ -136,7 +137,7 @@ class File(Document): if not self.attached_to_doctype: return - if not self.attached_to_name or not isinstance(self.attached_to_name, (str, int)): + if not self.attached_to_name or not isinstance(self.attached_to_name, str | int): frappe.throw(_("Attached To Name must be a string or an integer"), frappe.ValidationError) if self.attached_to_field and SPECIAL_CHAR_PATTERN.search(self.attached_to_field): @@ -369,9 +370,7 @@ class File(Document): return if self.file_type not in allowed_extensions.splitlines(): - frappe.throw( - _("File type of {0} is not allowed").format(self.file_type), exc=FileTypeNotAllowed - ) + frappe.throw(_("File type of {0} is not allowed").format(self.file_type), exc=FileTypeNotAllowed) def validate_duplicate_entry(self): if not self.flags.ignore_duplicate_entry_error and not self.is_folder: @@ -710,9 +709,7 @@ class File(Document): def create_attachment_record(self): icon = ' ' if self.is_private else "" - file_url = ( - quote(frappe.safe_encode(self.file_url), safe="/:") if self.file_url else self.file_name - ) + file_url = quote(frappe.safe_encode(self.file_url), safe="/:") if self.file_url else self.file_name file_name = self.file_name or self.file_url self.add_comment_in_reference_doc( @@ -756,6 +753,16 @@ class File(Document): self.save_file(content=optimized_content, overwrite=True) self.save() + @property + def unique_url(self) -> str: + """Unique URL contains file ID in URL to speed up permisison checks.""" + from urllib.parse import urlencode + + if self.is_private: + return self.file_url + "?" + urlencode({"fid": self.name}) + else: + return self.file_url + @staticmethod def zip_files(files): zip_file = io.BytesIO() diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 43dc51c8b1..682a09011e 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -222,9 +222,7 @@ class TestSameContent(FrappeTestCase): doctype, docname = make_test_doc() from frappe.custom.doctype.property_setter.property_setter import make_property_setter - limit_property = make_property_setter( - "ToDo", None, "max_attachments", 1, "int", for_doctype=True - ) + limit_property = make_property_setter("ToDo", None, "max_attachments", 1, "int", for_doctype=True) file1 = frappe.get_doc( { "doctype": "File", @@ -451,9 +449,7 @@ class TestFile(FrappeTestCase): test_file.file_url = None test_file.file_name = "/usr/bin/man" - self.assertRaisesRegex( - ValidationError, "There is some problem with the file url", test_file.validate - ) + self.assertRaisesRegex(ValidationError, "There is some problem with the file url", test_file.validate) test_file.file_url = None test_file.file_name = "_file" @@ -670,9 +666,7 @@ class TestAttachmentsAccess(FrappeTestCase): frappe.set_user("test4@example.com") user_files = [file.file_name for file in get_files_in_folder("Home")["files"]] - user_attachments_files = [ - file.file_name for file in get_files_in_folder("Home/Attachments")["files"] - ] + user_attachments_files = [file.file_name for file in get_files_in_folder("Home/Attachments")["files"]] self.assertIn("test_sm_standalone.txt", system_manager_files) self.assertNotIn("test_sm_standalone.txt", user_files) diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index a1b6612d4c..9a0b4a8f5c 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -263,7 +263,7 @@ def extract_images_from_html(doc: "Document", content: str, is_private: bool = F } ) _file.save(ignore_permissions=True) - file_url = _file.file_url + file_url = _file.unique_url frappe.flags.has_dataurl = True return f' len(after_save_items)): frappe.throw(_("Please hide the standard navbar items instead of deleting them")) diff --git a/frappe/core/doctype/package/package.py b/frappe/core/doctype/package/package.py index 812a589940..9f0e959092 100644 --- a/frappe/core/doctype/package/package.py +++ b/frappe/core/doctype/package/package.py @@ -29,6 +29,7 @@ class Package(Document): package_name: DF.Data readme: DF.MarkdownEditor | None # end: auto-generated types + def validate(self): if not self.package_name: self.package_name = self.name.lower().replace(" ", "-") diff --git a/frappe/core/doctype/package/test_package.py b/frappe/core/doctype/package/test_package.py index 8af076f1be..25c5187701 100644 --- a/frappe/core/doctype/package/test_package.py +++ b/frappe/core/doctype/package/test_package.py @@ -17,7 +17,7 @@ class TestPackage(FrappeTestCase): make_test_web_page() # make release - frappe.get_doc(dict(doctype="Package Release", package="Test Package", publish=1)).insert() + frappe.get_doc(doctype="Package Release", package="Test Package", publish=1).insert() self.assertTrue(os.path.exists(frappe.get_site_path("packages", "test-package"))) self.assertTrue( @@ -26,7 +26,11 @@ class TestPackage(FrappeTestCase): self.assertTrue( os.path.exists( frappe.get_site_path( - "packages", "test-package", "test_module_for_package", "doctype", "test_doctype_for_package" + "packages", + "test-package", + "test_module_for_package", + "doctype", + "test_doctype_for_package", ) ) ) @@ -49,62 +53,52 @@ class TestPackage(FrappeTestCase): def make_test_package(): if not frappe.db.exists("Package", "Test Package"): frappe.get_doc( - dict( - doctype="Package", name="Test Package", package_name="test-package", readme="# Test Package" - ) + doctype="Package", name="Test Package", package_name="test-package", readme="# Test Package" ).insert() def make_test_module(): if not frappe.db.exists("Module Def", "Test Module for Package"): frappe.get_doc( - dict( - doctype="Module Def", - module_name="Test Module for Package", - custom=1, - app_name="frappe", - package="Test Package", - ) + doctype="Module Def", + module_name="Test Module for Package", + custom=1, + app_name="frappe", + package="Test Package", ).insert() def make_test_doctype(): if not frappe.db.exists("DocType", "Test DocType for Package"): frappe.get_doc( - dict( - doctype="DocType", - name="Test DocType for Package", - custom=1, - module="Test Module for Package", - autoname="Prompt", - fields=[dict(fieldname="test_field", fieldtype="Data", label="Test Field")], - ) + doctype="DocType", + name="Test DocType for Package", + custom=1, + module="Test Module for Package", + autoname="Prompt", + fields=[dict(fieldname="test_field", fieldtype="Data", label="Test Field")], ).insert() def make_test_server_script(): if not frappe.db.exists("Server Script", "Test Script for Package"): frappe.get_doc( - dict( - doctype="Server Script", - name="Test Script for Package", - module="Test Module for Package", - script_type="DocType Event", - reference_doctype="Test DocType for Package", - doctype_event="Before Save", - script='frappe.msgprint("Test")', - ) + doctype="Server Script", + name="Test Script for Package", + module="Test Module for Package", + script_type="DocType Event", + reference_doctype="Test DocType for Package", + doctype_event="Before Save", + script='frappe.msgprint("Test")', ).insert() def make_test_web_page(): if not frappe.db.exists("Web Page", "test-web-page-for-package"): frappe.get_doc( - dict( - doctype="Web Page", - module="Test Module for Package", - main_section="Some content", - published=1, - title="Test Web Page for Package", - ) + doctype="Web Page", + module="Test Module for Package", + main_section="Some content", + published=1, + title="Test Web Page for Package", ).insert() diff --git a/frappe/core/doctype/package_import/package_import.py b/frappe/core/doctype/package_import/package_import.py index a1c499067d..52dc08c406 100644 --- a/frappe/core/doctype/package_import/package_import.py +++ b/frappe/core/doctype/package_import/package_import.py @@ -27,6 +27,7 @@ class PackageImport(Document): force: DF.Check log: DF.Code | None # end: auto-generated types + def validate(self): if self.activate: self.import_package() diff --git a/frappe/core/doctype/package_release/package_release.py b/frappe/core/doctype/package_release/package_release.py index 8ebe0b3082..faf9369a9c 100644 --- a/frappe/core/doctype/package_release/package_release.py +++ b/frappe/core/doctype/package_release/package_release.py @@ -27,6 +27,7 @@ class PackageRelease(Document): publish: DF.Check release_notes: DF.MarkdownEditor | None # end: auto-generated types + def set_version(self): # set the next patch release by default doctype = frappe.qb.DocType("Package Release") @@ -115,12 +116,10 @@ class PackageRelease(Document): # make attachment file = frappe.get_doc( - dict( - doctype="File", - file_url="/" + os.path.join("files", filename), - attached_to_doctype=self.doctype, - attached_to_name=self.name, - ) + doctype="File", + file_url="/" + os.path.join("files", filename), + attached_to_doctype=self.doctype, + attached_to_name=self.name, ) file.flags.ignore_duplicate_entry_error = True diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index ce72220953..48d783f14e 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -33,6 +33,7 @@ class Page(Document): system_page: DF.Check title: DF.Data | None # end: auto-generated types + def autoname(self): """ Creates a url friendly name for this page. @@ -87,14 +88,13 @@ class Page(Document): if not os.path.exists(path + ".js"): with open(path + ".js", "w") as f: f.write( - """frappe.pages['%s'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ + f"""frappe.pages['{self.name}'].on_page_load = function(wrapper) {{ + var page = frappe.ui.make_app_page({{ parent: wrapper, - title: '%s', + title: '{self.title}', single_column: true - }); -}""" - % (self.name, self.title) + }}); +}}""" ) def as_dict(self, no_nulls=False): @@ -121,9 +121,7 @@ class Page(Document): """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 frappe.get_all("Has Role", fields=["role"], filters={"parent": self.name}) - ] + allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.name})] custom_roles = get_custom_allowed_roles("page", self.name) allowed.extend(custom_roles) @@ -170,7 +168,9 @@ class Page(Document): try: out = frappe.get_attr( "{app}.{module}.page.{page}.{page}.get_context".format( - app=frappe.local.module_app[scrub(self.module)], module=scrub(self.module), page=page_name + app=frappe.local.module_app[scrub(self.module)], + module=scrub(self.module), + page=page_name, ) )(context) diff --git a/frappe/core/doctype/page/test_page.py b/frappe/core/doctype/page/test_page.py index d8cf90b8d9..82208be1c1 100644 --- a/frappe/core/doctype/page/test_page.py +++ b/frappe/core/doctype/page/test_page.py @@ -14,7 +14,7 @@ class TestPage(FrappeTestCase): def test_naming(self): self.assertRaises( frappe.NameError, - frappe.get_doc(dict(doctype="Page", page_name="DocType", module="Core")).insert, + frappe.get_doc(doctype="Page", page_name="DocType", module="Core").insert, ) @unittest.skipUnless( diff --git a/frappe/core/doctype/patch_log/patch_log.py b/frappe/core/doctype/patch_log/patch_log.py index 12582c1199..05f15e70ff 100644 --- a/frappe/core/doctype/patch_log/patch_log.py +++ b/frappe/core/doctype/patch_log/patch_log.py @@ -20,6 +20,7 @@ class PatchLog(Document): skipped: DF.Check traceback: DF.Code | None # end: auto-generated types + pass diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 8710e35f64..d9f8360c75 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -38,6 +38,7 @@ class PreparedReport(Document): report_name: DF.Data status: DF.Literal["Error", "Queued", "Completed", "Started"] # end: auto-generated types + @property def queued_by(self): return self.owner @@ -217,9 +218,7 @@ def delete_prepared_reports(reports): def create_json_gz_file(data, dt, dn): # Storing data in CSV file causes information loss # Reports like P&L Statement were completely unsuable because of this - json_filename = "{}.json.gz".format( - frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M") - ) + json_filename = "{}.json.gz".format(frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M")) encoded_content = frappe.safe_encode(frappe.as_json(data)) compressed_content = gzip.compress(encoded_content) diff --git a/frappe/core/doctype/recorder/recorder.json b/frappe/core/doctype/recorder/recorder.json index 391f808f31..72291dbfe2 100644 --- a/frappe/core/doctype/recorder/recorder.json +++ b/frappe/core/doctype/recorder/recorder.json @@ -20,7 +20,9 @@ "section_break_sgro", "form_dict", "section_break_9jhm", - "sql_queries" + "sql_queries", + "section_break_optn", + "profile" ], "fields": [ { @@ -107,6 +109,16 @@ "fieldtype": "Data", "hidden": 1, "label": "Event Type" + }, + { + "fieldname": "section_break_optn", + "fieldtype": "Section Break" + }, + { + "fieldname": "profile", + "fieldtype": "Code", + "label": "cProfile Output", + "read_only": 1 } ], "hide_toolbar": 1, @@ -114,7 +126,7 @@ "index_web_pages_for_search": 1, "is_virtual": 1, "links": [], - "modified": "2024-01-03 16:45:47.110048", + "modified": "2024-02-01 22:13:26.505174", "modified_by": "Administrator", "module": "Core", "name": "Recorder", diff --git a/frappe/core/doctype/recorder/recorder.py b/frappe/core/doctype/recorder/recorder.py index 347a237743..26ccfcf378 100644 --- a/frappe/core/doctype/recorder/recorder.py +++ b/frappe/core/doctype/recorder/recorder.py @@ -24,6 +24,7 @@ class Recorder(Document): method: DF.Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] number_of_queries: DF.Int path: DF.Data | None + profile: DF.Code | None request_headers: DF.Code | None sql_queries: DF.Table[RecorderQuery] time: DF.Datetime | None diff --git a/frappe/core/doctype/recorder/recorder_list.js b/frappe/core/doctype/recorder/recorder_list.js index a0eadae260..35498032ed 100644 --- a/frappe/core/doctype/recorder/recorder_list.js +++ b/frappe/core/doctype/recorder/recorder_list.js @@ -10,12 +10,7 @@ frappe.listview_settings["Recorder"] = { } listview.page.add_button(__("Clear"), () => { - frappe.call({ - method: "frappe.recorder.delete", - callback: function () { - listview.refresh(); - }, - }); + frappe.xcall("frappe.recorder.delete").then(listview.refresh); }); listview.page.add_menu_item(__("Import"), () => { @@ -88,18 +83,125 @@ frappe.listview_settings["Recorder"] = { }, setup_recorder_controls(listview) { + let me = this; listview.page.set_primary_action(listview.enabled ? __("Stop") : __("Start"), () => { - frappe.call({ - method: listview.enabled ? "frappe.recorder.stop" : "frappe.recorder.start", - callback: function () { - listview.refresh(); - }, - }); - listview.enabled = !listview.enabled; - this.refresh_controls(listview); + if (listview.enabled) { + me.stop_recorder(listview); + } else { + me.start_recorder(listview); + } }); }, + stop_recorder(listview) { + let me = this; + frappe.xcall("frappe.recorder.stop", {}).then(() => { + listview.refresh(); + listview.enabled = false; + me.refresh_controls(listview); + }); + }, + + start_recorder(listview) { + let me = this; + frappe.prompt( + [ + { + fieldtype: "Section Break", + fieldname: "req_job_section", + }, + { + fieldtype: "Column Break", + fieldname: "web_request_columns", + label: "Web Requests", + }, + { + fieldname: "record_requests", + fieldtype: "Check", + label: "Record Web Requests", + default: 1, + }, + { + fieldname: "request_filter", + fieldtype: "Data", + label: "Request path filter", + default: "/", + depends_on: "record_requests", + description: `This will be used for filtering paths which will be recorded. + You can use this to avoid slowing down other traffic. + e.g. /api/method/erpnext. Leave it empty to record every request.`, + }, + { + fieldtype: "Column Break", + fieldname: "background_col", + label: "Background Jobs", + }, + + { + fieldname: "record_jobs", + fieldtype: "Check", + label: "Record Background Jobs", + default: 1, + }, + { + fieldname: "jobs_filter", + fieldtype: "Data", + label: "Background Jobs filter", + default: "", + depends_on: "record_jobs", + description: `This will be used for filtering jobs which will be recorded. + You can use this to avoid slowing down other jobs. e.g. email_queue.pull. + Leave it empty to record every job.`, + }, + { + fieldtype: "Section Break", + fieldname: "sql_section", + label: "SQL", + }, + { + fieldname: "record_sql", + fieldtype: "Check", + label: "Record SQL queries", + default: 1, + }, + { + fieldname: "explain", + fieldtype: "Check", + label: "Generate EXPLAIN for SQL queries", + default: 1, + }, + { + fieldname: "capture_stack", + fieldtype: "Check", + label: "Capture callstack of SQL queries", + default: 1, + }, + { + fieldtype: "Section Break", + fieldname: "python_section", + label: "Python", + }, + { + fieldname: "profile", + fieldtype: "Check", + label: "Run cProfile", + default: 0, + description: + "Warning: cProfile adds a lot of overhead. For best results, disable stack capturing when using cProfile.", + }, + ], + (values) => { + frappe.xcall("frappe.recorder.start", values).then(() => { + listview.refresh(); + listview.enabled = true; + me.refresh_controls(listview); + }); + }, + __("Configure Recorder"), + __("Start Recordig") + ); + }, + update_indicators(listview) { if (listview.enabled) { listview.page.set_indicator(__("Active"), "green"); diff --git a/frappe/core/doctype/recorder_query/recorder_query.py b/frappe/core/doctype/recorder_query/recorder_query.py index 185c927dbe..c797769dda 100644 --- a/frappe/core/doctype/recorder_query/recorder_query.py +++ b/frappe/core/doctype/recorder_query/recorder_query.py @@ -23,9 +23,10 @@ class RecorderQuery(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - query: DF.Data + query: DF.Data | None stack: DF.Text | None # end: auto-generated types + pass def db_insert(self, *args, **kwargs): diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index bb39142327..c108c8ee6e 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -55,7 +55,8 @@ class Report(Document): if not self.is_standard: self.is_standard = "No" if ( - frappe.session.user == "Administrator" and getattr(frappe.local.conf, "developer_mode", 0) == 1 + frappe.session.user == "Administrator" + and getattr(frappe.local.conf, "developer_mode", 0) == 1 ): self.is_standard = "Yes" @@ -108,9 +109,7 @@ class Report(Document): """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 frappe.get_all("Has Role", fields=["role"], filters={"parent": self.name}) - ] + allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.name})] custom_roles = get_custom_allowed_roles("report", self.name) @@ -132,9 +131,7 @@ class Report(Document): return if self.is_standard == "Yes" and frappe.conf.developer_mode: - export_to_files( - record_list=[["Report", self.name]], record_module=self.module, create_init=True - ) + export_to_files(record_list=[["Report", self.name]], record_module=self.module, create_init=True) self.create_report_py() @@ -304,7 +301,7 @@ class Report(Document): if filters: for key, value in filters.items(): condition, _value = "=", value - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): condition, _value = value _filters.append([key, condition, _value]) @@ -339,7 +336,7 @@ class Report(Document): def build_standard_report_columns(self, columns, group_by_args): _columns = [] - for (fieldname, doctype) in columns: + for fieldname, doctype in columns: meta = frappe.get_meta(doctype) if meta.get_field(fieldname): @@ -363,7 +360,7 @@ class Report(Document): def build_data_dict(self, result, columns): data = [] for row in result: - if isinstance(row, (list, tuple)): + if isinstance(row, list | tuple): _row = frappe._dict() for i, val in enumerate(row): _row[columns[i].get("fieldname")] = val diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 4f9c229ab8..b22f4b491c 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -131,7 +131,12 @@ class TestReport(FrappeTestCase): self.assertListEqual(["email"], [column.get("fieldname") for column in columns]) admin_dict = frappe.core.utils.find(result, lambda d: d["name"] == "Administrator") self.assertDictEqual( - {"name": "Administrator", "user_type": "System User", "email": "admin@example.com"}, admin_dict + { + "name": "Administrator", + "user_type": "System User", + "email": "admin@example.com", + }, + admin_dict, ) def test_report_with_custom_column(self): @@ -157,11 +162,17 @@ class TestReport(FrappeTestCase): result = response.get("result") columns = response.get("columns") self.assertListEqual( - ["name", "email", "user_type"], [column.get("fieldname") for column in columns] + ["name", "email", "user_type"], + [column.get("fieldname") for column in columns], ) admin_dict = frappe.core.utils.find(result, lambda d: d["name"] == "Administrator") self.assertDictEqual( - {"name": "Administrator", "user_type": "System User", "email": "admin@example.com"}, admin_dict + { + "name": "Administrator", + "user_type": "System User", + "email": "admin@example.com", + }, + admin_dict, ) def test_report_permissions(self): @@ -169,9 +180,7 @@ class TestReport(FrappeTestCase): frappe.db.delete("Has Role", {"parent": frappe.session.user, "role": "Test Has Role"}) frappe.db.commit() if not frappe.db.exists("Role", "Test Has Role"): - role = frappe.get_doc({"doctype": "Role", "role_name": "Test Has Role"}).insert( - ignore_permissions=True - ) + frappe.get_doc({"doctype": "Role", "role_name": "Test Has Role"}).insert(ignore_permissions=True) if not frappe.db.exists("Report", "Test Report"): report = frappe.get_doc( @@ -226,9 +235,7 @@ class TestReport(FrappeTestCase): def test_format_method(self): if frappe.db.exists("Report", "User Activity Report Without Sort"): frappe.delete_doc("Report", "User Activity Report Without Sort") - with open( - os.path.join(os.path.dirname(__file__), "user_activity_report_without_sort.json") - ) as f: + with open(os.path.join(os.path.dirname(__file__), "user_activity_report_without_sort.json")) as f: frappe.get_doc(json.loads(f.read())).insert() report = frappe.get_doc("Report", "User Activity Report Without Sort") @@ -257,18 +264,18 @@ class TestReport(FrappeTestCase): report.report_script = """ totals = {} for user in frappe.get_all('User', fields = ['name', 'user_type', 'creation']): - if not user.user_type in totals: - totals[user.user_type] = 0 - totals[user.user_type] = totals[user.user_type] + 1 + if not user.user_type in totals: + totals[user.user_type] = 0 + totals[user.user_type] = totals[user.user_type] + 1 data = [ - [ - {'fieldname': 'type', 'label': 'Type'}, - {'fieldname': 'value', 'label': 'Value'} - ], - [ - {"type":key, "value": value} for key, value in totals.items() - ] + [ + {'fieldname': 'type', 'label': 'Type'}, + {'fieldname': 'value', 'label': 'Value'} + ], + [ + {"type":key, "value": value} for key, value in totals.items() + ] ] """ report.save() @@ -303,13 +310,13 @@ data = [ report.report_script = """ totals = {} for user in frappe.get_all('User', fields = ['name', 'user_type', 'creation']): - if not user.user_type in totals: - totals[user.user_type] = 0 - totals[user.user_type] = totals[user.user_type] + 1 + if not user.user_type in totals: + totals[user.user_type] = 0 + totals[user.user_type] = totals[user.user_type] + 1 result = [ - {"type":key, "value": value} for key, value in totals.items() - ] + {"type":key, "value": value} for key, value in totals.items() + ] """ report.save() @@ -348,15 +355,40 @@ result = [ report_settings = {"tree": True, "parent_field": "parent_value"} columns = [ - {"fieldname": "parent_column", "label": "Parent Column", "fieldtype": "Data", "width": 10}, - {"fieldname": "column_1", "label": "Column 1", "fieldtype": "Float", "width": 10}, - {"fieldname": "column_2", "label": "Column 2", "fieldtype": "Float", "width": 10}, + { + "fieldname": "parent_column", + "label": "Parent Column", + "fieldtype": "Data", + "width": 10, + }, + { + "fieldname": "column_1", + "label": "Column 1", + "fieldtype": "Float", + "width": 10, + }, + { + "fieldname": "column_2", + "label": "Column 2", + "fieldtype": "Float", + "width": 10, + }, ] result = [ {"parent_column": "Parent 1", "column_1": 200, "column_2": 150.50}, - {"parent_column": "Child 1", "column_1": 100, "column_2": 75.25, "parent_value": "Parent 1"}, - {"parent_column": "Child 2", "column_1": 100, "column_2": 75.25, "parent_value": "Parent 1"}, + { + "parent_column": "Child 1", + "column_1": 100, + "column_2": 75.25, + "parent_value": "Parent 1", + }, + { + "parent_column": "Child 2", + "column_1": 100, + "column_2": 75.25, + "parent_value": "Parent 1", + }, ] result = add_total_row( @@ -373,13 +405,13 @@ result = [ def test_cte_in_query_report(self): cte_query = textwrap.dedent( """ - with enabled_users as ( - select name - from `tabUser` - where enabled = 1 - ) - select * from enabled_users; - """ + with enabled_users as ( + select name + from `tabUser` + where enabled = 1 + ) + select * from enabled_users; + """ ) report = frappe.get_doc( diff --git a/frappe/core/doctype/report_column/report_column.py b/frappe/core/doctype/report_column/report_column.py index 6a256c967d..5da73830c6 100644 --- a/frappe/core/doctype/report_column/report_column.py +++ b/frappe/core/doctype/report_column/report_column.py @@ -37,4 +37,5 @@ class ReportColumn(Document): parenttype: DF.Data width: DF.Int # end: auto-generated types + pass diff --git a/frappe/core/doctype/report_filter/report_filter.py b/frappe/core/doctype/report_filter/report_filter.py index ea126d86fe..f8ce4fd3b8 100644 --- a/frappe/core/doctype/report_filter/report_filter.py +++ b/frappe/core/doctype/report_filter/report_filter.py @@ -38,4 +38,5 @@ class ReportFilter(Document): parenttype: DF.Data wildcard_filter: DF.Check # end: auto-generated types + pass diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 87ff615e0f..e868b5b978 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -43,6 +43,7 @@ class Role(Document): two_factor_auth: DF.Check view_switcher: DF.Check # end: auto-generated types + def before_rename(self, old, new, merge=False): if old in STANDARD_ROLES: frappe.throw(frappe._("Standard roles cannot be renamed")) @@ -125,9 +126,7 @@ def get_user_info(users, field="email"): def get_users(role): return [ d.parent - for d in frappe.get_all( - "Has Role", filters={"role": role, "parenttype": "User"}, fields=["parent"] - ) + for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"}, fields=["parent"]) ] diff --git a/frappe/core/doctype/role/test_role.py b/frappe/core/doctype/role/test_role.py index 58aadfcbca..1ab5a29f39 100644 --- a/frappe/core/doctype/role/test_role.py +++ b/frappe/core/doctype/role/test_role.py @@ -30,9 +30,9 @@ class TestUser(FrappeTestCase): frappe.delete_doc_if_exists("User", "test-user-for-desk-access@example.com") frappe.delete_doc_if_exists("Role", "desk-access-test") user = frappe.get_doc( - dict(doctype="User", email="test-user-for-desk-access@example.com", first_name="test") + doctype="User", email="test-user-for-desk-access@example.com", first_name="test" ).insert() - role = frappe.get_doc(dict(doctype="Role", role_name="desk-access-test", desk_access=0)).insert() + role = frappe.get_doc(doctype="Role", role_name="desk-access-test", desk_access=0).insert() user.add_roles(role.name) user.save() self.assertTrue(user.user_type == "Website User") @@ -46,7 +46,6 @@ class TestUser(FrappeTestCase): self.assertTrue(user.user_type == "Website User") def test_get_users_by_role(self): - role = "System Manager" sys_managers = get_info_based_on_role(role, field="name") diff --git a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py index 5d92a12e57..226afb5ec9 100644 --- a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py +++ b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py @@ -24,6 +24,7 @@ class RolePermissionforPageandReport(Document): roles: DF.Table[HasRole] set_role_for: DF.Literal["", "Page", "Report"] # end: auto-generated types + @frappe.whitelist() def set_report_page_data(self): self.set_custom_roles() diff --git a/frappe/core/doctype/role_profile/role_profile.json b/frappe/core/doctype/role_profile/role_profile.json index 7cd60a16d1..c182c5559d 100644 --- a/frappe/core/doctype/role_profile/role_profile.json +++ b/frappe/core/doctype/role_profile/role_profile.json @@ -39,10 +39,11 @@ "links": [ { "link_doctype": "User", - "link_fieldname": "role_profile_name" + "link_fieldname": "role_profile", + "table_fieldname": "role_profiles" } ], - "modified": "2021-12-03 15:45:45.270963", + "modified": "2024-02-10 12:22:24.803943", "modified_by": "Administrator", "module": "Core", "name": "Role Profile", @@ -75,6 +76,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "role_profile", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/role_profile/role_profile.py b/frappe/core/doctype/role_profile/role_profile.py index c026a4926a..59ddeca898 100644 --- a/frappe/core/doctype/role_profile/role_profile.py +++ b/frappe/core/doctype/role_profile/role_profile.py @@ -20,33 +20,22 @@ class RoleProfile(Document): role_profile: DF.Data roles: DF.Table[HasRole] # end: auto-generated types + def autoname(self): """set name as Role Profile name""" self.name = self.role_profile def on_update(self): - self.queue_action("update_all_users", now=frappe.flags.in_test, enqueue_after_commit=True) + self.queue_action( + "update_all_users", + now=frappe.flags.in_test or frappe.flags.in_install, + enqueue_after_commit=True, + queue="long", + ) def update_all_users(self): """Changes in role_profile reflected across all its user""" - 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) + users = frappe.get_all("User Role Profile", filters={"role_profile": self.name}, pluck="parent") + for user in users: + user = frappe.get_doc("User", user) + user.save() # resaving syncs roles diff --git a/frappe/core/doctype/role_profile/test_role_profile.py b/frappe/core/doctype/role_profile/test_role_profile.py index 34fb9741f9..4fe78f15e3 100644 --- a/frappe/core/doctype/role_profile/test_role_profile.py +++ b/frappe/core/doctype/role_profile/test_role_profile.py @@ -7,9 +7,9 @@ test_dependencies = ["Role"] class TestRoleProfile(FrappeTestCase): - def test_make_new_role_profile(self): + def test_make_new_role_profiles(self): frappe.delete_doc_if_exists("Role Profile", "Test 1", force=1) - new_role_profile = frappe.get_doc(dict(doctype="Role Profile", role_profile="Test 1")).insert() + new_role_profile = frappe.get_doc(doctype="Role Profile", role_profile="Test 1").insert() self.assertEqual(new_role_profile.role_profile, "Test 1") @@ -22,6 +22,8 @@ class TestRoleProfile(FrappeTestCase): random_user = frappe.mock("email") random_user_name = frappe.mock("name") + user_role_profile = [{"role_profile": "Test 1"}] + random_user = frappe.get_doc( { "doctype": "User", @@ -29,11 +31,12 @@ class TestRoleProfile(FrappeTestCase): "enabled": 1, "first_name": random_user_name, "new_password": "Eastern_43A1W", - "role_profile_name": "Test 1", + "role_profiles": user_role_profile, } ).insert(ignore_permissions=True, ignore_if_duplicate=True) - self.assertListEqual( - [role.role for role in random_user.roles], [role.role for role in new_role_profile.roles] + + self.assertEqual( + {role.role for role in random_user.roles}, {role.role for role in new_role_profile.roles} ) # clear roles @@ -44,3 +47,87 @@ class TestRoleProfile(FrappeTestCase): # user roles with the role profile should also be updated random_user.reload() self.assertListEqual(random_user.roles, []) + + def test_multiple_role_profiles(self): + frappe.delete_doc_if_exists("Role Profile", "_Test Role Profile 1", force=1) + frappe.delete_doc_if_exists("Role Profile", "_Test Role Profile 2", force=1) + + role_profile_one = frappe.get_doc(doctype="Role Profile", role_profile="_Test Role Profile 1").insert( + ignore_if_duplicate=True + ) + role_profile_two = frappe.get_doc(doctype="Role Profile", role_profile="_Test Role Profile 2").insert( + ignore_if_duplicate=True + ) + + self.assertEqual(role_profile_one.role_profile, "_Test Role Profile 1") + self.assertEqual(role_profile_two.role_profile, "_Test Role Profile 2") + + # Create new role for test + frappe.get_doc(doctype="Role", role_name="_Test Role 1").insert(ignore_if_duplicate=True) + frappe.get_doc(doctype="Role", role_name="_Test Role 2").insert(ignore_if_duplicate=True) + frappe.get_doc(doctype="Role", role_name="_Test Role 3").insert(ignore_if_duplicate=True) + # add role + role_profile_one.update({"roles": [{"role": "_Test Role 1"}, {"role": "_Test Role 2"}]}) + role_profile_one.save() + + role_profile_two.update({"roles": [{"role": "_Test Role 2"}, {"role": "_Test Role 3"}]}) + role_profile_two.save() + + self.assertEqual(role_profile_one.roles[0].role, "_Test Role 1") + self.assertEqual(role_profile_two.roles[1].role, "_Test Role 3") + + # create user with a role profile + + user_one = frappe.get_doc( + { + "doctype": "User", + "email": frappe.mock("email"), + "enabled": 1, + "first_name": frappe.mock("name"), + "new_password": "Eastern_43A1W", + "role_profiles": [ + {"role_profile": "_Test Role Profile 1"}, + {"role_profile": "_Test Role Profile 2"}, + ], + } + ).insert(ignore_permissions=True, ignore_if_duplicate=True) + + user_two = frappe.get_doc( + { + "doctype": "User", + "email": frappe.mock("email"), + "enabled": 1, + "first_name": frappe.mock("name"), + "new_password": "Eastern_43A1W", + "role_profiles": [{"role_profile": "_Test Role Profile 2"}], + } + ).insert(ignore_permissions=True, ignore_if_duplicate=True) + + for role in role_profile_one.roles: + self.assertIn(role.role, [role.role for role in user_one.roles]) + + self.assertEqual( + {role.role for role in user_two.roles}, {role.role for role in role_profile_two.roles} + ) + + def test_update_role_profile(self): + role_profile = frappe.get_doc("Role Profile", "_Test Role Profile 1") + + user = frappe.get_doc( + { + "doctype": "User", + "email": frappe.mock("email"), + "enabled": 1, + "first_name": frappe.mock("name"), + "new_password": "Eastern_43A1W", + "role_profiles": [{"role_profile": "_Test Role Profile 1"}], + } + ).insert(ignore_permissions=True, ignore_if_duplicate=True) + + role_profile.update( + {"roles": [{"role": "_Test Role 1"}, {"role": "_Test Role 3"}, {"role": "_Test Role 2"}]} + ) + role_profile.save() + + user.reload() + self.assertEqual({role.role for role in user.roles}, {role.role for role in role_profile.roles}) diff --git a/frappe/core/doctype/rq_job/rq_job.py b/frappe/core/doctype/rq_job/rq_job.py index ca745b01bc..995ceda4c7 100644 --- a/frappe/core/doctype/rq_job/rq_job.py +++ b/frappe/core/doctype/rq_job/rq_job.py @@ -54,9 +54,7 @@ class RQJob(Document): job_name: DF.Data | None queue: DF.Literal["default", "short", "long"] started_at: DF.Datetime | None - status: DF.Literal[ - "queued", "started", "finished", "failed", "deferred", "scheduled", "canceled" - ] + status: DF.Literal["queued", "started", "finished", "failed", "deferred", "scheduled", "canceled"] time_taken: DF.Duration | None timeout: DF.Duration | None # end: auto-generated types @@ -79,7 +77,6 @@ class RQJob(Document): @staticmethod def get_list(args): - start = cint(args.get("start")) page_length = cint(args.get("page_length")) or 20 @@ -88,9 +85,7 @@ class RQJob(Document): matched_job_ids = RQJob.get_matching_job_ids(args)[start : start + page_length] conn = get_redis_conn() - jobs = [ - serialize_job(job) for job in Job.fetch_many(job_ids=matched_job_ids, connection=conn) if job - ] + jobs = [serialize_job(job) for job in Job.fetch_many(job_ids=matched_job_ids, connection=conn) if job] return sorted(jobs, key=lambda j: j.modified, reverse=order_desc) diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index 82a4e20a83..57c857f7ab 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -32,7 +32,6 @@ class TestRQJob(FrappeTestCase): self.assertEqual(frappe.get_doc("RQ Job", job.id).status, status) def test_serialization(self): - job = frappe.enqueue(method=self.BG_JOB, queue="short") rq_job = frappe.get_doc("RQ Job", job.id) @@ -60,7 +59,6 @@ class TestRQJob(FrappeTestCase): self.assertEqual(rq_job.job_name, "test_func") def test_get_list_filtering(self): - # Check failed job clearning and filtering remove_failed_jobs() jobs = RQJob.get_list({"filters": [["RQ Job", "status", "=", "failed"]]}) @@ -176,9 +174,7 @@ class TestRQJob(FrappeTestCase): jobs = [frappe.enqueue(method=self.BG_JOB, queue="short", fail=True) for _ in range(limit * 2)] self.check_status(jobs[-1], "failed") - self.assertLessEqual( - RQJob.get_count({"filters": [["RQ Job", "status", "=", "failed"]]}), limit * 1.1 - ) + self.assertLessEqual(RQJob.get_count({"filters": [["RQ Job", "status", "=", "failed"]]}), limit * 1.1) def test_func(fail=False, sleep=0): diff --git a/frappe/core/doctype/rq_worker/rq_worker.py b/frappe/core/doctype/rq_worker/rq_worker.py index c7e1022576..34df52c0af 100644 --- a/frappe/core/doctype/rq_worker/rq_worker.py +++ b/frappe/core/doctype/rq_worker/rq_worker.py @@ -37,7 +37,6 @@ class RQWorker(Document): # end: auto-generated types def load_from_db(self): - all_workers = get_workers() workers = [w for w in all_workers if w.pid == cint(self.name)] if not workers: diff --git a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py index e4bfe21e2d..72e1e6b6c4 100644 --- a/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py +++ b/frappe/core/doctype/scheduled_job_log/scheduled_job_log.py @@ -21,6 +21,7 @@ class ScheduledJobLog(Document): scheduled_job_type: DF.Link status: DF.Literal["Scheduled", "Complete", "Failed"] # end: auto-generated types + @staticmethod def clear_old_logs(days=90): table = frappe.qb.DocType("Scheduled Job Log") diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index f95c06fdbe..ccab34d93c 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -45,6 +45,7 @@ class ScheduledJobType(Document): server_script: DF.Link | None stopped: DF.Check # end: auto-generated types + def autoname(self): self.name = ".".join(self.method.split(".")[-2:]) @@ -148,7 +149,7 @@ class ScheduledJobType(Document): return if not self.scheduler_log: self.scheduler_log = frappe.get_doc( - dict(doctype="Scheduled Job Log", scheduled_job_type=self.name) + doctype="Scheduled Job Log", scheduled_job_type=self.name ).insert(ignore_permissions=True) self.scheduler_log.db_set("status", status) if frappe.debug_log: @@ -236,9 +237,7 @@ def insert_single_event(frequency: str, event: str, cron_format: str = None): } ) - if not frappe.db.exists( - "Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr} - ): + if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr}): savepoint = "scheduled_job_type_creation" try: frappe.db.savepoint(savepoint) diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py index 6d77c876dc..861726f6d4 100644 --- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -33,9 +33,7 @@ class TestScheduledJobType(FrappeTestCase): # check if jobs are synced after change in hooks updated_scheduler_events = {"hourly": ["frappe.email.queue.flush"]} sync_jobs(updated_scheduler_events) - updated_scheduled_job = frappe.get_doc( - "Scheduled Job Type", {"method": "frappe.email.queue.flush"} - ) + updated_scheduled_job = frappe.get_doc("Scheduled Job Type", {"method": "frappe.email.queue.flush"}) self.assertEqual(updated_scheduled_job.frequency, "Hourly") def test_daily_job(self): diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 4c007696cb..7a899ed0af 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -28,9 +28,13 @@ frappe.ui.form.on("Server Script", { check_safe_exec(frm) { frappe.xcall("frappe.core.doctype.server_script.server_script.enabled").then((enabled) => { if (enabled === false) { + let docs_link = + "https://frappeframework.com/docs/user/en/desk/scripting/server-script"; + let docs = `${__("Official Documentation")}`; + frm.dashboard.clear_comment(); let msg = __("Server Scripts feature is not available on this site.") + " "; - msg += __("Please contact your system administrator to enable this feature."); + msg += __("To enable server scripts, read the {0}.", [docs]); frm.dashboard.add_comment(msg, "yellow", true); } }); diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 50f5bfcfe8..295289184a 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -56,7 +56,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization\nOn Payment Paid\nOn Payment Failed" }, { "depends_on": "eval:doc.script_type==='API'", @@ -149,7 +149,7 @@ "link_fieldname": "server_script" } ], - "modified": "2023-10-14 11:24:46.478533", + "modified": "2024-02-06 07:09:45.478533", "modified_by": "Administrator", "module": "Core", "name": "Server Script", @@ -173,4 +173,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index fbd3ca6f50..d5b405b96d 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -45,6 +45,8 @@ class ServerScript(Document): "Before Save (Submitted Document)", "After Save (Submitted Document)", "On Payment Authorization", + "On Payment Paid", + "On Payment Failed", ] enable_rate_limit: DF.Check event_frequency: DF.Literal[ @@ -67,6 +69,7 @@ class ServerScript(Document): script: DF.Code script_type: DF.Literal["DocType Event", "Scheduler Event", "Permission Query", "API"] # end: auto-generated types + def validate(self): frappe.only_for("Script Manager", True) self.sync_scheduled_jobs() @@ -207,7 +210,7 @@ class ServerScript(Document): if key.startswith("_"): continue value = obj[key] - if isinstance(value, (NamespaceDict, dict)) and value: + if isinstance(value, NamespaceDict | dict) and value: if key == "form_dict": out.append(["form_dict", 7]) continue @@ -219,7 +222,7 @@ class ServerScript(Document): score = 0 elif isinstance(value, ModuleType): score = 10 - elif isinstance(value, (FunctionType, MethodType)): + elif isinstance(value, FunctionType | MethodType): score = 9 elif isinstance(value, type): score = 8 diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index d0ae253d29..869084f841 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -17,6 +17,8 @@ EVENT_MAP = { "after_delete": "After Delete", "before_update_after_submit": "Before Save (Submitted Document)", "on_update_after_submit": "After Save (Submitted Document)", + "on_payment_paid": "On Payment Paid", + "on_payment_failed": "On Payment Failed", "on_payment_authorized": "On Payment Authorization", } diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index b83d1edda4..ee4782f9d3 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -111,14 +111,14 @@ class TestServerScript(FrappeTestCase): frappe.cache.delete_value("server_script_map") def test_doctype_event(self): - todo = frappe.get_doc(dict(doctype="ToDo", description="hello")).insert() + todo = frappe.get_doc(doctype="ToDo", description="hello").insert() self.assertEqual(todo.status, "Open") - todo = frappe.get_doc(dict(doctype="ToDo", description="test todo")).insert() + todo = frappe.get_doc(doctype="ToDo", description="test todo").insert() self.assertEqual(todo.status, "Closed") self.assertRaises( - frappe.ValidationError, frappe.get_doc(dict(doctype="ToDo", description="validate me")).insert + frappe.ValidationError, frappe.get_doc(doctype="ToDo", description="validate me").insert ) def test_api(self): @@ -157,9 +157,7 @@ class TestServerScript(FrappeTestCase): server_script.disabled = 0 server_script.save() - self.assertRaises( - AttributeError, frappe.get_doc(dict(doctype="ToDo", description="test me")).insert - ) + self.assertRaises(AttributeError, frappe.get_doc(doctype="ToDo", description="test me").insert) server_script.disabled = 1 server_script.save() @@ -169,9 +167,7 @@ class TestServerScript(FrappeTestCase): server_script.disabled = 0 server_script.save() - self.assertRaises( - AttributeError, frappe.get_doc(dict(doctype="ToDo", description="test me")).insert - ) + self.assertRaises(AttributeError, frappe.get_doc(doctype="ToDo", description="test me").insert) server_script.disabled = 1 server_script.save() @@ -222,7 +218,7 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run() name="test_nested_scripts_1", script_type="API", api_method="test_nested_scripts_1", - script=f"""log("nothing")""", + script="""log("nothing")""", ) script.insert() script.execute_method() @@ -232,13 +228,12 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run() name="test_nested_scripts_2", script_type="API", api_method="test_nested_scripts_2", - script=f"""frappe.call("test_nested_scripts_1")""", + script="""frappe.call("test_nested_scripts_1")""", ) script.insert() script.execute_method() def test_server_script_rate_limiting(self): - # why not script1 = frappe.get_doc( doctype="Server Script", name="rate_limited_server_script", diff --git a/frappe/core/doctype/session_default/session_default.py b/frappe/core/doctype/session_default/session_default.py index c5697ac848..0e4e835980 100644 --- a/frappe/core/doctype/session_default/session_default.py +++ b/frappe/core/doctype/session_default/session_default.py @@ -19,4 +19,5 @@ class SessionDefault(Document): parenttype: DF.Data ref_doctype: DF.Link | None # end: auto-generated types + pass diff --git a/frappe/core/doctype/session_default_settings/session_default_settings.py b/frappe/core/doctype/session_default_settings/session_default_settings.py index 5df080c4f5..8d9b15eb74 100644 --- a/frappe/core/doctype/session_default_settings/session_default_settings.py +++ b/frappe/core/doctype/session_default_settings/session_default_settings.py @@ -20,6 +20,7 @@ class SessionDefaultSettings(Document): session_defaults: DF.Table[SessionDefault] # end: auto-generated types + pass diff --git a/frappe/core/doctype/session_default_settings/test_session_default_settings.py b/frappe/core/doctype/session_default_settings/test_session_default_settings.py index 532e805141..9d1800bc39 100644 --- a/frappe/core/doctype/session_default_settings/test_session_default_settings.py +++ b/frappe/core/doctype/session_default_settings/test_session_default_settings.py @@ -19,13 +19,13 @@ class TestSessionDefaultSettings(FrappeTestCase): set_session_default_values({"role": "Website Manager"}) todo = frappe.get_doc( - dict(doctype="ToDo", description="test session defaults set", assigned_by="Administrator") + doctype="ToDo", description="test session defaults set", assigned_by="Administrator" ).insert() self.assertEqual(todo.role, "Website Manager") def test_clear_session_defaults(self): clear_session_defaults() todo = frappe.get_doc( - dict(doctype="ToDo", description="test session defaults cleared", assigned_by="Administrator") + doctype="ToDo", description="test session defaults cleared", assigned_by="Administrator" ).insert() self.assertNotEqual(todo.role, "Website Manager") diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.py b/frappe/core/doctype/sms_parameter/sms_parameter.py index a415f1fc25..96c1c64536 100644 --- a/frappe/core/doctype/sms_parameter/sms_parameter.py +++ b/frappe/core/doctype/sms_parameter/sms_parameter.py @@ -20,4 +20,5 @@ class SMSParameter(Document): parenttype: DF.Data value: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py index f2609ad05b..29c3508af6 100644 --- a/frappe/core/doctype/sms_settings/sms_settings.py +++ b/frappe/core/doctype/sms_settings/sms_settings.py @@ -23,6 +23,7 @@ class SMSSettings(Document): sms_gateway_url: DF.SmallText use_post: DF.Check # end: auto-generated types + pass @@ -62,7 +63,6 @@ def get_contact_number(contact_name, ref_doctype, ref_name): @frappe.whitelist() def send_sms(receiver_list, msg, sender_name="", success_msg=True): - import json if isinstance(receiver_list, str): diff --git a/frappe/core/doctype/submission_queue/submission_queue.py b/frappe/core/doctype/submission_queue/submission_queue.py index ba2c74c3dd..bad6f88af4 100644 --- a/frappe/core/doctype/submission_queue/submission_queue.py +++ b/frappe/core/doctype/submission_queue/submission_queue.py @@ -32,6 +32,7 @@ class SubmissionQueue(Document): ref_doctype: DF.Link | None status: DF.Literal["Queued", "Finished", "Failed"] # end: auto-generated types + @property def created_at(self): return self.creation diff --git a/frappe/core/doctype/success_action/success_action.py b/frappe/core/doctype/success_action/success_action.py index c32fe326f2..730368c1be 100644 --- a/frappe/core/doctype/success_action/success_action.py +++ b/frappe/core/doctype/success_action/success_action.py @@ -19,4 +19,5 @@ class SuccessAction(Document): next_actions: DF.Data | None ref_doctype: DF.Link # end: auto-generated types + pass diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index db1511c893..5fe1193877 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -84,9 +84,7 @@ class SystemSettings(Document): password_reset_limit: DF.Int reset_password_link_expiry_duration: DF.Duration | None reset_password_template: DF.Link | None - rounding_method: DF.Literal[ - "Banker's Rounding (legacy)", "Banker's Rounding", "Commercial Rounding" - ] + rounding_method: DF.Literal["Banker's Rounding (legacy)", "Banker's Rounding", "Commercial Rounding"] session_expiry: DF.Data | None setup_complete: DF.Check strip_exif_metadata_from_uploaded_images: DF.Check @@ -132,6 +130,9 @@ class SystemSettings(Document): self.validate_backup_limit() self.validate_file_extensions() + if not self.link_field_results_limit: + self.link_field_results_limit = 10 + if self.link_field_results_limit > 50: self.link_field_results_limit = 50 label = _(self.meta.get_label("link_field_results_limit")) @@ -145,9 +146,7 @@ class SystemSettings(Document): social_login_enabled = frappe.db.exists("Social Login Key", {"enable_social_login": 1}) ldap_enabled = frappe.db.get_single_value("LDAP Settings", "enabled") - login_with_email_link_enabled = frappe.db.get_single_value( - "System Settings", "login_with_email_link" - ) + login_with_email_link_enabled = frappe.db.get_single_value("System Settings", "login_with_email_link") if not (social_login_enabled or ldap_enabled or login_with_email_link_enabled): frappe.throw( diff --git a/frappe/core/doctype/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py index 203580a614..ab1ef6e5ca 100644 --- a/frappe/core/doctype/transaction_log/transaction_log.py +++ b/frappe/core/doctype/transaction_log/transaction_log.py @@ -29,6 +29,7 @@ class TransactionLog(Document): timestamp: DF.Datetime | None transaction_hash: DF.SmallText | None # end: auto-generated types + def before_insert(self): index = get_current_index() self.row_index = index diff --git a/frappe/core/doctype/translation/translation.py b/frappe/core/doctype/translation/translation.py index bb5e517160..a9ecc25b4c 100644 --- a/frappe/core/doctype/translation/translation.py +++ b/frappe/core/doctype/translation/translation.py @@ -26,6 +26,7 @@ class Translation(Document): source_text: DF.Code translated_text: DF.Code # end: auto-generated types + def validate(self): if is_html(self.source_text): self.remove_html_from_source() diff --git a/frappe/core/doctype/user/patches/__init__.py b/frappe/core/doctype/user/patches/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user/patches/hash_reset_password_tokens.py b/frappe/core/doctype/user/patches/hash_reset_password_tokens.py new file mode 100644 index 0000000000..85a7f5ec80 --- /dev/null +++ b/frappe/core/doctype/user/patches/hash_reset_password_tokens.py @@ -0,0 +1,16 @@ +import frappe +from frappe.utils.data import sha256_hash + + +def execute(): + """hash reset password tokens""" + + users = frappe.get_all("User", {"reset_password_key": ("is", "set")}, ["name", "reset_password_key"]) + for user in users: + frappe.db.set_value( + "User", + user.name, + "reset_password_key", + sha256_hash(user.reset_password_key), + update_modified=False, + ) diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index dc973c9e8f..910844a8b7 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -3,6 +3,7 @@ import json import time from unittest.mock import patch +from urllib.parse import parse_qs, urlparse import frappe import frappe.exceptions @@ -17,7 +18,7 @@ from frappe.core.doctype.user.user import ( from frappe.desk.notifications import extract_mentions from frappe.frappeclient import FrappeClient from frappe.model.delete_doc import delete_doc -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import get_url user_module = frappe.core.doctype.user.user @@ -32,10 +33,14 @@ class TestUser(FrappeTestCase): frappe.db.set_single_value("System Settings", "password_reset_limit", 3) frappe.set_user("Administrator") + @staticmethod + def reset_password(user) -> str: + link = user.reset_password() + return parse_qs(urlparse(link).query)["key"][0] + def test_user_type(self): - new_user = frappe.get_doc( - dict(doctype="User", email="test-for-type@example.com", first_name="Tester") - ).insert(ignore_if_duplicate=True) + user_id = frappe.generate_hash() + "@example.com" + new_user = frappe.get_doc(doctype="User", email=user_id, first_name="Tester").insert() self.assertEqual(new_user.user_type, "Website User") # social login userid for frappe @@ -113,9 +118,7 @@ class TestUser(FrappeTestCase): frappe.db.set_single_value("Website Settings", "_test", "_test_val") self.assertEqual(frappe.db.get_value("Website Settings", None, "_test"), "_test_val") - self.assertEqual( - frappe.db.get_value("Website Settings", "Website Settings", "_test"), "_test_val" - ) + self.assertEqual(frappe.db.get_value("Website Settings", "Website Settings", "_test"), "_test_val") def test_high_permlevel_validations(self): user = frappe.get_meta("User") @@ -153,7 +156,7 @@ class TestUser(FrappeTestCase): def test_delete_user(self): new_user = frappe.get_doc( - dict(doctype="User", email="test-for-delete@example.com", first_name="Tester Delete User") + doctype="User", email="test-for-delete@example.com", first_name="Tester Delete User" ).insert(ignore_if_duplicate=True) self.assertEqual(new_user.user_type, "Website User") @@ -192,9 +195,7 @@ class TestUser(FrappeTestCase): # Score 1; should now fail result = test_password_strength("bee2ve") self.assertEqual(result["feedback"]["password_policy_validation_passed"], False) - self.assertRaises( - frappe.exceptions.ValidationError, handle_password_test_fail, result["feedback"] - ) + self.assertRaises(frappe.exceptions.ValidationError, handle_password_test_fail, result["feedback"]) self.assertRaises( frappe.exceptions.ValidationError, handle_password_test_fail, result ) # test backwards compatibility @@ -273,11 +274,8 @@ class TestUser(FrappeTestCase): """ self.assertListEqual(extract_mentions(comment), ["test@example.com", "test1@example.com"]) + @change_settings("System Settings", commit=True, password_reset_limit=1) def test_rate_limiting_for_reset_password(self): - # Allow only one reset request for a day - frappe.db.set_single_value("System Settings", "password_reset_limit", 1) - frappe.db.commit() - url = get_url() data = {"cmd": "frappe.core.doctype.user.user.reset_password", "user": "test@test.com"} @@ -333,9 +331,7 @@ class TestUser(FrappeTestCase): self.assertEqual(frappe.cache.hget("redirect_after_login", random_user), "/welcome") # re-register - self.assertTupleEqual( - sign_up(random_user, random_user_name, "/welcome"), (0, "Already Registered") - ) + self.assertTupleEqual(sign_up(random_user, random_user_name, "/welcome"), (0, "Already Registered")) # disabled user user = frappe.get_doc("User", random_user) @@ -357,6 +353,7 @@ class TestUser(FrappeTestCase): "/signup", ) + @change_settings("System Settings", password_reset_limit=6) def test_reset_password(self): from frappe.auth import CookieManager, LoginManager from frappe.utils import set_request @@ -369,12 +366,11 @@ class TestUser(FrappeTestCase): frappe.local.login_manager = LoginManager() # used by rate limiter when calling reset_password frappe.local.request_ip = "127.0.0.69" - frappe.db.set_single_value("System Settings", "password_reset_limit", 6) frappe.set_user("testpassword@example.com") test_user = frappe.get_doc("User", "testpassword@example.com") - test_user.reset_password() - self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/app") + key = self.reset_password(test_user) + self.assertEqual(update_password(new_password, key=key), "/app") self.assertEqual( update_password(new_password, key="wrong_key"), "The reset password link has either been used before or is invalid", @@ -393,9 +389,7 @@ class TestUser(FrappeTestCase): } # password strength failure test - with patch.object( - user_module, "test_password_strength", return_value=password_strength_response - ): + with patch.object(user_module, "test_password_strength", return_value=password_strength_response): self.assertRaisesRegex( frappe.exceptions.ValidationError, "Fix password", @@ -417,7 +411,9 @@ class TestUser(FrappeTestCase): test_user = frappe.get_doc("User", "test2@example.com") self.assertEqual(reset_password(user="test2@example.com"), None) test_user.reload() - self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/") + link = sendmail.call_args_list[0].kwargs["args"]["link"] + key = parse_qs(urlparse(link).query)["key"][0] + self.assertEqual(update_password(new_password, key=key), "/") update_password(old_password, old_password=new_password) self.assertEqual( frappe.message_log[0].get("message"), @@ -443,16 +439,16 @@ class TestUser(FrappeTestCase): sorted(m.get("module_name") for m in get_modules_from_all_apps()), ) + @change_settings("System Settings", reset_password_link_expiry_duration=1) def test_reset_password_link_expiry(self): new_password = "new_password" - # set the reset password expiry to 1 second - frappe.db.set_single_value("System Settings", "reset_password_link_expiry_duration", 1) frappe.set_user("testpassword@example.com") test_user = frappe.get_doc("User", "testpassword@example.com") - test_user.reset_password() - time.sleep(1) # sleep for 1 sec to expire the reset link + key = self.reset_password(test_user) + time.sleep(1) + self.assertEqual( - update_password(new_password, key=test_user.reset_password_key), + update_password(new_password, key=key), "The reset password link has been expired", ) diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index d9467397c3..448c77a026 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -27,22 +27,15 @@ frappe.ui.form.on("User", { } }, - role_profile_name: function (frm) { - if (frm.doc.role_profile_name) { - frappe.call({ - method: "frappe.core.doctype.user.user.get_role_profile", - args: { - role_profile: frm.doc.role_profile_name, - }, - callback: function (data) { - frm.set_value("roles", []); - $.each(data.message || [], function (i, v) { - var d = frm.add_child("roles"); - d.role = v.role; - }); - frm.roles_editor.show(); - }, + role_profiles: function (frm) { + if (frm.doc.role_profiles && frm.doc.role_profiles.length) { + frm.roles_editor.disable = 1; + frm.call("populate_role_profile_roles").then(() => { + frm.roles_editor.show(); }); + } else { + frm.roles_editor.disable = 0; + frm.roles_editor.show(); } }, @@ -85,7 +78,7 @@ frappe.ui.form.on("User", { frm.roles_editor = new frappe.RoleEditor( role_area, frm, - frm.doc.role_profile_name ? 1 : 0 + frm.doc.role_profiles && frm.doc.role_profiles.length ? 1 : 0 ); if (frm.doc.user_type == "System User") { @@ -225,7 +218,8 @@ frappe.ui.form.on("User", { frm.trigger("enabled"); if (frm.roles_editor && frm.can_edit_roles) { - frm.roles_editor.disable = frm.doc.role_profile_name ? 1 : 0; + frm.roles_editor.disable = + frm.doc.role_profiles && frm.doc.role_profiles.length ? 1 : 0; frm.roles_editor.show(); } diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 30a1ccc865..894216a3e0 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -26,6 +26,7 @@ "roles_permissions_tab", "sb1", "role_profile_name", + "role_profiles", "roles_html", "roles", "sb_allow_modules", @@ -212,9 +213,9 @@ "read_only": 1 }, { - "allow_in_quick_entry": 1, "fieldname": "role_profile_name", "fieldtype": "Link", + "hidden": 1, "label": "Role Profile", "options": "Role Profile", "permlevel": 1 @@ -328,6 +329,7 @@ "hidden": 1, "label": "Reset Password Key", "no_copy": 1, + "permlevel": 1, "print_hide": 1, "read_only": 1 }, @@ -626,6 +628,7 @@ "fieldtype": "Datetime", "hidden": 1, "label": "Last Reset Password Key Generated On", + "permlevel": 1, "read_only": 1 }, { @@ -700,6 +703,14 @@ "fieldtype": "Small Text", "hidden": 1, "label": "Onboarding Status" + }, + { + "allow_in_quick_entry": 1, + "fieldname": "role_profiles", + "fieldtype": "Table MultiSelect", + "label": "Role Profiles", + "options": "User Role Profile", + "permlevel": 1 } ], "icon": "fa fa-user", @@ -762,7 +773,7 @@ "link_fieldname": "user" } ], - "modified": "2023-09-18 22:19:49.933972", + "modified": "2024-02-11 13:16:29.574666", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 640048d93a..4f5009b9c8 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import json from collections.abc import Iterable from datetime import timedelta @@ -30,7 +31,8 @@ from frappe.utils import ( now_datetime, today, ) -from frappe.utils.deprecations import deprecated +from frappe.utils.data import sha256_hash +from frappe.utils.deprecations import deprecated, deprecation_warning from frappe.utils.password import check_password, get_password_reset_limit from frappe.utils.password import update_password as _update_password from frappe.utils.user import get_system_managers @@ -48,6 +50,7 @@ class User(Document): from frappe.core.doctype.defaultvalue.defaultvalue import DefaultValue from frappe.core.doctype.has_role.has_role import HasRole from frappe.core.doctype.user_email.user_email import UserEmail + from frappe.core.doctype.user_role_profile.user_role_profile import UserRoleProfile from frappe.core.doctype.user_social_login.user_social_login import UserSocialLogin from frappe.types import DF @@ -99,6 +102,7 @@ class User(Document): reset_password_key: DF.Data | None restrict_ip: DF.SmallText | None role_profile_name: DF.Link | None + role_profiles: DF.TableMultiSelect[UserRoleProfile] roles: DF.Table[HasRole] send_me_a_copy: DF.Check send_welcome_email: DF.Check @@ -112,6 +116,7 @@ class User(Document): user_type: DF.Link | None username: DF.Data | None # end: auto-generated types + __new_password = None def __setup__(self): @@ -152,12 +157,14 @@ class User(Document): self.validate_email_type(self.email) self.validate_email_type(self.name) self.add_system_manager_role() + self.move_role_profile_name_to_role_profiles() self.populate_role_profile_roles() self.check_roles_added() self.set_system_user() self.set_full_name() self.check_enable_disable() self.ensure_unique_roles() + self.ensure_unique_role_profiles() self.remove_all_roles_for_guest() self.validate_username() self.remove_disabled_roles() @@ -166,25 +173,48 @@ class User(Document): self.validate_allowed_modules() self.validate_user_image() self.set_time_zone() - if self.language == "Loading...": self.language = None - if (self.name not in ["Administrator", "Guest"]) and ( - not self.get_social_login_userid("frappe") - ): + if (self.name not in ["Administrator", "Guest"]) and (not self.get_social_login_userid("frappe")): self.set_social_login_userid("frappe", frappe.generate_hash(length=39)) + @frappe.whitelist() def populate_role_profile_roles(self): - if self.role_profile_name: - role_profile = frappe.get_doc("Role Profile", self.role_profile_name) - self.set("roles", []) - self.append_roles(*[role.role for role in role_profile.roles]) + if not self.role_profiles: + return + + new_roles = set() + for role_profile in self.role_profiles: + role_profile = frappe.get_cached_doc("Role Profile", role_profile.role_profile) + new_roles.update(role.role for role in role_profile.roles) + + # Remove invalid roles and add new ones + self.roles = [r for r in self.roles if r.role in new_roles] + self.append_roles(*new_roles) @deprecated def validate_roles(self): self.populate_role_profile_roles() + def move_role_profile_name_to_role_profiles(self): + """This handles old role_profile_name field if programatically set. + + This behaviour will be remoed in future versions.""" + if not self.role_profile_name: + return + + current_role_profiles = [r.role_profile for r in self.role_profiles] + if self.role_profile_name in current_role_profiles: + self.role_profile_name = None + return + + deprecation_warning( + "The field `role_profile_name` is deprecated and will be removed in v16, use `role_profiles` child table instead." + ) + self.append("role_profiles", {"role_profile": self.role_profile_name}) + self.role_profile_name = None + def validate_allowed_modules(self): if self.module_profile: module_profile = frappe.get_doc("Module Profile", self.module_profile) @@ -267,7 +297,6 @@ class User(Document): and not self.get_other_system_managers() and cint(frappe.db.get_single_value("System Settings", "setup_complete")) ): - msgprint(_("Adding System Manager to this User as there must be atleast one System Manager")) self.append("roles", {"doctype": "Has Role", "role": "System Manager"}) @@ -376,10 +405,11 @@ class User(Document): pass def reset_password(self, send_email=False, password_expired=False): - from frappe.utils import get_url, random_string + from frappe.utils import get_url - key = random_string(32) - self.db_set("reset_password_key", key) + key = frappe.generate_hash() + hashed_key = sha256_hash(key) + self.db_set("reset_password_key", hashed_key) self.db_set("last_reset_password_key_generated_on", now_datetime()) url = "/update-password?key=" + key @@ -411,7 +441,6 @@ class User(Document): return (self.first_name or "") + (self.first_name and " " or "") + (self.last_name or "") def password_reset_mail(self, link): - reset_password_template = frappe.db.get_system_setting("reset_password_template") self.send_login_mail( @@ -577,10 +606,9 @@ class User(Document): has_fields = [d.get("name") for d in desc if d.get("name") in ["owner", "modified_by"]] for field in has_fields: frappe.db.sql( - """UPDATE `%s` - SET `%s` = %s - WHERE `%s` = %s""" - % (tab, field, "%s", field, "%s"), + """UPDATE `{}` + SET `{}` = {} + WHERE `{}` = {}""".format(tab, field, "%s", field, "%s"), (new_name, old_name), ) @@ -592,7 +620,7 @@ class User(Document): def append_roles(self, *roles): """Add roles to user""" - current_roles = [d.role for d in self.get("roles")] + current_roles = {d.role for d in self.get("roles")} for role in roles: if role in current_roles: continue @@ -622,12 +650,18 @@ class User(Document): self.get("roles").remove(role) def ensure_unique_roles(self): - exists = [] - for i, d in enumerate(self.get("roles")): + exists = set() + for d in list(self.roles): if (not d.role) or (d.role in exists): - self.get("roles").remove(d) - else: - exists.append(d.role) + self.roles.remove(d) + exists.add(d.role) + + def ensure_unique_role_profiles(self): + seen = set() + for rp in list(self.role_profiles): + if rp.role_profile in seen: + self.role_profiles.remove(rp) + seen.add(rp.role_profile) def validate_username(self): if not self.username and self.is_new() and self.first_name: @@ -671,9 +705,7 @@ class User(Document): if not username: # @firstname_last_name - username = _check_suggestion( - frappe.scrub("{} {}".format(self.first_name, self.last_name or "")) - ) + username = _check_suggestion(frappe.scrub("{} {}".format(self.first_name, self.last_name or ""))) if username: frappe.msgprint(_("Suggested Username: {0}").format(username)) @@ -681,9 +713,7 @@ class User(Document): return username def username_exists(self, username=None): - return frappe.db.get_value( - "User", {"username": username or self.username, "name": ("!=", self.name)} - ) + return frappe.db.get_value("User", {"username": username or self.username, "name": ("!=", self.name)}) def get_blocked_modules(self): """Return list of modules blocked for that user.""" @@ -725,12 +755,8 @@ class User(Document): 3. If allow_login_using_user_name is set, you can use username while finding the user. """ - login_with_mobile = cint( - frappe.db.get_single_value("System Settings", "allow_login_using_mobile_number") - ) - login_with_username = cint( - frappe.db.get_single_value("System Settings", "allow_login_using_user_name") - ) + login_with_mobile = cint(frappe.get_system_settings("allow_login_using_mobile_number")) + login_with_username = cint(frappe.get_system_settings("allow_login_using_user_name")) or_filters = [{"name": user_name}] if login_with_mobile: @@ -818,10 +844,10 @@ def update_password( """Update password for the current user. Args: - new_password (str): New password. - logout_all_sessions (int, optional): If set to 1, all other sessions will be logged out. Defaults to 0. - key (str, optional): Password reset key. Defaults to None. - old_password (str, optional): Old password. Defaults to None. + new_password (str): New password. + logout_all_sessions (int, optional): If set to 1, all other sessions will be logged out. Defaults to 0. + key (str, optional): Password reset key. Defaults to None. + old_password (str, optional): Old password. Defaults to None. """ if len(new_password) > MAX_PASSWORD_SIZE: @@ -840,9 +866,7 @@ def update_password( else: user = res["user"] - logout_all_sessions = cint(logout_all_sessions) or frappe.db.get_single_value( - "System Settings", "logout_on_password_reset" - ) + logout_all_sessions = cint(logout_all_sessions) or frappe.get_system_settings("logout_on_password_reset") _update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions)) user_doc, redirect_url = reset_user_data(user) @@ -865,9 +889,7 @@ def update_password( @frappe.whitelist(allow_guest=True) -def test_password_strength( - new_password: str, key=None, old_password=None, user_data: tuple | None = None -): +def test_password_strength(new_password: str, key=None, old_password=None, user_data: tuple | None = None): from frappe.utils.deprecations import deprecation_warning from frappe.utils.password_strength import test_password_strength as _test_password_strength @@ -896,6 +918,7 @@ def test_password_strength( password_policy_validation_passed = True result["feedback"]["password_policy_validation_passed"] = password_policy_validation_passed + result.pop("password", None) return result @@ -927,13 +950,14 @@ def _get_user_for_update_password(key, old_password): # verify old password result = frappe._dict() if key: + hashed_key = sha256_hash(key) user = frappe.db.get_value( - "User", {"reset_password_key": key}, ["name", "last_reset_password_key_generated_on"] + "User", {"reset_password_key": hashed_key}, ["name", "last_reset_password_key_generated_on"] ) result.user, last_reset_password_key_generated_on = user or (None, None) if result.user: reset_password_link_expiry = cint( - frappe.db.get_single_value("System Settings", "reset_password_link_expiry_duration") + frappe.get_system_settings("reset_password_link_expiry_duration") ) if ( reset_password_link_expiry @@ -1018,13 +1042,12 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]: @frappe.whitelist(allow_guest=True) -@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60) +@rate_limit(limit=get_password_reset_limit, seconds=60 * 60) def reset_password(user: str) -> str: - if user == "Administrator": - return "not allowed" - try: user: User = frappe.get_doc("User", user) + if user.name == "Administrator": + return "not allowed" if not user.enabled: return "disabled" @@ -1057,21 +1080,21 @@ def user_query(doctype, txt, searchfield, start, page_len, filters): txt = f"%{txt}%" return frappe.db.sql( """SELECT `name`, CONCAT_WS(' ', first_name, middle_name, last_name) - FROM `tabUser` - WHERE `enabled`=1 - {user_type_condition} - AND `docstatus` < 2 - AND `name` NOT IN ({standard_users}) - AND ({key} LIKE %(txt)s - OR CONCAT_WS(' ', first_name, middle_name, last_name) LIKE %(txt)s) - {fcond} {mcond} - ORDER BY - CASE WHEN `name` LIKE %(txt)s THEN 0 ELSE 1 END, - CASE WHEN concat_ws(' ', first_name, middle_name, last_name) LIKE %(txt)s - THEN 0 ELSE 1 END, - NAME asc - LIMIT %(page_len)s OFFSET %(start)s - """.format( + FROM `tabUser` + WHERE `enabled`=1 + {user_type_condition} + AND `docstatus` < 2 + AND `name` NOT IN ({standard_users}) + AND ({key} LIKE %(txt)s + OR CONCAT_WS(' ', first_name, middle_name, last_name) LIKE %(txt)s) + {fcond} {mcond} + ORDER BY + CASE WHEN `name` LIKE %(txt)s THEN 0 ELSE 1 END, + CASE WHEN concat_ws(' ', first_name, middle_name, last_name) LIKE %(txt)s + THEN 0 ELSE 1 END, + NAME asc + LIMIT %(page_len)s OFFSET %(start)s + """.format( user_type_condition=user_type_condition, standard_users=", ".join(frappe.db.escape(u) for u in STANDARD_USERS), key=searchfield, @@ -1090,9 +1113,7 @@ def get_total_users(): FROM `tabUser` WHERE `enabled` = 1 AND `user_type` = 'System User' - AND `name` NOT IN ({})""".format( - ", ".join(["%s"] * len(STANDARD_USERS)) - ), + AND `name` NOT IN ({})""".format(", ".join(["%s"] * len(STANDARD_USERS))), STANDARD_USERS, )[0][0] ) @@ -1123,9 +1144,7 @@ def get_active_users(): """select count(*) from `tabUser` where enabled = 1 and user_type != 'Website User' and name not in ({}) - and hour(timediff(now(), last_active)) < 72""".format( - ", ".join(["%s"] * len(STANDARD_USERS)) - ), + and hour(timediff(now(), last_active)) < 72""".format(", ".join(["%s"] * len(STANDARD_USERS))), STANDARD_USERS, )[0][0] @@ -1139,8 +1158,8 @@ def get_active_website_users(): """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' - and hour(timediff(now(), last_active)) < 72""" + where enabled = 1 and user_type = 'Website User' + and hour(timediff(now(), last_active)) < 72""" )[0][0] @@ -1166,7 +1185,6 @@ def notify_admin_access_to_system_manager(login_manager=None): and login_manager.user == "Administrator" and frappe.local.conf.notify_admin_access_to_system_manager ): - site = '{0}'.format(frappe.local.request.host_url) date_and_time = "{}".format(format_datetime(now_datetime(), format_string="medium")) ip_address = frappe.local.request_ip @@ -1209,11 +1227,6 @@ def throttle_user_creation(): frappe.throw(_("Throttled")) -@frappe.whitelist() -def get_role_profile(role_profile: str): - return frappe.get_doc("Role Profile", {"role_profile": role_profile}).roles - - @frappe.whitelist() def get_module_profile(module_profile: str): module_profile = frappe.get_doc("Module Profile", {"module_profile_name": module_profile}) @@ -1254,34 +1267,37 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False): except frappe.DuplicateEntryError: pass else: - contact = frappe.get_doc("Contact", contact_name) - contact.first_name = user.first_name - contact.last_name = user.last_name - contact.gender = user.gender + try: + contact = frappe.get_doc("Contact", contact_name) + contact.first_name = user.first_name + contact.last_name = user.last_name + contact.gender = user.gender - # Add mobile number if phone does not exists in contact - if user.phone and not any(new_contact.phone == user.phone for new_contact in contact.phone_nos): - # Set primary phone if there is no primary phone number - contact.add_phone( - user.phone, - is_primary_phone=not any( - new_contact.is_primary_phone == 1 for new_contact in contact.phone_nos - ), - ) + # Add mobile number if phone does not exists in contact + if user.phone and not any(new_contact.phone == user.phone for new_contact in contact.phone_nos): + # Set primary phone if there is no primary phone number + contact.add_phone( + user.phone, + is_primary_phone=not any( + new_contact.is_primary_phone == 1 for new_contact in contact.phone_nos + ), + ) - # Add mobile number if mobile does not exists in contact - if user.mobile_no and not any( - new_contact.phone == user.mobile_no for new_contact in contact.phone_nos - ): - # Set primary mobile if there is no primary mobile number - contact.add_phone( - user.mobile_no, - is_primary_mobile_no=not any( - new_contact.is_primary_mobile_no == 1 for new_contact in contact.phone_nos - ), - ) + # Add mobile number if mobile does not exists in contact + if user.mobile_no and not any( + new_contact.phone == user.mobile_no for new_contact in contact.phone_nos + ): + # Set primary mobile if there is no primary mobile number + contact.add_phone( + user.mobile_no, + is_primary_mobile_no=not any( + new_contact.is_primary_mobile_no == 1 for new_contact in contact.phone_nos + ), + ) - contact.save(ignore_permissions=True) + contact.save(ignore_permissions=True) + except frappe.TimestampMismatchError: + raise frappe.RetryBackgroundJobError def get_restricted_ip_list(user): diff --git a/frappe/core/doctype/user_document_type/user_document_type.py b/frappe/core/doctype/user_document_type/user_document_type.py index 3280aecd5d..a2b519b8ca 100644 --- a/frappe/core/doctype/user_document_type/user_document_type.py +++ b/frappe/core/doctype/user_document_type/user_document_type.py @@ -27,4 +27,5 @@ class UserDocumentType(Document): submit: DF.Check write: DF.Check # end: auto-generated types + pass diff --git a/frappe/core/doctype/user_email/user_email.py b/frappe/core/doctype/user_email/user_email.py index de9a5176f6..bf08615d0a 100644 --- a/frappe/core/doctype/user_email/user_email.py +++ b/frappe/core/doctype/user_email/user_email.py @@ -22,4 +22,5 @@ class UserEmail(Document): parenttype: DF.Data used_oauth: DF.Check # end: auto-generated types + pass diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py index 4318114440..24ef170fb8 100644 --- a/frappe/core/doctype/user_group/user_group.py +++ b/frappe/core/doctype/user_group/user_group.py @@ -19,6 +19,7 @@ class UserGroup(Document): user_group_members: DF.TableMultiSelect[UserGroupMember] # end: auto-generated types + def after_insert(self): frappe.cache.delete_key("user_groups") diff --git a/frappe/core/doctype/user_group_member/user_group_member.py b/frappe/core/doctype/user_group_member/user_group_member.py index c85278414a..48b127eb86 100644 --- a/frappe/core/doctype/user_group_member/user_group_member.py +++ b/frappe/core/doctype/user_group_member/user_group_member.py @@ -19,4 +19,5 @@ class UserGroupMember(Document): parenttype: DF.Data user: DF.Link # end: auto-generated types + pass diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index a22316e50d..3e8d677b04 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -156,9 +156,7 @@ class TestUserPermission(FrappeTestCase): doc.is_tree = 1 doc.insert() - parent_record = frappe.get_doc( - {"doctype": "Person", "person_name": "Parent", "is_group": 1} - ).insert() + parent_record = frappe.get_doc({"doctype": "Person", "person_name": "Parent", "is_group": 1}).insert() child_record = frappe.get_doc( { diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index ae43eb2d9d..02e0304e4c 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -28,6 +28,7 @@ class UserPermission(Document): is_default: DF.Check user: DF.Link # end: auto-generated types + def validate(self): self.validate_user_permission() self.validate_default_permission() @@ -120,7 +121,6 @@ def get_user_permissions(user=None): fields=["allow", "for_value", "applicable_for", "is_default", "hide_descendants"], filters=dict(user=user), ): - meta = frappe.get_meta(perm.allow) add_doc_to_perm(perm, perm.for_value, perm.is_default) @@ -259,9 +259,7 @@ def add_user_permissions(data): return 1 elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1: remove_apply_to_all(data.user, data.doctype, data.docname) - update_applicable( - perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname - ) + update_applicable(perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname) for applicable in data.applicable_doctypes: if applicable not in perm_applied_docs: insert_user_perm( diff --git a/frappe/core/doctype/user_role_profile/__init__.py b/frappe/core/doctype/user_role_profile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_role_profile/user_role_profile.json b/frappe/core/doctype/user_role_profile/user_role_profile.json new file mode 100644 index 0000000000..686556477f --- /dev/null +++ b/frappe/core/doctype/user_role_profile/user_role_profile.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-01-29 13:43:27.545608", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "role_profile" + ], + "fields": [ + { + "fieldname": "role_profile", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Role Profile", + "options": "Role Profile" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-01-29 14:25:09.513552", + "modified_by": "Administrator", + "module": "Core", + "name": "User Role Profile", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/user_role_profile/user_role_profile.py b/frappe/core/doctype/user_role_profile/user_role_profile.py new file mode 100644 index 0000000000..cf37ca5cf3 --- /dev/null +++ b/frappe/core/doctype/user_role_profile/user_role_profile.py @@ -0,0 +1,23 @@ +# Copyright (c) 2024, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class UserRoleProfile(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 + + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + role_profile: DF.Link | None + # end: auto-generated types + + pass diff --git a/frappe/core/doctype/user_select_document_type/user_select_document_type.py b/frappe/core/doctype/user_select_document_type/user_select_document_type.py index 5cdd269e5e..b917e79bff 100644 --- a/frappe/core/doctype/user_select_document_type/user_select_document_type.py +++ b/frappe/core/doctype/user_select_document_type/user_select_document_type.py @@ -19,4 +19,5 @@ class UserSelectDocumentType(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/user_social_login/user_social_login.py b/frappe/core/doctype/user_social_login/user_social_login.py index e08eefb3f8..64b5abdf0b 100644 --- a/frappe/core/doctype/user_social_login/user_social_login.py +++ b/frappe/core/doctype/user_social_login/user_social_login.py @@ -20,4 +20,5 @@ class UserSocialLogin(Document): userid: DF.Data | None username: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index 0d7c2f9c9f..1b976e556c 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -31,8 +31,8 @@ class UserType(Document): user_doctypes: DF.Table[UserDocumentType] user_id_field: DF.Literal user_type_modules: DF.Table[UserTypeModule] - # end: auto-generated types + def validate(self): self.set_modules() self.add_select_perm_doctypes() @@ -333,7 +333,6 @@ def apply_permissions_for_non_standard_user_type(doc, method=None): "User Permission", {"user": doc.get(data[1]), "allow": data[0], "for_value": doc.name}, "name" ) ): - perm_data = frappe.db.get_value( "User Permission", {"allow": doc.doctype, "for_value": doc.name}, ["name", "user"] ) diff --git a/frappe/core/doctype/user_type_module/user_type_module.py b/frappe/core/doctype/user_type_module/user_type_module.py index 9972677690..b8ab63b2f7 100644 --- a/frappe/core/doctype/user_type_module/user_type_module.py +++ b/frappe/core/doctype/user_type_module/user_type_module.py @@ -19,4 +19,5 @@ class UserTypeModule(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index 89b4a995ea..4b8f9d854b 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -21,6 +21,7 @@ class Version(Document): docname: DF.Data ref_doctype: DF.Link # end: auto-generated types + def update_version_info(self, old: Document | None, new: Document) -> bool: """Update changed info and return true if change contains useful data.""" if not old: diff --git a/frappe/core/doctype/view_log/view_log.py b/frappe/core/doctype/view_log/view_log.py index 441bccedf9..42d93fff35 100644 --- a/frappe/core/doctype/view_log/view_log.py +++ b/frappe/core/doctype/view_log/view_log.py @@ -18,6 +18,7 @@ class ViewLog(Document): reference_name: DF.DynamicLink | None viewed_by: DF.Data | None # end: auto-generated types + @staticmethod def clear_old_logs(days=180): from frappe.query_builder import Interval diff --git a/frappe/core/form_tour/user_list_tour/user_list_tour.json b/frappe/core/form_tour/user_list_tour/user_list_tour.json deleted file mode 100644 index 83ae481d25..0000000000 --- a/frappe/core/form_tour/user_list_tour/user_list_tour.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "creation": "2023-05-24 12:53:02.844582", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "List", - "modified": "2023-05-24 13:21:29.552864", - "modified_by": "Administrator", - "module": "Core", - "name": "User List Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"List\",\"User\",\"List\"]", - "reference_doctype": "User", - "report_name": "", - "save_on_complete": 0, - "steps": [ - { - "description": "List view shows all the documents for a particular DocType. Here you can see all the current enabled users in the system. ", - "element_selector": ".frappe-list", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Top Center", - "title": "Users List", - "ui_tour": 1 - }, - { - "description": "These are filters. You can use them to narrow down list of records.", - "element_selector": ".standard-filter-section.flex", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Bottom", - "title": "Filters", - "ui_tour": 1 - }, - { - "description": "When standard filters are not enough you can use advance filters. ", - "element_selector": ".filter-selector > .btn-group", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "ondemand_description": "Advance filters are applied on fields with different operators. \n
\nClick on \"Apply Filters\" to continue.", - "popover_element": 0, - "position": "Left", - "title": "Advanced Filters", - "ui_tour": 1 - }, - { - "description": "Let's create a new user.", - "element_selector": ".btn-primary.primary-action", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 1, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "parent_element_selector": "", - "popover_element": 0, - "position": "Bottom", - "title": "New User", - "ui_tour": 1 - } - ], - "title": "User List Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "List", - "workspace_name": "" -} \ No newline at end of file diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index f25ec6d4ad..308bbfedba 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -253,6 +253,10 @@ frappe.PermissionEngine = class PermissionEngine { if (!d.is_submittable && ["submit", "cancel", "amend"].includes(r)) return; if (d.in_create && ["create", "delete"].includes(r)) return; this.add_check(perm_container, d, r); + + if (d.if_owner && r == "report") { + perm_container.find("div[data-fieldname='report']").toggle(false); + } }); // buttons @@ -414,6 +418,13 @@ frappe.PermissionEngine = class PermissionEngine { chk.prop("checked", !chk.prop("checked")); } else { me.get_perm(args.role)[args.ptype] = args.value; + + if (args.ptype == "if_owner") { + let report_checkbox = chk + .closest("div.row") + .find("div[data-fieldname='report']"); + report_checkbox.toggle(!args.value); + } } }, }); diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 71d6a4a002..d19d42d2af 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -109,9 +109,7 @@ def add(parent, role, permlevel): @frappe.whitelist() -def update( - doctype: str, role: str, permlevel: int, ptype: str, value=None, if_owner=0 -) -> str | None: +def update(doctype: str, role: str, permlevel: int, ptype: str, value=None, if_owner=0) -> str | None: """Update role permission params. Args: @@ -129,8 +127,15 @@ def update( frappe.clear_cache(doctype=doctype) frappe.only_for("System Manager") + + if ptype == "report" and value == "1" and if_owner == "1": + frappe.throw(_("Cannot set 'Report' permission if 'Only If Creator' permission is set")) + out = update_permission_property(doctype, role, permlevel, ptype, value, if_owner=if_owner) + if ptype == "if_owner" and value == "1": + update_permission_property(doctype, role, permlevel, "report", "0", if_owner=value) + frappe.db.after_commit.add(clear_cache) return "refresh" if out else None diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index b865c23b11..bf1694b164 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -36,11 +36,7 @@ def get_columns_and_fields(doctype): if df.in_list_view and df.fieldtype in data_fieldtypes: fields.append(f"`{df.fieldname}`") fieldtype = f"Link/{df.options}" if df.fieldtype == "Link" else df.fieldtype - columns.append( - "{label}:{fieldtype}:{width}".format( - label=df.label, fieldtype=fieldtype, width=df.width or 100 - ) - ) + columns.append(f"{df.label}:{fieldtype}:{df.width or 100}") return columns, fields diff --git a/frappe/custom/doctype/client_script/client_script.py b/frappe/custom/doctype/client_script/client_script.py index ca7b829a12..f241ca2437 100644 --- a/frappe/custom/doctype/client_script/client_script.py +++ b/frappe/custom/doctype/client_script/client_script.py @@ -19,6 +19,7 @@ class ClientScript(Document): script: DF.Code | None view: DF.Literal["List", "Form"] # end: auto-generated types + def on_update(self): frappe.clear_cache(doctype=self.dt) diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index 031d53de20..d0c61b6d8c 100644 --- a/frappe/custom/doctype/custom_field/custom_field.js +++ b/frappe/custom/doctype/custom_field/custom_field.js @@ -112,28 +112,30 @@ frappe.ui.form.on("Custom Field", { } }, add_rename_field(frm) { - frm.add_custom_button(__("Rename Fieldname"), () => { - frappe.prompt( - { - fieldtype: "Data", - label: __("Fieldname"), - fieldname: "fieldname", - reqd: 1, - default: frm.doc.fieldname, - }, - function (data) { - frappe.call({ - method: "frappe.custom.doctype.custom_field.custom_field.rename_fieldname", - args: { - custom_field: frm.doc.name, - fieldname: data.fieldname, - }, - }); - }, - __("Rename Fieldname"), - __("Rename") - ); - }); + if (!frm.is_new()) { + frm.add_custom_button(__("Rename Fieldname"), () => { + frappe.prompt( + { + fieldtype: "Data", + label: __("Fieldname"), + fieldname: "fieldname", + reqd: 1, + default: frm.doc.fieldname, + }, + function (data) { + frappe.call({ + method: "frappe.custom.doctype.custom_field.custom_field.rename_fieldname", + args: { + custom_field: frm.doc.name, + fieldname: data.fieldname, + }, + }); + }, + __("Rename Fieldname"), + __("Rename") + ); + }); + } }, }); diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index ce86da5e91..594eabff94 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -115,6 +115,7 @@ class CustomField(Document): unique: DF.Check width: DF.Data | None # end: auto-generated types + def autoname(self): self.set_fieldname() self.name = self.dt + "-" + self.fieldname @@ -222,9 +223,7 @@ class CustomField(Document): frappe.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname}) # update doctype layouts - doctype_layouts = frappe.get_all( - "DocType Layout", filters={"document_type": self.dt}, pluck="name" - ) + doctype_layouts = frappe.get_all("DocType Layout", filters={"document_type": self.dt}, pluck="name") for layout in doctype_layouts: layout_doc = frappe.get_doc("DocType Layout", layout) @@ -374,9 +373,7 @@ def rename_fieldname(custom_field: str, fieldname: str): frappe.clear_cache() -def _update_fieldname_references( - field: CustomField, old_fieldname: str, new_fieldname: str -) -> None: +def _update_fieldname_references(field: CustomField, old_fieldname: str, new_fieldname: str) -> None: # Passwords are stored in auth table, so column name needs to be updated there. if field.fieldtype == "Password": Auth = frappe.qb.Table("__Auth") diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 5cf05978e1..7d374785dc 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -345,9 +345,7 @@ class CustomizeForm(Document): ) and (df.get(prop) == 0) ): - frappe.msgprint( - _("Row {0}: Not allowed to disable Mandatory for standard fields").format(df.idx) - ) + frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields").format(df.idx)) return False elif ( @@ -414,7 +412,9 @@ class CustomizeForm(Document): original = frappe.get_doc(doctype, d.name) for prop, prop_type in field_map.items(): if d.get(prop) != original.get(prop): - self.make_property_setter(prop, d.get(prop), prop_type, apply_on=doctype, row_name=d.name) + self.make_property_setter( + prop, d.get(prop), prop_type, apply_on=doctype, row_name=d.name + ) items.append(d.name) else: # custom - just insert/update @@ -523,9 +523,7 @@ class CustomizeForm(Document): if not is_standard_or_system_generated_field(df): frappe.delete_doc("Custom Field", df.name) - def make_property_setter( - self, prop, value, property_type, fieldname=None, apply_on=None, row_name=None - ): + def make_property_setter(self, prop, value, property_type, fieldname=None, apply_on=None, row_name=None): delete_property_setter(self.doc_type, prop, fieldname, row_name) property_value = self.get_existing_property_value(prop, fieldname) @@ -594,13 +592,11 @@ class CustomizeForm(Document): max_length = cint(frappe.db.type_map.get(df.fieldtype)[1]) fieldname = df.fieldname docs = frappe.db.sql( - """ + f""" SELECT name, {fieldname}, LENGTH({fieldname}) AS len - FROM `tab{doctype}` + FROM `tab{self.doc_type}` WHERE LENGTH({fieldname}) > {max_length} - """.format( - fieldname=fieldname, doctype=self.doc_type, max_length=max_length - ), + """, as_dict=True, ) label = df.label diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 7354c55efa..edf72eaf40 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -69,27 +69,21 @@ class TestCustomizeForm(FrappeTestCase): def test_save_customization_property(self): d = self.get_customize_form("Event") self.assertEqual( - frappe.db.get_value( - "Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value" - ), + frappe.db.get_value("Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value"), None, ) d.allow_copy = 1 d.run_method("save_customization") self.assertEqual( - frappe.db.get_value( - "Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value" - ), + frappe.db.get_value("Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value"), "1", ) d.allow_copy = 0 d.run_method("save_customization") self.assertEqual( - frappe.db.get_value( - "Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value" - ), + frappe.db.get_value("Property Setter", {"doc_type": "Event", "property": "allow_copy"}, "value"), None, ) @@ -340,9 +334,7 @@ class TestCustomizeForm(FrappeTestCase): frappe.clear_cache() user_group = frappe.get_meta("Event") - self.assertFalse( - [d.name for d in (user_group.links or []) if d.link_doctype == "User Group Member"] - ) + self.assertFalse([d.name for d in (user_group.links or []) if d.link_doctype == "User Group Member"]) def test_custom_action(self): test_route = "/app/List/DocType" 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 67c6c8ba95..0dfd7a6c45 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -130,6 +130,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.is_virtual", "fieldname": "in_list_view", "fieldtype": "Check", "label": "In List View" @@ -483,7 +484,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-12-08 15:52:37.525003", + "modified": "2024-02-01 15:56:39.171633", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.py b/frappe/custom/doctype/customize_form_field/customize_form_field.py index 59b0155a98..76ab6535e3 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.py +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.py @@ -110,4 +110,5 @@ class CustomizeFormField(Document): unique: DF.Check width: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py index c155f32ed1..63d199448e 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.py +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py @@ -26,6 +26,7 @@ class DocTypeLayout(Document): fields: DF.Table[DocTypeLayoutField] route: DF.Data # end: auto-generated types + def validate(self): if not self.route: self.route = slug(self.name) diff --git a/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py index 7d22ee3c7d..df1270874a 100644 --- a/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py +++ b/frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py @@ -5,14 +5,12 @@ def execute(): for web_form_name in frappe.get_all("Web Form", pluck="name"): web_form = frappe.get_doc("Web Form", web_form_name) doctype_layout = frappe.get_doc( - dict( - doctype="DocType Layout", - document_type=web_form.doc_type, - name=web_form.title, - route=web_form.route, - fields=[ - dict(fieldname=d.fieldname, label=d.label) for d in web_form.web_form_fields if d.fieldname - ], - ) + doctype="DocType Layout", + document_type=web_form.doc_type, + name=web_form.title, + route=web_form.route, + fields=[ + dict(fieldname=d.fieldname, label=d.label) for d in web_form.web_form_fields if d.fieldname + ], ).insert() print(doctype_layout.name) diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py index 470517b589..ffd3b61ccf 100644 --- a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py +++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py @@ -20,4 +20,5 @@ class DocTypeLayoutField(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 56f3144722..9b1ce34414 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -30,6 +30,7 @@ class PropertySetter(Document): row_name: DF.Data | None value: DF.SmallText | None # end: auto-generated types + def autoname(self): self.name = "{doctype}-{field}-{property}".format( doctype=self.doc_type, field=self.field_name or self.row_name or "main", property=self.property diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py index a42c5c361a..07b8e18b4d 100644 --- a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py @@ -18,9 +18,7 @@ def get_columns(): # Each app is shown in order as a column installed_apps = frappe.get_installed_apps(_ensure_on_bench=True) - columns += [ - {"label": app, "fieldname": app, "fieldtype": values_field_type} for app in installed_apps - ] + columns += [{"label": app, "fieldname": app, "fieldtype": values_field_type} for app in installed_apps] return columns @@ -37,7 +35,7 @@ def get_data(): v = delist(v) - if isinstance(v, (dict, list)): + if isinstance(v, dict | list): try: return frappe.as_json(v) except Exception: diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py index 530bd4c700..12348e2f99 100644 --- a/frappe/database/__init__.py +++ b/frappe/database/__init__.py @@ -23,17 +23,17 @@ def setup_database(force, verbose=None, no_mariadb_socket=False): ) -def bootstrap_database(db_name, verbose=None, source_sql=None): +def bootstrap_database(verbose=None, source_sql=None): import frappe if frappe.conf.db_type == "postgres": import frappe.database.postgres.setup_db - return frappe.database.postgres.setup_db.bootstrap_database(db_name, verbose, source_sql) + return frappe.database.postgres.setup_db.bootstrap_database(verbose, source_sql) else: import frappe.database.mariadb.setup_db - return frappe.database.mariadb.setup_db.bootstrap_database(db_name, verbose, source_sql) + return frappe.database.mariadb.setup_db.bootstrap_database(verbose, source_sql) def drop_user_and_database(db_name, db_user): @@ -55,18 +55,14 @@ def get_db(host=None, user=None, password=None, port=None, cur_db_name=None): if frappe.conf.db_type == "postgres": import frappe.database.postgres.database - return frappe.database.postgres.database.PostgresDatabase( - host, user, password, port, cur_db_name - ) + return frappe.database.postgres.database.PostgresDatabase(host, user, password, port, cur_db_name) else: import frappe.database.mariadb.database return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port, cur_db_name) -def get_command( - host=None, port=None, user=None, password=None, db_name=None, extra=None, dump=False -): +def get_command(host=None, port=None, user=None, password=None, db_name=None, extra=None, dump=False): import frappe if frappe.conf.db_type == "postgres": @@ -75,12 +71,7 @@ def get_command( else: bin, bin_name = which("psql"), "psql" - host = frappe.utils.esc(host, "$ ") - user = frappe.utils.esc(user, "$ ") - db_name = frappe.utils.esc(db_name, "$ ") - if password: - password = frappe.utils.esc(password, "$ ") conn_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}" else: conn_string = f"postgresql://{user}@{host}:{port}/{db_name}" @@ -96,10 +87,6 @@ def get_command( else: bin, bin_name = which("mariadb") or which("mysql"), "mariadb" - host = frappe.utils.esc(host, "$ ") - user = frappe.utils.esc(user, "$ ") - db_name = frappe.utils.esc(db_name, "$ ") - command = [ f"--user={user}", f"--host={host}", @@ -107,7 +94,6 @@ def get_command( ] if password: - password = frappe.utils.esc(password, "$ ") command.append(f"--password={password}") if dump: diff --git a/frappe/database/database.py b/frappe/database/database.py index e8e29dbc2a..83f8e5bbcf 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -29,9 +29,8 @@ from frappe.database.utils import ( from frappe.exceptions import DoesNotExistError, ImplicitCommitError from frappe.monitor import get_trace_id from frappe.query_builder.functions import Count -from frappe.utils import CallbackManager +from frappe.utils import CallbackManager, cint, get_datetime, get_table_name, getdate, now, sbool from frappe.utils import cast as cast_fieldtype -from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool from frappe.utils.deprecations import deprecation_warning if TYPE_CHECKING: @@ -104,8 +103,8 @@ class Database: def connect(self): """Connects to a database as set in `site_config.json`.""" - self._conn: Union["MariadbConnection", "PostgresConnection"] = self.get_connection() - self._cursor: Union["MariadbCursor", "PostgresCursor"] = self._conn.cursor() + self._conn: "MariadbConnection" | "PostgresConnection" = self.get_connection() + self._cursor: "MariadbCursor" | "PostgresCursor" = self._conn.cursor() try: if execution_timeout := get_query_execution_timeout(): @@ -184,7 +183,7 @@ class Database: {"name": "a%", "owner":"test@example.com"}) """ - if isinstance(query, (MySQLQueryBuilder, PostgreSQLQueryBuilder)): + if isinstance(query, MySQLQueryBuilder | PostgreSQLQueryBuilder): frappe.errprint("Use run method to execute SQL queries generated by Query Engine") debug = debug or getattr(self, "debug", False) @@ -213,7 +212,7 @@ class Database: if values == EmptyQueryValues: values = None - elif not isinstance(values, (tuple, dict, list)): + elif not isinstance(values, tuple | dict | list): values = (values,) query, values = self._transform_query(query, values) @@ -303,7 +302,7 @@ class Database: elif as_dict: keys = [column[0] for column in self._cursor.description] for row in result: - row = frappe._dict(zip(keys, row)) + row = frappe._dict(zip(keys, row, strict=False)) if update: row.update(update) yield row @@ -369,8 +368,10 @@ class Database: return self._cursor.mogrify(query, values) except AttributeError: if isinstance(values, dict): - return query % {k: frappe.db.escape(v) if isinstance(v, str) else v for k, v in values.items()} - elif isinstance(values, (list, tuple)): + return query % { + k: frappe.db.escape(v) if isinstance(v, str) else v for k, v in values.items() + } + elif isinstance(values, list | tuple): return query % tuple(frappe.db.escape(v) if isinstance(v, str) else v for v in values) return query, values @@ -437,7 +438,7 @@ class Database: if result: keys = [column[0] for column in self._cursor.description] - return [frappe._dict(zip(keys, row)) for row in result] + return [frappe._dict(zip(keys, row, strict=False)) for row in result] @staticmethod def clear_db_table_cache(query): @@ -620,8 +621,11 @@ class Database: skip_locked=skip_locked, ) except Exception as e: - if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): - # table or column not found, return None + if ignore and ( + frappe.db.is_missing_column(e) + or frappe.db.is_table_missing(e) + or str(e).startswith("Invalid DocType") + ): out = None elif (not ignore) and frappe.db.is_table_missing(e): # table not found, look in singles @@ -788,9 +792,7 @@ class Database: ) singles_data = ((doctype, key, sbool(value)) for key, value in to_update.items()) - frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data).run( - debug=debug - ) + frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data).run(debug=debug) frappe.clear_document_cache(doctype, doctype) if doctype in self.value_cache: diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 4a37ab6cc3..908e7bb479 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -291,20 +291,18 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): def create_global_search_table(self): if "__global_search" not in self.get_tables(): self.sql( - """create table __global_search( + f"""create table __global_search( doctype varchar(100), - name varchar({0}), - title varchar({0}), + name varchar({self.VARCHAR_LEN}), + title varchar({self.VARCHAR_LEN}), content text, fulltext(content), - route varchar({0}), + route varchar({self.VARCHAR_LEN}), published int(1) not null default 0, unique `doctype_name` (doctype, name)) COLLATE=utf8mb4_unicode_ci ENGINE=MyISAM - CHARACTER SET=utf8mb4""".format( - self.VARCHAR_LEN - ) + CHARACTER SET=utf8mb4""" ) def create_user_settings_table(self): @@ -324,7 +322,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): def get_table_columns_description(self, table_name): """Return list of columns with descriptions.""" return self.sql( - """select + f"""select column_name as 'name', column_type as 'type', column_default as 'default', @@ -340,9 +338,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): column_key = 'UNI' as 'unique', (is_nullable = 'NO') AS 'not_nullable' from information_schema.columns as columns - where table_name = '{table_name}' """.format( - table_name=table_name - ), + where table_name = '{table_name}' """, as_dict=1, ) @@ -363,15 +359,11 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): def has_index(self, table_name, index_name): return self.sql( - """SHOW INDEX FROM `{table_name}` - WHERE Key_name='{index_name}'""".format( - table_name=table_name, index_name=index_name - ) + f"""SHOW INDEX FROM `{table_name}` + WHERE Key_name='{index_name}'""" ) - def get_column_index( - self, table_name: str, fieldname: str, unique: bool = False - ) -> frappe._dict | None: + def get_column_index(self, table_name: str, fieldname: str, unique: bool = False) -> frappe._dict | None: """Check if column exists for a specific fields in specified order. This differs from db.has_index because it doesn't rely on index name but columns inside an @@ -383,6 +375,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): WHERE Column_name = "{fieldname}" AND Seq_in_index = 1 AND Non_unique={int(not unique)} + AND Index_type != 'FULLTEXT' """, as_dict=True, ) @@ -408,9 +401,8 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): if not self.has_index(table_name, index_name): self.commit() self.sql( - """ALTER TABLE `%s` - ADD INDEX `%s`(%s)""" - % (table_name, index_name, ", ".join(fields)) + """ALTER TABLE `{}` + ADD INDEX `{}`({})""".format(table_name, index_name, ", ".join(fields)) ) def add_unique(self, doctype, fields, constraint_name=None): @@ -426,9 +418,8 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database): ): self.commit() self.sql( - """alter table `tab%s` - add unique `%s`(%s)""" - % (doctype, constraint_name, ", ".join(fields)) + """alter table `tab{}` + add unique `{}`({})""".format(doctype, constraint_name, ", ".join(fields)) ) def updatedb(self, doctype, meta=None): diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 0486ab9463..ccd8c2c1d9 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -67,19 +67,14 @@ class MariaDBTable(DBTable): for col in self.columns.values(): col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) - add_column_query = [ - f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column - ] + add_column_query = [f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column] columns_to_modify = set(self.change_type + self.set_default + self.change_nullability) modify_column_query = [ f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}" for col in columns_to_modify ] modify_column_query.extend( - [ - f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)" - for col in self.add_unique - ] + [f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)" for col in self.add_unique] ) add_index_query = [ f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)" @@ -128,9 +123,9 @@ class MariaDBTable(DBTable): if e.args[0] == DUP_ENTRY: fieldname = str(e).split("'")[-2] frappe.throw( - _("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format( - fieldname, self.table_name - ) + _( + "{0} field cannot be set as unique in {1}, as there are non-unique existing values" + ).format(fieldname, self.table_name) ) raise diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py index 6dc3e9ecb2..96e80e741c 100644 --- a/frappe/database/mariadb/setup_db.py +++ b/frappe/database/mariadb/setup_db.py @@ -67,17 +67,17 @@ def drop_user_and_database( dbman.delete_user(db_user) -def bootstrap_database(db_name, verbose, source_sql=None): +def bootstrap_database(verbose, source_sql=None): import sys - frappe.connect(db_name=db_name) + frappe.connect() if not check_database_settings(): print("Database settings do not match expected values; stopping database setup.") sys.exit(1) import_db_from_sql(source_sql, verbose) - frappe.connect(db_name=db_name) + frappe.connect() if "tabDefaultValue" not in frappe.db.get_tables(cached=False): from click import secho @@ -113,10 +113,7 @@ def check_database_settings(): result = True for key, expected_value in REQUIRED_MARIADB_CONFIG.items(): if mariadb_variables.get(key) != expected_value: - print( - "For key %s. Expected value %s, found value %s" - % (key, expected_value, mariadb_variables.get(key)) - ) + print(f"For key {key}. Expected value {expected_value}, found value {mariadb_variables.get(key)}") result = False if not result: @@ -164,9 +161,7 @@ def get_root_connection(): ) if not frappe.flags.root_password: - frappe.flags.root_password = frappe.conf.get("root_password") or getpass( - "MySQL root password: " - ) + frappe.flags.root_password = frappe.conf.get("root_password") or getpass("MySQL root password: ") frappe.local.flags.root_connection = frappe.database.get_db( host=frappe.conf.db_host, diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 76cca1faea..003455b5dd 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -218,9 +218,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database): from information_schema.tables where table_catalog='{}' and table_type = 'BASE TABLE' - and table_schema='{}'""".format( - self.cur_db_name, frappe.conf.get("db_schema", "public") - ) + and table_schema='{}'""".format(self.cur_db_name, frappe.conf.get("db_schema", "public")) ) ] @@ -290,16 +288,14 @@ class PostgresDatabase(PostgresExceptionUtil, Database): def create_global_search_table(self): if "__global_search" not in self.get_tables(): self.sql( - """create table "__global_search"( + f"""create table "__global_search"( doctype varchar(100), - name varchar({0}), - title varchar({0}), + name varchar({self.VARCHAR_LEN}), + title varchar({self.VARCHAR_LEN}), content text, - route varchar({0}), + route varchar({self.VARCHAR_LEN}), published int not null default 0, - unique (doctype, name))""".format( - self.VARCHAR_LEN - ) + unique (doctype, name))""" ) def create_user_settings_table(self): @@ -342,10 +338,8 @@ class PostgresDatabase(PostgresExceptionUtil, Database): def has_index(self, table_name, index_name): return self.sql( - """SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' - and indexname='{index_name}' limit 1""".format( - table_name=table_name, index_name=index_name - ) + f"""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' + and indexname='{index_name}' limit 1""" ) def add_index(self, doctype: str, fields: list, index_name: str = None): @@ -374,16 +368,15 @@ class PostgresDatabase(PostgresExceptionUtil, Database): ): self.commit() self.sql( - """ALTER TABLE `tab%s` - ADD CONSTRAINT %s UNIQUE (%s)""" - % (doctype, constraint_name, ", ".join(fields)) + """ALTER TABLE `tab{}` + ADD CONSTRAINT {} UNIQUE ({})""".format(doctype, constraint_name, ", ".join(fields)) ) def get_table_columns_description(self, table_name): """Return list of columns with description.""" # pylint: disable=W1401 return self.sql( - """ + f""" SELECT a.column_name AS name, CASE LOWER(a.data_type) WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')') @@ -404,9 +397,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database): ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%') WHERE a.table_name = '{table_name}' GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length, a.is_nullable; - """.format( - table_name=table_name - ), + """, as_dict=1, ) @@ -449,7 +440,7 @@ def modify_query(query): def modify_values(values): def modify_value(value): - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): value = tuple(modify_values(value)) elif isinstance(value, int): @@ -463,7 +454,7 @@ def modify_values(values): if isinstance(values, dict): for k, v in values.items(): values[k] = modify_value(v) - elif isinstance(values, (tuple, list)): + elif isinstance(values, tuple | list): new_values = [] for val in values: new_values.append(modify_value(val)) diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 48fd66e31a..018de93f41 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -54,16 +54,14 @@ class PostgresTable(DBTable): def create_indexes(self): create_index_query = "" - for key, col in self.columns.items(): + for col in self.columns.values(): if ( col.set_index and col.fieldtype in frappe.db.type_map and frappe.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext") ): create_index_query += ( - 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( - index_name=col.fieldname, table_name=self.table_name, field=col.fieldname - ) + f'CREATE INDEX IF NOT EXISTS "{col.fieldname}" ON `{self.table_name}`(`{col.fieldname}`);' ) if create_index_query: # nosemgrep @@ -115,17 +113,13 @@ class PostgresTable(DBTable): for col in self.add_index: # if index key not exists create_contraint_query += ( - 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( - index_name=col.fieldname, table_name=self.table_name, field=col.fieldname - ) + f'CREATE INDEX IF NOT EXISTS "{col.fieldname}" ON `{self.table_name}`(`{col.fieldname}`);' ) for col in self.add_unique: # if index key not exists - create_contraint_query += ( - 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format( - index_name=col.fieldname, table_name=self.table_name, field=col.fieldname - ) + create_contraint_query += 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format( + index_name=col.fieldname, table_name=self.table_name, field=col.fieldname ) drop_contraint_query = "" @@ -181,9 +175,9 @@ class PostgresTable(DBTable): elif frappe.db.is_duplicate_entry(e): fieldname = str(e).split("'")[-2] frappe.throw( - _("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format( - fieldname, self.table_name - ) + _( + "{0} field cannot be set as unique in {1}, as there are non-unique existing values" + ).format(fieldname, self.table_name) ) else: raise e diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 855f31b89c..89e9f513aa 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -18,9 +18,7 @@ def setup_database(): else: root_conn.sql(f"CREATE USER \"{frappe.conf.db_user}\" WITH PASSWORD '{frappe.conf.db_password}'") root_conn.sql(f'CREATE DATABASE "{frappe.conf.db_name}"') - root_conn.sql( - f'GRANT ALL PRIVILEGES ON DATABASE "{frappe.conf.db_name}" TO "{frappe.conf.db_user}"' - ) + root_conn.sql(f'GRANT ALL PRIVILEGES ON DATABASE "{frappe.conf.db_name}" TO "{frappe.conf.db_user}"') if psql_version := root_conn.sql("SELECT VERSION()", as_dict=True): version_string = psql_version[0].get("version") or "PostgreSQL 14" major_version = cint(re.split(r"[\w\.]", version_string)[1]) @@ -29,11 +27,11 @@ def setup_database(): root_conn.close() -def bootstrap_database(db_name, verbose, source_sql=None): - frappe.connect(db_name=db_name) +def bootstrap_database(verbose, source_sql=None): + frappe.connect() import_db_from_sql(source_sql, verbose) - frappe.connect(db_name=db_name) + frappe.connect() if "tabDefaultValue" not in frappe.db.get_tables(): import sys diff --git a/frappe/database/query.py b/frappe/database/query.py index 8d3888e494..a94f07ac06 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -99,7 +99,7 @@ class Engine: # add fields self.fields = self.parse_fields(fields) if not self.fields: - self.fields = [getattr(self.table, "name")] + self.fields = [self.table.name] self.query._child_queries = [] for field in self.fields: @@ -117,7 +117,7 @@ class Engine: if filters is None: return - if isinstance(filters, (str, int)): + if isinstance(filters, str | int): filters = {"name": str(filters)} if isinstance(filters, Criterion): @@ -126,14 +126,14 @@ class Engine: elif isinstance(filters, dict): self.apply_dict_filters(filters) - elif isinstance(filters, (list, tuple)): - if all(isinstance(d, (str, int)) for d in filters) and len(filters) > 0: + elif isinstance(filters, list | tuple): + if all(isinstance(d, str | int) for d in filters) and len(filters) > 0: self.apply_dict_filters({"name": ("in", filters)}) else: for filter in filters: - if isinstance(filter, (str, int, Criterion, dict)): + if isinstance(filter, str | int | Criterion | dict): self.apply_filters(filter) - elif isinstance(filter, (list, tuple)): + elif isinstance(filter, list | tuple): self.apply_list_filters(filter) def apply_list_filters(self, filter: list): @@ -150,7 +150,7 @@ class Engine: def apply_dict_filters(self, filters: dict[str, str | int | list]): for field, value in filters.items(): operator = "=" - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): operator, value = value self._apply_filter(field, value, operator) @@ -164,9 +164,7 @@ class Engine: if not isinstance(_field, str): pass - elif not self.validate_filters and ( - dynamic_field := DynamicTableField.parse(field, self.doctype) - ): + elif not self.validate_filters and (dynamic_field := DynamicTableField.parse(field, self.doctype)): # apply implicit join if link field's field is referenced self.query = dynamic_field.apply_join(self.query) _field = dynamic_field.field @@ -189,7 +187,7 @@ class Engine: if isinstance(_value, bool): _value = int(_value) - elif not _value and isinstance(_value, (list, tuple)): + elif not _value and isinstance(_value, list | tuple): _value = ("",) # Nested set @@ -249,7 +247,11 @@ class Engine: ) field = ( - (Field(initial_fields) if "`" not in initial_fields else PseudoColumnMapper(initial_fields)) + ( + Field(initial_fields) + if "`" not in initial_fields + else PseudoColumnMapper(initial_fields) + ) if not has_primitive_operator else field ) @@ -277,7 +279,7 @@ class Engine: return MARIADB_SPECIFIC_COMMENT.sub("", stripped_field) return stripped_field - if isinstance(fields, (list, tuple)): + if isinstance(fields, list | tuple): return [_sanitize_field(field) for field in fields] elif isinstance(fields, str): return _sanitize_field(fields) @@ -302,10 +304,10 @@ class Engine: if not fields: return [] fields = self.sanitize_fields(fields) - if isinstance(fields, (list, tuple, set)) and None in fields and Field not in fields: + if isinstance(fields, list | tuple | set) and None in fields and Field not in fields: return [] - if not isinstance(fields, (list, tuple)): + if not isinstance(fields, list | tuple): fields = [fields] def parse_field(field: str): diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 1387cbc549..e7258399bc 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -126,7 +126,6 @@ class DBTable: ) if "varchar" in frappe.db.type_map.get(col.fieldtype, ()): - # validate length range new_length = cint(col.length) or cint(frappe.db.VARCHAR_LEN) if not (1 <= new_length <= 1000): @@ -145,9 +144,7 @@ class DBTable: try: # check for truncation max_length = frappe.db.sql( - """SELECT MAX(CHAR_LENGTH(`{fieldname}`)) FROM `tab{doctype}`""".format( - fieldname=col.fieldname, doctype=self.doctype - ) + f"""SELECT MAX(CHAR_LENGTH(`{col.fieldname}`)) FROM `tab{self.doctype}`""" ) except frappe.db.InternalError as e: @@ -393,16 +390,9 @@ def get_definition(fieldtype, precision=None, length=None): return coltype -def add_column( - doctype, column_name, fieldtype, precision=None, length=None, default=None, not_null=False -): - if column_name in frappe.db.get_table_columns(doctype): - # already exists - return - +def add_column(doctype, column_name, fieldtype, precision=None, length=None, default=None, not_null=False): frappe.db.commit() - - query = "alter table `tab{}` add column {} {}".format( + query = "alter table `tab{}` add column if not exists {} {}".format( doctype, column_name, get_definition(fieldtype, precision, length), diff --git a/frappe/database/sequence.py b/frappe/database/sequence.py index 1b22dfdd6a..f68ff7c0e6 100644 --- a/frappe/database/sequence.py +++ b/frappe/database/sequence.py @@ -31,7 +31,6 @@ def create_sequence( min_value: int = 0, max_value: int = 0, ) -> str: - query = "create sequence" if not temporary else "create temporary sequence" sequence_name = scrub(doctype_name + slug) @@ -90,7 +89,6 @@ def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int: def set_next_val( doctype_name: str, next_val: int, *, slug: str = "_id_seq", is_val_used: bool = False ) -> None: - is_val_used = "false" if not is_val_used else "true" db.multisql( diff --git a/frappe/defaults.py b/frappe/defaults.py index d8ba0dc93b..af95c273a1 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -21,7 +21,7 @@ def get_user_default(key, user=None): d = user_defaults.get(key, None) if is_a_user_permission_key(key): - if d and isinstance(d, (list, tuple)) and len(d) == 1: + if d and isinstance(d, list | tuple) and len(d) == 1: # Use User Permission value when only when it has a single value d = d[0] else: @@ -31,7 +31,7 @@ def get_user_default(key, user=None): # If no default value is found, use the User Permission value d = user_permission_default - value = isinstance(d, (list, tuple)) and d[0] or d + value = isinstance(d, list | tuple) and d[0] or d if not_in_user_permission(key, value, user): return @@ -61,14 +61,14 @@ def get_user_default_as_list(key, user=None): d = user_defaults.get(key, None) if is_a_user_permission_key(key): - if d and isinstance(d, (list, tuple)) and len(d) == 1: + if d and isinstance(d, list | tuple) and len(d) == 1: # Use User Permission value when only when it has a single value d = [d[0]] else: d = user_defaults.get(frappe.scrub(key), None) - d = list(filter(None, (not isinstance(d, (list, tuple))) and [d] or d)) + d = list(filter(None, (not isinstance(d, list | tuple)) and [d] or d)) # filter default values if not found in user permission return [value for value in d if not not_in_user_permission(key, value)] @@ -135,7 +135,7 @@ def add_global_default(key, value): def get_global_default(key): d = get_defaults().get(key, None) - value = isinstance(d, (list, tuple)) and d[0] or d + value = isinstance(d, list | tuple) and d[0] or d if not_in_user_permission(key, value): return diff --git a/frappe/deferred_insert.py b/frappe/deferred_insert.py index 5c7e7a7f0d..fd370d8ac5 100644 --- a/frappe/deferred_insert.py +++ b/frappe/deferred_insert.py @@ -13,7 +13,7 @@ queue_prefix = "insert_queue_for_" def deferred_insert(doctype: str, records: list[Union[dict, "Document"]] | str): - if isinstance(records, (dict, list)): + if isinstance(records, dict | list): _records = json.dumps(records) else: _records = records diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index fa2746f86c..26968f8430 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -176,7 +176,6 @@ class Workspace: def _prepare_item(self, item): if item.dependencies: - dependencies = [dep.strip() for dep in item.dependencies.split(",")] incomplete_dependencies = [d for d in dependencies if not self._doctype_contains_a_record(d)] @@ -194,6 +193,9 @@ class Workspace: item["count"] = count + if item.get("link_type") == "DocType": + item["description"] = frappe.get_meta(item.link_to).description + # Translate label item["label"] = _(item.label) if item.label else _(item.name) @@ -203,8 +205,7 @@ class Workspace: from frappe.utils import has_common allowed = [ - d.role - for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": custom_block_name}) + d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": custom_block_name}) ] if not allowed: @@ -544,9 +545,7 @@ def save_new_widget(doc, page, blocks, new_widgets): new_widget(widgets.custom_block, "Workspace Custom Block", "custom_blocks") ) if widgets.number_card: - doc.number_cards.extend( - new_widget(widgets.number_card, "Workspace Number Card", "number_cards") - ) + doc.number_cards.extend(new_widget(widgets.number_card, "Workspace Number Card", "number_cards")) if widgets.card: doc.build_links_table_from_card(widgets.card) @@ -560,13 +559,11 @@ def save_new_widget(doc, page, blocks, new_widgets): json_config = widgets and dumps(widgets, sort_keys=True, indent=4) # Error log body - log = """ - page: {} - config: {} - exception: {} - """.format( - page, json_config, e - ) + log = f""" + page: {page} + config: {json_config} + exception: {e} + """ doc.log_error("Could not save customization", log) raise diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index a0f5a45326..e7b2a0e62d 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -66,9 +66,7 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None, timeout=1000, ) else: - frappe.throw( - _("Bulk operations only support up to 500 documents."), title=_("Too Many Documents") - ) + frappe.throw(_("Bulk operations only support up to 500 documents."), title=_("Too Many Documents")) def _bulk_action(doctype, docnames, action, data, task_id=None): diff --git a/frappe/desk/doctype/calendar_view/calendar_view.py b/frappe/desk/doctype/calendar_view/calendar_view.py index e5ce017669..cd74381079 100644 --- a/frappe/desk/doctype/calendar_view/calendar_view.py +++ b/frappe/desk/doctype/calendar_view/calendar_view.py @@ -19,4 +19,5 @@ class CalendarView(Document): start_date_field: DF.Literal subject_field: DF.Literal # end: auto-generated types + pass diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py index cd004745a3..d618bb240b 100644 --- a/frappe/desk/doctype/console_log/console_log.py +++ b/frappe/desk/doctype/console_log/console_log.py @@ -18,4 +18,5 @@ class ConsoleLog(Document): script: DF.Code | None type: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.py b/frappe/desk/doctype/custom_html_block/custom_html_block.py index 493b7ee4e4..35f9c3cc63 100644 --- a/frappe/desk/doctype/custom_html_block/custom_html_block.py +++ b/frappe/desk/doctype/custom_html_block/custom_html_block.py @@ -22,6 +22,7 @@ class CustomHTMLBlock(Document): script: DF.Code | None style: DF.Code | None # end: auto-generated types + pass diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 225a8d6435..59efa6e873 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -30,14 +30,13 @@ class Dashboard(Document): is_standard: DF.Check module: DF.Link | None # end: auto-generated types + def on_update(self): if self.is_default: # make all other dashboards non-default DashBoard = DocType("Dashboard") - frappe.qb.update(DashBoard).set(DashBoard.is_default, 0).where( - DashBoard.name != self.name - ).run() + frappe.qb.update(DashBoard).set(DashBoard.is_default, 0).where(DashBoard.name != self.name).run() if frappe.conf.developer_mode and self.is_standard: export_to_files( @@ -111,9 +110,7 @@ def get_permitted_cards(dashboard_name): def get_non_standard_charts_in_dashboard(dashboard): non_standard_charts = [doc.name for doc in frappe.get_list("Dashboard Chart", {"is_standard": 0})] - return [ - chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts - ] + return [chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts] def get_non_standard_cards_in_dashboard(dashboard): @@ -127,9 +124,7 @@ def get_non_standard_warning_message(non_standard_docs_map): def get_html(docs, doctype): html = f"

{frappe.bold(doctype)}

" for doc in docs: - html += ''.format( - doctype=doctype, doc=doc - ) + html += f'' html += "
" return html diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 8e008e30c6..318f139a33 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -37,9 +37,7 @@ def get_permission_query_conditions(user): report_condition = False module_condition = False - allowed_doctypes = [ - frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read() - ] + allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] allowed_reports = [frappe.db.escape(report) for report in get_allowed_report_names()] allowed_modules = [ frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user() @@ -55,11 +53,9 @@ def get_permission_query_conditions(user): ) if allowed_modules: module_condition = """`tabDashboard Chart`.`module` in ({allowed_modules}) - or `tabDashboard Chart`.`module` is NULL""".format( - allowed_modules=",".join(allowed_modules) - ) + or `tabDashboard Chart`.`module` is NULL""".format(allowed_modules=",".join(allowed_modules)) - return """ + return f""" ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') and {doctype_condition}) or @@ -67,11 +63,7 @@ def get_permission_query_conditions(user): and {report_condition})) and ({module_condition}) - """.format( - doctype_condition=doctype_condition, - report_condition=report_condition, - module_condition=module_condition, - ) + """ def has_permission(doc, ptype, user): @@ -252,9 +244,7 @@ def get_heatmap_chart_config(chart, filters, heatmap_year): doctype, fields=[ timestamp_field, - "{aggregate_function}({value_field})".format( - aggregate_function=aggregate_function, value_field=value_field - ), + f"{aggregate_function}({value_field})", ], filters=filters, group_by=f"date({datefield})", @@ -271,7 +261,6 @@ def get_heatmap_chart_config(chart, filters, heatmap_year): def get_group_by_chart_config(chart, filters): - aggregate_function = get_aggregate_function(chart.group_by_type) value_field = chart.aggregate_function_based_on or "1" group_by_field = chart.group_by_based_on @@ -312,7 +301,7 @@ def get_result(data, timegrain, from_date, to_date, chart_type): result = [[date, 0] for date in dates] data_index = 0 if data: - for i, d in enumerate(result): + for d in result: count = 0 while data_index < len(data) and getdate(data[data_index][0]) <= d[0]: d[1] += data[data_index][1] @@ -378,6 +367,7 @@ class DashboardChart(Document): x_field: DF.Literal y_axis: DF.Table[DashboardChartField] # end: auto-generated types + def on_update(self): frappe.cache.delete_key(f"chart-data:{self.name}") if frappe.conf.developer_mode and self.is_standard: diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index ddbabedcb4..7afe2258d8 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -34,17 +34,15 @@ class TestDashboardChart(FrappeTestCase): frappe.delete_doc("Dashboard Chart", "Test Dashboard Chart") frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Dashboard Chart", - chart_type="Count", - document_type="DocType", - based_on="creation", - timespan="Last Year", - time_interval="Monthly", - filters_json="{}", - timeseries=1, - ) + doctype="Dashboard Chart", + chart_name="Test Dashboard Chart", + chart_type="Count", + document_type="DocType", + based_on="creation", + timespan="Last Year", + time_interval="Monthly", + filters_json="{}", + timeseries=1, ).insert() cur_date = datetime.now() - relativedelta(years=1) @@ -64,17 +62,15 @@ class TestDashboardChart(FrappeTestCase): frappe.db.delete("Error Log") frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Empty Dashboard Chart", - chart_type="Count", - document_type="Error Log", - based_on="creation", - timespan="Last Year", - time_interval="Monthly", - filters_json="[]", - timeseries=1, - ) + doctype="Dashboard Chart", + chart_name="Test Empty Dashboard Chart", + chart_type="Count", + document_type="Error Log", + based_on="creation", + timespan="Last Year", + time_interval="Monthly", + filters_json="[]", + timeseries=1, ).insert() cur_date = datetime.now() - relativedelta(years=1) @@ -94,20 +90,18 @@ class TestDashboardChart(FrappeTestCase): frappe.db.delete("Error Log") # create one data point - frappe.get_doc(dict(doctype="Error Log", creation="2018-06-01 00:00:00")).insert() + frappe.get_doc(doctype="Error Log", creation="2018-06-01 00:00:00").insert() frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Empty Dashboard Chart 2", - chart_type="Count", - document_type="Error Log", - based_on="creation", - timespan="Last Year", - time_interval="Monthly", - filters_json="[]", - timeseries=1, - ) + doctype="Dashboard Chart", + chart_name="Test Empty Dashboard Chart 2", + chart_type="Count", + document_type="Error Log", + based_on="creation", + timespan="Last Year", + time_interval="Monthly", + filters_json="[]", + timeseries=1, ).insert() cur_date = datetime.now() - relativedelta(years=1) @@ -130,14 +124,12 @@ class TestDashboardChart(FrappeTestCase): frappe.get_doc({"doctype": "ToDo", "description": "test"}).insert() frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Group By Dashboard Chart", - chart_type="Group By", - document_type="ToDo", - group_by_based_on="status", - filters_json="[]", - ) + doctype="Dashboard Chart", + chart_name="Test Group By Dashboard Chart", + chart_type="Group By", + document_type="ToDo", + group_by_based_on="status", + filters_json="[]", ).insert() result = get(chart_name="Test Group By Dashboard Chart", refresh=1) @@ -152,20 +144,18 @@ class TestDashboardChart(FrappeTestCase): frappe.delete_doc("Dashboard Chart", "Test Daily Dashboard Chart") frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Daily Dashboard Chart", - chart_type="Sum", - document_type="Communication", - based_on="communication_date", - value_based_on="rating", - timespan="Select Date Range", - time_interval="Daily", - from_date=datetime(2019, 1, 6), - to_date=datetime(2019, 1, 11), - filters_json="[]", - timeseries=1, - ) + doctype="Dashboard Chart", + chart_name="Test Daily Dashboard Chart", + chart_type="Sum", + document_type="Communication", + based_on="communication_date", + value_based_on="rating", + timespan="Select Date Range", + time_interval="Daily", + from_date=datetime(2019, 1, 6), + to_date=datetime(2019, 1, 11), + filters_json="[]", + timeseries=1, ).insert() result = get(chart_name="Test Daily Dashboard Chart", refresh=1) @@ -183,20 +173,18 @@ class TestDashboardChart(FrappeTestCase): frappe.delete_doc("Dashboard Chart", "Test Weekly Dashboard Chart") frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Weekly Dashboard Chart", - chart_type="Sum", - document_type="Communication", - based_on="communication_date", - value_based_on="rating", - timespan="Select Date Range", - time_interval="Weekly", - from_date=datetime(2018, 12, 30), - to_date=datetime(2019, 1, 15), - filters_json="[]", - timeseries=1, - ) + doctype="Dashboard Chart", + chart_name="Test Weekly Dashboard Chart", + chart_type="Sum", + document_type="Communication", + based_on="communication_date", + value_based_on="rating", + timespan="Select Date Range", + time_interval="Weekly", + from_date=datetime(2018, 12, 30), + to_date=datetime(2019, 1, 15), + filters_json="[]", + timeseries=1, ).insert() with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"): @@ -212,20 +200,18 @@ class TestDashboardChart(FrappeTestCase): frappe.delete_doc("Dashboard Chart", "Test Average Dashboard Chart") frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Average Dashboard Chart", - chart_type="Average", - document_type="Communication", - based_on="communication_date", - value_based_on="rating", - timespan="Select Date Range", - time_interval="Weekly", - from_date=datetime(2018, 12, 30), - to_date=datetime(2019, 1, 15), - filters_json="[]", - timeseries=1, - ) + doctype="Dashboard Chart", + chart_name="Test Average Dashboard Chart", + chart_type="Average", + document_type="Communication", + based_on="communication_date", + value_based_on="rating", + timespan="Select Date Range", + time_interval="Weekly", + from_date=datetime(2018, 12, 30), + to_date=datetime(2019, 1, 15), + filters_json="[]", + timeseries=1, ).insert() with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"): @@ -237,32 +223,26 @@ class TestDashboardChart(FrappeTestCase): frappe.delete_doc_if_exists("Dashboard Chart", "Test Dashboard Chart Date Label") frappe.get_doc( - dict( - doctype="Dashboard Chart", - chart_name="Test Dashboard Chart Date Label", - chart_type="Count", - document_type="DocType", - based_on="creation", - timespan="Select Date Range", - time_interval="Weekly", - from_date=datetime(2018, 12, 30), - to_date=datetime(2019, 1, 15), - filters_json="[]", - timeseries=1, - ) + doctype="Dashboard Chart", + chart_name="Test Dashboard Chart Date Label", + chart_type="Count", + document_type="DocType", + based_on="creation", + timespan="Select Date Range", + time_interval="Weekly", + from_date=datetime(2018, 12, 30), + to_date=datetime(2019, 1, 15), + filters_json="[]", + timeseries=1, ).insert() with patch.object(frappe.utils.data, "get_user_date_format", return_value="dd.mm.yyyy"): result = get(chart_name="Test Dashboard Chart Date Label") - self.assertEqual( - sorted(result.get("labels")), sorted(["05.01.2019", "12.01.2019", "19.01.2019"]) - ) + self.assertEqual(sorted(result.get("labels")), sorted(["05.01.2019", "12.01.2019", "19.01.2019"])) with patch.object(frappe.utils.data, "get_user_date_format", return_value="mm-dd-yyyy"): result = get(chart_name="Test Dashboard Chart Date Label") - self.assertEqual( - sorted(result.get("labels")), sorted(["01-19-2019", "01-05-2019", "01-12-2019"]) - ) + self.assertEqual(sorted(result.get("labels")), sorted(["01-19-2019", "01-05-2019", "01-12-2019"])) def insert_test_records(): diff --git a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py index 0c0dfb6f64..1578e9504d 100644 --- a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py +++ b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py @@ -20,4 +20,5 @@ class DashboardChartField(Document): parenttype: DF.Data y_field: DF.Literal # end: auto-generated types + pass diff --git a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py index d95e9bb436..edaf07a089 100644 --- a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py +++ b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.py @@ -20,4 +20,5 @@ class DashboardChartLink(Document): parenttype: DF.Data width: DF.Literal["Half", "Full"] # end: auto-generated types + pass diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py index 0ba5a895e9..faf38263cd 100644 --- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py +++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py @@ -33,7 +33,6 @@ class DashboardChartSource(Document): source_name: DF.Data timeseries: DF.Check # end: auto-generated types + def on_update(self): - export_to_files( - record_list=[[self.doctype, self.name]], record_module=self.module, create_init=True - ) + export_to_files(record_list=[[self.doctype, self.name]], record_module=self.module, create_init=True) diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py index b88745a757..5d63e9c397 100644 --- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py @@ -21,6 +21,7 @@ class DashboardSettings(Document): chart_config: DF.Code | None user: DF.Link | None # end: auto-generated types + pass @@ -55,6 +56,4 @@ def save_chart_config(reset, config, chart_name): chart_config[chart_name] = {} chart_config[chart_name].update(config) - frappe.db.set_value( - "Dashboard Settings", frappe.session.user, "chart_config", json.dumps(chart_config) - ) + frappe.db.set_value("Dashboard Settings", frappe.session.user, "chart_config", json.dumps(chart_config)) diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 1c3c2d1c86..bb1076dd46 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -37,8 +37,8 @@ class DesktopIcon(Document): reverse: DF.Check standard: DF.Check type: DF.Literal["module", "list", "link", "page", "query-report"] - # end: auto-generated types + def validate(self): if not self.label: self.label = self.module_name @@ -97,9 +97,7 @@ def get_desktop_icons(user=None): icon.blocked = 1 standard_map[icon.module_name] = icon - user_icons = frappe.get_all( - "Desktop Icon", fields=fields, filters={"standard": 0, "owner": user} - ) + user_icons = frappe.get_all("Desktop Icon", fields=fields, filters={"standard": 0, "owner": user}) # update hidden property for icon in user_icons: @@ -127,7 +125,6 @@ def get_desktop_icons(user=None): user_icon_names = [icon.module_name for icon in user_icons] for standard_icon in standard_icons: if standard_icon.module_name not in user_icon_names: - # if blocked, hidden too! if standard_icon.blocked: standard_icon.hidden = 1 @@ -175,9 +172,7 @@ def add_user_icon(_doctype, _report=None, label=None, link=None, type="link", st else: idx = ( - frappe.db.sql("select max(idx) from `tabDesktop Icon` where owner=%s", frappe.session.user)[0][ - 0 - ] + frappe.db.sql("select max(idx) from `tabDesktop Icon` where owner=%s", frappe.session.user)[0][0] or frappe.db.sql("select count(*) from `tabDesktop Icon` where standard=1")[0][0] ) @@ -334,8 +329,7 @@ def set_hidden(module_name, user=None, hidden=1): def get_all_icons(): return [ - d.module_name - for d in frappe.get_all("Desktop Icon", filters={"standard": 1}, fields=["module_name"]) + d.module_name for d in frappe.get_all("Desktop Icon", filters={"standard": 1}, fields=["module_name"]) ] diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 0442c73d23..0765ad9f50 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -78,6 +78,7 @@ class Event(Document): tuesday: DF.Check wednesday: DF.Check # end: auto-generated types + def validate(self): if not self.starts_on: self.starts_on = now_datetime() @@ -88,9 +89,7 @@ class Event(Document): if self.starts_on and self.ends_on: self.validate_from_to_dates("starts_on", "ends_on") - if ( - self.repeat_on == "Daily" and self.ends_on and getdate(self.starts_on) != getdate(self.ends_on) - ): + if self.repeat_on == "Daily" and self.ends_on and getdate(self.starts_on) != getdate(self.ends_on): frappe.throw(_("Daily Events should finish on the Same Day.")) if self.sync_with_google_calendar and not self.google_calendar: @@ -222,9 +221,7 @@ def delete_communication(event, reference_doctype, reference_docname): def get_permission_query_conditions(user): if not user: user = frappe.session.user - return """(`tabEvent`.`event_type`='Public' or `tabEvent`.`owner`={user})""".format( - user=frappe.db.escape(user), - ) + return f"""(`tabEvent`.`event_type`='Public' or `tabEvent`.`owner`={frappe.db.escape(user)})""" def has_permission(doc, user): @@ -359,9 +356,7 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[ ) new_event.starts_on = date + " " + e.starts_on.split(" ")[1] - new_event.ends_on = new_event.ends_on = ( - enddate + " " + e.ends_on.split(" ")[1] if e.ends_on else None - ) + new_event.ends_on = new_event.ends_on = enddate + " " + e.ends_on.split(" ")[1] if e.ends_on else None add_events.append(new_event) @@ -536,12 +531,9 @@ def delete_events(ref_type, ref_name, delete_event=False): # Close events if ends_on or repeat_till is less than now_datetime def set_status_of_events(): - events = frappe.get_list( - "Event", filters={"status": "Open"}, fields=["name", "ends_on", "repeat_till"] - ) + events = frappe.get_list("Event", filters={"status": "Open"}, fields=["name", "ends_on", "repeat_till"]) for event in events: if (event.ends_on and getdate(event.ends_on) < getdate(nowdate())) or ( event.repeat_till and getdate(event.repeat_till) < getdate(nowdate()) ): - frappe.db.set_value("Event", event.name, "status", "Closed") diff --git a/frappe/desk/doctype/event_participants/event_participants.py b/frappe/desk/doctype/event_participants/event_participants.py index 8bd00f1f17..48a3e37f3d 100644 --- a/frappe/desk/doctype/event_participants/event_participants.py +++ b/frappe/desk/doctype/event_participants/event_participants.py @@ -19,4 +19,5 @@ class EventParticipants(Document): reference_docname: DF.DynamicLink reference_doctype: DF.Link # end: auto-generated types + pass diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index c492871466..0227561bbd 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -40,6 +40,7 @@ class FormTour(Document): view_name: DF.Literal["Workspaces", "List", "Form", "Tree", "Page"] workspace_name: DF.Link | None # end: auto-generated types + def before_save(self): if self.is_standard and not self.module: if self.workspace_name: @@ -98,9 +99,7 @@ def update_user_status(value, step): app="frappe_ui_tours", properties={"is_completed": tour.is_completed}, ) - frappe.db.set_value( - "User", frappe.session.user, "onboarding_status", value, update_modified=False - ) + frappe.db.set_value("User", frappe.session.user, "onboarding_status", value, update_modified=False) frappe.cache.hdel("bootinfo", frappe.session.user) diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.py b/frappe/desk/doctype/form_tour_step/form_tour_step.py index 47f9f96025..f0cbbf633f 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.py +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.py @@ -54,4 +54,5 @@ class FormTourStep(Document): title: DF.Data ui_tour: DF.Check # end: auto-generated types + pass diff --git a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py index 10ca85f74e..dd9ecaf556 100644 --- a/frappe/desk/doctype/global_search_doctype/global_search_doctype.py +++ b/frappe/desk/doctype/global_search_doctype/global_search_doctype.py @@ -19,4 +19,5 @@ class GlobalSearchDocType(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py index 879cde2910..f84c5e5108 100644 --- a/frappe/desk/doctype/global_search_settings/global_search_settings.py +++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py @@ -18,6 +18,7 @@ class GlobalSearchSettings(Document): allowed_in_global_search: DF.Table[GlobalSearchDocType] # end: auto-generated types + def validate(self): dts, core_dts, repeated_dts = [], [], [] diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index 49604f6cd0..c8d26acb03 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -27,6 +27,7 @@ class KanbanBoard(Document): reference_doctype: DF.Link show_labels: DF.Check # end: auto-generated types + def validate(self): self.validate_column_name() @@ -51,9 +52,7 @@ def get_permission_query_conditions(user): if user == "Administrator": return "" - return """(`tabKanban Board`.private=0 or `tabKanban Board`.owner={user})""".format( - user=frappe.db.escape(user) - ) + return f"""(`tabKanban Board`.private=0 or `tabKanban Board`.owner={frappe.db.escape(user)})""" def has_permission(doc, ptype, user): @@ -130,9 +129,7 @@ def update_order(board_name, order): @frappe.whitelist() -def update_order_for_single_card( - board_name, docname, from_colname, to_colname, old_index, new_index -): +def update_order_for_single_card(board_name, docname, from_colname, to_colname, old_index, new_index): """Save the order of cards in columns""" board = frappe.get_doc("Kanban Board", board_name) doctype = board.reference_doctype diff --git a/frappe/desk/doctype/kanban_board_column/kanban_board_column.py b/frappe/desk/doctype/kanban_board_column/kanban_board_column.py index 73e781965f..70142e0bc6 100644 --- a/frappe/desk/doctype/kanban_board_column/kanban_board_column.py +++ b/frappe/desk/doctype/kanban_board_column/kanban_board_column.py @@ -15,17 +15,7 @@ class KanbanBoardColumn(Document): column_name: DF.Data | None indicator: DF.Literal[ - "Blue", - "Cyan", - "Gray", - "Green", - "Light Blue", - "Orange", - "Pink", - "Purple", - "Red", - "Red", - "Yellow", + "Blue", "Cyan", "Gray", "Green", "Light Blue", "Orange", "Pink", "Purple", "Red", "Red", "Yellow" ] order: DF.Code | None parent: DF.Data @@ -33,4 +23,5 @@ class KanbanBoardColumn(Document): parenttype: DF.Data status: DF.Literal["Active", "Archived"] # end: auto-generated types + pass diff --git a/frappe/desk/doctype/list_filter/list_filter.py b/frappe/desk/doctype/list_filter/list_filter.py index 5d2aad5ba0..2b91b39590 100644 --- a/frappe/desk/doctype/list_filter/list_filter.py +++ b/frappe/desk/doctype/list_filter/list_filter.py @@ -18,4 +18,5 @@ class ListFilter(Document): for_user: DF.Link | None reference_doctype: DF.Link | None # end: auto-generated types + pass 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 70ee6db623..29a12707bc 100644 --- a/frappe/desk/doctype/list_view_settings/list_view_settings.py +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py @@ -21,12 +21,12 @@ class ListViewSettings(Document): fields: DF.Code | None total_fields: DF.Literal["", "4", "5", "6", "7", "8", "9", "10"] # end: auto-generated types + pass @frappe.whitelist() def save_listview_settings(doctype, listview_settings, removed_listview_fields): - listview_settings = frappe.parse_json(listview_settings) removed_listview_fields = frappe.parse_json(removed_listview_fields) @@ -48,9 +48,7 @@ def save_listview_settings(doctype, listview_settings, removed_listview_fields): def set_listview_fields(doctype, listview_fields, removed_listview_fields): meta = frappe.get_meta(doctype) - listview_fields = [ - f.get("fieldname") for f in frappe.parse_json(listview_fields) if f.get("fieldname") - ] + listview_fields = [f.get("fieldname") for f in frappe.parse_json(listview_fields) if f.get("fieldname")] for field in removed_listview_fields: set_in_list_view_property(doctype, meta.get_field(field), "0") diff --git a/frappe/desk/doctype/module_onboarding/module_onboarding.py b/frappe/desk/doctype/module_onboarding/module_onboarding.py index 1c7a2adeaf..14049229ef 100644 --- a/frappe/desk/doctype/module_onboarding/module_onboarding.py +++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py @@ -27,6 +27,7 @@ class ModuleOnboarding(Document): success_message: DF.Data title: DF.Data # end: auto-generated types + def on_update(self): if frappe.conf.developer_mode: export_to_files(record_list=[["Module Onboarding", self.name]], record_module=self.module) diff --git a/frappe/desk/doctype/note/note.py b/frappe/desk/doctype/note/note.py index 9cc14cbc1e..e303f9b80f 100644 --- a/frappe/desk/doctype/note/note.py +++ b/frappe/desk/doctype/note/note.py @@ -23,11 +23,15 @@ class Note(Document): seen_by: DF.Table[NoteSeenBy] title: DF.Data # end: auto-generated types + def validate(self): if self.notify_on_login and not self.expire_notification_on: # expire this notification in a week (default) self.expire_notification_on = frappe.utils.add_days(self.creation, 7) + if not self.public and self.notify_on_login: + self.notify_on_login = 0 + if not self.content: self.content = "" diff --git a/frappe/desk/doctype/note/test_note.py b/frappe/desk/doctype/note/test_note.py index 426fb5a16e..94ab270e71 100644 --- a/frappe/desk/doctype/note/test_note.py +++ b/frappe/desk/doctype/note/test_note.py @@ -13,9 +13,7 @@ class TestNote(FrappeTestCase): frappe.db.delete("Note") frappe.db.delete("Note Seen By") - return frappe.get_doc( - dict(doctype="Note", title="test note", content="test note content") - ).insert() + return frappe.get_doc(doctype="Note", title="test note", content="test note content").insert() def test_version(self): note = self.insert_note() diff --git a/frappe/desk/doctype/note_seen_by/note_seen_by.py b/frappe/desk/doctype/note_seen_by/note_seen_by.py index 151ac2e1ff..3ec518bd1b 100644 --- a/frappe/desk/doctype/note_seen_by/note_seen_by.py +++ b/frappe/desk/doctype/note_seen_by/note_seen_by.py @@ -18,4 +18,5 @@ class NoteSeenBy(Document): parenttype: DF.Data user: DF.Link | None # end: auto-generated types + pass diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index c585a81826..70e726fd97 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -29,8 +29,8 @@ class NotificationLog(Document): read: DF.Check subject: DF.Text | None type: DF.Literal["Mention", "Energy Point", "Assignment", "Share", "Alert"] - # end: auto-generated types + def after_insert(self): frappe.publish_realtime("notification", after_commit=True, user=self.for_user) set_notifications_as_unseen(self.for_user) diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py index a43455149f..4f42038bbf 100644 --- a/frappe/desk/doctype/notification_log/test_notification_log.py +++ b/frappe/desk/doctype/notification_log/test_notification_log.py @@ -30,9 +30,7 @@ class TestNotificationLog(FrappeTestCase): self.assertEqual(log_type, "Share") email = get_last_email_queue() - content = "Subject: {} shared a document ToDo".format( - frappe.utils.get_fullname(frappe.session.user) - ) + content = f"Subject: {frappe.utils.get_fullname(frappe.session.user)} shared a document ToDo" self.assertTrue(content in email.message) diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index dcdf430c4e..41a6991d6f 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -30,6 +30,7 @@ class NotificationSettings(Document): subscribed_documents: DF.TableMultiSelect[NotificationSubscribedDocument] user: DF.Link | None # end: auto-generated types + def on_update(self): from frappe.desk.notifications import clear_notification_config diff --git a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py index 5bc6b95155..56bb1c62cb 100644 --- a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py +++ b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py @@ -19,4 +19,5 @@ class NotificationSubscribedDocument(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index eec086f3bb..aea6743ce1 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -42,6 +42,7 @@ class NumberCard(Document): stats_time_interval: DF.Literal["Daily", "Weekly", "Monthly", "Yearly"] type: DF.Literal["Document Type", "Report", "Custom"] # end: auto-generated types + def autoname(self): if not self.name: self.name = self.label @@ -87,9 +88,7 @@ def get_permission_query_conditions(user=None): doctype_condition = False module_condition = False - allowed_doctypes = [ - frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read() - ] + allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] allowed_modules = [ frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user() ] @@ -100,17 +99,13 @@ def get_permission_query_conditions(user=None): ) if allowed_modules: module_condition = """`tabNumber Card`.`module` in ({allowed_modules}) - or `tabNumber Card`.`module` is NULL""".format( - allowed_modules=",".join(allowed_modules) - ) + or `tabNumber Card`.`module` is NULL""".format(allowed_modules=",".join(allowed_modules)) - return """ + return f""" {doctype_condition} and {module_condition} - """.format( - doctype_condition=doctype_condition, module_condition=module_condition - ) + """ def has_permission(doc, ptype, user): @@ -146,11 +141,7 @@ def get_result(doc, filters, to_date=None): if function == "count": fields = [f"{function}(*) as result"] else: - fields = [ - "{function}({based_on}) as result".format( - function=function, based_on=doc.aggregate_function_based_on - ) - ] + fields = [f"{function}({doc.aggregate_function_based_on}) as result"] if not filters: filters = [] diff --git a/frappe/desk/doctype/number_card_link/number_card_link.py b/frappe/desk/doctype/number_card_link/number_card_link.py index 8155a82085..4f8591ccaf 100644 --- a/frappe/desk/doctype/number_card_link/number_card_link.py +++ b/frappe/desk/doctype/number_card_link/number_card_link.py @@ -19,4 +19,5 @@ class NumberCardLink(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/desk/doctype/onboarding_permission/onboarding_permission.py b/frappe/desk/doctype/onboarding_permission/onboarding_permission.py index 47c6f00647..e42c7b436c 100644 --- a/frappe/desk/doctype/onboarding_permission/onboarding_permission.py +++ b/frappe/desk/doctype/onboarding_permission/onboarding_permission.py @@ -19,4 +19,5 @@ class OnboardingPermission(Document): parenttype: DF.Data role: DF.Link # end: auto-generated types + pass diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index e21e23ab95..f3f9ce4472 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -43,6 +43,7 @@ class OnboardingStep(Document): value_to_validate: DF.Data | None video_url: DF.Data | None # end: auto-generated types + def before_export(self, doc): doc.is_complete = 0 doc.is_skipped = 0 diff --git a/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py b/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py index 0ee27bd634..d8f707fcff 100644 --- a/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py +++ b/frappe/desk/doctype/onboarding_step_map/onboarding_step_map.py @@ -19,4 +19,5 @@ class OnboardingStepMap(Document): parenttype: DF.Data step: DF.Link # end: auto-generated types + pass diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py index f34f750f9c..b91ad749bc 100644 --- a/frappe/desk/doctype/system_console/system_console.py +++ b/frappe/desk/doctype/system_console/system_console.py @@ -23,6 +23,7 @@ class SystemConsole(Document): show_processlist: DF.Check type: DF.Literal["Python", "SQL"] # end: auto-generated types + def run(self): frappe.only_for("System Manager") try: @@ -41,7 +42,7 @@ class SystemConsole(Document): else: frappe.db.rollback() frappe.get_doc( - dict(doctype="Console Log", script=self.console, type=self.type, committed=self.commit) + doctype="Console Log", script=self.console, type=self.type, committed=self.commit ).insert() frappe.db.commit() diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index 0ebfb3b9c4..b9cb1601ce 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -18,6 +18,7 @@ class Tag(Document): description: DF.SmallText | None # end: auto-generated types + pass @@ -173,9 +174,7 @@ def update_tags(doc, tags): deleted_tags = list(set(existing_tags) - set(new_tags)) for tag in deleted_tags: - frappe.db.delete( - "Tag Link", {"document_type": doc.doctype, "document_name": doc.name, "tag": tag} - ) + frappe.db.delete("Tag Link", {"document_type": doc.doctype, "document_name": doc.name, "tag": tag}) @frappe.whitelist() diff --git a/frappe/desk/doctype/tag_link/tag_link.py b/frappe/desk/doctype/tag_link/tag_link.py index 18639b4051..52000b6b63 100644 --- a/frappe/desk/doctype/tag_link/tag_link.py +++ b/frappe/desk/doctype/tag_link/tag_link.py @@ -19,4 +19,5 @@ class TagLink(Document): tag: DF.Link | None title: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/desk/doctype/todo/test_todo.py b/frappe/desk/doctype/todo/test_todo.py index 4880fad9d5..231fc482f0 100644 --- a/frappe/desk/doctype/todo/test_todo.py +++ b/frappe/desk/doctype/todo/test_todo.py @@ -11,9 +11,7 @@ test_dependencies = ["User"] class TestToDo(FrappeTestCase): def test_delete(self): - todo = frappe.get_doc( - dict(doctype="ToDo", description="test todo", assigned_by="Administrator") - ).insert() + todo = frappe.get_doc(doctype="ToDo", description="test todo", assigned_by="Administrator").insert() frappe.db.delete("Deleted Document") todo.delete() @@ -24,9 +22,7 @@ class TestToDo(FrappeTestCase): self.assertEqual(todo.as_json(), deleted.data) def test_fetch(self): - todo = frappe.get_doc( - dict(doctype="ToDo", description="test todo", assigned_by="Administrator") - ).insert() + todo = frappe.get_doc(doctype="ToDo", description="test todo", assigned_by="Administrator").insert() self.assertEqual( todo.assigned_by_full_name, frappe.db.get_value("User", todo.assigned_by, "full_name") ) @@ -40,9 +36,7 @@ class TestToDo(FrappeTestCase): frappe.clear_cache(doctype="ToDo") - todo = frappe.get_doc( - dict(doctype="ToDo", description="test todo", assigned_by="Administrator") - ).insert() + todo = frappe.get_doc(doctype="ToDo", description="test todo", assigned_by="Administrator").insert() self.assertFalse(todo.assigned_by_full_name) todo_meta = frappe.get_doc("DocType", "ToDo") @@ -126,12 +120,10 @@ class TestToDo(FrappeTestCase): frappe.clear_cache(doctype="ToDo") todo = frappe.get_doc( - dict( - doctype="ToDo", - description="test todo", - assigned_by="Administrator", - assigned_by_full_name="Admin", - ) + doctype="ToDo", + description="test todo", + assigned_by="Administrator", + assigned_by_full_name="Admin", ).insert() self.assertEqual(todo.assigned_by_full_name, "Admin") diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index d6427f9388..6fedc70ed4 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -34,12 +34,12 @@ class ToDo(Document): sender: DF.Data | None status: DF.Literal["Open", "Closed", "Cancelled"] # end: auto-generated types + DocType = "ToDo" def validate(self): self._assignment = None if self.is_new(): - if self.assigned_by == self.allocated_to: assignment_message = frappe._("{0} self assigned this task: {1}").format( get_fullname(self.assigned_by), self.description @@ -83,9 +83,7 @@ class ToDo(Document): def delete_communication_links(self): # unlink todo from linked comments - return frappe.db.delete( - "Communication Link", {"link_doctype": self.doctype, "link_name": self.name} - ) + return frappe.db.delete("Communication Link", {"link_doctype": self.doctype, "link_name": self.name}) def update_in_reference(self): if not (self.reference_type and self.reference_name): diff --git a/frappe/desk/doctype/workspace/test_workspace.py b/frappe/desk/doctype/workspace/test_workspace.py index 22665e1dd2..4570ab576b 100644 --- a/frappe/desk/doctype/workspace/test_workspace.py +++ b/frappe/desk/doctype/workspace/test_workspace.py @@ -30,9 +30,7 @@ class TestWorkspace(FrappeTestCase): def create_module(module_name): - module = frappe.get_doc( - {"doctype": "Module Def", "module_name": module_name, "app_name": "frappe"} - ) + module = frappe.get_doc({"doctype": "Module Def", "module_name": module_name, "app_name": "frappe"}) module.insert(ignore_if_duplicate=True) return module diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 814be47124..bcec8e2b52 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -65,6 +65,7 @@ class Workspace(Document): shortcuts: DF.Table[WorkspaceShortcut] title: DF.Data # end: auto-generated types + def validate(self): self.title = strip_html(self.title) @@ -164,7 +165,6 @@ class Workspace(Document): return cards def build_links_table_from_card(self, config): - for idx, card in enumerate(config): links = loads(card.get("links")) @@ -247,9 +247,7 @@ def new_page(new_page): if page.get("public") and not is_workspace_manager(): return elif ( - not page.get("public") - and page.get("for_user") != frappe.session.user - and not is_workspace_manager() + not page.get("public") and page.get("for_user") != frappe.session.user and not is_workspace_manager() ): frappe.throw(_("Cannot create private workspace of other users"), frappe.PermissionError) @@ -294,11 +292,7 @@ def update_page(name, title, icon, indicator_color, parent, public): public = frappe.parse_json(public) doc = frappe.get_doc("Workspace", name) - if ( - not doc.get("public") - and doc.get("for_user") != frappe.session.user - and not is_workspace_manager() - ): + if not doc.get("public") and doc.get("for_user") != frappe.session.user and not is_workspace_manager(): frappe.throw( _("Need Workspace Manager role to edit private workspace of other users"), frappe.PermissionError, @@ -320,9 +314,7 @@ def update_page(name, title, icon, indicator_color, parent, public): rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True) # update new name and public in child pages - child_docs = frappe.get_all( - "Workspace", filters={"parent_page": doc.title, "public": doc.public} - ) + child_docs = frappe.get_all("Workspace", filters={"parent_page": doc.title, "public": doc.public}) if child_docs: for child in child_docs: child_doc = frappe.get_doc("Workspace", child.name) @@ -349,11 +341,7 @@ def hide_unhide_page(page_name: str, is_hidden: bool): _("Need Workspace Manager role to hide/unhide public workspaces"), frappe.PermissionError ) - if ( - not page.get("public") - and page.get("for_user") != frappe.session.user - and not is_workspace_manager() - ): + if not page.get("public") and page.get("for_user") != frappe.session.user and not is_workspace_manager(): frappe.throw(_("Cannot update private workspace of other users"), frappe.PermissionError) page.is_hidden = int(is_hidden) @@ -465,9 +453,7 @@ def sort_page(workspace_pages, pages): def last_sequence_id(doc): - doc_exists = frappe.db.exists( - {"doctype": "Workspace", "public": doc.public, "for_user": doc.for_user} - ) + doc_exists = frappe.db.exists({"doctype": "Workspace", "public": doc.public, "for_user": doc.for_user}) if not doc_exists: return 0 diff --git a/frappe/desk/doctype/workspace_chart/workspace_chart.py b/frappe/desk/doctype/workspace_chart/workspace_chart.py index 7f9d8eff74..8afcf9ba05 100644 --- a/frappe/desk/doctype/workspace_chart/workspace_chart.py +++ b/frappe/desk/doctype/workspace_chart/workspace_chart.py @@ -20,4 +20,5 @@ class WorkspaceChart(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.py b/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.py index 933689cc2f..b4fb0254a7 100644 --- a/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.py +++ b/frappe/desk/doctype/workspace_custom_block/workspace_custom_block.py @@ -20,4 +20,5 @@ class WorkspaceCustomBlock(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/desk/doctype/workspace_number_card/workspace_number_card.py b/frappe/desk/doctype/workspace_number_card/workspace_number_card.py index 0959d8a958..1a220424b8 100644 --- a/frappe/desk/doctype/workspace_number_card/workspace_number_card.py +++ b/frappe/desk/doctype/workspace_number_card/workspace_number_card.py @@ -20,4 +20,5 @@ class WorkspaceNumberCard(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py index 35bfb171e1..ca2e55f817 100644 --- a/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py +++ b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py @@ -21,4 +21,5 @@ class WorkspaceQuickList(Document): parenttype: DF.Data quick_list_filter: DF.Code | None # end: auto-generated types + pass diff --git a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py index 9e908974fa..22dceb9386 100644 --- a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py +++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py @@ -16,7 +16,7 @@ class WorkspaceShortcut(Document): color: DF.Color | None doc_view: DF.Literal[ - "", "List", "Report Builder", "Dashboard", "Tree", "New", "Calendar", "Kanban" + "", "List", "Report Builder", "Dashboard", "Tree", "New", "Calendar", "Kanban", "Image" ] format: DF.Data | None icon: DF.Data | None @@ -31,4 +31,5 @@ class WorkspaceShortcut(Document): type: DF.Literal["DocType", "Report", "Page", "Dashboard", "URL"] url: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index c7dc3cab5b..bb3c760190 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -98,7 +98,9 @@ def add(args=None, *, ignore_permissions=False): # if assignee does not have permissions, share or inform if not frappe.has_permission(doc=doc, user=assign_to): if frappe.get_system_settings("disable_document_sharing"): - msg = _("User {0} is not permitted to access this document.").format(frappe.bold(assign_to)) + msg = _("User {0} is not permitted to access this document.").format( + frappe.bold(assign_to) + ) msg += "
" + _( "As document sharing is disabled, please give them the required permissions before assigning." ) @@ -170,9 +172,7 @@ def close_all_assignments(doctype, name, ignore_permissions=False): @frappe.whitelist() def remove(doctype, name, assign_to, ignore_permissions=False): - return set_status( - doctype, name, "", assign_to, status="Cancelled", ignore_permissions=ignore_permissions - ) + return set_status(doctype, name, "", assign_to, status="Cancelled", ignore_permissions=ignore_permissions) @frappe.whitelist() @@ -194,14 +194,10 @@ def close(doctype: str, name: str, assign_to: str, ignore_permissions=False): if assign_to != frappe.session.user: frappe.throw(_("Only the assignee can complete this to-do.")) - return set_status( - doctype, name, "", assign_to, status="Closed", ignore_permissions=ignore_permissions - ) + return set_status(doctype, name, "", assign_to, status="Closed", ignore_permissions=ignore_permissions) -def set_status( - doctype, name, todo=None, assign_to=None, status="Cancelled", ignore_permissions=False -): +def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled", ignore_permissions=False): """remove from todo""" if not ignore_permissions: @@ -258,9 +254,7 @@ def clear(doctype, name, ignore_permissions=False): return True -def notify_assignment( - assigned_by, allocated_to, doc_type, doc_name, action="CLOSE", description=None -): +def notify_assignment(assigned_by, allocated_to, doc_type, doc_name, action="CLOSE", description=None): """ Notify assignee that there is a change in assignment """ @@ -279,9 +273,9 @@ def notify_assignment( description_html = f"
{description}
" if description else None if action == "CLOSE": - subject = _( - "Your assignment on {0} {1} has been removed by {2}", lang=assigned_user.language - ).format(frappe.bold(_(doc_type)), get_title_html(title), frappe.bold(user_name)) + subject = _("Your assignment on {0} {1} has been removed by {2}", lang=assigned_user.language).format( + frappe.bold(_(doc_type)), get_title_html(title), frappe.bold(user_name) + ) else: user_name = frappe.bold(user_name) document_type = frappe.bold(_(doc_type, lang=assigned_user.language)) diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py index d698c647da..bae67792bc 100644 --- a/frappe/desk/form/document_follow.py +++ b/frappe/desk/form/document_follow.py @@ -141,7 +141,9 @@ def get_message_for_user(frequency, user): { "reference_docname": document_follow.ref_docname, "reference_doctype": document_follow.ref_doctype, - "reference_url": get_url_to_form(document_follow.ref_doctype, document_follow.ref_docname), + "reference_url": get_url_to_form( + document_follow.ref_doctype, document_follow.ref_docname + ), } ) return message, valid_document_follows diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 45bf42fd8f..23300b0a1c 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -84,7 +84,9 @@ class SubmittableDocumentTree: child_docs = self.get_next_level_children(parent_dt, parent_docs) self.visited_documents[parent_dt].extend(parent_docs) for linked_dt, linked_names in child_docs.items(): - not_visited_child_docs = set(linked_names) - set(self.visited_documents.get(linked_dt, [])) + not_visited_child_docs = set(linked_names) - set( + self.visited_documents.get(linked_dt, []) + ) next_level_children[linked_dt].extend(not_visited_child_docs) self.to_be_visited_documents = next_level_children @@ -203,9 +205,7 @@ def get_references_across_doctypes( each["child_table"] for each in itertools.chain(*child_tables_by_doctype.values()) ] - references_by_link_fields = get_references_across_doctypes_by_link_field( - to_doctypes, limit_link_doctypes - ) + references_by_link_fields = get_references_across_doctypes_by_link_field(to_doctypes, limit_link_doctypes) references_by_dlink_fields = get_references_across_doctypes_by_dynamic_link_field( to_doctypes, limit_link_doctypes ) @@ -214,7 +214,7 @@ def get_references_across_doctypes( for k, v in references_by_dlink_fields.items(): references.setdefault(k, []).extend(v) - for doctype, links in references.items(): + for links in references.values(): for link in links: link["is_child"] = link["doctype"] in all_child_tables return references @@ -342,18 +342,14 @@ def get_referencing_documents( return {from_table: frappe.get_all(from_table, filters, pluck="name", order_by=None)} filters.extend(child_filters or []) - res = frappe.get_all( - from_table, filters=filters, fields=["name", "parenttype", "parent"], order_by=None - ) + res = frappe.get_all(from_table, filters=filters, fields=["name", "parenttype", "parent"], order_by=None) documents = defaultdict(list) for parent, rows in itertools.groupby(res, key=lambda row: row["parenttype"]): if allowed_parents and parent not in allowed_parents: continue filters = (parent_filters or []) + [["name", "in", tuple(row.parent for row in rows)]] - documents[parent].extend( - frappe.get_all(parent, filters=filters, pluck="name", order_by=None) or [] - ) + documents[parent].extend(frappe.get_all(parent, filters=filters, pluck="name", order_by=None) or []) return documents @@ -455,7 +451,9 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di try: if link.get("filters"): - ret = frappe.get_all(doctype=dt, fields=fields, filters=link.get("filters"), order_by=None) + ret = frappe.get_all( + doctype=dt, fields=fields, filters=link.get("filters"), order_by=None + ) elif link.get("get_parent"): ret = None @@ -464,7 +462,9 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di if not frappe.get_meta(doctype).istable: continue - me = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True, order_by=None) + me = frappe.db.get_value( + doctype, name, ["parenttype", "parent"], as_dict=True, order_by=None + ) if me and me.parenttype == dt: ret = frappe.get_all( doctype=dt, fields=fields, filters=[[dt, "name", "=", me.parent]], order_by=None @@ -478,7 +478,9 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di # dynamic link if link.get("doctype_fieldname"): - filters.append([link.get("child_doctype"), link.get("doctype_fieldname"), "=", doctype]) + filters.append( + [link.get("child_doctype"), link.get("doctype_fieldname"), "=", doctype] + ) ret = frappe.get_all( doctype=dt, @@ -574,16 +576,13 @@ def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False) def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): - filters = [["fieldtype", "=", "Link"], ["options", "=", doctype]] if without_ignore_user_permissions_enabled: filters.append(["ignore_user_permissions", "!=", 1]) # find links of parents links = frappe.get_all("DocField", fields=["parent", "fieldname"], filters=filters, as_list=1) - links += frappe.get_all( - "Custom Field", fields=["dt as parent", "fieldname"], filters=filters, as_list=1 - ) + links += frappe.get_all("Custom Field", fields=["dt as parent", "fieldname"], filters=filters, as_list=1) ret = {} @@ -614,6 +613,10 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): if options in ret: del ret[options] + virtual_doctypes = frappe.get_all("DocType", {"is_virtual": 1}, pluck="name") + for dt in virtual_doctypes: + ret.pop(dt, None) + return ret @@ -640,7 +643,11 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F if is_single(df.doctype): continue - is_child = frappe.get_meta(df.doctype).istable + meta = frappe.get_meta(df.doctype) + if meta.is_virtual: + continue + + is_child = meta.istable possible_link = frappe.get_all( df.doctype, filters={df.doctype_fieldname: doctype}, diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index cc515c0ff1..d3c448fa03 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -212,9 +212,7 @@ def get_communications(doctype, name, start=0, limit=20): return _get_communications(doctype, name, cint(start), cint(limit)) -def get_comments( - doctype: str, name: str, comment_type: str | list[str] = "Comment" -) -> list[frappe._dict]: +def get_comments(doctype: str, name: str, comment_type: str | list[str] = "Comment") -> list[frappe._dict]: if isinstance(comment_type, list): comment_types = comment_type @@ -288,11 +286,9 @@ def get_communication_data( conditions = "" if after: # find after a particular date - conditions += """ - AND C.creation > {} - """.format( - after - ) + conditions += f""" + AND C.creation > {after} + """ if doctype == "User": conditions += """ @@ -300,27 +296,23 @@ def get_communication_data( """ # communications linked to reference_doctype - part1 = """ + part1 = f""" SELECT {fields} FROM `tabCommunication` as C WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message') AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s) {conditions} - """.format( - fields=fields, conditions=conditions - ) + """ # communications linked in Timeline Links - part2 = """ + part2 = f""" SELECT {fields} FROM `tabCommunication` as C INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message') AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s {conditions} - """.format( - fields=fields, conditions=conditions - ) + """ return frappe.db.sql( """ @@ -330,9 +322,7 @@ def get_communication_data( ORDER BY creation DESC LIMIT %(limit)s OFFSET %(start)s - """.format( - part1=part1, part2=part2, group_by=(group_by or "") - ), + """.format(part1=part1, part2=part2, group_by=(group_by or "")), dict( doctype=doctype, name=name, diff --git a/frappe/desk/form/test_form.py b/frappe/desk/form/test_form.py index f256b03d27..5028fedf96 100644 --- a/frappe/desk/form/test_form.py +++ b/frappe/desk/form/test_form.py @@ -11,10 +11,3 @@ class TestForm(FrappeTestCase): results = get_linked_docs("Role", "System Manager", linkinfo=get_linked_doctypes("Role")) self.assertTrue("User" in results) self.assertTrue("DocType" in results) - - -if __name__ == "__main__": - import unittest - - frappe.connect() - unittest.main() diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 28377572c3..b93ebdb5bc 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -70,7 +70,6 @@ def update_comment(name, content): @frappe.whitelist() def get_next(doctype, value, prev, filters=None, sort_order="desc", sort_field="modified"): - prev = int(prev) if not filters: filters = [] @@ -106,6 +105,4 @@ def get_next(doctype, value, prev, filters=None, sort_order="desc", sort_field=" def get_pdf_link(doctype, docname, print_format="Standard", no_letterhead=0): - return "/api/method/frappe.utils.print_format.download_pdf?doctype={doctype}&name={docname}&format={print_format}&no_letterhead={no_letterhead}".format( - doctype=doctype, docname=docname, print_format=print_format, no_letterhead=no_letterhead - ) + return f"/api/method/frappe.utils.print_format.download_pdf?doctype={doctype}&name={docname}&format={print_format}&no_letterhead={no_letterhead}" diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json deleted file mode 100644 index afd0583cfb..0000000000 --- a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "creation": "2023-05-18 12:08:23.196462", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "", - "modified": "2023-05-24 12:43:43.741781", - "modified_by": "Administrator", - "module": "Desk", - "name": "Main Workspace Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"Workspaces\",\"*\"]", - "reference_doctype": "", - "report_name": "", - "save_on_complete": 0, - "steps": [ - { - "description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.", - "element_selector": "#navbar-search", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "parent_element_selector": ".input-group.search-bar", - "popover_element": 0, - "position": "Left", - "title": "Awesomebar", - "ui_tour": 1 - }, - { - "description": "These are workspaces. Each module workspace provides insightful information and shortcuts on one page. \n\n

\n\nTip: You can build custom workspaces for your needs.", - "element_selector": ".col-lg-2.layout-side-section", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Workspace List", - "ui_tour": 1 - }, - { - "description": "
Click to visit the Workspace
", - "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Users\"]", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 1, - "is_table_field": 0, - "modal_trigger": 0, - "next_form_tour": "New Tools Tour", - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Users Workspace", - "ui_tour": 1 - } - ], - "title": "Main Workspace Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "Workspaces", - "workspace_name": "" -} \ No newline at end of file diff --git a/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json b/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json deleted file mode 100644 index 97159ba6e3..0000000000 --- a/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "creation": "2023-05-24 12:50:23.740052", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "", - "modified": "2023-05-24 13:01:56.539128", - "modified_by": "Administrator", - "module": "Desk", - "name": "Users Workspace Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"Workspaces\",\"Users\"]", - "reference_doctype": "", - "report_name": "", - "save_on_complete": 0, - "steps": [ - { - "description": "This is Users Workspace. You'll find all shortcuts for user, roles and permission management here.", - "element_selector": ".codex-editor", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Left", - "title": "Workspace", - "ui_tour": 1 - }, - { - "description": "This is a shortcut to User DocType. \n
\n\nLet's Click on the User shortcut to explore all users in System.", - "element_selector": "[shortcut_name=\"User\"]", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 1, - "is_table_field": 0, - "modal_trigger": 0, - "next_form_tour": "User List Tour", - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Users Shortcut", - "ui_tour": 1 - } - ], - "title": "Users Workspace Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "Workspaces", - "workspace_name": "Users" -} \ No newline at end of file diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py index 91aa2084cc..4800a7d945 100644 --- a/frappe/desk/leaderboard.py +++ b/frappe/desk/leaderboard.py @@ -45,8 +45,6 @@ def get_energy_point_leaderboard(date_range, company=None, field=None, limit=Non for user in energy_point_users: user_id = user["name"] user["name"] = get_fullname(user["name"]) - user["formatted_name"] = '{}'.format( - user_id, get_fullname(user_id) - ) + user["formatted_name"] = f'{get_fullname(user_id)}' return energy_point_users diff --git a/frappe/desk/link_preview.py b/frappe/desk/link_preview.py index 7778d9e373..9c26493e0e 100644 --- a/frappe/desk/link_preview.py +++ b/frappe/desk/link_preview.py @@ -12,9 +12,7 @@ def get_preview_data(doctype, docname): preview_fields = [ field.fieldname for field in meta.fields - if field.in_preview - and field.fieldtype not in no_value_fields - and field.fieldtype not in table_fields + if field.in_preview and field.fieldtype not in no_value_fields and field.fieldtype not in table_fields ] # no preview fields defined, build list from mandatory fields diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 4c728bdee9..3ec0338e83 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -25,7 +25,7 @@ def get_notifications(): "open_count_doctype": {}, "targets": {}, } - if frappe.flags.in_install or not frappe.db.get_single_value("System Settings", "setup_complete"): + if frappe.flags.in_install or not frappe.get_system_settings("setup_complete"): return out config = get_notification_config() @@ -130,7 +130,9 @@ def get_notifications_for_targets(config, notification_percent): for doc in doc_list: value = doc[value_field] target = doc[target_field] - doc_target_percents[doctype][doc.name] = (value / target * 100) if value < target else 100 + doc_target_percents[doctype][doc.name] = ( + (value / target * 100) if value < target else 100 + ) return doc_target_percents @@ -281,7 +283,7 @@ def get_open_count(doctype, name, items=None): try: external_links_data_for_d = get_external_links(d, name, links) out["external_links_found"].append(external_links_data_for_d) - except Exception as e: + except Exception: out["external_links_found"].append({"doctype": d, "open_count": 0, "count": 0}) else: external_links_data_for_d = get_external_links(d, name, links) diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py index ffc7d26317..f3dd5f2975 100644 --- a/frappe/desk/page/backups/backups.py +++ b/frappe/desk/page/backups/backups.py @@ -95,9 +95,7 @@ def schedule_files_backup(user_email): ) frappe.msgprint(_("Queued for backup. You will receive an email with the download link")) else: - frappe.msgprint( - _("Backup job is already queued. You will receive an email with the download link") - ) + frappe.msgprint(_("Backup job is already queued. You will receive an email with the download link")) def backup_files_and_notify_user(user_email=None): diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 0ae465ba55..9394b8a6b4 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -15,7 +15,6 @@ from . import install_fixtures def get_setup_stages(args): # nosemgrep - # App setup stage functions should not include frappe.db.commit # That is done by frappe after successful completion of all stages stages = [ @@ -35,9 +34,7 @@ def get_setup_stages(args): # nosemgrep # post executing hooks "status": "Wrapping up", "fail_msg": "Failed to complete setup", - "tasks": [ - {"fn": run_post_setup_complete, "args": args, "fail_msg": "Failed to complete setup"} - ], + "tasks": [{"fn": run_post_setup_complete, "args": args, "fail_msg": "Failed to complete setup"}], } ) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 952d30a274..3003573d81 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -97,9 +97,7 @@ def generate_report_result( columns.insert(custom_column["insert_after_index"] + 1, custom_column) # all columns which are not in original report - report_custom_columns = [ - column for column in columns if column["fieldname"] not in report_column_names - ] + report_custom_columns = [column for column in columns if column["fieldname"] not in report_column_names] if report_custom_columns: result = add_custom_column_data(report_custom_columns, result) @@ -126,7 +124,7 @@ def normalize_result(result, columns): # 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)): + if result and isinstance(result[0], list | tuple): for row in result: row_obj = {} for idx, column_name in enumerate(column_names): @@ -291,9 +289,9 @@ def get_prepared_report_result(report, filters, dn="", user=None): try: if data := json.loads(doc.get_prepared_data().decode("utf-8")): report_data = get_report_data(doc, data) - except Exception: + except Exception as e: doc.log_error("Prepared report render failed") - frappe.msgprint(_("Prepared report render failed")) + frappe.msgprint(_("Prepared report render failed") + f": {str(e)}") doc = None return report_data | {"prepared_report": True, "doc": doc} @@ -324,9 +322,7 @@ def export_query(): if isinstance(visible_idx, str): visible_idx = json.loads(visible_idx) - data = run( - report_name, form_params.filters, custom_columns=custom_columns, are_default_filters=False - ) + data = run(report_name, form_params.filters, custom_columns=custom_columns, are_default_filters=False) data = frappe._dict(data) data.filters = form_params.applied_filters @@ -365,9 +361,7 @@ def format_duration_fields(data: frappe._dict) -> None: row[index] = format_duration(row[index]) -def build_xlsx_data( - data, visible_idx, include_indentation, include_filters=False, ignore_visible_idx=False -): +def build_xlsx_data(data, visible_idx, include_indentation, include_filters=False, ignore_visible_idx=False): EXCEL_TYPES = ( str, bool, @@ -508,13 +502,12 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None): @frappe.whitelist() def get_data_for_custom_field(doctype, field, names=None): - if not frappe.has_permission(doctype, "read"): frappe.throw(_("Not Permitted to read {0}").format(doctype), frappe.PermissionError) filters = {} if names: - if isinstance(names, (str, bytearray)): + if isinstance(names, str | bytearray): names = frappe.json.loads(names) filters.update({"name": ["in", names]}) @@ -598,7 +591,9 @@ def get_filtered_data(ref_doctype, columns, data, user): for row in data: # Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed if ( - linked_doctypes.get(ref_doctype) and shared and row.get(linked_doctypes[ref_doctype]) in shared + linked_doctypes.get(ref_doctype) + and shared + and row.get(linked_doctypes[ref_doctype]) in shared ): result.append(row) @@ -664,7 +659,7 @@ def has_match( cell_value = None if isinstance(row, dict): cell_value = row.get(idx) - elif isinstance(row, (list, tuple)): + elif isinstance(row, list | tuple): cell_value = row[idx] if ( @@ -696,10 +691,10 @@ def get_linked_doctypes(columns, data): columns_dict = get_columns_dict(columns) - for idx, col in enumerate(columns): + for idx in range(len(columns)): df = columns_dict[idx] if df.get("fieldtype") == "Link": - if data and isinstance(data[0], (list, tuple)): + if data and isinstance(data[0], list | tuple): linked_doctypes[df["options"]] = idx else: # dict @@ -710,7 +705,7 @@ def get_linked_doctypes(columns, data): for row in data: if row: if len(row) != len(columns_with_value): - if isinstance(row, (list, tuple)): + if isinstance(row, list | tuple): row = enumerate(row) elif isinstance(row, dict): row = row.items() diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 746c6b299f..ae0d915b7c 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -168,9 +168,7 @@ def raise_invalid_field(fieldname): def is_standard(fieldname): if "." in fieldname: fieldname = fieldname.split(".")[1].strip("`") - return ( - fieldname in default_fields or fieldname in optional_fields or fieldname in child_table_fields - ) + return fieldname in default_fields or fieldname in optional_fields or fieldname in child_table_fields def extract_fieldname(field): @@ -201,7 +199,7 @@ def get_meta_and_docfield(fieldname, data): def update_wildcard_field_param(data): if (isinstance(data.fields, str) and data.fields == "*") or ( - isinstance(data.fields, (list, tuple)) and len(data.fields) == 1 and data.fields[0] == "*" + isinstance(data.fields, list | tuple) and len(data.fields) == 1 and data.fields[0] == "*" ): data.fields = get_permitted_fields(data.doctype, parenttype=data.parenttype) return True @@ -392,10 +390,10 @@ def append_totals_row(data): for row in data: for i in range(len(row)): - if isinstance(row[i], (float, int)): + if isinstance(row[i], float | int): totals[i] = (totals[i] or 0) + row[i] - if not isinstance(totals[0], (int, float)): + if not isinstance(totals[0], int | float): totals[0] = "Total" data.append(totals) @@ -485,7 +483,9 @@ def delete_bulk(doctype, items): if len(items) >= 5: frappe.publish_realtime( "progress", - dict(progress=[i + 1, len(items)], title=_("Deleting {0}").format(doctype), description=d), + dict( + progress=[i + 1, len(items)], title=_("Deleting {0}").format(doctype), description=d + ), user=frappe.session.user, ) # Commit after successful deletion @@ -568,7 +568,7 @@ def get_stats(stats, doctype, filters=None): except frappe.db.SQLError: pass - except frappe.db.InternalError as e: + except frappe.db.InternalError: # raised when _user_tags column is added on the fly pass @@ -620,7 +620,6 @@ def get_filter_dashboard_data(stats, doctype, filters=None): )[0][1], ] if data and data[1] != 0: - stats[tag["name"]].append(data) else: stats[tag["name"]] = tagcount @@ -656,17 +655,13 @@ def get_match_cond(doctype, as_condition=True): def build_match_conditions(doctype, user=None, as_condition=True): - match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions( - as_condition=as_condition - ) + match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions(as_condition=as_condition) if as_condition: return match_conditions.replace("%", "%%") return match_conditions -def get_filters_cond( - doctype, filters, conditions, ignore_permissions=None, with_match_conditions=False -): +def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with_match_conditions=False): if isinstance(filters, str): filters = json.loads(filters) @@ -678,7 +673,7 @@ def get_filters_cond( for f in filters: if isinstance(f[1], str) and f[1][0] == "!": flt.append([doctype, f[0], "!=", f[1][1:]]) - elif isinstance(f[1], (list, tuple)) and f[1][0].lower() in ( + elif isinstance(f[1], list | tuple) and f[1][0].lower() in ( "=", ">", "<", @@ -692,7 +687,6 @@ def get_filters_cond( "between", "is", ): - flt.append([doctype, f[0], f[1][0], f[1][1]]) else: flt.append([doctype, f[0], "=", f[1]]) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 8b76cd35e1..d4d8139908 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -72,7 +72,6 @@ def search_widget( reference_doctype: str | None = None, ignore_user_permissions: bool = False, ): - start = cint(start) if isinstance(filters, str): @@ -319,9 +318,7 @@ def get_users_for_mentions(): def get_user_groups(): - return frappe.get_all( - "User Group", fields=["name as id", "name as value"], update={"is_group": True} - ) + return frappe.get_all("User Group", fields=["name as id", "name as value"], update={"is_group": True}) @frappe.whitelist() diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index 5cb9677f3e..434592914e 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -80,7 +80,6 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter com_doctypes = [] if len(txt) < 2: - for name in frappe.get_hooks("communication_doctypes"): try: module = load_doctype_module(name, suffix="_dashboard") @@ -94,9 +93,7 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter d[0] for d in frappe.db.get_values("DocType", {"issingle": 0, "istable": 0, "hide_toolbar": 0}) ] - return [ - [dt] for dt in com_doctypes if txt.lower().replace("%", "") in dt.lower() and dt in can_read - ] + return [[dt] for dt in com_doctypes if txt.lower().replace("%", "") in dt.lower() and dt in can_read] def get_cached_contacts(txt): diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.json b/frappe/email/doctype/auto_email_report/auto_email_report.json index 75a9e99c96..41beac3e3a 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.json +++ b/frappe/email/doctype/auto_email_report/auto_email_report.json @@ -25,6 +25,7 @@ "to_date_field", "column_break_17", "dynamic_date_period", + "use_first_day_of_period", "email_settings", "email_to", "day_of_week", @@ -87,6 +88,7 @@ }, { "default": "100", + "depends_on": "eval:doc.report_type=='Report Builder'", "fieldname": "no_of_rows", "fieldtype": "Int", "label": "No of Rows (Max 500)" @@ -207,10 +209,18 @@ "fieldtype": "Link", "label": "Sender", "options": "Email Account" + }, + { + "default": "0", + "depends_on": "eval: doc.dynamic_date_period != 'Daily'", + "description": "To begin the date range at the start of the chosen period. For example, if 'Year' is selected as the period, the report will start from January 1st of the current year.", + "fieldname": "use_first_day_of_period", + "fieldtype": "Check", + "label": "Use First Day of Period" } ], "links": [], - "modified": "2022-09-08 15:31:55.031023", + "modified": "2024-02-04 13:31:08.624648", "modified_by": "Administrator", "module": "Email", "name": "Auto Email Report", 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 6fe2596d7f..9ec44ab383 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import calendar +import datetime from datetime import timedelta from email.utils import formataddr @@ -14,8 +15,13 @@ from frappe.utils import ( add_to_date, cint, format_time, + get_first_day, + get_first_day_of_week, get_link_to_form, + get_quarter_start, get_url_to_report, + get_year_start, + getdate, global_date_format, now, now_datetime, @@ -36,9 +42,7 @@ class AutoEmailReport(Document): from frappe.types import DF data_modified_till: DF.Int - day_of_week: DF.Literal[ - "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" - ] + day_of_week: DF.Literal["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] description: DF.TextEditor | None dynamic_date_period: DF.Literal[ "", "Daily", "Weekly", "Monthly", "Quarterly", "Half Yearly", "Yearly" @@ -57,8 +61,10 @@ class AutoEmailReport(Document): send_if_data: DF.Check sender: DF.Link | None to_date_field: DF.Literal + use_first_day_of_period: DF.Check user: DF.Link # end: auto-generated types + def autoname(self): self.name = _(self.report) if frappe.db.exists("Auto Email Report", self.name): @@ -92,7 +98,7 @@ class AutoEmailReport(Document): max_reports_per_user = ( cint(frappe.local.conf.max_reports_per_user) # kept for backward compatibilty - or cint(frappe.db.get_single_value("System Settings", "max_auto_email_report_per_user")) + or cint(frappe.get_system_settings("max_auto_email_report_per_user")) or 20 ) @@ -182,7 +188,6 @@ class AutoEmailReport(Document): frappe.throw(_("Invalid Output Format")) def get_html_table(self, columns=None, data=None): - date_time = global_date_format(now()) + " " + format_time(now()) report_doctype = frappe.db.get_value("Report", self.report, "ref_doctype") @@ -207,17 +212,37 @@ class AutoEmailReport(Document): self.filters = frappe.parse_json(self.filters) to_date = today() - from_date_value = { - "Daily": ("days", -1), - "Weekly": ("weeks", -1), - "Monthly": ("months", -1), - "Quarterly": ("months", -3), - "Half Yearly": ("months", -6), - "Yearly": ("years", -1), - }[self.dynamic_date_period] - from_date = add_to_date(to_date, **{from_date_value[0]: from_date_value[1]}) + if self.use_first_day_of_period: + from_date = to_date + if self.dynamic_date_period == "Daily": + from_date = add_to_date(to_date, days=-1) + elif self.dynamic_date_period == "Weekly": + from_date = get_first_day_of_week(from_date) + elif self.dynamic_date_period == "Monthly": + from_date = get_first_day(from_date) + elif self.dynamic_date_period == "Quarterly": + from_date = get_quarter_start(from_date) + elif self.dynamic_date_period == "Half Yearly": + from_date = get_half_year_start(from_date) + elif self.dynamic_date_period == "Yearly": + from_date = get_year_start(from_date) + self.set_date_filters(from_date, to_date) + else: + from_date_value = { + "Daily": ("days", -1), + "Weekly": ("weeks", -1), + "Monthly": ("months", -1), + "Quarterly": ("months", -3), + "Half Yearly": ("months", -6), + "Yearly": ("years", -1), + }[self.dynamic_date_period] + + from_date = add_to_date(to_date, **{from_date_value[0]: from_date_value[1]}) + self.set_date_filters(from_date, to_date) + + def set_date_filters(self, from_date, to_date): self.filters[self.from_date_field] = from_date self.filters[self.to_date_field] = to_date @@ -296,7 +321,7 @@ def send_daily(): continue try: auto_email_report.send() - except Exception as e: + except Exception: auto_email_report.log_error(f"Failed to send {auto_email_report.name} Auto Email Report") @@ -332,3 +357,23 @@ def update_field_types(columns): col.fieldtype = "Data" col.options = "" return columns + + +DATE_FORMAT = "%Y-%m-%d" + + +def get_half_year_start(as_str=False): + """ + Returns the first day of the current half-year based on the current date. + """ + today_date = getdate(today()) + + half_year = 1 if today_date.month <= 6 else 2 + + year = today_date.year if half_year == 1 else today_date.year + 1 + month = 1 if half_year == 1 else 7 + day = 1 + + result_date = datetime.date(year, month, day) + + return result_date if not as_str else result_date.strftime(DATE_FORMAT) diff --git a/frappe/email/doctype/auto_email_report/test_auto_email_report.py b/frappe/email/doctype/auto_email_report/test_auto_email_report.py index 1f806f22b5..2fb37eafec 100644 --- a/frappe/email/doctype/auto_email_report/test_auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/test_auto_email_report.py @@ -46,17 +46,15 @@ class TestAutoEmailReport(FrappeTestCase): def get_auto_email_report(): if not frappe.db.exists("Auto Email Report", "Permitted Documents For User"): auto_email_report = frappe.get_doc( - dict( - doctype="Auto Email Report", - report="Permitted Documents For User", - report_type="Script Report", - user="Administrator", - enabled=1, - email_to="test@example.com", - format="HTML", - frequency="Daily", - filters=json.dumps(dict(user="Administrator", doctype="DocType")), - ) + doctype="Auto Email Report", + report="Permitted Documents For User", + report_type="Script Report", + user="Administrator", + enabled=1, + email_to="test@example.com", + format="HTML", + frequency="Daily", + filters=json.dumps(dict(user="Administrator", doctype="DocType")), ).insert() else: auto_email_report = frappe.get_doc("Auto Email Report", "Permitted Documents For User") diff --git a/frappe/email/doctype/document_follow/document_follow.py b/frappe/email/doctype/document_follow/document_follow.py index 4a2933276e..ba84dce825 100644 --- a/frappe/email/doctype/document_follow/document_follow.py +++ b/frappe/email/doctype/document_follow/document_follow.py @@ -17,4 +17,5 @@ class DocumentFollow(Document): ref_doctype: DF.Link user: DF.Link # end: auto-generated types + pass diff --git a/frappe/email/doctype/document_follow/test_document_follow.py b/frappe/email/doctype/document_follow/test_document_follow.py index 845f6e93bb..568879d415 100644 --- a/frappe/email/doctype/document_follow/test_document_follow.py +++ b/frappe/email/doctype/document_follow/test_document_follow.py @@ -91,9 +91,7 @@ class TestDocumentFollow(FrappeTestCase): frappe.set_user(user.name) event = get_event() - add_comment( - event.doctype, event.name, "This is a test comment", "Administrator@example.com", "Bosh" - ) + add_comment(event.doctype, event.name, "This is a test comment", "Administrator@example.com", "Bosh") documents_followed = get_events_followed_by_user(event.name, user.name) self.assertTrue(documents_followed) @@ -103,9 +101,7 @@ class TestDocumentFollow(FrappeTestCase): frappe.set_user(user.name) event = get_event() - add_comment( - event.doctype, event.name, "This is a test comment", "Administrator@example.com", "Bosh" - ) + add_comment(event.doctype, event.name, "This is a test comment", "Administrator@example.com", "Bosh") documents_followed = get_events_followed_by_user(event.name, user.name) self.assertFalse(documents_followed) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 2c22354365..3fa72d4680 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -4,7 +4,6 @@ import email.utils import functools import imaplib -import socket import time from datetime import datetime, timedelta from poplib import error_proto @@ -97,7 +96,13 @@ class EmailAccount(Document): send_notification_to: DF.SmallText | None send_unsubscribe_message: DF.Check service: DF.Literal[ - "", "GMail", "Sendgrid", "SparkPost", "Yahoo Mail", "Outlook.com", "Yandex.Mail" + "", + "GMail", + "Sendgrid", + "SparkPost", + "Yahoo Mail", + "Outlook.com", + "Yandex.Mail", ] signature: DF.TextEditor | None smtp_port: DF.Data | None @@ -111,7 +116,9 @@ class EmailAccount(Document): use_ssl_for_outgoing: DF.Check use_starttls: DF.Check use_tls: DF.Check + validate_ssl_certificate: DF.Check # end: auto-generated types + DOCTYPE = "Email Account" def autoname(self): @@ -191,20 +198,27 @@ class EmailAccount(Document): self.default_incoming = False messages.append( _("{} has been disabled. It can only be enabled if {} is checked.").format( - frappe.bold(_("Default Incoming")), frappe.bold(_("Enable Incoming")) + frappe.bold(_("Default Incoming")), + frappe.bold(_("Enable Incoming")), ) ) if not self.enable_outgoing and self.default_outgoing: self.default_outgoing = False messages.append( _("{} has been disabled. It can only be enabled if {} is checked.").format( - frappe.bold(_("Default Outgoing")), frappe.bold(_("Enable Outgoing")) + frappe.bold(_("Default Outgoing")), + frappe.bold(_("Enable Outgoing")), ) ) if messages: if len(messages) == 1: (as_list, messages) = (0, messages[0]) - frappe.msgprint(messages, as_list=as_list, indicator="orange", title=_("Defaults Updated")) + frappe.msgprint( + messages, + as_list=as_list, + indicator="orange", + title=_("Defaults Updated"), + ) def on_update(self): """Check there is only one default of each type.""" @@ -284,7 +298,11 @@ class EmailAccount(Document): "loginfailed", ] - other_error_codes = ["err[auth]", "errtemporaryerror", "loginviayourwebbrowser"] + other_error_codes = [ + "err[auth]", + "errtemporaryerror", + "loginviayourwebbrowser", + ] all_error_codes = auth_error_codes + other_error_codes @@ -563,7 +581,15 @@ class EmailAccount(Document): seen_status = messages.get("seen_status", {}).get(uid) if self.email_sync_option != "UNSEEN" or seen_status != "SEEN": # only append the emails with status != 'SEEN' if sync option is set to 'UNSEEN' - mails.append(InboundMail(message, self, frappe.safe_decode(uid), seen_status, append_to)) + mails.append( + InboundMail( + message, + self, + frappe.safe_decode(uid), + seen_status, + append_to, + ) + ) if not self.enable_incoming: return [] @@ -618,7 +644,9 @@ class EmailAccount(Document): def send_auto_reply(self, communication, email): """Send auto reply if set.""" - from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts + from frappe.core.doctype.communication.email import ( + set_incoming_outgoing_accounts, + ) if self.enable_auto_reply: set_incoming_outgoing_accounts(communication) @@ -672,7 +700,8 @@ class EmailAccount(Document): frappe.throw(_("Automatic Linking can be activated only if Incoming is enabled.")) if frappe.db.exists( - "Email Account", {"enable_automatic_linking": 1, "name": ("!=", self.name)} + "Email Account", + {"enable_automatic_linking": 1, "name": ("!=", self.name)}, ): frappe.throw(_("Automatic Linking can be activated only for one Email Account.")) @@ -695,9 +724,7 @@ class EmailAccount(Document): @frappe.whitelist() -def get_append_to( - doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None -): +def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None): txt = txt if txt else "" filters = {"istable": 0, "issingle": 0, "email_append_to": 1} @@ -721,7 +748,9 @@ def notify_unreplied(): """Sends email notifications if there are unreplied Communications and `notify_if_unreplied` is set as true.""" for email_account in frappe.get_all( - "Email Account", "name", filters={"enable_incoming": 1, "notify_if_unreplied": 1} + "Email Account", + "name", + filters={"enable_incoming": 1, "notify_if_unreplied": 1}, ): email_account = frappe.get_doc("Email Account", email_account.name) @@ -749,7 +778,8 @@ def notify_unreplied(): { "creation": ( ">", - datetime.now() - timedelta(seconds=(email_account.unreplied_for_mins or 30) * 60 * 3), + datetime.now() + - timedelta(seconds=(email_account.unreplied_for_mins or 30) * 60 * 3), ) }, ], @@ -777,7 +807,12 @@ def pull(now=False): doctype = frappe.qb.DocType("Email Account") email_accounts = ( frappe.qb.from_(doctype) - .select(doctype.name, doctype.auth_method, doctype.connected_app, doctype.connected_user) + .select( + doctype.name, + doctype.auth_method, + doctype.connected_app, + doctype.connected_user, + ) .where(doctype.enable_incoming == 1) .where(doctype.awaiting_password == 0) .run(as_dict=1) @@ -823,10 +858,9 @@ def pull_from_email_account(email_account): def get_max_email_uid(email_account): - # get maximum uid of emails - max_uid = 1 + """get maximum uid of emails""" - result = frappe.get_all( + if result := frappe.get_all( "Communication", filters={ "communication_medium": "Email", @@ -834,17 +868,12 @@ def get_max_email_uid(email_account): "email_account": email_account, }, fields=["max(uid) as uid"], - ) - - if not result: - return 1 - else: + ): return cint(result[0].get("uid", 0)) + 1 + return 1 -def setup_user_email_inbox( - email_account, awaiting_password, email_id, enable_outgoing, used_oauth -): +def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_outgoing, used_oauth): """setup email inbox for user""" from frappe.core.doctype.user.user import ask_pass_update @@ -874,7 +903,9 @@ def setup_user_email_inbox( # check if inbox is alreay configured user_inbox = ( frappe.db.get_value( - "User Email", {"email_account": email_account, "parent": user_name}, ["name"] + "User Email", + {"email_account": email_account, "parent": user_name}, + ["name"], ) or None ) @@ -889,9 +920,7 @@ def setup_user_email_inbox( UserEmail = frappe.qb.DocType("User Email") frappe.qb.update(UserEmail).set(UserEmail.awaiting_password, (awaiting_password or 0)).set( UserEmail.enable_outgoing, (enable_outgoing or 0) - ).set(UserEmail.used_oauth, (used_oauth or 0)).where( - UserEmail.email_account == email_account - ).run() + ).set(UserEmail.used_oauth, (used_oauth or 0)).where(UserEmail.email_account == email_account).run() else: users = " and ".join([frappe.bold(user.get("name")) for user in user_names]) @@ -905,7 +934,9 @@ def remove_user_email_inbox(email_account): return users = frappe.get_all( - "User Email", filters={"email_account": email_account}, fields=["parent as name"] + "User Email", + filters={"email_account": email_account}, + fields=["parent as name"], ) for user in users: diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index 8af48b591b..72fd813712 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -132,9 +132,7 @@ class TestEmailAccount(FrappeTestCase): TestEmailAccount.mocked_email_receive(email_account, messages) comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"}) - self.assertTrue( - "From: "Microsoft Outlook" <test_sender@example.com>" in comm.content - ) + self.assertTrue("From: "Microsoft Outlook" <test_sender@example.com>" in comm.content) self.assertTrue( "This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content ) @@ -155,9 +153,7 @@ class TestEmailAccount(FrappeTestCase): TestEmailAccount.mocked_email_receive(email_account, messages) comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"}) - self.assertTrue( - "From: "Microsoft Outlook" <test_sender@example.com>" in comm.content - ) + self.assertTrue("From: "Microsoft Outlook" <test_sender@example.com>" in comm.content) self.assertTrue( "This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content ) @@ -272,7 +268,7 @@ class TestEmailAccount(FrappeTestCase): frappe.db.delete("Email Queue") # reference document for testing - event = frappe.get_doc(dict(doctype="Event", subject="test-message")).insert() + event = frappe.get_doc(doctype="Event", subject="test-message").insert() # send a mail against this frappe.sendmail( @@ -290,7 +286,9 @@ class TestEmailAccount(FrappeTestCase): messages = { # append_to = ToDo '"INBOX"': { - "latest_messages": [f.read().replace("{{ message_id }}", "<" + last_mail.message_id + ">")], + "latest_messages": [ + f.read().replace("{{ message_id }}", "<" + last_mail.message_id + ">") + ], "seen_status": {2: "UNSEEN"}, "uid_list": [2], } @@ -415,10 +413,13 @@ class TestEmailAccount(FrappeTestCase): @patch("frappe.email.receive.EmailServer.select_imap_folder", return_value=True) @patch("frappe.email.receive.EmailServer.logout", side_effect=lambda: None) def mocked_get_inbound_mails( - email_account, messages={}, mocked_logout=None, mocked_select_imap_folder=None + email_account, messages=None, mocked_logout=None, mocked_select_imap_folder=None ): from frappe.email.receive import EmailServer + if messages is None: + messages = {} + def get_mocked_messages(**kwargs): return messages.get(kwargs["folder"], {}) @@ -430,8 +431,11 @@ class TestEmailAccount(FrappeTestCase): @patch("frappe.email.receive.EmailServer.select_imap_folder", return_value=True) @patch("frappe.email.receive.EmailServer.logout", side_effect=lambda: None) def mocked_email_receive( - email_account, messages={}, mocked_logout=None, mocked_select_imap_folder=None + email_account, messages=None, mocked_logout=None, mocked_select_imap_folder=None ): + if messages is None: + messages = {} + def get_mocked_messages(**kwargs): return messages.get(kwargs["folder"], {}) diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py index fef6e1b303..55927f94d7 100644 --- a/frappe/email/doctype/email_domain/email_domain.py +++ b/frappe/email/doctype/email_domain/email_domain.py @@ -73,8 +73,10 @@ class EmailDomain(Document): use_ssl_for_outgoing: DF.Check use_starttls: DF.Check use_tls: DF.Check - + validate_ssl_certificate: DF.Check + validate_ssl_certificate_for_outgoing: DF.Check # end: auto-generated types + def validate(self): """Validate POP3/IMAP and SMTP connections.""" diff --git a/frappe/email/doctype/email_flag_queue/email_flag_queue.py b/frappe/email/doctype/email_flag_queue/email_flag_queue.py index 1f20aece88..a8faa4928f 100644 --- a/frappe/email/doctype/email_flag_queue/email_flag_queue.py +++ b/frappe/email/doctype/email_flag_queue/email_flag_queue.py @@ -19,4 +19,5 @@ class EmailFlagQueue(Document): is_completed: DF.Check uid: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py index 9619802edb..8c80a52fcb 100755 --- a/frappe/email/doctype/email_group/email_group.py +++ b/frappe/email/doctype/email_group/email_group.py @@ -25,6 +25,7 @@ class EmailGroup(Document): 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})] self.get("__onload").import_types = [ @@ -104,7 +105,7 @@ def import_from(name, doctype): @frappe.whitelist() def add_subscribers(name, email_list): - if not isinstance(email_list, (list, tuple)): + if not isinstance(email_list, list | tuple): email_list = email_list.replace(",", "\n").split("\n") template = frappe.db.get_value("Email Group", name, "welcome_email_template") diff --git a/frappe/email/doctype/email_group_member/email_group_member.py b/frappe/email/doctype/email_group_member/email_group_member.py index e8694d2284..437f712272 100644 --- a/frappe/email/doctype/email_group_member/email_group_member.py +++ b/frappe/email/doctype/email_group_member/email_group_member.py @@ -18,6 +18,7 @@ class EmailGroupMember(Document): email_group: DF.Link unsubscribed: DF.Check # end: auto-generated types + def after_delete(self): email_group = frappe.get_doc("Email Group", self.email_group) email_group.update_total_subscribers() diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 91ca2bbdbb..e33c12d64f 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -65,6 +65,7 @@ class EmailQueue(Document): unsubscribe_method: DF.Data | None unsubscribe_param: DF.Data | None # end: auto-generated types + DOCTYPE = "Email Queue" def set_recipients(self, recipients): @@ -131,12 +132,12 @@ class EmailQueue(Document): def attachments_list(self): return json.loads(self.attachments) if self.attachments else [] - def get_email_account(self): + def get_email_account(self, raise_error=False): if self.email_account: return frappe.get_cached_doc("Email Account", self.email_account) return EmailAccount.find_outgoing( - match_by_email=self.sender, match_by_doctype=self.reference_doctype + match_by_email=self.sender, match_by_doctype=self.reference_doctype, _raise_error=raise_error ) def is_to_be_sent(self): @@ -158,6 +159,7 @@ class EmailQueue(Document): return with SendMailContext(self, smtp_server_instance) as ctx: + ctx.fetch_smtp_server() message = None for recipient in self.recipients: if recipient.is_mail_sent(): @@ -194,9 +196,7 @@ class EmailQueue(Document): # Delete queue table ( - frappe.qb.from_(email_queue) - .delete() - .where(email_queue.modified < (Now() - Interval(days=days))) + frappe.qb.from_(email_queue).delete().where(email_queue.modified < (Now() - Interval(days=days))) ).run() # delete child tables, note that this has potential to leave some orphan @@ -233,14 +233,16 @@ class SendMailContext: smtp_server_instance: SMTPServer = None, ): self.queue_doc: EmailQueue = queue_doc - self.email_account_doc = queue_doc.get_email_account() - - self.smtp_server: SMTPServer = smtp_server_instance or self.email_account_doc.get_smtp_server() - + self.smtp_server: SMTPServer = smtp_server_instance self.sent_to_atleast_one_recipient = any( rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent() ) + def fetch_smtp_server(self): + self.email_account_doc = self.queue_doc.get_email_account(raise_error=True) + if not self.smtp_server: + self.smtp_server = self.email_account_doc.get_smtp_server() + def __enter__(self): self.queue_doc.update_status(status="Sending", commit=True) return self @@ -302,9 +304,7 @@ class SendMailContext: if not message: return "" - message = message.replace( - self.message_placeholder("tracker"), self.get_tracker_str(recipient_email) - ) + message = message.replace(self.message_placeholder("tracker"), self.get_tracker_str(recipient_email)) message = message.replace( self.message_placeholder("unsubscribe_url"), self.get_unsubscribe_str(recipient_email) ) @@ -408,11 +408,9 @@ def bulk_retry(queues): ) email_queue = frappe.qb.DocType("Email Queue") - frappe.qb.update(email_queue).set(email_queue.status, "Not Sent").set( - email_queue.modified, now() - ).set(email_queue.modified_by, frappe.session.user).where( - email_queue.name.isin(queues) & email_queue.status == "Error" - ).run() + frappe.qb.update(email_queue).set(email_queue.status, "Not Sent").set(email_queue.modified, now()).set( + email_queue.modified_by, frappe.session.user + ).where(email_queue.name.isin(queues) & email_queue.status == "Error").run() @frappe.whitelist() @@ -431,9 +429,7 @@ def toggle_sending(enable): def on_doctype_update(): """Add index in `tabCommunication` for `(reference_doctype, reference_name)`""" - frappe.db.add_index( - "Email Queue", ("status", "send_after", "priority", "creation"), "index_bulk_flush" - ) + frappe.db.add_index("Email Queue", ("status", "send_after", "priority", "creation"), "index_bulk_flush") frappe.db.add_index("Email Queue", ["message_id(140)"]) @@ -733,7 +729,7 @@ class QueueBuilder: recipients = list(set([r] + self.final_cc() + self.bcc)) q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True) if not smtp_server_instance: - email_account = q.get_email_account() + email_account = q.get_email_account(raise_error=True) smtp_server_instance = email_account.get_smtp_server() with suppress(Exception): diff --git a/frappe/email/doctype/email_queue/test_email_queue.py b/frappe/email/doctype/email_queue/test_email_queue.py index 7d76039b47..74ea856bf3 100644 --- a/frappe/email/doctype/email_queue/test_email_queue.py +++ b/frappe/email/doctype/email_queue/test_email_queue.py @@ -87,9 +87,7 @@ class TestEmailQueue(FrappeTestCase): def get_server(q): return q.get_email_account().get_smtp_server() - self.assertIs( - get_server(frappe.new_doc("Email Queue")), get_server(frappe.new_doc("Email Queue")) - ) + self.assertIs(get_server(frappe.new_doc("Email Queue")), get_server(frappe.new_doc("Email Queue"))) q1 = frappe.new_doc("Email Queue", email_account="_Test Email Account 1") q2 = frappe.new_doc("Email Queue", email_account="_Test Email Account 1") diff --git a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py index 2d0c5678f7..343c3e6840 100644 --- a/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py +++ b/frappe/email/doctype/email_queue_recipient/email_queue_recipient.py @@ -21,6 +21,7 @@ class EmailQueueRecipient(Document): recipient: DF.Data | None status: DF.Literal["", "Not Sent", "Sent"] # end: auto-generated types + DOCTYPE = "Email Queue Recipient" def is_mail_to_be_sent(self): diff --git a/frappe/email/doctype/email_rule/email_rule.py b/frappe/email/doctype/email_rule/email_rule.py index 8eab691b95..33b1f7dc24 100644 --- a/frappe/email/doctype/email_rule/email_rule.py +++ b/frappe/email/doctype/email_rule/email_rule.py @@ -16,4 +16,5 @@ class EmailRule(Document): email_id: DF.Data | None is_spam: DF.Check # end: auto-generated types + pass diff --git a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py index 5cfc4a38d1..200a6487a5 100644 --- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py +++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py @@ -20,13 +20,12 @@ class EmailUnsubscribe(Document): reference_doctype: DF.Link | None reference_name: DF.DynamicLink | None # end: auto-generated types + def validate(self): if not self.global_unsubscribe and not (self.reference_doctype and self.reference_name): frappe.throw(_("Reference DocType and Reference Name are required"), frappe.MandatoryError) - if not self.global_unsubscribe and frappe.db.get_value( - self.doctype, self.name, "global_unsubscribe" - ): + if not self.global_unsubscribe and frappe.db.get_value(self.doctype, self.name, "global_unsubscribe"): frappe.throw(_("Delete this record to allow sending to this email address")) if self.global_unsubscribe: diff --git a/frappe/email/doctype/imap_folder/imap_folder.py b/frappe/email/doctype/imap_folder/imap_folder.py index efbe2073ea..e5a8bd8756 100644 --- a/frappe/email/doctype/imap_folder/imap_folder.py +++ b/frappe/email/doctype/imap_folder/imap_folder.py @@ -22,4 +22,5 @@ class IMAPFolder(Document): uidnext: DF.Data | None uidvalidity: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index e7c902697f..e03cc506e6 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -3,7 +3,7 @@ "allow_guest_to_view": 1, "allow_rename": 1, "creation": "2013-01-10 16:34:31", - "description": "Create and Send Newsletters", + "description": "Create and send emails to a specific group of subscribers periodically.", "doctype": "DocType", "document_type": "Other", "engine": "InnoDB", @@ -244,8 +244,7 @@ "fieldname": "campaign", "fieldtype": "Link", "label": "Campaign", - "options": "Marketing Campaign", - "reqd": 0 + "options": "Marketing Campaign" } ], "has_web_view": 1, @@ -254,7 +253,7 @@ "index_web_pages_for_search": 1, "is_published_field": "published", "links": [], - "modified": "2023-12-29 18:04:13.270523", + "modified": "2024-01-30 14:05:50.645802", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index ea33937d49..45d5c35d99 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -337,9 +337,7 @@ def subscribe(email, email_group=None): content = """

{}. {}.

{}

- """.format( - *translatable_content - ) + """.format(*translatable_content) frappe.sendmail( email, diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index 9677b94de3..7c355af0f0 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -147,9 +147,7 @@ class TestNewsletter(TestNewsletterMixin, FrappeTestCase): def test_unsubscribe(self): name = self.send_newsletter() to_unsubscribe = choice(emails) - group = frappe.get_all( - "Newsletter Email Group", filters={"parent": name}, fields=["email_group"] - ) + group = frappe.get_all("Newsletter Email Group", filters={"parent": name}, fields=["email_group"]) flush() confirmed_unsubscribe(to_unsubscribe, group[0].email_group) diff --git a/frappe/email/doctype/newsletter_attachment/newsletter_attachment.py b/frappe/email/doctype/newsletter_attachment/newsletter_attachment.py index dcb3753124..e0a3f3aa56 100644 --- a/frappe/email/doctype/newsletter_attachment/newsletter_attachment.py +++ b/frappe/email/doctype/newsletter_attachment/newsletter_attachment.py @@ -19,4 +19,5 @@ class NewsletterAttachment(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py index ed3398c2c2..59b06e446d 100644 --- a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py +++ b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py @@ -19,4 +19,5 @@ class NewsletterEmailGroup(Document): parenttype: DF.Data total_subscribers: DF.ReadOnly | None # end: auto-generated types + pass diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 28a45ce35e..79a90b811d 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -67,6 +67,7 @@ class Notification(Document): subject: DF.Data | None value_changed: DF.Literal # end: auto-generated types + def onload(self): """load message""" if self.is_standard: @@ -381,7 +382,6 @@ def get_context(context): if (doc.docstatus == 0 and not print_settings.allow_print_for_draft) or ( doc.docstatus == 2 and not print_settings.allow_print_for_cancelled ): - # ignoring attachment as draft and cancelled documents are not allowed to print status = "Draft" if doc.docstatus == 0 else "Cancelled" frappe.throw( diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index 576d9e9a66..e9043b2a38 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -87,9 +87,7 @@ class TestNotification(FrappeTestCase): ) ) - self.assertEqual( - frappe.db.get_value("Communication", communication.name, "subject"), "__testing__" - ) + self.assertEqual(frappe.db.get_value("Communication", communication.name, "subject"), "__testing__") def test_condition(self): """Check notification is triggered based on a condition.""" @@ -263,7 +261,6 @@ class TestNotification(FrappeTestCase): ) def test_cc_jinja(self): - frappe.db.delete("User", {"email": "test_jinja@example.com"}) frappe.db.delete("Email Queue") frappe.db.delete("Email Queue Recipient") @@ -282,9 +279,7 @@ class TestNotification(FrappeTestCase): ) ) - self.assertTrue( - frappe.db.get_value("Email Queue Recipient", {"recipient": "test_jinja@example.com"}) - ) + self.assertTrue(frappe.db.get_value("Email Queue Recipient", {"recipient": "test_jinja@example.com"})) frappe.db.delete("User", {"email": "test_jinja@example.com"}) frappe.db.delete("Email Queue") diff --git a/frappe/email/doctype/notification_recipient/notification_recipient.py b/frappe/email/doctype/notification_recipient/notification_recipient.py index 1a1b397a80..004a7f8353 100644 --- a/frappe/email/doctype/notification_recipient/notification_recipient.py +++ b/frappe/email/doctype/notification_recipient/notification_recipient.py @@ -22,4 +22,5 @@ class NotificationRecipient(Document): receiver_by_document_field: DF.Literal receiver_by_role: DF.Link | None # end: auto-generated types + pass diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 80348df394..34308f1af8 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -220,9 +220,7 @@ class EMail: """Set plain text from HTML""" self.set_text(to_markdown(html)) - def set_message( - self, message, mime_type="text/html", as_attachment=0, filename="attachment.html" - ): + def set_message(self, message, mime_type="text/html", as_attachment=0, filename="attachment.html"): """Append the message with MIME content to the root node (as attachment)""" from email.mime.text import MIMEText @@ -243,9 +241,7 @@ class EMail: self.add_attachment(_file.file_name, content) - def add_attachment( - self, fname, fcontent, content_type=None, parent=None, content_id=None, inline=False - ): + def add_attachment(self, fname, fcontent, content_type=None, parent=None, content_id=None, inline=False): """add attachment""" if not parent: @@ -355,7 +351,6 @@ def get_formatted_html( sender=None, with_container=False, ): - email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender) rendered_email = frappe.get_template("templates/emails/standard.html").render( @@ -518,9 +513,7 @@ def replace_filename_with_cid(message): content_id = random_string(10) - inline_images.append( - {"filename": filename, "filecontent": filecontent, "content_id": content_id} - ) + inline_images.append({"filename": filename, "filecontent": filecontent, "content_id": content_id}) message = re.sub(f"""embed=['"]{re.escape(img_path)}['"]""", f'src="cid:{content_id}"', message) diff --git a/frappe/email/inbox.py b/frappe/email/inbox.py index ef5187e7e1..e5215ec397 100644 --- a/frappe/email/inbox.py +++ b/frappe/email/inbox.py @@ -124,9 +124,7 @@ def mark_as_spam(communication: str, sender: str): set_value("Communication", communication, "email_status", "Spam") -def link_communication_to_document( - doc, reference_doctype, reference_name, ignore_communication_links -): +def link_communication_to_document(doc, reference_doctype, reference_name, ignore_communication_links): if not ignore_communication_links: doc.reference_doctype = reference_doctype doc.reference_name = reference_name diff --git a/frappe/email/oauth.py b/frappe/email/oauth.py index 87feb8ca11..497ee65d45 100644 --- a/frappe/email/oauth.py +++ b/frappe/email/oauth.py @@ -15,7 +15,6 @@ class Oauth: access_token: str, mechanism: str = "XOAUTH2", ) -> None: - self.email_account = email_account self.email = email self._mechanism = mechanism diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 6c78383b0c..7cdc5511de 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -62,9 +62,7 @@ def get_emails_sent_today(email_account=None): return frappe.db.sql(q, q_args)[0][0] -def get_unsubscribe_message( - unsubscribe_message: str, expose_recipients: str -) -> "frappe._dict[str, str]": +def get_unsubscribe_message(unsubscribe_message: str, expose_recipients: str) -> "frappe._dict[str, str]": unsubscribe_message = unsubscribe_message or _("Unsubscribe") unsubscribe_link = f'{unsubscribe_message}' unsubscribe_html = _("{0} to stop receiving emails of this type").format(unsubscribe_link) @@ -82,9 +80,7 @@ def get_unsubscribe_message( return frappe._dict(html=html, text=text) -def get_unsubcribed_url( - reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params -): +def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params): params = { "email": cstr(email), "doctype": cstr(reference_doctype), @@ -157,7 +153,8 @@ def flush(): failed_email_queues.append(row.name) if ( - len(failed_email_queues) / len(email_queue_batch) > EMAIL_QUEUE_BATCH_FAILURE_THRESHOLD_PERCENT + len(failed_email_queues) / len(email_queue_batch) + > EMAIL_QUEUE_BATCH_FAILURE_THRESHOLD_PERCENT and len(failed_email_queues) > EMAIL_QUEUE_BATCH_FAILURE_THRESHOLD_COUNT ): frappe.throw(_("Email Queue flushing aborted due to too many failures.")) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 72a2dfce82..ff8995e7c1 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -237,9 +237,7 @@ class EmailServer: ).where(EmailAccount.name == self.settings.email_account_name).run() sync_count = 100 if uid_validity else int(self.settings.initial_sync_count) - from_uid = ( - 1 if uidnext < (sync_count + 1) or (uidnext - sync_count) < 1 else uidnext - sync_count - ) + from_uid = 1 if uidnext < (sync_count + 1) or (uidnext - sync_count) < 1 else uidnext - sync_count # sync last 100 email self.settings.email_sync_rule = f"UID {from_uid}:{uidnext}" self.uid_reindexed = True @@ -445,9 +443,7 @@ class Email: if not email: return decoded = "" - for part, encoding in decode_header( - frappe.as_unicode(email).replace('"', " ").replace("'", " ") - ): + for part, encoding in decode_header(frappe.as_unicode(email).replace('"', " ").replace("'", " ")): if encoding: decoded += part.decode(encoding, "replace") else: @@ -673,7 +669,7 @@ class InboundMail(Email): content = self.content for file in attachments: if file.name in self.cid_map and self.cid_map[file.name]: - content = content.replace(f"cid:{self.cid_map[file.name]}", file.file_url) + content = content.replace(f"cid:{self.cid_map[file.name]}", file.unique_url) return content def is_notification(self): @@ -850,9 +846,7 @@ class InboundMail(Email): @staticmethod def get_users_linked_to_account(email_account): """Get list of users who linked to Email account.""" - users = frappe.get_all( - "User Email", filters={"email_account": email_account.name}, fields=["parent"] - ) + users = frappe.get_all("User Email", filters={"email_account": email_account.name}, fields=["parent"]) return list({user.get("parent") for user in users}) @staticmethod diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index c2c69d8e3d..a95adafac7 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -133,9 +133,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> test - """.format( - inline_images[0].get("content_id") - ) + """.format(inline_images[0].get("content_id")) self.assertEqual(message, processed_message) def test_inline_styling(self): diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py index 0e18fbf483..3ed55414c2 100644 --- a/frappe/frappeclient.py +++ b/frappe/frappeclient.py @@ -61,7 +61,7 @@ class FrappeClient: self.logout() def _login(self, username, password): - """Login/start a sesion. Called internally on init""" + """Login/start a session. Called internally on init""" r = self.session.post( self.url, params={"cmd": "login", "usr": username, "pwd": password}, @@ -106,9 +106,7 @@ class FrappeClient: headers=self.headers, ) - def get_list( - self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=None - ): + def get_list(self, doctype, fields='["name"]', filters=None, limit_start=0, limit_page_length=None): """Return list of records of a particular type.""" if not isinstance(fields, str): fields = json.dumps(fields) @@ -247,9 +245,7 @@ class FrappeClient: } return self.post_request(params) - def migrate_doctype( - self, doctype, filters=None, update=None, verbose=1, exclude=None, preprocess=None - ): + def migrate_doctype(self, doctype, filters=None, update=None, verbose=1, exclude=None, preprocess=None): """Migrate records from another doctype""" meta = frappe.get_meta(doctype) tables = {} @@ -360,7 +356,7 @@ class FrappeClient: def preprocess(self, params): """convert dicts, lists to json""" for key, value in params.items(): - if isinstance(value, (dict, list)): + if isinstance(value, dict | list): params[key] = json.dumps(value) return params diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 8ff39c37d7..8ce03b9405 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -2648,6 +2648,9 @@ "currency": "TZS", "currency_name": "Tanzanian Shilling", "number_format": "#,###.##", + "timezones": [ + "Africa/Dar_es_Salaam" + ], "isd": "+255" }, "Thailand": { diff --git a/frappe/geo/doctype/country/country.py b/frappe/geo/doctype/country/country.py index a2caad81c7..01e217a3db 100644 --- a/frappe/geo/doctype/country/country.py +++ b/frappe/geo/doctype/country/country.py @@ -20,6 +20,7 @@ class Country(Document): time_format: DF.Data | None time_zones: DF.Text | None # end: auto-generated types + # NOTE: During installation country docs are bulk inserted. pass diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json index 00dfe248c9..9a4df0f117 100644 --- a/frappe/geo/doctype/currency/currency.json +++ b/frappe/geo/doctype/currency/currency.json @@ -4,7 +4,7 @@ "allow_rename": 1, "autoname": "field:currency_name", "creation": "2013-01-28 10:06:02", - "description": "**Currency** Master", + "description": "Currency list stores the currency value, its symbol and fraction unit", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", @@ -82,7 +82,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-17 15:37:31.605278", + "modified": "2024-01-30 13:18:12.053557", "modified_by": "Administrator", "module": "Geo", "name": "Currency", diff --git a/frappe/geo/doctype/currency/currency.py b/frappe/geo/doctype/currency/currency.py index 3c3e9e923f..715adf19c0 100644 --- a/frappe/geo/doctype/currency/currency.py +++ b/frappe/geo/doctype/currency/currency.py @@ -37,6 +37,7 @@ class Currency(Document): symbol: DF.Data | None symbol_on_right: DF.Check # end: auto-generated types + # NOTE: During installation country docs are bulk inserted. def validate(self): frappe.clear_cache() diff --git a/frappe/geo/languages.json b/frappe/geo/languages.json index c0f09020d4..33092a2c4b 100644 --- a/frappe/geo/languages.json +++ b/frappe/geo/languages.json @@ -203,6 +203,10 @@ "code": "ml", "name": "\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02" }, + { + "code": "mn", + "name": "\u041c\u043e\u043d\u0433\u043e\u043b (Mongolian)" + }, { "code": "mr", "name": "\u092e\u0930\u093e\u0920\u0940" diff --git a/frappe/gettext/extractors/html_template.py b/frappe/gettext/extractors/html_template.py new file mode 100644 index 0000000000..34f51e4032 --- /dev/null +++ b/frappe/gettext/extractors/html_template.py @@ -0,0 +1,26 @@ +from jinja2.ext import babel_extract + +from .utils import extract_messages_from_code + + +def extract(*args, **kwargs): + """Extract messages from Jinja and JS microtemplates. + + Reuse the babel_extract function from jinja2.ext, but handle our own implementation of `_()`. + To handle JS microtemplates, parse all code again using regex.""" + fileobj = args[0] or kwargs["fileobj"] + print(fileobj.name) + code = fileobj.read().decode("utf-8") + + for lineno, funcname, messages, comments in babel_extract(*args, **kwargs): + if funcname == "_" and isinstance(messages, tuple) and len(messages) > 1: + funcname = "pgettext" + messages = (messages[-1], messages[0]) # (context, message) + + yield lineno, funcname, messages, comments + + for lineno, message, context in extract_messages_from_code(code): + if context: + yield lineno, "pgettext", (context, message), [] + else: + yield lineno, "_", message, [] diff --git a/frappe/gettext/extractors/jinja2.py b/frappe/gettext/extractors/jinja2.py deleted file mode 100644 index ee07ac6cee..0000000000 --- a/frappe/gettext/extractors/jinja2.py +++ /dev/null @@ -1,11 +0,0 @@ -from jinja2.ext import babel_extract - - -def extract(*args, **kwargs): - """Reuse the babel_extract function from jinja2.ext, but handle our own implementation of `_()`""" - for lineno, funcname, messages, comments in babel_extract(*args, **kwargs): - if funcname == "_" and isinstance(messages, tuple) and len(messages) > 1: - funcname = "pgettext" - messages = (messages[-1], messages[0]) # (context, message) - - yield lineno, funcname, messages, comments diff --git a/frappe/gettext/extractors/module_onboarding.py b/frappe/gettext/extractors/module_onboarding.py index 3468501074..518b995d8b 100644 --- a/frappe/gettext/extractors/module_onboarding.py +++ b/frappe/gettext/extractors/module_onboarding.py @@ -25,6 +25,4 @@ def extract(fileobj, *args, **kwargs): yield None, "_", subtitle, [f"Subtitle of the Module Onboarding '{onboarding_name}'"] if success_message := data.get("success_message"): - yield None, "_", success_message, [ - f"Success message of the Module Onboarding '{onboarding_name}'" - ] + yield None, "_", success_message, [f"Success message of the Module Onboarding '{onboarding_name}'"] diff --git a/frappe/gettext/extractors/navbar.py b/frappe/gettext/extractors/navbar.py index abf51cd9a5..924d3a4403 100644 --- a/frappe/gettext/extractors/navbar.py +++ b/frappe/gettext/extractors/navbar.py @@ -14,24 +14,34 @@ def extract(fileobj, *args, **kwargs): module = get_module(fileobj.name) if hasattr(module, "standard_navbar_items"): - standard_navbar_items = getattr(module, "standard_navbar_items") + standard_navbar_items = module.standard_navbar_items for nav_item in standard_navbar_items: if label := nav_item.get("item_label"): item_type = nav_item.get("item_type") - yield None, "_", label, [ - "Label of a standard navbar item", - f"Type: {item_type}", - ] + yield ( + None, + "_", + label, + [ + "Label of a standard navbar item", + f"Type: {item_type}", + ], + ) if hasattr(module, "standard_help_items"): - standard_help_items = getattr(module, "standard_help_items") + standard_help_items = module.standard_help_items for help_item in standard_help_items: if label := help_item.get("item_label"): item_type = nav_item.get("item_type") - yield None, "_", label, [ - "Label of a standard help item", - f"Type: {item_type}", - ] + yield ( + None, + "_", + label, + [ + "Label of a standard help item", + f"Type: {item_type}", + ], + ) def get_module(path): diff --git a/frappe/gettext/extractors/onboarding_step.py b/frappe/gettext/extractors/onboarding_step.py index 1188b5399e..70f36ada2d 100644 --- a/frappe/gettext/extractors/onboarding_step.py +++ b/frappe/gettext/extractors/onboarding_step.py @@ -27,6 +27,9 @@ def extract(fileobj, *args, **kwargs): yield None, "_", description, [f"Description of the Onboarding Step '{step_title}'"] if report_description := data.get("report_description"): - yield None, "_", report_description, [ - f"Description of a report in the Onboarding Step '{step_title}'" - ] + yield ( + None, + "_", + report_description, + [f"Description of a report in the Onboarding Step '{step_title}'"], + ) diff --git a/frappe/gettext/extractors/utils.py b/frappe/gettext/extractors/utils.py new file mode 100644 index 0000000000..e088a8409b --- /dev/null +++ b/frappe/gettext/extractors/utils.py @@ -0,0 +1,81 @@ +import re + +import frappe + +TRANSLATE_PATTERN = re.compile( + r"_\(\s*" # starts with literal `_(`, ignore following whitespace/newlines + # BEGIN: message search + r"([\"']{,3})" # start of message string identifier - allows: ', ", """, '''; 1st capture group + r"(?P((?!\1).)*)" # Keep matching until string closing identifier is met which is same as 1st capture group + r"\1" # match exact string closing identifier + # END: message search + # BEGIN: python context search + r"(\s*,\s*context\s*=\s*" # capture `context=` with ignoring whitespace + r"([\"'])" # start of context string identifier; 5th capture group + r"(?P((?!\5).)*)" # capture context string till closing id is found + r"\5" # match context string closure + r")?" # match 0 or 1 context strings + # END: python context search + # BEGIN: JS context search + r"(\s*,\s*(.)*?\s*(,\s*" # skip message format replacements: ["format", ...] | null | [] + r"([\"'])" # start of context string; 11th capture group + r"(?P((?!\11).)*)" # capture context string till closing id is found + r"\11" # match context string closure + r")*" + r")*" # match one or more context string + # END: JS context search + r"\s*\)" # Closing function call ignore leading whitespace/newlines +) + + +def extract_messages_from_code(code): + """ + Extracts translatable strings from a code file + :param code: code from which translatable files are to be extracted + """ + from jinja2 import TemplateError + + from frappe.model.utils import InvalidIncludePath, render_include + + try: + code = frappe.as_unicode(render_include(code)) + + # Exception will occur when it encounters John Resig's microtemplating code + except (TemplateError, ImportError, InvalidIncludePath, OSError) as e: + if isinstance(e, InvalidIncludePath) and hasattr(frappe.local, "message_log"): + frappe.clear_last_message() + + messages = [] + + for m in TRANSLATE_PATTERN.finditer(code): + message = m.group("message") + context = m.group("py_context") or m.group("js_context") + pos = m.start() + + if is_translatable(message): + messages.append([pos, message, context]) + + return add_line_number(messages, code) + + +def is_translatable(m): + return bool( + re.search("[a-zA-Z]", m) + and not m.startswith("fa fa-") + and not m.endswith("px") + and not m.startswith("eval:") + ) + + +def add_line_number(messages, code): + ret = [] + messages = sorted(messages, key=lambda x: x[0]) + newlines = [m.start() for m in re.compile(r"\n").finditer(code)] + line = 1 + newline_i = 0 + for pos, message, context in messages: + while newline_i < len(newlines) and pos > newlines[newline_i]: + line += 1 + newline_i += 1 + ret.append([line, message, context]) + return ret diff --git a/frappe/handler.py b/frappe/handler.py index d889c67b23..770ead5a5f 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -218,9 +218,7 @@ def upload_file(): frappe.local.uploaded_file = content frappe.local.uploaded_filename = filename - if content is not None and ( - frappe.session.user == "Guest" or (user and not user.has_desk_access()) - ): + if content is not None and (frappe.session.user == "Guest" or (user and not user.has_desk_access())): filetype = guess_type(filename)[0] if filetype not in ALLOWED_MIMETYPES: frappe.throw(_("You can only upload JPG, PNG, PDF, TXT or Microsoft documents.")) @@ -250,7 +248,7 @@ def check_write_permission(doctype: str = None, name: str = None): if doctype and name: try: doc = frappe.get_doc(doctype, name) - doc.has_permission("write") + doc.check_permission("write") except frappe.DoesNotExistError: # doc has not been inserted yet, name is set to "new-some-doctype" check_doctype = True diff --git a/frappe/hooks.py b/frappe/hooks.py index 36a7748588..907f337c7b 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -91,9 +91,7 @@ on_session_creation = [ "frappe.core.doctype.user.user.notify_admin_access_to_system_manager", ] -on_logout = ( - "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults" -) +on_logout = "frappe.core.doctype.session_default_settings.session_default_settings.clear_session_defaults" # PDF pdf_header_html = "frappe.utils.pdf.pdf_header_html" @@ -139,9 +137,7 @@ has_permission = { "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.has_permission", } -has_website_permission = { - "Address": "frappe.contacts.doctype.address.address.has_website_permission" -} +has_website_permission = {"Address": "frappe.contacts.doctype.address.address.has_website_permission"} jinja = { "methods": "frappe.utils.jinja_globals", @@ -532,15 +528,15 @@ standard_help_items = [ # log doctype cleanups to automatically add in log settings default_log_clearing_doctypes = { - "Error Log": 30, - "Activity Log": 90, + "Error Log": 14, "Email Queue": 30, - "Scheduled Job Log": 90, - "Route History": 90, - "Submission Queue": 30, - "Prepared Report": 30, + "Scheduled Job Log": 7, + "Submission Queue": 7, + "Prepared Report": 14, "Webhook Request Log": 30, - "Integration Request": 90, "Unhandled Email": 30, "Reminder": 30, + "Integration Request": 90, + "Activity Log": 90, + "Route History": 90, } diff --git a/frappe/installer.py b/frappe/installer.py index 1a4e8cbbb4..feeee06df9 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -12,6 +12,7 @@ from contextlib import suppress from shutil import which import click +from semantic_version import Version import frappe from frappe.defaults import _clear_cache @@ -20,9 +21,10 @@ from frappe.utils.dashboard import sync_dashboards from frappe.utils.synchronization import filelock -def _is_scheduler_enabled() -> bool: +def _is_scheduler_enabled(site) -> bool: enable_scheduler = False try: + frappe.init(site=site) frappe.connect() enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) except Exception: @@ -64,7 +66,7 @@ def _new_site( print("--no-mariadb-socket requires db_type to be set to mariadb.") sys.exit(1) - frappe.init(site=site, site_ready=False) + frappe.init(site=site) if not db_name: import hashlib @@ -78,7 +80,7 @@ def _new_site( try: # enable scheduler post install? - enable_scheduler = _is_scheduler_enabled() + enable_scheduler = _is_scheduler_enabled(site) except Exception: enable_scheduler = False @@ -103,9 +105,7 @@ def _new_site( setup=setup_db, ) - apps_to_install = ( - ["frappe"] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) - ) + apps_to_install = ["frappe"] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) for app in apps_to_install: # NOTE: not using force here for 2 reasons: @@ -170,7 +170,6 @@ def install_db( setup_database(force, verbose, no_mariadb_socket) bootstrap_database( - db_name=frappe.conf.db_name, verbose=verbose, source_sql=source_sql, ) @@ -451,9 +450,7 @@ def _delete_modules(modules: list[str], dry_run: bool) -> list[str]: return drop_doctypes -def _delete_linked_documents( - module_name: str, doctype_linkfield_map: dict[str, str], dry_run: bool -) -> None: +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"): @@ -557,7 +554,7 @@ def make_conf( ) sites_path = frappe.local.sites_path frappe.destroy() - frappe.init(site, sites_path=sites_path, site_ready=False) + frappe.init(site, sites_path=sites_path) def make_site_config( @@ -768,42 +765,43 @@ def extract_files(site_name, file_path): def is_downgrade(sql_file_path, verbose=False): - """checks if input db backup will get downgraded on current bench""" + """Check if input db backup will get downgraded on current bench - # This function is only tested with mariadb - # TODO: Add postgres support + This function is only tested with mariadb. + TODO: Add postgres support + """ if frappe.conf.db_type != "mariadb": return False - from semantic_version import Version - - 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 match := re.search(r"Frappe (\d+\.\d+\.\d+)", header[0]): - backup_version = match.group(1) + backup_version = get_backup_version(sql_file_path) or get_old_backup_version(sql_file_path) + current_version = Version(frappe.__version__) # Assume it's not a downgrade if we can't determine backup version if backup_version is None: return False - current_version = Version(frappe.__version__) - downgrade = Version(backup_version) < current_version + is_downgrade = backup_version > current_version - if verbose and downgrade: + if verbose and is_downgrade: print(f"Your site will be downgraded from Frappe {current_version} to {backup_version}") - return downgrade + return is_downgrade -def extract_version_from_dump(sql_file_path: str) -> str | None: +def get_old_backup_version(sql_file_path: str) -> Version | None: + """Return the frappe version used to create the specified database dump. + + This methods supports older versions of Frappe wich used a different format. """ - Extract frappe version from DB dump + header = get_db_dump_header(sql_file_path).split("\n") + if match := re.search(r"Frappe (\d+\.\d+\.\d+)", header[0]): + backup_version = match[1] - :param sql_file_path: The path to the dump file - :return: The frappe version used to create the backup - """ + return Version(backup_version) if backup_version else None + + +def get_backup_version(sql_file_path: str) -> Version | None: + """Return the frappe version used to create the specified database dump.""" header = get_db_dump_header(sql_file_path).split("\n") metadata = "" if "begin frappe metadata" in header[0]: @@ -813,7 +811,8 @@ def extract_version_from_dump(sql_file_path: str) -> str | None: metadata += line.replace("--", "").strip() + "\n" parser = configparser.ConfigParser() parser.read_string(metadata) - return parser["frappe"]["version"] + return Version(parser["frappe"]["version"]) + return None @@ -838,10 +837,10 @@ def partial_restore(sql_file_path, verbose=False): warn = click.style( "Delete the tables you want to restore manually before attempting" - " partial restore operation for PostreSQL databases", + " partial restore operation for PostgreSQL databases", fg="yellow", ) - warnings.warn(warn) + warnings.warn(warn, stacklevel=2) else: click.secho("Unsupported database type", fg="red") return diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index d571b2ba00..07f5c10b01 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -41,6 +41,7 @@ class ConnectedApp(Document): token_uri: DF.Data | None userinfo_uri: DF.Data | None # end: auto-generated types + """Connect to a remote oAuth Server. Retrieve and store user's access token in a Token Cache. """ diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 216b7defec..edcbba1c3f 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -44,6 +44,7 @@ class DropboxSettings(Document): send_email_for_successful_backup: DF.Check send_notifications_to: DF.Data # end: auto-generated types + def onload(self): if not self.app_access_key and frappe.conf.dropbox_access_key: self.set_onload("dropbox_setup_via_site_config", 1) @@ -105,7 +106,7 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): if isinstance(error_log, str): error_message = error_log + "\n" + frappe.get_traceback() else: - file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log)] + file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log, strict=False)] error_message = "\n".join(file_and_error) + "\n" + frappe.get_traceback() send_email(False, "Dropbox", "Dropbox Settings", "send_notifications_to", error_message) @@ -144,9 +145,7 @@ def backup_to_dropbox(upload_db_backup=True): return did_not_upload, list(set(error_log)) -def upload_from_folder( - path, is_private, dropbox_folder, dropbox_client, did_not_upload, error_log -): +def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not_upload, error_log): if not os.path.exists(path): return diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 8430e5c80c..e36fe8b2b8 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -83,6 +83,7 @@ class GoogleCalendar(Document): refresh_token: DF.Password | None user: DF.Link # end: auto-generated types + def validate(self): google_settings = frappe.get_single("Google Settings") if not google_settings.enable: @@ -239,7 +240,7 @@ def check_google_calendar(account, google_calendar): # If no Calendar ID create a new Calendar calendar = { "summary": account.calendar_name, - "timeZone": frappe.db.get_single_value("System Settings", "time_zone"), + "timeZone": frappe.get_system_settings("time_zone"), } created_calendar = google_calendar.calendars().insert(body=calendar).execute() frappe.db.set_value( @@ -376,9 +377,7 @@ def insert_event_to_calendar(account, event, recurrence=None): "pulled_from_google_calendar": 1, } calendar_event.update( - google_calendar_to_repeat_on( - recurrence=recurrence, start=event.get("start"), end=event.get("end") - ) + google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end")) ) frappe.get_doc(calendar_event).insert(ignore_permissions=True) @@ -392,9 +391,7 @@ def update_event_in_calendar(account, event, recurrence=None): calendar_event.description = event.get("description") calendar_event.google_meet_link = event.get("hangoutLink") calendar_event.update( - google_calendar_to_repeat_on( - recurrence=recurrence, start=event.get("start"), end=event.get("end") - ) + google_calendar_to_repeat_on(recurrence=recurrence, start=event.get("start"), end=event.get("end")) ) calendar_event.save(ignore_permissions=True) @@ -772,9 +769,7 @@ def get_attendees(doc): if participant.get("email"): attendees.append({"email": participant.email}) else: - email_not_found.append( - {"dt": participant.reference_doctype, "dn": participant.reference_docname} - ) + email_not_found.append({"dt": participant.reference_doctype, "dn": participant.reference_docname}) if email_not_found: frappe.msgprint( diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index fa316de026..8116419423 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -30,6 +30,7 @@ class GoogleContacts(Document): push_to_google_contacts: DF.Check refresh_token: DF.Password | None # end: auto-generated types + def validate(self): if not frappe.db.get_single_value("Google Settings", "enable"): frappe.throw(_("Enable Google API in Google Settings.")) @@ -176,12 +177,14 @@ def sync_contacts_from_google_contacts(g_contact): for email in connection.get("emailAddresses", []): contact.add_email( - email_id=email.get("value"), is_primary=1 if email.get("metadata").get("primary") else 0 + email_id=email.get("value"), + is_primary=1 if email.get("metadata").get("primary") else 0, ) for phone in connection.get("phoneNumbers", []): contact.add_phone( - phone=phone.get("value"), is_primary_phone=1 if phone.get("metadata").get("primary") else 0 + phone=phone.get("value"), + is_primary_phone=1 if phone.get("metadata").get("primary") else 0, ) contact.insert(ignore_permissions=True) diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index 394f43bbee..cd1c8c2577 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -41,6 +41,7 @@ class GoogleDrive(Document): refresh_token: DF.Data | None send_email_for_successful_backup: DF.Check # end: auto-generated types + def validate(self): doc_before_save = self.get_doc_before_save() if doc_before_save and doc_before_save.backup_folder_name != self.backup_folder_name: @@ -66,9 +67,7 @@ def authorize_access(reauthorize=False, code=None): Google Contact Name is set to flags to set_value after Authorization Code is obtained. """ - oauth_code = ( - frappe.db.get_single_value("Google Drive", "authorization_code") if not code else code - ) + oauth_code = frappe.db.get_single_value("Google Drive", "authorization_code") if not code else code oauth_obj = GoogleOAuth("drive") if not oauth_code or reauthorize: @@ -130,9 +129,7 @@ def check_for_folder_in_google_drive(): google_drive.files().list(q="mimeType='application/vnd.google-apps.folder'").execute() ) except HttpError as e: - frappe.throw( - _("Google Drive - Could not find folder in Google Drive - Error Code {0}").format(e) - ) + frappe.throw(_("Google Drive - Could not find folder in Google Drive - Error Code {0}").format(e)) for f in google_drive_folders.get("files"): if f.get("name") == account.backup_folder_name: diff --git a/frappe/integrations/doctype/integration_request/integration_request.py b/frappe/integrations/doctype/integration_request/integration_request.py index a81e702dfc..2271fe0ab6 100644 --- a/frappe/integrations/doctype/integration_request/integration_request.py +++ b/frappe/integrations/doctype/integration_request/integration_request.py @@ -30,6 +30,7 @@ class IntegrationRequest(Document): status: DF.Literal["", "Queued", "Authorized", "Completed", "Cancelled", "Failed"] url: DF.SmallText | None # end: auto-generated types + def autoname(self): if self.flags._name: self.name = self.flags._name diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py index f653d8da61..c2abf05d87 100644 --- a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py +++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py @@ -20,4 +20,5 @@ class LDAPGroupMapping(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 93d970d95d..011d6c88b4 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -63,6 +63,7 @@ class LDAPSettings(Document): require_trusted_certificate: DF.Literal["No", "Yes"] ssl_tls_mode: DF.Literal["Off", "StartTLS"] # end: auto-generated types + def validate(self): self.default_user_type = self.default_user_type or "Website User" @@ -77,7 +78,6 @@ class LDAPSettings(Document): and self.ldap_search_string and "{0}" in self.ldap_search_string ): - conn = self.connect_to_ldap( base_dn=self.base_dn, password=self.get_password(raise_exception=False) ) @@ -91,7 +91,9 @@ class LDAPSettings(Document): ) conn.search( - search_base=self.ldap_search_path_group, search_filter="(objectClass=*)", attributes=["cn"] + search_base=self.ldap_search_path_group, + search_filter="(objectClass=*)", + attributes=["cn"], ) except LDAPAttributeError as ex: @@ -190,9 +192,7 @@ class LDAPSettings(Document): lower_groups = [g.lower() for g in additional_groups or []] all_mapped_roles = {r.erpnext_role for r in self.ldap_groups} - matched_roles = { - r.erpnext_role for r in self.ldap_groups if r.ldap_group.lower() in lower_groups - } + matched_roles = {r.erpnext_role for r in self.ldap_groups if r.ldap_group.lower() in lower_groups} unmatched_roles = all_mapped_roles.difference(matched_roles) needed_roles.update(matched_roles) roles_to_remove = current_roles.intersection(unmatched_roles) @@ -337,9 +337,7 @@ class LDAPSettings(Document): def reset_password(self, user, password, logout_sessions=False): search_filter = f"({self.ldap_email_field}={user})" - conn = self.connect_to_ldap( - self.base_dn, self.get_password(raise_exception=False), read_only=False - ) + conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False), read_only=False) if conn.search( search_base=self.ldap_search_path_user, diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py index 8f9ebf712c..92918e8197 100644 --- a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py @@ -31,7 +31,6 @@ class LDAP_TestCase: def mock_ldap_connection(f): @functools.wraps(f) def wrapped(self, *args, **kwargs): - with mock.patch( "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap", return_value=self.connection, @@ -279,7 +278,8 @@ class LDAP_TestCase: ) self.assertTrue( - kwargs["raise_exceptions"], "ldap3.Connection must raise exceptions for error handling" + kwargs["raise_exceptions"], + "ldap3.Connection must raise exceptions for error handling", ) self.assertTrue( @@ -479,7 +479,9 @@ class LDAP_TestCase: # Existing user self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user]) - self.assertTrue(sync_roles_method.called, "User roles need to be updated for an existing user") + self.assertTrue( + sync_roles_method.called, "User roles need to be updated for an existing user" + ) self.assertTrue( update_user_fields_method.called, "User fields need to be updated for an existing user" ) @@ -487,7 +489,7 @@ class LDAP_TestCase: @mock_ldap_connection def test_get_ldap_attributes(self): method_return = self.test_class.get_ldap_attributes() - self.assertTrue(type(method_return) is list) + self.assertTrue(isinstance(method_return, list)) @mock_ldap_connection def test_fetch_ldap_groups(self): @@ -555,9 +557,7 @@ class LDAP_TestCase: if ( "ACCESS:test3" in search_filter ): # posix.user does not have str in ldap.description auth should fail - with self.assertRaises(frappe.exceptions.ValidationError) as display_massage: - self.test_class.authenticate("posix.user", "posix_user_password") self.assertTrue(str(display_massage.exception).lower() == "invalid username or password") @@ -599,7 +599,7 @@ class LDAP_TestCase: test_ldap_entry = self.connection.entries[0] method_return = self.test_class.convert_ldap_entry_to_dict(test_ldap_entry) - self.assertTrue(type(method_return) is dict) # must be dict + self.assertTrue(isinstance(method_return, dict)) # must be dict self.assertTrue(len(method_return) == 6) # there are 6 fields in mock_ldap for use diff --git a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py index cfc9009b0a..50e15b56ca 100644 --- a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py +++ b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py @@ -25,4 +25,5 @@ class OAuthAuthorizationCode(Document): user: DF.Link | None validity: DF.Literal["Valid", "Invalid"] # end: auto-generated types + pass diff --git a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py index f393d2bd77..6b6259da77 100644 --- a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py +++ b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py @@ -23,6 +23,7 @@ class OAuthBearerToken(Document): status: DF.Literal["Active", "Revoked"] user: DF.Link | None # end: auto-generated types + def validate(self): if not self.expiration_time: self.expiration_time = frappe.utils.datetime.datetime.strptime( diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.py b/frappe/integrations/doctype/oauth_client/oauth_client.py index d026822981..604dd02d89 100644 --- a/frappe/integrations/doctype/oauth_client/oauth_client.py +++ b/frappe/integrations/doctype/oauth_client/oauth_client.py @@ -26,6 +26,7 @@ class OAuthClient(Document): skip_authorization: DF.Check user: DF.Link | None # end: auto-generated types + def validate(self): self.client_id = self.name if not self.client_secret: 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 b66f7e9479..74fa9fdd80 100644 --- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py @@ -17,15 +17,12 @@ class OAuthProviderSettings(Document): skip_authorization: DF.Literal["Force", "Auto"] # end: auto-generated types + pass def get_oauth_settings(): """Return OAuth settings.""" return frappe._dict( - { - "skip_authorization": frappe.db.get_single_value( - "OAuth Provider Settings", "skip_authorization" - ) - } + {"skip_authorization": frappe.db.get_single_value("OAuth Provider Settings", "skip_authorization")} ) diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py index 8ab50d01fe..2506fd03eb 100644 --- a/frappe/integrations/doctype/oauth_scope/oauth_scope.py +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py @@ -19,4 +19,5 @@ class OAuthScope(Document): parenttype: DF.Data scope: DF.Data | None # end: auto-generated types + pass diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.py b/frappe/integrations/doctype/query_parameters/query_parameters.py index e69329176e..bb07f785e2 100644 --- a/frappe/integrations/doctype/query_parameters/query_parameters.py +++ b/frappe/integrations/doctype/query_parameters/query_parameters.py @@ -20,4 +20,5 @@ class QueryParameters(Document): parenttype: DF.Data value: DF.Data # end: auto-generated types + pass diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 1b583a2bd6..d37c94ef67 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -39,6 +39,7 @@ class S3BackupSettings(Document): secret_access_key: DF.Password send_email_for_successful_backup: DF.Check # end: auto-generated types + def validate(self): if not self.enabled: return @@ -112,7 +113,7 @@ def take_backups_s3(retry_count=0): "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", queue="long", timeout=1500, - **args + **args, ) else: notify() diff --git a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py index ff5f8e9761..e802199532 100644 --- a/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py +++ b/frappe/integrations/doctype/slack_webhook_url/slack_webhook_url.py @@ -32,6 +32,7 @@ class SlackWebhookURL(Document): webhook_name: DF.Data webhook_url: DF.Data # end: auto-generated types + pass 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 54f2c3ae1b..8dbea0ae55 100644 --- a/frappe/integrations/doctype/social_login_key/social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/social_login_key.py @@ -60,6 +60,7 @@ class SocialLoginKey(Document): ] user_id_property: DF.Data | None # end: auto-generated types + def autoname(self): self.name = frappe.scrub(self.provider_name) @@ -74,9 +75,7 @@ class SocialLoginKey(Document): if not self.redirect_url: frappe.throw(_("Please enter Redirect URL"), exc=RedirectUrlNotSetError) if self.enable_social_login and not self.client_id: - frappe.throw( - _("Please enter Client ID before social login is enabled"), exc=ClientIDNotSetError - ) + frappe.throw(_("Please enter Client ID before social login is enabled"), exc=ClientIDNotSetError) if self.enable_social_login and not self.client_secret: frappe.throw( _("Please enter Client Secret before social login is enabled"), exc=ClientSecretNotSetError diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 5619030499..b07f525c3d 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -32,6 +32,7 @@ class TokenCache(Document): token_type: DF.Data | None user: DF.Link | None # end: auto-generated types + def get_auth_header(self): if self.access_token: return {"Authorization": "Bearer " + self.get_password("access_token")} diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py index 1cd08aeca1..9cbab711ab 100644 --- a/frappe/integrations/doctype/webhook/__init__.py +++ b/frappe/integrations/doctype/webhook/__init__.py @@ -25,12 +25,7 @@ def run_webhooks(doc, method): frappe_flags = frappe.local.flags - if ( - frappe_flags.in_import - or frappe_flags.in_patch - or frappe_flags.in_install - or frappe_flags.in_migrate - ): + if frappe_flags.in_import or frappe_flags.in_patch or frappe_flags.in_install or frappe_flags.in_migrate: return # load all webhooks from cache / DB diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index c0148f5f67..eae78f3564 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -204,7 +204,6 @@ class TestWebhook(FrappeTestCase): self.assertTrue(frappe.get_all("Webhook Request Log", pluck="name")) def test_webhook_with_array_body(self): - """Check if array request body are supported.""" wh_config = { "doctype": "Webhook", diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index 1728da97d7..47edb8a401 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -91,7 +91,7 @@ }, { "fieldname": "request_url", - "fieldtype": "Data", + "fieldtype": "Small Text", "in_list_view": 1, "label": "Request URL", "reqd": 1 @@ -221,7 +221,7 @@ "link_fieldname": "webhook" } ], - "modified": "2023-06-16 10:21:00.971833", + "modified": "2024-02-05 17:49:50.203001", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 64d38a2ae7..7aad3da182 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -39,7 +39,7 @@ class Webhook(Document): preview_request_body: DF.Code | None request_method: DF.Literal["POST", "PUT", "DELETE"] request_structure: DF.Literal["", "Form URL-Encoded", "JSON"] - request_url: DF.Data + request_url: DF.SmallText timeout: DF.Int webhook_data: DF.Table[WebhookData] webhook_docevent: DF.Literal[ @@ -56,6 +56,7 @@ class Webhook(Document): webhook_json: DF.Code | None webhook_secret: DF.Password | None # end: auto-generated types + def validate(self): self.validate_docevent() self.validate_condition() diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.py b/frappe/integrations/doctype/webhook_data/webhook_data.py index 7461115dc5..72392aefb6 100644 --- a/frappe/integrations/doctype/webhook_data/webhook_data.py +++ b/frappe/integrations/doctype/webhook_data/webhook_data.py @@ -20,4 +20,5 @@ class WebhookData(Document): parentfield: DF.Data parenttype: DF.Data # end: auto-generated types + pass diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.py b/frappe/integrations/doctype/webhook_header/webhook_header.py index a313ebf495..fde5631f6d 100644 --- a/frappe/integrations/doctype/webhook_header/webhook_header.py +++ b/frappe/integrations/doctype/webhook_header/webhook_header.py @@ -14,10 +14,11 @@ class WebhookHeader(Document): if TYPE_CHECKING: from frappe.types import DF - key: DF.Data | None + key: DF.SmallText | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data - value: DF.Data | None + value: DF.SmallText | None # end: auto-generated types + pass diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py index 0b0b9aa060..98d963df93 100644 --- a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py +++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py @@ -23,6 +23,7 @@ class WebhookRequestLog(Document): user: DF.Link | None webhook: DF.Link | None # end: auto-generated types + @staticmethod def clear_old_logs(days=30): from frappe.query_builder import Interval diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py index bae811d41d..f276004427 100644 --- a/frappe/integrations/frappe_providers/frappecloud.py +++ b/frappe/integrations/frappe_providers/frappecloud.py @@ -12,11 +12,7 @@ def frappecloud_migrator(local_site): request = requests.get(request_url) if request.status_code / 100 != 2: - print( - "Request exitted with Status Code: {}\nPayload: {}".format( - request.status_code, html2text(request.text) - ) - ) + print(f"Request exited with Status Code: {request.status_code}\nPayload: {html2text(request.text)}") click.secho( "Some errors occurred while recovering the migration script. Please contact us @ Frappe Cloud if this issue persists", fg="yellow", diff --git a/frappe/integrations/google_oauth.py b/frappe/integrations/google_oauth.py index 7f24c611bf..c3a154f0e9 100644 --- a/frappe/integrations/google_oauth.py +++ b/frappe/integrations/google_oauth.py @@ -39,7 +39,7 @@ class GoogleOAuth: self.domain = domain.lower() self.scopes = ( " ".join(_SCOPES[self.domain]) - if isinstance(_SCOPES[self.domain], (list, tuple)) + if isinstance(_SCOPES[self.domain], list | tuple) else _SCOPES[self.domain] ) @@ -145,9 +145,7 @@ def handle_response( raise_err: bool = False, ): if "error" in response: - frappe.log_error( - frappe._(error_title), frappe._(response.get("error_description", error_message)) - ) + frappe.log_error(frappe._(error_title), frappe._(response.get("error_description", error_message))) if raise_err: frappe.throw(frappe._(error_title), GoogleAuthenticationError, frappe._(error_message)) @@ -158,9 +156,7 @@ def handle_response( def is_valid_access_token(access_token: str) -> bool: - response = get( - "https://oauth2.googleapis.com/tokeninfo", params={"access_token": access_token} - ).json() + response = get("https://oauth2.googleapis.com/tokeninfo", params={"access_token": access_token}).json() if "error" in response: return False @@ -196,6 +192,4 @@ def callback(state: str, code: str = None, error: str = None) -> None: ) frappe.local.response["type"] = "redirect" - frappe.local.response[ - "location" - ] = f"{redirect}?{failure_query_param if error else success_query_param}" + frappe.local.response["location"] = f"{redirect}?{failure_query_param if error else success_query_param}" diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index 724d3b32a7..41f2e67b04 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -49,9 +49,10 @@ def approve(*args, **kwargs): r = frappe.request try: - (scopes, frappe.flags.oauth_credentials,) = get_oauth_server().validate_authorization_request( - r.url, r.method, r.get_data(), r.headers - ) + ( + scopes, + frappe.flags.oauth_credentials, + ) = get_oauth_server().validate_authorization_request(r.url, r.method, r.get_data(), r.headers) headers, body, status = get_oauth_server().create_authorization_response( uri=frappe.flags.oauth_credentials["redirect_uri"], @@ -72,23 +73,20 @@ def approve(*args, **kwargs): @frappe.whitelist(allow_guest=True) def authorize(**kwargs): - success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params( - sanitize_kwargs(kwargs) - ) + success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params(sanitize_kwargs(kwargs)) failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" if frappe.session.user == "Guest": # Force login, redirect to preauth again. frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/login?" + encode_params( - {"redirect-to": frappe.request.url} - ) + frappe.local.response["location"] = "/login?" + encode_params({"redirect-to": frappe.request.url}) else: try: r = frappe.request - (scopes, frappe.flags.oauth_credentials,) = get_oauth_server().validate_authorization_request( - r.url, r.method, r.get_data(), r.headers - ) + ( + scopes, + frappe.flags.oauth_credentials, + ) = get_oauth_server().validate_authorization_request(r.url, r.method, r.get_data(), r.headers) skip_auth = frappe.db.get_value( "OAuth Client", diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py index e2e2fed40f..8cd29d5ecc 100644 --- a/frappe/integrations/offsite_backup_utils.py +++ b/frappe/integrations/offsite_backup_utils.py @@ -29,21 +29,16 @@ def send_email(success, service_name, doctype, email_field, error_status=None): ) else: subject = "[Warning] Backup Upload Failed" - message = """ + message = f"""

Backup Upload Failed!

-

Oops, your automated backup to {} failed.

-

Error message: {}

-

Please contact your system manager for more information.

""".format( - service_name, error_status - ) +

Oops, your automated backup to {service_name} failed.

+

Error message: {error_status}

+

Please contact your system manager for more information.

""" frappe.sendmail(recipients=recipients, subject=subject, message=message) def get_recipients(doctype, email_field): - if not frappe.db: - frappe.connect() - return split_emails(frappe.db.get_value(doctype, None, email_field)) diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 14ae944192..c350fdb280 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -10,24 +10,24 @@ from frappe import _ from frappe.utils import get_request_session -def make_request( - method: str, url: str, 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 {} try: s = get_request_session() - frappe.flags.integration_request = s.request( + response = frappe.flags.integration_request = s.request( method, url, data=data, auth=auth, headers=headers, json=json, params=params ) - frappe.flags.integration_request.raise_for_status() + response.raise_for_status() - if frappe.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8": - return parse_qs(frappe.flags.integration_request.text) - - return frappe.flags.integration_request.json() + if response.headers.get("content-type") == "text/plain; charset=utf-8": + return parse_qs(response.text) + elif not response.text: + return + else: + response.json() except Exception as exc: frappe.log_error() raise exc @@ -157,5 +157,5 @@ def get_json(obj): def json_handler(obj): - if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): + if isinstance(obj, datetime.date | datetime.timedelta | datetime.datetime): return str(obj) diff --git a/frappe/locale/main.pot b/frappe/locale/main.pot index 863603eb01..ae11132373 100644 --- a/frappe/locale/main.pot +++ b/frappe/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Frappe Framework VERSION\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" -"POT-Creation-Date: 2024-01-12 01:53+0053\n" -"PO-Revision-Date: 2024-01-12 01:53+0053\n" +"POT-Creation-Date: 2024-01-29 18:10+0053\n" +"PO-Revision-Date: 2024-01-29 18:10+0053\n" "Last-Translator: developers@frappe.io\n" "Language-Team: developers@frappe.io\n" "MIME-Version: 1.0\n" @@ -188,7 +188,7 @@ msgstr "" msgid "'In Global Search' is not allowed for field {0} of type {1}" msgstr "" -#: core/doctype/doctype/doctype.py:1305 +#: core/doctype/doctype/doctype.py:1301 msgid "'In Global Search' not allowed for type {0} in row {1}" msgstr "" @@ -208,7 +208,7 @@ msgstr "" msgid "'{0}' is not a valid URL" msgstr "" -#: core/doctype/doctype/doctype.py:1299 +#: core/doctype/doctype/doctype.py:1295 msgid "'{0}' not allowed for type {1} in row {2}" msgstr "" @@ -229,7 +229,7 @@ msgctxt "Web Page" msgid "0 is highest" msgstr "" -#: public/js/frappe/form/grid_row.js:786 +#: public/js/frappe/form/grid_row.js:806 msgid "1 = True & 0 = False" msgstr "" @@ -253,7 +253,7 @@ msgstr "" msgid "1 comment" msgstr "" -#: tests/test_utils.py:647 +#: tests/test_utils.py:648 msgid "1 day ago" msgstr "" @@ -261,15 +261,15 @@ msgstr "" msgid "1 hour" msgstr "" -#: public/js/frappe/utils/pretty_date.js:52 tests/test_utils.py:645 +#: public/js/frappe/utils/pretty_date.js:52 tests/test_utils.py:646 msgid "1 hour ago" msgstr "" -#: public/js/frappe/utils/pretty_date.js:48 tests/test_utils.py:643 +#: public/js/frappe/utils/pretty_date.js:48 tests/test_utils.py:644 msgid "1 minute ago" msgstr "" -#: public/js/frappe/utils/pretty_date.js:66 tests/test_utils.py:651 +#: public/js/frappe/utils/pretty_date.js:66 tests/test_utils.py:652 msgid "1 month ago" msgstr "" @@ -277,35 +277,35 @@ msgstr "" msgid "1 record will be exported" msgstr "" -#: tests/test_utils.py:642 +#: tests/test_utils.py:643 msgid "1 second ago" msgstr "" -#: public/js/frappe/utils/pretty_date.js:62 tests/test_utils.py:649 +#: public/js/frappe/utils/pretty_date.js:62 tests/test_utils.py:650 msgid "1 week ago" msgstr "" -#: public/js/frappe/utils/pretty_date.js:70 tests/test_utils.py:653 +#: public/js/frappe/utils/pretty_date.js:70 tests/test_utils.py:654 msgid "1 year ago" msgstr "" -#: tests/test_utils.py:646 +#: tests/test_utils.py:647 msgid "2 hours ago" msgstr "" -#: tests/test_utils.py:652 +#: tests/test_utils.py:653 msgid "2 months ago" msgstr "" -#: tests/test_utils.py:650 +#: tests/test_utils.py:651 msgid "2 weeks ago" msgstr "" -#: tests/test_utils.py:654 +#: tests/test_utils.py:655 msgid "2 years ago" msgstr "" -#: tests/test_utils.py:644 +#: tests/test_utils.py:645 msgid "3 minutes ago" msgstr "" @@ -321,7 +321,7 @@ msgstr "" msgid "5 Records" msgstr "" -#: tests/test_utils.py:648 +#: tests/test_utils.py:649 msgid "5 days ago" msgstr "" @@ -343,7 +343,7 @@ msgctxt "Document Naming Rule Condition" msgid "<=" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:564 +#: public/js/frappe/widgets/widget_dialog.js:570 msgid "{0} is not a valid URL" msgstr "" @@ -715,7 +715,7 @@ msgstr "" msgid "A DocType (Document Type) is used to insert forms in ERPNext. Forms such as Customer, Orders, and Invoices are Doctypes in the backend. You can also create new DocTypes to create new forms in ERPNext as per your business needs." msgstr "" -#: core/doctype/doctype/doctype.py:1015 +#: core/doctype/doctype/doctype.py:1011 msgid "A DocType's name should start with a letter and can only consist of letters, numbers, spaces, underscores and hyphens" msgstr "" @@ -983,7 +983,7 @@ msgctxt "Social Login Key" msgid "Access Token URL" msgstr "" -#: auth.py:444 +#: auth.py:455 msgid "Access not allowed from this IP Address" msgstr "" @@ -1001,7 +1001,7 @@ msgstr "" #. Name of a role #: automation/doctype/auto_repeat/auto_repeat.json -#: contacts/doctype/contact/contact.json +#: contacts/doctype/contact/contact.json geo/doctype/currency/currency.json msgid "Accounts Manager" msgstr "" @@ -1202,9 +1202,9 @@ msgstr "" #: core/page/permission_manager/permission_manager.js:465 #: email/doctype/email_group/email_group.js:60 -#: public/js/frappe/form/grid_row.js:468 +#: public/js/frappe/form/grid_row.js:469 #: public/js/frappe/form/sidebar/assign_to.js:100 -#: public/js/frappe/list/bulk_operations.js:372 +#: public/js/frappe/list/bulk_operations.js:393 #: public/js/frappe/views/dashboard/dashboard_view.js:440 #: public/js/frappe/views/reports/query_report.js:265 #: public/js/frappe/views/reports/query_report.js:293 @@ -1283,7 +1283,7 @@ msgid "Add Custom Tags" msgstr "" #: public/js/frappe/widgets/widget_dialog.js:159 -#: public/js/frappe/widgets/widget_dialog.js:683 +#: public/js/frappe/widgets/widget_dialog.js:689 msgid "Add Filters" msgstr "" @@ -1346,16 +1346,16 @@ msgstr "" msgid "Add Subscribers" msgstr "" -#: public/js/frappe/list/bulk_operations.js:360 +#: public/js/frappe/list/bulk_operations.js:381 msgid "Add Tags" msgstr "" -#: public/js/frappe/list/list_view.js:1834 +#: public/js/frappe/list/list_view.js:1858 msgctxt "Button in list view actions menu" msgid "Add Tags" msgstr "" -#: public/js/frappe/views/communication.js:320 +#: public/js/frappe/views/communication.js:362 msgid "Add Template" msgstr "" @@ -1381,7 +1381,7 @@ msgctxt "Event" msgid "Add Video Conferencing" msgstr "" -#: public/js/frappe/form/form_tour.js:203 +#: public/js/frappe/form/form_tour.js:205 msgid "Add a Row" msgstr "" @@ -1557,7 +1557,7 @@ msgstr "" msgid "Addresses And Contacts" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:536 +#: public/js/frappe/ui/toolbar/search_utils.js:552 msgid "Administration" msgstr "" @@ -1604,8 +1604,8 @@ msgctxt "User Permission" msgid "Advanced Control" msgstr "" -#: public/js/frappe/form/controls/link.js:315 -#: public/js/frappe/form/controls/link.js:317 +#: public/js/frappe/form/controls/link.js:316 +#: public/js/frappe/form/controls/link.js:318 msgid "Advanced Search" msgstr "" @@ -2398,7 +2398,7 @@ msgstr "" msgid "App not found for module: {0}" msgstr "" -#: __init__.py:1677 +#: __init__.py:1731 msgid "App {0} is not installed" msgstr "" @@ -2477,7 +2477,7 @@ msgctxt "Property Setter" msgid "Applied On" msgstr "" -#: public/js/frappe/list/list_view.js:1819 +#: public/js/frappe/list/list_view.js:1843 msgctxt "Button in list view actions menu" msgid "Apply Assignment Rule" msgstr "" @@ -2579,11 +2579,15 @@ msgstr "" msgid "Archived Columns" msgstr "" +#: public/js/frappe/list/list_view.js:1822 +msgid "Are you sure you want to clear the assignments?" +msgstr "" + #: public/js/frappe/form/grid.js:269 msgid "Are you sure you want to delete all rows?" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:885 +#: public/js/frappe/views/workspace/workspace.js:891 msgid "Are you sure you want to delete page {0}?" msgstr "" @@ -2765,10 +2769,6 @@ msgctxt "Assignment Rule" msgid "Assignment Days" msgstr "" -#: automation/doctype/assignment_rule/assignment_rule.py:64 -msgid "Assignment Day{0} {1} has been repeated." -msgstr "" - #. Name of a DocType #: automation/doctype/assignment_rule/assignment_rule.json msgid "Assignment Rule" @@ -2803,7 +2803,7 @@ msgstr "" msgid "Assignment Rule User" msgstr "" -#: automation/doctype/assignment_rule/assignment_rule.py:53 +#: automation/doctype/assignment_rule/assignment_rule.py:54 msgid "Assignment Rule is not allowed on {0} document type" msgstr "" @@ -2835,16 +2835,16 @@ msgctxt "Notification Settings" msgid "Assignments" msgstr "" -#: public/js/frappe/form/grid_row.js:629 +#: public/js/frappe/form/grid_row.js:649 msgid "At least one column is required to show in the grid." msgstr "" #: website/doctype/web_form/web_form.js:64 -msgid "Atleast one field is required in Web Form Fields Table" +msgid "At least one field is required in Web Form Fields Table" msgstr "" #: core/doctype/data_export/data_export.js:44 -msgid "Atleast one field of Parent Document Type is mandatory" +msgid "At least one field of Parent Document Type is mandatory" msgstr "" #: public/js/frappe/form/controls/attach.js:5 @@ -3252,7 +3252,7 @@ msgctxt "Email Account" msgid "Auto Reply Message" msgstr "" -#: automation/doctype/assignment_rule/assignment_rule.py:179 +#: automation/doctype/assignment_rule/assignment_rule.py:176 msgid "Auto assignment failed: {0}" msgstr "" @@ -3327,11 +3327,11 @@ msgctxt "User" msgid "Automatic" msgstr "" -#: email/doctype/email_account/email_account.py:675 +#: email/doctype/email_account/email_account.py:677 msgid "Automatic Linking can be activated only for one Email Account." msgstr "" -#: email/doctype/email_account/email_account.py:670 +#: email/doctype/email_account/email_account.py:672 msgid "Automatic Linking can be activated only if Incoming is enabled." msgstr "" @@ -3543,7 +3543,11 @@ msgctxt "System Settings" msgid "Background Workers" msgstr "" -#: integrations/doctype/google_drive/google_drive.js:31 +#: integrations/doctype/google_drive/google_drive.py:173 +msgid "Backing up Data." +msgstr "" + +#: integrations/doctype/google_drive/google_drive.js:32 msgid "Backing up to Google Drive." msgstr "" @@ -4142,11 +4146,11 @@ msgstr "" msgid "Bulk Delete" msgstr "" -#: public/js/frappe/list/bulk_operations.js:256 +#: public/js/frappe/list/bulk_operations.js:277 msgid "Bulk Edit" msgstr "" -#: public/js/frappe/form/grid.js:1151 +#: public/js/frappe/form/grid.js:1153 msgid "Bulk Edit {0}" msgstr "" @@ -4453,7 +4457,7 @@ msgstr "" msgid "Camera" msgstr "" -#: public/js/frappe/utils/utils.js:1711 +#: public/js/frappe/utils/utils.js:1712 #: website/report/website_analytics/website_analytics.js:39 msgid "Campaign" msgstr "" @@ -4480,7 +4484,7 @@ msgstr "" msgid "Can not rename as column {0} is already present on DocType." msgstr "" -#: core/doctype/doctype/doctype.py:1114 +#: core/doctype/doctype/doctype.py:1110 msgid "Can only change to/from Autoincrement naming rule when there is no data in the doctype" msgstr "" @@ -4495,12 +4499,12 @@ msgstr "" msgid "Can't rename {0} to {1} because {0} doesn't exist." msgstr "" -#: core/doctype/doctype/doctype_list.js:113 +#: core/doctype/doctype/doctype_list.js:130 #: public/js/frappe/form/reminders.js:54 msgid "Cancel" msgstr "" -#: public/js/frappe/list/list_view.js:1889 +#: public/js/frappe/list/list_view.js:1913 msgctxt "Button in list view actions menu" msgid "Cancel" msgstr "" @@ -4553,7 +4557,7 @@ msgstr "" msgid "Cancel Scheduling" msgstr "" -#: public/js/frappe/list/list_view.js:1894 +#: public/js/frappe/list/list_view.js:1918 msgctxt "Title of confirmation dialog" msgid "Cancel {0} documents?" msgstr "" @@ -4622,7 +4626,7 @@ msgstr "" msgid "Cannot Remove" msgstr "" -#: model/base_document.py:1034 +#: model/base_document.py:1053 msgid "Cannot Update After Submit" msgstr "" @@ -4638,7 +4642,7 @@ msgstr "" msgid "Cannot cancel before submitting. See Transition {0}" msgstr "" -#: public/js/frappe/list/bulk_operations.js:229 +#: public/js/frappe/list/bulk_operations.js:250 msgid "Cannot cancel {0}." msgstr "" @@ -4658,7 +4662,7 @@ msgstr "" msgid "Cannot change state of Cancelled Document. Transition row {0}" msgstr "" -#: core/doctype/doctype/doctype.py:1104 +#: core/doctype/doctype/doctype.py:1100 msgid "Cannot change to/from autoincrement autoname in Customize Form" msgstr "" @@ -4666,7 +4670,7 @@ msgstr "" msgid "Cannot create a {0} against a child document: {1}" msgstr "" -#: desk/doctype/workspace/workspace.py:250 +#: desk/doctype/workspace/workspace.py:254 msgid "Cannot create private workspace of other users" msgstr "" @@ -4678,11 +4682,11 @@ msgstr "" msgid "Cannot delete or cancel because {0} {1} is linked with {2} {3} {4}" msgstr "" -#: desk/doctype/workspace/workspace.py:417 +#: desk/doctype/workspace/workspace.py:423 msgid "Cannot delete private workspace of other users" msgstr "" -#: desk/doctype/workspace/workspace.py:410 +#: desk/doctype/workspace/workspace.py:416 msgid "Cannot delete public workspace without Workspace Manager role" msgstr "" @@ -4786,16 +4790,16 @@ msgstr "" msgid "Cannot share {0} with submit permission as the doctype {1} is not submittable" msgstr "" -#: public/js/frappe/list/bulk_operations.js:226 +#: public/js/frappe/list/bulk_operations.js:247 msgid "Cannot submit {0}." msgstr "" -#: desk/doctype/workspace/workspace.py:351 +#: desk/doctype/workspace/workspace.py:357 msgid "Cannot update private workspace of other users" msgstr "" #: desk/doctype/bulk_update/bulk_update.js:26 -#: public/js/frappe/list/bulk_operations.js:301 +#: public/js/frappe/list/bulk_operations.js:322 msgid "Cannot update {0}" msgstr "" @@ -4807,7 +4811,7 @@ msgstr "" msgid "Cannot use {0} in order/group by" msgstr "" -#: public/js/frappe/list/bulk_operations.js:232 +#: public/js/frappe/list/bulk_operations.js:253 msgid "Cannot {0} {1}." msgstr "" @@ -4835,7 +4839,7 @@ msgstr "" msgid "Card Label" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:227 +#: public/js/frappe/widgets/widget_dialog.js:233 msgid "Card Links" msgstr "" @@ -5133,11 +5137,11 @@ msgctxt "Form Tour Step" msgid "Child Doctype" msgstr "" -#: core/doctype/doctype/doctype.py:1588 +#: core/doctype/doctype/doctype.py:1584 msgid "Child Table {0} for field {1}" msgstr "" -#: core/doctype/doctype/doctype_list.js:37 +#: core/doctype/doctype/doctype_list.js:52 msgid "Child Tables are shown as a Grid in other DocTypes" msgstr "" @@ -5147,11 +5151,11 @@ msgctxt "DocType" msgid "Child Tables are shown as a Grid in other DocTypes" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:614 +#: public/js/frappe/widgets/widget_dialog.js:620 msgid "Choose Existing Card or create New Card" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1385 +#: public/js/frappe/views/workspace/workspace.js:1391 msgid "Choose a block or continue typing" msgstr "" @@ -5186,7 +5190,7 @@ msgstr "" msgid "Clear" msgstr "" -#: public/js/frappe/views/communication.js:325 +#: public/js/frappe/views/communication.js:367 msgid "Clear & Add Template" msgstr "" @@ -5194,6 +5198,11 @@ msgstr "" msgid "Clear & Add template" msgstr "" +#: public/js/frappe/list/list_view.js:1819 +msgctxt "Button in list view actions menu" +msgid "Clear Assignment" +msgstr "" + #: public/js/frappe/ui/keyboard.js:275 msgid "Clear Cache and Reload" msgstr "" @@ -5212,7 +5221,7 @@ msgstr "" msgid "Clear User Permissions" msgstr "" -#: public/js/frappe/views/communication.js:326 +#: public/js/frappe/views/communication.js:368 msgid "Clear the email message and add the template" msgstr "" @@ -5228,7 +5237,7 @@ msgstr "" msgid "Click here to verify" msgstr "" -#: integrations/doctype/google_drive/google_drive.js:46 +#: integrations/doctype/google_drive/google_drive.js:47 msgid "Click on Authorize Google Drive Access to authorize Google Drive Access." msgstr "" @@ -5465,7 +5474,7 @@ msgctxt "OAuth Authorization Code" msgid "Code challenge method" msgstr "" -#: public/js/frappe/form/form_tour.js:268 +#: public/js/frappe/form/form_tour.js:270 #: public/js/frappe/widgets/base_widget.js:157 msgid "Collapse" msgstr "" @@ -5517,8 +5526,8 @@ msgstr "" #. Name of a DocType #: public/js/frappe/views/reports/query_report.js:1140 -#: public/js/frappe/widgets/widget_dialog.js:505 -#: public/js/frappe/widgets/widget_dialog.js:657 +#: public/js/frappe/widgets/widget_dialog.js:511 +#: public/js/frappe/widgets/widget_dialog.js:663 #: website/doctype/color/color.json msgid "Color" msgstr "" @@ -5665,7 +5674,7 @@ msgstr "" msgid "Column Name cannot be empty" msgstr "" -#: public/js/frappe/form/grid_row.js:593 +#: public/js/frappe/form/grid_row.js:613 msgid "Column width cannot be zero." msgstr "" @@ -6332,7 +6341,7 @@ msgctxt "Web Page" msgid "Content Type" msgstr "" -#: desk/doctype/workspace/workspace.py:79 +#: desk/doctype/workspace/workspace.py:82 msgid "Content data shoud be a list" msgstr "" @@ -6392,7 +6401,7 @@ msgctxt "Social Login Key" msgid "Controls whether new users can sign up using this Social Login Key. If unset, Website Settings is respected. " msgstr "" -#: public/js/frappe/utils/utils.js:1030 +#: public/js/frappe/utils/utils.js:1031 msgid "Copied to clipboard." msgstr "" @@ -6452,11 +6461,11 @@ msgctxt "Number Card" msgid "Count" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:499 +#: public/js/frappe/widgets/widget_dialog.js:505 msgid "Count Customizations" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:484 +#: public/js/frappe/widgets/widget_dialog.js:490 msgid "Count Filter" msgstr "" @@ -6531,7 +6540,7 @@ msgstr "" #: public/js/frappe/views/file/file_view.js:112 #: public/js/frappe/views/interaction.js:18 #: public/js/frappe/views/reports/query_report.js:1172 -#: public/js/frappe/views/workspace/workspace.js:1217 +#: public/js/frappe/views/workspace/workspace.js:1223 #: workflow/page/workflow_builder/workflow_builder.js:46 msgid "Create" msgstr "" @@ -6554,7 +6563,7 @@ msgctxt "User Document Type" msgid "Create" msgstr "" -#: core/doctype/doctype/doctype_list.js:85 +#: core/doctype/doctype/doctype_list.js:102 msgid "Create & Continue" msgstr "" @@ -6584,7 +6593,7 @@ msgstr "" msgid "Create Custom Fields" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:925 +#: public/js/frappe/views/workspace/workspace.js:931 msgid "Create Duplicate" msgstr "" @@ -6606,7 +6615,7 @@ msgstr "" msgid "Create New" msgstr "" -#: core/doctype/doctype/doctype_list.js:83 +#: core/doctype/doctype/doctype_list.js:100 msgid "Create New DocType" msgstr "" @@ -6618,7 +6627,7 @@ msgstr "" msgid "Create User Email" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:465 +#: public/js/frappe/views/workspace/workspace.js:471 msgid "Create Workspace" msgstr "" @@ -6626,7 +6635,7 @@ msgstr "" msgid "Create a Reminder" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:521 +#: public/js/frappe/ui/toolbar/search_utils.js:537 msgid "Create a new ..." msgstr "" @@ -6634,8 +6643,8 @@ msgstr "" msgid "Create a new record" msgstr "" -#: public/js/frappe/form/controls/link.js:291 -#: public/js/frappe/form/controls/link.js:293 +#: public/js/frappe/form/controls/link.js:292 +#: public/js/frappe/form/controls/link.js:294 #: public/js/frappe/form/link_selector.js:139 #: public/js/frappe/list/list_view.js:470 msgid "Create a new {0}" @@ -7103,7 +7112,7 @@ msgctxt "Translation" msgid "Custom Translation" msgstr "" -#: core/doctype/doctype/doctype_list.js:65 +#: core/doctype/doctype/doctype_list.js:82 msgid "Custom?" msgstr "" @@ -7149,7 +7158,7 @@ msgstr "" msgid "Customization onboarding is all done!" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:511 +#: public/js/frappe/views/workspace/workspace.js:517 msgid "Customizations Discarded" msgstr "" @@ -7340,7 +7349,7 @@ msgstr "" msgid "Daily Event Digest is sent for Calendar Events where reminders are set." msgstr "" -#: desk/doctype/event/event.py:93 +#: desk/doctype/event/event.py:94 msgid "Daily Events should finish on the Same Day." msgstr "" @@ -7381,7 +7390,7 @@ msgstr "" #. Name of a DocType #: core/page/dashboard_view/dashboard_view.js:10 #: desk/doctype/dashboard/dashboard.json -#: public/js/frappe/ui/toolbar/search_utils.js:546 +#: public/js/frappe/ui/toolbar/search_utils.js:562 msgid "Dashboard" msgstr "" @@ -7571,7 +7580,7 @@ msgstr "" msgid "Data Too Long" msgstr "" -#: model/base_document.py:703 +#: model/base_document.py:722 msgid "Data missing in table" msgstr "" @@ -8000,11 +8009,11 @@ msgctxt "DocType" msgid "Default View" msgstr "" -#: core/doctype/doctype/doctype.py:1327 +#: core/doctype/doctype/doctype.py:1323 msgid "Default for 'Check' type of field {0} must be either '0' or '1'" msgstr "" -#: core/doctype/doctype/doctype.py:1340 +#: core/doctype/doctype/doctype.py:1336 msgid "Default value for {0} must be in the list of options." msgstr "" @@ -8049,12 +8058,12 @@ msgstr "" #: public/js/frappe/form/toolbar.js:423 #: public/js/frappe/views/reports/report_view.js:1645 #: public/js/frappe/views/treeview.js:313 -#: public/js/frappe/views/workspace/workspace.js:823 +#: public/js/frappe/views/workspace/workspace.js:829 #: templates/discussions/reply_card.html:35 msgid "Delete" msgstr "" -#: public/js/frappe/list/list_view.js:1857 +#: public/js/frappe/list/list_view.js:1881 msgctxt "Button in list view actions menu" msgid "Delete" msgstr "" @@ -8089,11 +8098,11 @@ msgstr "" msgid "Delete Kanban Board" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:824 +#: public/js/frappe/views/workspace/workspace.js:830 msgid "Delete Workspace" msgstr "" -#: public/js/frappe/form/footer/form_timeline.js:696 +#: public/js/frappe/form/footer/form_timeline.js:719 msgid "Delete comment?" msgstr "" @@ -8101,12 +8110,12 @@ msgstr "" msgid "Delete this record to allow sending to this email address" msgstr "" -#: public/js/frappe/list/list_view.js:1862 +#: public/js/frappe/list/list_view.js:1886 msgctxt "Title of confirmation dialog" msgid "Delete {0} item permanently?" msgstr "" -#: public/js/frappe/list/list_view.js:1868 +#: public/js/frappe/list/list_view.js:1892 msgctxt "Title of confirmation dialog" msgid "Delete {0} items permanently?" msgstr "" @@ -8235,6 +8244,7 @@ msgid "Descendants Of (inclusive)" msgstr "" #: desk/report/todo/todo.py:39 public/js/frappe/form/reminders.js:44 +#: public/js/frappe/widgets/widget_dialog.js:227 msgid "Description" msgstr "" @@ -8335,6 +8345,12 @@ msgctxt "Website Slideshow Item" msgid "Description" msgstr "" +#. Label of a HTML Editor field in DocType 'Workspace Link' +#: desk/doctype/workspace_link/workspace_link.json +msgctxt "Workspace Link" +msgid "Description" +msgstr "" + #. Description of the 'Blog Intro' (Small Text) field in DocType 'Blog Post' #: website/doctype/blog_post/blog_post.json msgctxt "Blog Post" @@ -8634,7 +8650,7 @@ msgstr "" #: public/js/frappe/views/communication.js:30 #: public/js/frappe/views/dashboard/dashboard_view.js:70 -#: public/js/frappe/views/workspace/workspace.js:502 +#: public/js/frappe/views/workspace/workspace.js:508 #: public/js/frappe/web_form/web_form.js:187 #: website/doctype/web_form/templates/web_form.html:41 msgid "Discard" @@ -8695,7 +8711,7 @@ msgctxt "LDAP Settings" msgid "Do not create new user if user with email does not exist in the system" msgstr "" -#: public/js/frappe/form/grid.js:1156 +#: public/js/frappe/form/grid.js:1158 msgid "Do not edit headers which are preset in the template" msgstr "" @@ -8852,7 +8868,7 @@ msgctxt "Workspace Shortcut" msgid "DocType" msgstr "" -#: core/doctype/doctype/doctype.py:1528 +#: core/doctype/doctype/doctype.py:1524 msgid "DocType {0} provided for the field {1} must have atleast one Link field" msgstr "" @@ -8895,7 +8911,7 @@ msgctxt "Property Setter" msgid "DocType Link" msgstr "" -#: core/doctype/doctype/doctype_list.js:10 +#: core/doctype/doctype/doctype_list.js:22 msgid "DocType Name" msgstr "" @@ -8916,11 +8932,11 @@ msgctxt "Workspace Shortcut" msgid "DocType View" msgstr "" -#: core/doctype/doctype/doctype.py:646 +#: core/doctype/doctype/doctype.py:642 msgid "DocType can not be merged" msgstr "" -#: core/doctype/doctype/doctype.py:640 +#: core/doctype/doctype/doctype.py:636 msgid "DocType can only be renamed by Administrator" msgstr "" @@ -8958,7 +8974,7 @@ msgstr "" msgid "DocType {} not found" msgstr "" -#: core/doctype/doctype/doctype.py:1009 +#: core/doctype/doctype/doctype.py:1005 msgid "DocType's name should not start or end with whitespace" msgstr "" @@ -8966,7 +8982,7 @@ msgstr "" msgid "DocTypes can not be modified, please use {0} instead" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:645 +#: public/js/frappe/widgets/widget_dialog.js:651 msgid "Doctype" msgstr "" @@ -8976,7 +8992,7 @@ msgctxt "Document Follow" msgid "Doctype" msgstr "" -#: core/doctype/doctype/doctype.py:1004 +#: core/doctype/doctype/doctype.py:1000 msgid "Doctype name is limited to {0} characters ({1})" msgstr "" @@ -8984,7 +9000,7 @@ msgstr "" msgid "Doctype required" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1303 +#: public/js/frappe/views/workspace/workspace.js:1309 msgid "Doctype with same route already exist. Please choose different title." msgstr "" @@ -9058,24 +9074,24 @@ msgctxt "Customize Form" msgid "Document Links" msgstr "" -#: core/doctype/doctype/doctype.py:1162 +#: core/doctype/doctype/doctype.py:1158 msgid "Document Links Row #{0}: Could not find field {1} in {2} DocType" msgstr "" -#: core/doctype/doctype/doctype.py:1182 +#: core/doctype/doctype/doctype.py:1178 msgid "Document Links Row #{0}: Invalid doctype or fieldname." msgstr "" -#: core/doctype/doctype/doctype.py:1145 +#: core/doctype/doctype/doctype.py:1141 msgid "Document Links Row #{0}: Parent DocType is mandatory for internal links" msgstr "" -#: core/doctype/doctype/doctype.py:1151 +#: core/doctype/doctype/doctype.py:1147 msgid "Document Links Row #{0}: Table Fieldname is mandatory for internal links" msgstr "" #: core/doctype/user_permission/user_permission_list.js:36 -#: public/js/frappe/form/form_tour.js:58 +#: public/js/frappe/form/form_tour.js:60 msgid "Document Name" msgstr "" @@ -9602,7 +9618,7 @@ msgstr "" #: public/js/frappe/views/workspace/blocks/header.js:46 #: public/js/frappe/views/workspace/blocks/paragraph.js:136 #: public/js/frappe/views/workspace/blocks/spacer.js:44 -#: public/js/frappe/views/workspace/workspace.js:565 +#: public/js/frappe/views/workspace/workspace.js:571 #: public/js/frappe/widgets/base_widget.js:33 msgid "Drag" msgstr "" @@ -9653,8 +9669,8 @@ msgid "Due Date Based On" msgstr "" #: public/js/frappe/form/toolbar.js:377 -#: public/js/frappe/views/workspace/workspace.js:808 -#: public/js/frappe/views/workspace/workspace.js:975 +#: public/js/frappe/views/workspace/workspace.js:814 +#: public/js/frappe/views/workspace/workspace.js:981 msgid "Duplicate" msgstr "" @@ -9666,12 +9682,12 @@ msgstr "" msgid "Duplicate Filter Name" msgstr "" -#: model/base_document.py:563 model/rename_doc.py:113 +#: model/base_document.py:582 model/rename_doc.py:113 msgid "Duplicate Name" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:547 -#: public/js/frappe/views/workspace/workspace.js:809 +#: public/js/frappe/views/workspace/workspace.js:553 +#: public/js/frappe/views/workspace/workspace.js:815 msgid "Duplicate Workspace" msgstr "" @@ -9679,7 +9695,7 @@ msgstr "" msgid "Duplicate current row" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:990 +#: public/js/frappe/views/workspace/workspace.js:996 msgid "Duplicate of {0} named as {1} is created successfully" msgstr "" @@ -9811,12 +9827,12 @@ msgstr "" #: printing/page/print_format_builder_beta/print_format_builder_beta.js:46 #: printing/page/print_format_builder_beta/print_format_builder_beta.js:85 #: public/js/frappe/form/controls/markdown_editor.js:31 -#: public/js/frappe/form/footer/form_timeline.js:638 +#: public/js/frappe/form/footer/form_timeline.js:661 #: public/js/frappe/form/toolbar.js:672 #: public/js/frappe/views/reports/query_report.js:809 #: public/js/frappe/views/reports/query_report.js:1617 -#: public/js/frappe/views/workspace/workspace.js:448 -#: public/js/frappe/views/workspace/workspace.js:802 +#: public/js/frappe/views/workspace/workspace.js:454 +#: public/js/frappe/views/workspace/workspace.js:808 #: public/js/frappe/widgets/base_widget.js:64 #: public/js/frappe/widgets/chart_widget.js:298 #: public/js/frappe/widgets/number_card_widget.js:314 @@ -9826,7 +9842,7 @@ msgstr "" msgid "Edit" msgstr "" -#: public/js/frappe/list/list_view.js:1943 +#: public/js/frappe/list/list_view.js:1967 msgctxt "Button in list view actions menu" msgid "Edit" msgstr "" @@ -9903,7 +9919,7 @@ msgctxt "Website Settings" msgid "Edit Values" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:803 +#: public/js/frappe/views/workspace/workspace.js:809 msgid "Edit Workspace" msgstr "" @@ -9923,7 +9939,7 @@ msgstr "" msgid "Edit {0}" msgstr "" -#: core/doctype/doctype/doctype_list.js:41 +#: core/doctype/doctype/doctype_list.js:57 msgid "Editable Grid" msgstr "" @@ -10373,7 +10389,7 @@ msgstr "" msgid "Email has been moved to trash" msgstr "" -#: public/js/frappe/views/communication.js:707 +#: public/js/frappe/views/communication.js:749 msgid "Email not sent to {0} (unsubscribed / disabled)" msgstr "" @@ -10696,7 +10712,7 @@ msgstr "" msgid "Enabled Scheduler" msgstr "" -#: email/doctype/email_account/email_account.py:896 +#: email/doctype/email_account/email_account.py:898 msgid "Enabled email inbox for user {0}" msgstr "" @@ -10873,7 +10889,7 @@ msgstr "" msgid "Enter Client Id and Client Secret in Google Settings." msgstr "" -#: public/js/frappe/views/communication.js:663 +#: public/js/frappe/views/communication.js:705 msgid "Enter Email Recipient(s)" msgstr "" @@ -10888,7 +10904,7 @@ msgctxt "Title of prompt dialog" msgid "Enter Value" msgstr "" -#: public/js/frappe/form/form_tour.js:56 +#: public/js/frappe/form/form_tour.js:58 msgid "Enter a name for this {0}" msgstr "" @@ -10939,8 +10955,8 @@ msgstr "" msgid "Equals" msgstr "" -#: desk/page/backups/backups.js:35 model/base_document.py:703 -#: model/base_document.py:708 public/js/frappe/ui/messages.js:22 +#: desk/page/backups/backups.js:35 model/base_document.py:722 +#: model/base_document.py:727 public/js/frappe/ui/messages.js:22 msgid "Error" msgstr "" @@ -11053,7 +11069,7 @@ msgstr "" msgid "Error in print format on line {0}: {1}" msgstr "" -#: email/doctype/email_account/email_account.py:586 +#: email/doctype/email_account/email_account.py:588 msgid "Error while connecting to email account {0}" msgstr "" @@ -11065,7 +11081,7 @@ msgstr "" msgid "Error: Document has been modified after you have opened it" msgstr "" -#: model/base_document.py:716 +#: model/base_document.py:735 msgid "Error: Value missing for {0}: {1}" msgstr "" @@ -11132,7 +11148,7 @@ msgctxt "Recorder" msgid "Event Type" msgstr "" -#: desk/doctype/event/event.py:263 +#: desk/doctype/event/event.py:264 msgid "Events in Today's Calendar" msgstr "" @@ -11334,7 +11350,7 @@ msgstr "" msgid "Export" msgstr "" -#: public/js/frappe/list/list_view.js:1965 +#: public/js/frappe/list/list_view.js:1989 msgctxt "Button in list view actions menu" msgid "Export" msgstr "" @@ -11531,7 +11547,7 @@ msgstr "" msgid "Failed to connect to server" msgstr "" -#: auth.py:649 +#: auth.py:660 msgid "Failed to decode token, please provide a valid base64-encoded token." msgstr "" @@ -11672,7 +11688,7 @@ msgid "Fetching default Global Search documents." msgstr "" #: desk/page/leaderboard/leaderboard.js:131 -#: public/js/frappe/list/bulk_operations.js:262 +#: public/js/frappe/list/bulk_operations.js:283 #: public/js/frappe/views/reports/query_report.js:235 #: public/js/frappe/views/reports/query_report.js:1706 msgid "Field" @@ -11720,11 +11736,11 @@ msgctxt "Web Form List Column" msgid "Field" msgstr "" -#: core/doctype/doctype/doctype.py:419 +#: core/doctype/doctype/doctype.py:415 msgid "Field \"route\" is mandatory for Web Views" msgstr "" -#: core/doctype/doctype/doctype.py:1477 +#: core/doctype/doctype/doctype.py:1473 msgid "Field \"title\" is mandatory if \"Website Search Field\" is set." msgstr "" @@ -11738,7 +11754,7 @@ msgctxt "Custom Field" msgid "Field Description" msgstr "" -#: core/doctype/doctype/doctype.py:1040 +#: core/doctype/doctype/doctype.py:1036 msgid "Field Missing" msgstr "" @@ -11786,7 +11802,7 @@ msgstr "" msgid "Field type cannot be changed for {0}" msgstr "" -#: database/database.py:783 +#: database/database.py:828 msgid "Field {0} does not exist on {1}" msgstr "" @@ -11844,11 +11860,11 @@ msgctxt "Webhook Data" msgid "Fieldname" msgstr "" -#: core/doctype/doctype/doctype.py:270 +#: core/doctype/doctype/doctype.py:266 msgid "Fieldname '{0}' conflicting with a {1} of the name {2} in {3}" msgstr "" -#: core/doctype/doctype/doctype.py:1039 +#: core/doctype/doctype/doctype.py:1035 msgid "Fieldname called {0} must exist to enable autonaming" msgstr "" @@ -11872,11 +11888,11 @@ msgstr "" msgid "Fieldname {0} cannot have special characters like {1}" msgstr "" -#: core/doctype/doctype/doctype.py:1850 +#: core/doctype/doctype/doctype.py:1846 msgid "Fieldname {0} conflicting with meta object" msgstr "" -#: core/doctype/doctype/doctype.py:495 public/js/form_builder/utils.js:302 +#: core/doctype/doctype/doctype.py:491 public/js/form_builder/utils.js:302 msgid "Fieldname {0} is restricted" msgstr "" @@ -12148,11 +12164,11 @@ msgctxt "Prepared Report" msgid "Filter Values" msgstr "" -#: utils/data.py:2021 +#: utils/data.py:2015 msgid "Filter must be a tuple or list (in a list)" msgstr "" -#: utils/data.py:2029 +#: utils/data.py:2023 msgid "Filter must have 4 values (doctype, fieldname, operator, value): {0}" msgstr "" @@ -12255,7 +12271,7 @@ msgctxt "Number Card" msgid "Filters Section" msgstr "" -#: public/js/frappe/form/controls/link.js:486 +#: public/js/frappe/form/controls/link.js:488 msgid "Filters applied for {0}" msgstr "" @@ -12269,14 +12285,14 @@ msgctxt "Report" msgid "Filters will be accessible via filters.

Send output as result = [result], or for old style data = [columns], [result]" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:556 +#: public/js/frappe/ui/toolbar/search_utils.js:572 msgid "Find '{0}' in ..." msgstr "" -#: public/js/frappe/ui/toolbar/awesome_bar.js:325 -#: public/js/frappe/ui/toolbar/awesome_bar.js:326 -#: public/js/frappe/ui/toolbar/search_utils.js:125 -#: public/js/frappe/ui/toolbar/search_utils.js:128 +#: public/js/frappe/ui/toolbar/awesome_bar.js:327 +#: public/js/frappe/ui/toolbar/awesome_bar.js:328 +#: public/js/frappe/ui/toolbar/search_utils.js:141 +#: public/js/frappe/ui/toolbar/search_utils.js:144 msgid "Find {0} in {1}" msgstr "" @@ -12410,11 +12426,11 @@ msgctxt "Report Filter" msgid "Fold" msgstr "" -#: core/doctype/doctype/doctype.py:1401 +#: core/doctype/doctype/doctype.py:1397 msgid "Fold can not be at the end of the form" msgstr "" -#: core/doctype/doctype/doctype.py:1399 +#: core/doctype/doctype/doctype.py:1395 msgid "Fold must come before a Section Break" msgstr "" @@ -12460,7 +12476,7 @@ msgstr "" msgid "Following fields have invalid values:" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:314 +#: public/js/frappe/widgets/widget_dialog.js:320 msgid "Following fields have missing values" msgstr "" @@ -12630,7 +12646,7 @@ msgstr "" msgid "For Document Type" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:529 +#: public/js/frappe/widgets/widget_dialog.js:535 msgid "For Example: {} Open" msgstr "" @@ -12723,7 +12739,7 @@ msgstr "" msgid "For updating, you can update only selective columns." msgstr "" -#: core/doctype/doctype/doctype.py:1692 +#: core/doctype/doctype/doctype.py:1688 msgid "For {0} at level {1} in {2} in row {3}" msgstr "" @@ -12870,7 +12886,7 @@ msgctxt "Webhook" msgid "Form URL-Encoded" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:528 +#: public/js/frappe/widgets/widget_dialog.js:534 msgid "Format" msgstr "" @@ -13108,7 +13124,7 @@ msgid "Full Width" msgstr "" #: public/js/frappe/views/reports/query_report.js:245 -#: public/js/frappe/widgets/widget_dialog.js:666 +#: public/js/frappe/widgets/widget_dialog.js:672 msgid "Function" msgstr "" @@ -13118,11 +13134,11 @@ msgctxt "Number Card" msgid "Function" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:673 +#: public/js/frappe/widgets/widget_dialog.js:679 msgid "Function Based On" msgstr "" -#: __init__.py:835 +#: __init__.py:875 msgid "Function {0} is not whitelisted." msgstr "" @@ -13206,12 +13222,12 @@ msgstr "" msgid "Generate New Report" msgstr "" -#: public/js/frappe/ui/toolbar/awesome_bar.js:366 +#: public/js/frappe/ui/toolbar/awesome_bar.js:368 msgid "Generate Random Password" msgstr "" #: public/js/frappe/ui/toolbar/toolbar.js:137 -#: public/js/frappe/utils/utils.js:1750 +#: public/js/frappe/utils/utils.js:1751 msgid "Generate Tracking URL" msgstr "" @@ -13838,6 +13854,12 @@ msgctxt "Auto Email Report" msgid "Half Yearly" msgstr "" +#. Option for the 'Repeat On' (Select) field in DocType 'Event' +#: desk/doctype/event/event.json +msgctxt "Event" +msgid "Half Yearly" +msgstr "" + #: public/js/frappe/utils/common.js:402 msgid "Half-yearly" msgstr "" @@ -14066,7 +14088,7 @@ msgctxt "Print Settings" msgid "Helvetica Neue" msgstr "" -#: public/js/frappe/utils/utils.js:1747 +#: public/js/frappe/utils/utils.js:1748 msgid "Here's your tracking URL" msgstr "" @@ -14134,7 +14156,7 @@ msgctxt "Form Tour Step" msgid "Hidden Fields" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:814 +#: public/js/frappe/views/workspace/workspace.js:820 #: public/js/frappe/widgets/base_widget.js:46 #: public/js/frappe/widgets/base_widget.js:176 msgid "Hide" @@ -14291,7 +14313,7 @@ msgstr "" msgid "Hide Weekends" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:815 +#: public/js/frappe/views/workspace/workspace.js:821 msgid "Hide Workspace" msgstr "" @@ -14501,9 +14523,9 @@ msgctxt "Comment" msgid "IP Address" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:632 -#: public/js/frappe/views/workspace/workspace.js:960 -#: public/js/frappe/views/workspace/workspace.js:1205 +#: public/js/frappe/views/workspace/workspace.js:638 +#: public/js/frappe/views/workspace/workspace.js:966 +#: public/js/frappe/views/workspace/workspace.js:1211 msgid "Icon" msgstr "" @@ -14606,7 +14628,7 @@ msgctxt "Workflow Document State" msgid "If Checked workflow status will not override status in list view" msgstr "" -#: core/doctype/doctype/doctype.py:1706 +#: core/doctype/doctype/doctype.py:1702 msgid "If Owner" msgstr "" @@ -14973,11 +14995,11 @@ msgctxt "Letter Head" msgid "Image Width" msgstr "" -#: core/doctype/doctype/doctype.py:1457 +#: core/doctype/doctype/doctype.py:1453 msgid "Image field must be a valid fieldname" msgstr "" -#: core/doctype/doctype/doctype.py:1459 +#: core/doctype/doctype/doctype.py:1455 msgid "Image field must be of type Attach Image" msgstr "" @@ -15221,7 +15243,7 @@ msgstr "" msgid "In Progress" msgstr "" -#: database/database.py:233 +#: database/database.py:241 msgid "In Read Only Mode" msgstr "" @@ -15351,7 +15373,7 @@ msgstr "" msgid "Incomplete Virtual Doctype Implementation" msgstr "" -#: auth.py:236 +#: auth.py:238 msgid "Incomplete login details" msgstr "" @@ -15433,9 +15455,9 @@ msgctxt "Workspace" msgid "Indicator Color" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:639 -#: public/js/frappe/views/workspace/workspace.js:967 -#: public/js/frappe/views/workspace/workspace.js:1211 +#: public/js/frappe/views/workspace/workspace.js:645 +#: public/js/frappe/views/workspace/workspace.js:973 +#: public/js/frappe/views/workspace/workspace.js:1217 msgid "Indicator color" msgstr "" @@ -15499,7 +15521,7 @@ msgstr "" msgid "Insert Column Before {0}" msgstr "" -#: public/js/frappe/form/controls/markdown_editor.js:81 +#: public/js/frappe/form/controls/markdown_editor.js:82 msgid "Insert Image in Markdown" msgstr "" @@ -15515,8 +15537,8 @@ msgctxt "Web Page" msgid "Insert Style" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:646 -#: public/js/frappe/ui/toolbar/search_utils.js:647 +#: public/js/frappe/ui/toolbar/search_utils.js:662 +#: public/js/frappe/ui/toolbar/search_utils.js:663 msgid "Install {0} from Marketplace" msgstr "" @@ -15540,11 +15562,11 @@ msgstr "" msgid "Installed Apps" msgstr "" -#: permissions.py:826 +#: permissions.py:829 msgid "Insufficient Permission Level for {0}" msgstr "" -#: database/query.py:371 desk/form/load.py:40 model/document.py:234 +#: database/query.py:372 desk/form/load.py:40 model/document.py:234 msgid "Insufficient Permission for {0}" msgstr "" @@ -15556,7 +15578,7 @@ msgstr "" msgid "Insufficient Permissions for editing Report" msgstr "" -#: core/doctype/doctype/doctype.py:447 +#: core/doctype/doctype/doctype.py:443 msgid "Insufficient attachment limit" msgstr "" @@ -15704,7 +15726,7 @@ msgctxt "OAuth Authorization Code" msgid "Invalid" msgstr "" -#: public/js/form_builder/utils.js:221 public/js/frappe/form/grid_row.js:748 +#: public/js/form_builder/utils.js:221 public/js/frappe/form/grid_row.js:768 #: public/js/frappe/form/layout.js:774 msgid "Invalid \"depends_on\" expression" msgstr "" @@ -15741,11 +15763,11 @@ msgstr "" msgid "Invalid DocType" msgstr "" -#: database/query.py:95 +#: database/query.py:96 msgid "Invalid DocType: {0}" msgstr "" -#: core/doctype/doctype/doctype.py:1223 +#: core/doctype/doctype/doctype.py:1219 msgid "Invalid Fieldname" msgstr "" @@ -15785,7 +15807,7 @@ msgstr "" msgid "Invalid Operation" msgstr "" -#: core/doctype/doctype/doctype.py:1582 core/doctype/doctype/doctype.py:1591 +#: core/doctype/doctype/doctype.py:1578 core/doctype/doctype/doctype.py:1587 msgid "Invalid Option" msgstr "" @@ -15811,7 +15833,7 @@ msgstr "" msgid "Invalid Phone Number" msgstr "" -#: auth.py:93 utils/oauth.py:184 utils/oauth.py:191 www/login.py:112 +#: auth.py:95 utils/oauth.py:184 utils/oauth.py:191 www/login.py:112 msgid "Invalid Request" msgstr "" @@ -15819,7 +15841,7 @@ msgstr "" msgid "Invalid Search Field {0}" msgstr "" -#: core/doctype/doctype/doctype.py:1165 +#: core/doctype/doctype/doctype.py:1161 msgid "Invalid Table Fieldname" msgstr "" @@ -15827,7 +15849,7 @@ msgstr "" msgid "Invalid Transition" msgstr "" -#: core/doctype/file/file.py:217 public/js/frappe/widgets/widget_dialog.js:565 +#: core/doctype/file/file.py:217 public/js/frappe/widgets/widget_dialog.js:571 #: utils/csvutils.py:199 utils/csvutils.py:220 msgid "Invalid URL" msgstr "" @@ -15860,11 +15882,11 @@ msgstr "" msgid "Invalid expression set in filter {0} ({1})" msgstr "" -#: utils/data.py:2128 +#: utils/data.py:2122 msgid "Invalid field name {0}" msgstr "" -#: core/doctype/doctype/doctype.py:1048 +#: core/doctype/doctype/doctype.py:1044 msgid "Invalid fieldname '{0}' in autoname" msgstr "" @@ -15872,7 +15894,7 @@ msgstr "" msgid "Invalid file path: {0}" msgstr "" -#: database/query.py:173 public/js/frappe/ui/filters/filter_list.js:199 +#: database/query.py:174 public/js/frappe/ui/filters/filter_list.js:199 msgid "Invalid filter: {0}" msgstr "" @@ -15923,7 +15945,7 @@ msgctxt "Error message in web form" msgid "Invalid values for fields:" msgstr "" -#: core/doctype/doctype/doctype.py:1515 +#: core/doctype/doctype/doctype.py:1511 msgid "Invalid {0} condition" msgstr "" @@ -15965,7 +15987,7 @@ msgctxt "DocType" msgid "Is Calendar and Gantt" msgstr "" -#: core/doctype/doctype/doctype_list.js:34 +#: core/doctype/doctype/doctype_list.js:49 msgid "Is Child Table" msgstr "" @@ -16127,7 +16149,7 @@ msgctxt "DocType" msgid "Is Published Field" msgstr "" -#: core/doctype/doctype/doctype.py:1466 +#: core/doctype/doctype/doctype.py:1462 msgid "Is Published Field must be a valid fieldname" msgstr "" @@ -16143,7 +16165,7 @@ msgctxt "Integration Request" msgid "Is Remote Request?" msgstr "" -#: core/doctype/doctype/doctype_list.js:48 +#: core/doctype/doctype/doctype_list.js:64 msgid "Is Single" msgstr "" @@ -16225,7 +16247,7 @@ msgctxt "Web Form" msgid "Is Standard" msgstr "" -#: core/doctype/doctype/doctype_list.js:25 +#: core/doctype/doctype/doctype_list.js:39 msgid "Is Submittable" msgstr "" @@ -16451,7 +16473,7 @@ msgstr "" msgid "Job is not running." msgstr "" -#: desk/doctype/event/event.js:51 +#: desk/doctype/event/event.js:55 msgid "Join video conference with {0}" msgstr "" @@ -16745,8 +16767,9 @@ msgid "LDAP settings incorrect. validation response was: {0}" msgstr "" #: printing/page/print_format_builder/print_format_builder.js:474 -#: public/js/frappe/widgets/widget_dialog.js:606 -#: public/js/frappe/widgets/widget_dialog.js:639 +#: public/js/frappe/widgets/widget_dialog.js:222 +#: public/js/frappe/widgets/widget_dialog.js:612 +#: public/js/frappe/widgets/widget_dialog.js:645 msgid "Label" msgstr "" @@ -17116,7 +17139,7 @@ msgid "Leave blank to repeat always" msgstr "" #: core/doctype/communication/mixins.py:206 -#: email/doctype/email_account/email_account.py:624 +#: email/doctype/email_account/email_account.py:626 msgid "Leave this conversation" msgstr "" @@ -17518,6 +17541,12 @@ msgstr "" msgid "Link Expired" msgstr "" +#. Label of a Int field in DocType 'System Settings' +#: core/doctype/system_settings/system_settings.json +msgctxt "System Settings" +msgid "Link Field Results Limit" +msgstr "" + #. Label of a Data field in DocType 'DocType Link' #: core/doctype/doctype_link/doctype_link.json msgctxt "DocType Link" @@ -17755,7 +17784,7 @@ msgctxt "Web Page" msgid "List as [{\"label\": _(\"Jobs\"), \"route\":\"jobs\"}]" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:526 +#: public/js/frappe/ui/toolbar/search_utils.js:542 msgid "Lists" msgstr "" @@ -17928,7 +17957,7 @@ msgstr "" msgid "Login is required to see web form list view. Enable {0} to see list settings" msgstr "" -#: auth.py:322 auth.py:325 +#: auth.py:324 auth.py:327 msgid "Login not allowed at this time" msgstr "" @@ -17968,7 +17997,7 @@ msgctxt "System Settings" msgid "Login with email link expiry (in minutes)" msgstr "" -#: auth.py:131 +#: auth.py:133 msgid "Login with username and password is not allowed." msgstr "" @@ -18407,7 +18436,7 @@ msgctxt "System Settings" msgid "Max auto email report per user" msgstr "" -#: core/doctype/doctype/doctype.py:1293 +#: core/doctype/doctype/doctype.py:1289 msgid "Max width for type Currency is 100px in row {0}" msgstr "" @@ -18455,7 +18484,7 @@ msgid "Me" msgstr "" #: public/js/frappe/form/sidebar/assign_to.js:194 -#: public/js/frappe/utils/utils.js:1719 +#: public/js/frappe/utils/utils.js:1720 #: website/report/website_analytics/website_analytics.js:40 msgid "Medium" msgstr "" @@ -18551,7 +18580,7 @@ msgctxt "Communication" msgid "Message" msgstr "" -#: __init__.py:527 public/js/frappe/ui/messages.js:267 +#: __init__.py:567 public/js/frappe/ui/messages.js:267 msgctxt "Default title of the message dialog" msgid "Message" msgstr "" @@ -18641,7 +18670,7 @@ msgctxt "Notification" msgid "Message Type" msgstr "" -#: public/js/frappe/views/communication.js:841 +#: public/js/frappe/views/communication.js:883 msgid "Message clipped" msgstr "" @@ -18857,7 +18886,7 @@ msgstr "" msgid "Missing DocType" msgstr "" -#: core/doctype/doctype/doctype.py:1477 +#: core/doctype/doctype/doctype.py:1473 msgid "Missing Field" msgstr "" @@ -18878,7 +18907,7 @@ msgid "Missing Value" msgstr "" #: public/js/frappe/ui/field_group.js:118 -#: public/js/frappe/widgets/widget_dialog.js:330 +#: public/js/frappe/widgets/widget_dialog.js:336 #: public/js/workflow_builder/store.js:97 #: workflow/doctype/workflow/workflow.js:71 msgid "Missing Values Required" @@ -18921,7 +18950,7 @@ msgstr "" msgid "Modified By" msgstr "" -#: core/doctype/doctype/doctype_list.js:17 +#: core/doctype/doctype/doctype_list.js:30 msgid "Module" msgstr "" @@ -19641,11 +19670,11 @@ msgctxt "Role" msgid "Navigation Settings" msgstr "" -#: desk/doctype/workspace/workspace.py:297 +#: desk/doctype/workspace/workspace.py:303 msgid "Need Workspace Manager role to edit private workspace of other users" msgstr "" -#: desk/doctype/workspace/workspace.py:343 +#: desk/doctype/workspace/workspace.py:349 msgid "Need Workspace Manager role to hide/unhide public workspaces" msgstr "" @@ -19776,7 +19805,7 @@ msgstr "" msgid "New Workflow Name" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1172 +#: public/js/frappe/views/workspace/workspace.js:1178 msgid "New Workspace" msgstr "" @@ -19784,7 +19813,7 @@ msgstr "" msgid "New password cannot be same as old password" msgstr "" -#: utils/change_log.py:306 +#: utils/change_log.py:322 msgid "New updates are available" msgstr "" @@ -19805,10 +19834,10 @@ msgstr "" #: public/js/frappe/form/quick_entry.js:124 public/js/frappe/form/toolbar.js:36 #: public/js/frappe/form/toolbar.js:196 public/js/frappe/form/toolbar.js:209 #: public/js/frappe/form/toolbar.js:490 -#: public/js/frappe/ui/toolbar/search_utils.js:151 -#: public/js/frappe/ui/toolbar/search_utils.js:152 -#: public/js/frappe/ui/toolbar/search_utils.js:201 -#: public/js/frappe/ui/toolbar/search_utils.js:202 +#: public/js/frappe/ui/toolbar/search_utils.js:167 +#: public/js/frappe/ui/toolbar/search_utils.js:168 +#: public/js/frappe/ui/toolbar/search_utils.js:217 +#: public/js/frappe/ui/toolbar/search_utils.js:218 #: public/js/frappe/views/treeview.js:350 #: website/doctype/web_form/web_form.py:310 msgid "New {0}" @@ -19831,7 +19860,7 @@ msgstr "" msgid "New {0}: {1}" msgstr "" -#: utils/change_log.py:298 +#: utils/change_log.py:314 msgid "New {} releases for the following apps are available" msgstr "" @@ -19886,7 +19915,7 @@ msgstr "" msgid "Newsletters" msgstr "" -#: public/js/frappe/form/form_tour.js:316 +#: public/js/frappe/form/form_tour.js:318 #: public/js/onboarding_tours/onboarding_tours.js:15 #: public/js/onboarding_tours/onboarding_tours.js:240 #: templates/includes/slideshow.html:38 website/utils.py:247 @@ -19960,7 +19989,7 @@ msgstr "" #: integrations/doctype/webhook/webhook.py:137 #: public/js/form_builder/utils.js:341 -#: public/js/frappe/form/controls/link.js:471 +#: public/js/frappe/form/controls/link.js:472 #: public/js/frappe/list/list_sidebar_group_by.js:223 #: public/js/frappe/views/reports/query_report.js:1513 #: website/doctype/help_article/templates/help_article.html:26 @@ -20074,7 +20103,7 @@ msgstr "" msgid "No Name Specified for {0}" msgstr "" -#: core/doctype/doctype/doctype.py:1684 +#: core/doctype/doctype/doctype.py:1680 msgid "No Permissions Specified" msgstr "" @@ -20130,7 +20159,7 @@ msgstr "" msgid "No changes made because old and new name are the same." msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1477 +#: public/js/frappe/views/workspace/workspace.js:1483 msgid "No changes made on the page" msgstr "" @@ -20216,7 +20245,7 @@ msgctxt "SMS Log" msgid "No of Sent SMS" msgstr "" -#: __init__.py:1027 client.py:109 client.py:151 +#: __init__.py:1067 client.py:109 client.py:151 msgid "No permission for {0}" msgstr "" @@ -20350,7 +20379,7 @@ msgctxt "DocField" msgid "Not Nullable" msgstr "" -#: __init__.py:921 app.py:354 desk/calendar.py:26 geo/utils.py:97 +#: __init__.py:961 app.py:354 desk/calendar.py:26 geo/utils.py:97 #: public/js/frappe/web_form/webform_script.js:15 #: website/doctype/web_form/web_form.py:603 #: website/page_renderers/not_permitted_page.py:20 www/login.py:177 @@ -20422,7 +20451,7 @@ msgstr "" msgid "Not active" msgstr "" -#: permissions.py:367 +#: permissions.py:370 msgid "Not allowed for {0}: {1}" msgstr "" @@ -20430,7 +20459,7 @@ msgstr "" msgid "Not allowed to attach {0} document, please enable Allow Print For {0} in Print Settings" msgstr "" -#: core/doctype/doctype/doctype.py:338 +#: core/doctype/doctype/doctype.py:334 msgid "Not allowed to create custom Virtual DocType." msgstr "" @@ -20454,12 +20483,12 @@ msgstr "" msgid "Not in Developer Mode" msgstr "" -#: core/doctype/doctype/doctype.py:332 +#: core/doctype/doctype/doctype.py:328 msgid "Not in Developer Mode! Set in site_config.json or make 'Custom' DocType." msgstr "" #: api/v1.py:88 api/v1.py:93 -#: core/doctype/system_settings/system_settings.py:199 handler.py:109 +#: core/doctype/system_settings/system_settings.py:208 handler.py:109 #: public/js/frappe/request.js:157 public/js/frappe/request.js:167 #: public/js/frappe/request.js:172 #: public/js/frappe/views/kanban/kanban_board.bundle.js:68 @@ -20506,7 +20535,7 @@ msgctxt "Google Drive" msgid "Note: By default emails for failed backups are sent." msgstr "" -#: public/js/frappe/utils/utils.js:775 +#: public/js/frappe/utils/utils.js:776 msgid "Note: Changing the Page Name will break previous URL to this page." msgstr "" @@ -20690,7 +20719,7 @@ msgstr "" #. Name of a DocType #: desk/doctype/number_card/number_card.json -#: public/js/frappe/widgets/widget_dialog.js:591 +#: public/js/frappe/widgets/widget_dialog.js:597 msgid "Number Card" msgstr "" @@ -20705,7 +20734,7 @@ msgctxt "Workspace Number Card" msgid "Number Card Name" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:621 +#: public/js/frappe/widgets/widget_dialog.js:627 msgid "Number Cards" msgstr "" @@ -20756,11 +20785,11 @@ msgctxt "Recorder" msgid "Number of Queries" msgstr "" -#: core/doctype/doctype/doctype.py:444 public/js/frappe/doctype/index.js:59 +#: core/doctype/doctype/doctype.py:440 public/js/frappe/doctype/index.js:59 msgid "Number of attachment fields are more than {}, limit updated to {}." msgstr "" -#: core/doctype/system_settings/system_settings.py:152 +#: core/doctype/system_settings/system_settings.py:161 msgid "Number of backups must be greater than zero." msgstr "" @@ -20979,7 +21008,7 @@ msgstr "" msgid "Onboarding complete" msgstr "" -#: core/doctype/doctype/doctype_list.js:28 +#: core/doctype/doctype/doctype_list.js:42 msgid "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended." msgstr "" @@ -21001,7 +21030,7 @@ msgstr "" msgid "One of" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1312 +#: public/js/frappe/views/workspace/workspace.js:1318 msgid "One of the child page with name {0} already exist in {1} Section. Please update the name of the child page first before moving" msgstr "" @@ -21021,7 +21050,7 @@ msgstr "" msgid "Only Administrator can save a standard report. Please rename and save." msgstr "" -#: recorder.py:234 +#: recorder.py:227 msgid "Only Administrator is allowed to use Recorder" msgstr "" @@ -21031,7 +21060,7 @@ msgctxt "Workflow Document State" msgid "Only Allow Edit For" msgstr "" -#: core/doctype/doctype/doctype.py:1561 +#: core/doctype/doctype/doctype.py:1557 msgid "Only Options allowed for Data field are:" msgstr "" @@ -21045,7 +21074,7 @@ msgstr "" msgid "Only Workspace Manager can edit public workspaces" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:536 +#: public/js/frappe/views/workspace/workspace.js:542 msgid "Only Workspace Manager can sort or edit this page" msgstr "" @@ -21087,7 +21116,7 @@ msgstr "" msgid "Only standard DocTypes are allowed to be customized from Customize Form." msgstr "" -#: desk/form/assign_to.py:181 +#: desk/form/assign_to.py:195 msgid "Only the assignee can complete this to-do." msgstr "" @@ -21193,13 +21222,13 @@ msgid "Open your authentication app on your mobile phone." msgstr "" #: desk/doctype/todo/todo_list.js:23 -#: public/js/frappe/ui/toolbar/search_utils.js:261 -#: public/js/frappe/ui/toolbar/search_utils.js:262 -#: public/js/frappe/ui/toolbar/search_utils.js:273 -#: public/js/frappe/ui/toolbar/search_utils.js:283 -#: public/js/frappe/ui/toolbar/search_utils.js:292 -#: public/js/frappe/ui/toolbar/search_utils.js:310 -#: public/js/frappe/ui/toolbar/search_utils.js:311 +#: public/js/frappe/ui/toolbar/search_utils.js:277 +#: public/js/frappe/ui/toolbar/search_utils.js:278 +#: public/js/frappe/ui/toolbar/search_utils.js:289 +#: public/js/frappe/ui/toolbar/search_utils.js:299 +#: public/js/frappe/ui/toolbar/search_utils.js:308 +#: public/js/frappe/ui/toolbar/search_utils.js:326 +#: public/js/frappe/ui/toolbar/search_utils.js:327 #: social/doctype/energy_point_log/energy_point_log_list.js:23 msgid "Open {0}" msgstr "" @@ -21228,7 +21257,7 @@ msgctxt "Activity Log" msgid "Operation" msgstr "" -#: utils/data.py:2063 +#: utils/data.py:2057 msgid "Operator must be one of {0}" msgstr "" @@ -21252,7 +21281,7 @@ msgstr "" msgid "Option 3" msgstr "" -#: core/doctype/doctype/doctype.py:1579 +#: core/doctype/doctype/doctype.py:1575 msgid "Option {0} for field {1} is not a child table" msgstr "" @@ -21310,7 +21339,7 @@ msgctxt "Web Template Field" msgid "Options" msgstr "" -#: core/doctype/doctype/doctype.py:1317 +#: core/doctype/doctype/doctype.py:1313 msgid "Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'" msgstr "" @@ -21320,7 +21349,7 @@ msgctxt "Custom Field" msgid "Options Help" msgstr "" -#: core/doctype/doctype/doctype.py:1601 +#: core/doctype/doctype/doctype.py:1597 msgid "Options for Rating field can range from 3 to 10" msgstr "" @@ -21328,7 +21357,7 @@ msgstr "" msgid "Options for select. Each option on a new line." msgstr "" -#: core/doctype/doctype/doctype.py:1334 +#: core/doctype/doctype/doctype.py:1330 msgid "Options for {0} must be set before setting the default value." msgstr "" @@ -21336,7 +21365,7 @@ msgstr "" msgid "Options is required for field {0} of type {1}" msgstr "" -#: model/base_document.py:767 +#: model/base_document.py:786 msgid "Options not set for link field {0}" msgstr "" @@ -21672,7 +21701,7 @@ msgctxt "Form Tour" msgid "Page Route" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1499 +#: public/js/frappe/views/workspace/workspace.js:1505 msgid "Page Saved Successfully" msgstr "" @@ -21713,7 +21742,7 @@ msgstr "" msgid "Page not found" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1299 +#: public/js/frappe/views/workspace/workspace.js:1305 msgid "Page with title {0} already exist." msgstr "" @@ -21729,9 +21758,9 @@ msgid "Parameter" msgstr "" #: public/js/frappe/model/model.js:132 -#: public/js/frappe/views/workspace/workspace.js:606 -#: public/js/frappe/views/workspace/workspace.js:934 -#: public/js/frappe/views/workspace/workspace.js:1181 +#: public/js/frappe/views/workspace/workspace.js:612 +#: public/js/frappe/views/workspace/workspace.js:940 +#: public/js/frappe/views/workspace/workspace.js:1187 msgid "Parent" msgstr "" @@ -21769,7 +21798,7 @@ msgctxt "Form Tour Step" msgid "Parent Field" msgstr "" -#: core/doctype/doctype/doctype.py:915 +#: core/doctype/doctype/doctype.py:911 msgid "Parent Field (Tree)" msgstr "" @@ -21779,7 +21808,7 @@ msgctxt "DocType" msgid "Parent Field (Tree)" msgstr "" -#: core/doctype/doctype/doctype.py:921 +#: core/doctype/doctype/doctype.py:917 msgid "Parent Field must be a valid fieldname" msgstr "" @@ -21789,7 +21818,7 @@ msgctxt "Top Bar Item" msgid "Parent Label" msgstr "" -#: core/doctype/doctype/doctype.py:1148 +#: core/doctype/doctype/doctype.py:1144 msgid "Parent Missing" msgstr "" @@ -21811,7 +21840,7 @@ msgstr "" msgid "Parent is the name of the document to which the data will get added to." msgstr "" -#: permissions.py:806 +#: permissions.py:809 msgid "Parentfield not specified in {0}: {1}" msgstr "" @@ -21910,7 +21939,7 @@ msgctxt "System Settings" msgid "Password Reset Link Generation Limit" msgstr "" -#: public/js/frappe/form/grid_row.js:790 +#: public/js/frappe/form/grid_row.js:810 msgid "Password cannot be filtered" msgstr "" @@ -21944,7 +21973,7 @@ msgstr "" msgid "Password set" msgstr "" -#: auth.py:239 +#: auth.py:241 msgid "Password size exceeded the maximum allowed size" msgstr "" @@ -22225,7 +22254,7 @@ msgctxt "System Settings" msgid "Permissions" msgstr "" -#: core/doctype/doctype/doctype.py:1775 core/doctype/doctype/doctype.py:1785 +#: core/doctype/doctype/doctype.py:1771 core/doctype/doctype/doctype.py:1781 msgid "Permissions Error" msgstr "" @@ -22425,7 +22454,7 @@ msgstr "" msgid "Please check the filter values set for Dashboard Chart: {}" msgstr "" -#: model/base_document.py:839 +#: model/base_document.py:858 msgid "Please check the value of \"Fetch From\" set for field {0}" msgstr "" @@ -22481,7 +22510,7 @@ msgstr "" msgid "Please duplicate this to make changes" msgstr "" -#: core/doctype/system_settings/system_settings.py:145 +#: core/doctype/system_settings/system_settings.py:154 msgid "Please enable atleast one Social Login Key or LDAP or Login With Email Link before disabling username/password based login." msgstr "" @@ -22489,7 +22518,7 @@ msgstr "" #: email/doctype/auto_email_report/auto_email_report.js:17 #: printing/page/print/print.js:611 printing/page/print/print.js:640 #: public/js/frappe/list/bulk_operations.js:117 -#: public/js/frappe/utils/utils.js:1416 +#: public/js/frappe/utils/utils.js:1417 msgid "Please enable pop-ups" msgstr "" @@ -22618,7 +22647,7 @@ msgstr "" msgid "Please select Entity Type first" msgstr "" -#: core/doctype/system_settings/system_settings.py:103 +#: core/doctype/system_settings/system_settings.py:105 msgid "Please select Minimum Password Score" msgstr "" @@ -22697,7 +22726,7 @@ msgstr "" msgid "Please set the series to be used." msgstr "" -#: core/doctype/system_settings/system_settings.py:116 +#: core/doctype/system_settings/system_settings.py:118 msgid "Please setup SMS before setting it as an authentication method, via SMS Settings" msgstr "" @@ -22717,7 +22746,7 @@ msgstr "" msgid "Please specify" msgstr "" -#: permissions.py:782 +#: permissions.py:785 msgid "Please specify a valid parent DocType for {0}" msgstr "" @@ -22898,7 +22927,7 @@ msgctxt "Web Form Field" msgid "Precision" msgstr "" -#: core/doctype/doctype/doctype.py:1349 +#: core/doctype/doctype/doctype.py:1345 msgid "Precision should be between 1 and 6" msgstr "" @@ -22954,7 +22983,7 @@ msgstr "" msgid "Preparing Report" msgstr "" -#: public/js/frappe/views/communication.js:321 +#: public/js/frappe/views/communication.js:363 msgid "Prepend the template to the email message" msgstr "" @@ -23075,7 +23104,7 @@ msgstr "" msgid "Print" msgstr "" -#: public/js/frappe/list/list_view.js:1849 +#: public/js/frappe/list/list_view.js:1873 msgctxt "Button in list view actions menu" msgid "Print" msgstr "" @@ -23523,9 +23552,9 @@ msgid "Provider Name" msgstr "" #: desk/doctype/note/note_list.js:6 public/js/frappe/views/interaction.js:78 -#: public/js/frappe/views/workspace/workspace.js:613 -#: public/js/frappe/views/workspace/workspace.js:941 -#: public/js/frappe/views/workspace/workspace.js:1187 +#: public/js/frappe/views/workspace/workspace.js:619 +#: public/js/frappe/views/workspace/workspace.js:947 +#: public/js/frappe/views/workspace/workspace.js:1193 msgid "Public" msgstr "" @@ -23746,6 +23775,12 @@ msgctxt "Dashboard Chart" msgid "Quarterly" msgstr "" +#. Option for the 'Repeat On' (Select) field in DocType 'Event' +#: desk/doctype/event/event.json +msgctxt "Event" +msgid "Quarterly" +msgstr "" + #. Label of a Data field in DocType 'Recorder Query' #: core/doctype/recorder_query/recorder_query.json msgctxt "Recorder Query" @@ -23819,7 +23854,7 @@ msgctxt "DocType" msgid "Queue in Background (BETA)" msgstr "" -#: utils/background_jobs.py:473 +#: utils/background_jobs.py:433 msgid "Queue should be one of {0}" msgstr "" @@ -24028,13 +24063,13 @@ msgstr "" msgid "Re-Run in Console" msgstr "" -#: email/doctype/email_account/email_account.py:630 +#: email/doctype/email_account/email_account.py:632 msgid "Re:" msgstr "" #: core/doctype/communication/communication.js:268 -#: public/js/frappe/form/footer/form_timeline.js:564 -#: public/js/frappe/views/communication.js:257 +#: public/js/frappe/form/footer/form_timeline.js:587 +#: public/js/frappe/views/communication.js:299 msgid "Re: {0}" msgstr "" @@ -24242,7 +24277,7 @@ msgstr "" msgid "Recent years are easy to guess." msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:516 +#: public/js/frappe/ui/toolbar/search_utils.js:532 msgid "Recents" msgstr "" @@ -24347,7 +24382,7 @@ msgctxt "Website Settings" msgid "Redirects" msgstr "" -#: sessions.py:148 +#: sessions.py:149 msgid "Redis cache server not running. Please contact Administrator / Tech support" msgstr "" @@ -24951,7 +24986,7 @@ msgstr "" msgid "Removed {0}" msgstr "" -#: custom/doctype/custom_field/custom_field.js:133 +#: custom/doctype/custom_field/custom_field.js:134 #: public/js/frappe/form/toolbar.js:234 public/js/frappe/form/toolbar.js:238 #: public/js/frappe/form/toolbar.js:398 public/js/frappe/model/model.js:737 #: public/js/frappe/views/treeview.js:295 @@ -24959,7 +24994,7 @@ msgid "Rename" msgstr "" #: custom/doctype/custom_field/custom_field.js:115 -#: custom/doctype/custom_field/custom_field.js:132 +#: custom/doctype/custom_field/custom_field.js:133 msgid "Rename Fieldname" msgstr "" @@ -24967,7 +25002,7 @@ msgstr "" msgid "Rename {0}" msgstr "" -#: core/doctype/doctype/doctype.py:688 +#: core/doctype/doctype/doctype.py:684 msgid "Renamed files and replaced code in controllers, please check!" msgstr "" @@ -25274,7 +25309,7 @@ msgctxt "Report" msgid "Report Type" msgstr "" -#: core/doctype/doctype/doctype.py:1750 +#: core/doctype/doctype/doctype.py:1746 msgid "Report cannot be set for Single types" msgstr "" @@ -25312,8 +25347,8 @@ msgstr "" msgid "Report with more than 10 columns looks better in Landscape mode." msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:235 -#: public/js/frappe/ui/toolbar/search_utils.js:236 +#: public/js/frappe/ui/toolbar/search_utils.js:251 +#: public/js/frappe/ui/toolbar/search_utils.js:252 msgid "Report {0}" msgstr "" @@ -25333,7 +25368,7 @@ msgstr "" msgid "Report:" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:531 +#: public/js/frappe/ui/toolbar/search_utils.js:547 msgid "Reports" msgstr "" @@ -25656,8 +25691,8 @@ msgctxt "Title of message showing restrictions in list view" msgid "Restrictions" msgstr "" -#: public/js/frappe/ui/toolbar/awesome_bar.js:354 -#: public/js/frappe/ui/toolbar/awesome_bar.js:369 +#: public/js/frappe/ui/toolbar/awesome_bar.js:356 +#: public/js/frappe/ui/toolbar/awesome_bar.js:371 msgid "Result" msgstr "" @@ -26181,19 +26216,19 @@ msgctxt "Role" msgid "Route: Example \"/desk\"" msgstr "" -#: model/base_document.py:710 model/base_document.py:751 model/document.py:591 +#: model/base_document.py:729 model/base_document.py:770 model/document.py:591 msgid "Row" msgstr "" -#: core/doctype/doctype/doctype.py:1772 core/doctype/doctype/doctype.py:1782 +#: core/doctype/doctype/doctype.py:1768 core/doctype/doctype/doctype.py:1778 msgid "Row # {0}: Non administrator user can not set the role {1} to the custom doctype" msgstr "" -#: model/base_document.py:868 +#: model/base_document.py:887 msgid "Row #{0}:" msgstr "" -#: core/doctype/doctype/doctype.py:492 +#: core/doctype/doctype/doctype.py:488 msgid "Row #{}: Fieldname is required" msgstr "" @@ -26259,7 +26294,7 @@ msgctxt "Energy Point Rule" msgid "Rule Name" msgstr "" -#: permissions.py:662 +#: permissions.py:665 msgid "Rule for this doctype, role, permlevel and if-owner combination already exists." msgstr "" @@ -26508,7 +26543,7 @@ msgstr "" #: desk/page/user_profile/user_profile_controller.js:319 #: printing/page/print/print.js:831 #: printing/page/print_format_builder/print_format_builder.js:160 -#: public/js/frappe/form/footer/form_timeline.js:638 +#: public/js/frappe/form/footer/form_timeline.js:661 #: public/js/frappe/form/quick_entry.js:156 #: public/js/frappe/list/list_settings.js:36 #: public/js/frappe/list/list_settings.js:244 @@ -26520,7 +26555,7 @@ msgstr "" #: public/js/frappe/views/kanban/kanban_view.js:340 #: public/js/frappe/views/reports/query_report.js:1785 #: public/js/frappe/views/reports/report_view.js:1631 -#: public/js/frappe/views/workspace/workspace.js:487 +#: public/js/frappe/views/workspace/workspace.js:493 #: public/js/frappe/widgets/base_widget.js:140 #: public/js/frappe/widgets/quick_list_widget.js:117 #: public/js/print_format_builder/print_format_builder.bundle.js:15 @@ -26565,7 +26600,7 @@ msgctxt "Form Tour" msgid "Save on Completion" msgstr "" -#: public/js/frappe/form/form_tour.js:287 +#: public/js/frappe/form/form_tour.js:289 msgid "Save the document." msgstr "" @@ -26578,7 +26613,7 @@ msgstr "" #: public/js/frappe/list/list_settings.js:40 #: public/js/frappe/views/kanban/kanban_settings.js:47 -#: public/js/frappe/views/workspace/workspace.js:499 +#: public/js/frappe/views/workspace/workspace.js:505 msgid "Saving" msgstr "" @@ -26847,6 +26882,12 @@ msgstr "" msgid "Search" msgstr "" +#. Label of a Section Break field in DocType 'System Settings' +#: core/doctype/system_settings/system_settings.json +msgctxt "System Settings" +msgid "Search" +msgstr "" + #. Label of a Check field in DocType 'Role' #: core/doctype/role/role.json msgctxt "Role" @@ -26879,7 +26920,7 @@ msgstr "" msgid "Search Results for" msgstr "" -#: core/doctype/doctype/doctype.py:1418 +#: core/doctype/doctype/doctype.py:1414 msgid "Search field {0} is not valid" msgstr "" @@ -27179,7 +27220,7 @@ msgstr "" msgid "Select Filters" msgstr "" -#: desk/doctype/event/event.py:96 +#: desk/doctype/event/event.py:97 msgid "Select Google Calendar to which event should be synced." msgstr "" @@ -27278,15 +27319,15 @@ msgstr "" msgid "Select a group node first." msgstr "" -#: core/doctype/doctype/doctype.py:1885 +#: core/doctype/doctype/doctype.py:1881 msgid "Select a valid Sender Field for creating documents from Email" msgstr "" -#: core/doctype/doctype/doctype.py:1869 +#: core/doctype/doctype/doctype.py:1865 msgid "Select a valid Subject field for creating documents from Email" msgstr "" -#: public/js/frappe/form/form_tour.js:313 +#: public/js/frappe/form/form_tour.js:315 msgid "Select an Image" msgstr "" @@ -27324,6 +27365,10 @@ msgstr "" msgid "Select records for assignment" msgstr "" +#: public/js/frappe/list/bulk_operations.js:216 +msgid "Select records for removing assignment" +msgstr "" + #. Description of the 'Insert After' (Select) field in DocType 'Custom Field' #: custom/doctype/custom_field/custom_field.json msgctxt "Custom Field" @@ -27625,7 +27670,7 @@ msgctxt "DocType" msgid "Sender Email Field" msgstr "" -#: core/doctype/doctype/doctype.py:1888 +#: core/doctype/doctype/doctype.py:1884 msgid "Sender Field should have Email in options" msgstr "" @@ -27763,7 +27808,7 @@ msgstr "" msgid "Series counter for {} updated to {} successfully" msgstr "" -#: core/doctype/doctype/doctype.py:1073 +#: core/doctype/doctype/doctype.py:1069 #: core/doctype/document_naming_settings/document_naming_settings.py:171 msgid "Series {0} already used in {1}" msgstr "" @@ -27874,7 +27919,7 @@ msgctxt "System Settings" msgid "Session Expiry (idle timeout)" msgstr "" -#: core/doctype/system_settings/system_settings.py:110 +#: core/doctype/system_settings/system_settings.py:112 msgid "Session Expiry must be in format {0}" msgstr "" @@ -28092,7 +28137,7 @@ msgstr "" #. Label of a Card Break in the Integrations Workspace #: integrations/workspace/integrations/integrations.json #: public/js/frappe/ui/toolbar/toolbar.js:254 -#: public/js/frappe/views/workspace/workspace.js:515 +#: public/js/frappe/views/workspace/workspace.js:521 msgid "Settings" msgstr "" @@ -28128,7 +28173,7 @@ msgid "Settings Dropdown" msgstr "" #. Label of a Card Break in the Website Workspace -#: public/js/frappe/ui/toolbar/search_utils.js:551 +#: public/js/frappe/ui/toolbar/search_utils.js:567 #: website/workspace/website/website.json msgid "Setup" msgstr "" @@ -28675,7 +28720,7 @@ msgstr "" msgid "Single DocTypes cannot be customized." msgstr "" -#: core/doctype/doctype/doctype_list.js:51 +#: core/doctype/doctype/doctype_list.js:67 msgid "Single Types have only one record no tables associated. Values are stored in tabSingles" msgstr "" @@ -28685,7 +28730,7 @@ msgctxt "DocType" msgid "Single Types have only one record no tables associated. Values are stored in tabSingles" msgstr "" -#: database/database.py:230 +#: database/database.py:238 msgid "Site is running in read only mode for maintenance or site update, this action can not be performed right now. Please try again later." msgstr "" @@ -28928,11 +28973,11 @@ msgctxt "Customize Form" msgid "Sort Order" msgstr "" -#: core/doctype/doctype/doctype.py:1501 +#: core/doctype/doctype/doctype.py:1497 msgid "Sort field {0} must be a valid fieldname" msgstr "" -#: public/js/frappe/utils/utils.js:1705 +#: public/js/frappe/utils/utils.js:1706 #: website/report/website_analytics/website_analytics.js:38 msgid "Source" msgstr "" @@ -29049,7 +29094,7 @@ msgstr "" msgid "Standard DocType can not be deleted." msgstr "" -#: core/doctype/doctype/doctype.py:228 +#: core/doctype/doctype/doctype.py:224 msgid "Standard DocType cannot have default print format, use Customize Form" msgstr "" @@ -29599,7 +29644,7 @@ msgctxt "DocType" msgid "Subject Field" msgstr "" -#: core/doctype/doctype/doctype.py:1878 +#: core/doctype/doctype/doctype.py:1874 msgid "Subject Field type should be Data, Text, Long Text, Small Text, Text Editor" msgstr "" @@ -29618,7 +29663,7 @@ msgstr "" msgid "Submit" msgstr "" -#: public/js/frappe/list/list_view.js:1916 +#: public/js/frappe/list/list_view.js:1940 msgctxt "Button in list view actions menu" msgid "Submit" msgstr "" @@ -29705,7 +29750,7 @@ msgstr "" msgid "Submit this document to confirm" msgstr "" -#: public/js/frappe/list/list_view.js:1921 +#: public/js/frappe/list/list_view.js:1945 msgctxt "Title of confirmation dialog" msgid "Submit {0} documents?" msgstr "" @@ -29766,7 +29811,7 @@ msgstr "" #: core/doctype/data_import/data_import.js:470 #: desk/doctype/bulk_update/bulk_update.js:31 #: desk/doctype/desktop_icon/desktop_icon.py:452 -#: public/js/frappe/form/grid.js:1133 +#: public/js/frappe/form/grid.js:1135 #: public/js/frappe/views/translation_manager.js:21 #: templates/pages/integrations/gcalendar-success.html:9 #: workflow/doctype/workflow_action/workflow_action.py:171 @@ -30034,7 +30079,7 @@ msgstr "" msgid "Syncing {0} of {1}" msgstr "" -#: utils/data.py:2424 +#: utils/data.py:2418 msgid "Syntax Error" msgstr "" @@ -30292,7 +30337,7 @@ msgctxt "DocType Link" msgid "Table Fieldname" msgstr "" -#: core/doctype/doctype/doctype.py:1154 +#: core/doctype/doctype/doctype.py:1150 msgid "Table Fieldname Missing" msgstr "" @@ -30320,7 +30365,7 @@ msgctxt "DocField" msgid "Table MultiSelect" msgstr "" -#: public/js/frappe/form/grid.js:1132 +#: public/js/frappe/form/grid.js:1134 msgid "Table updated" msgstr "" @@ -30345,14 +30390,14 @@ msgid "Tag Link" msgstr "" #: model/__init__.py:148 model/meta.py:52 -#: public/js/frappe/list/bulk_operations.js:365 +#: public/js/frappe/list/bulk_operations.js:386 #: public/js/frappe/list/list_sidebar.js:226 public/js/frappe/model/meta.js:204 #: public/js/frappe/model/model.js:123 #: public/js/frappe/ui/toolbar/awesome_bar.js:171 msgid "Tags" msgstr "" -#: integrations/doctype/google_drive/google_drive.js:28 +#: integrations/doctype/google_drive/google_drive.js:29 msgid "Take Backup" msgstr "" @@ -30573,7 +30618,7 @@ msgstr "" msgid "The Auto Repeat for this document has been disabled." msgstr "" -#: public/js/frappe/form/grid.js:1155 +#: public/js/frappe/form/grid.js:1157 msgid "The CSV format is case sensitive" msgstr "" @@ -30622,7 +30667,7 @@ msgid "" "" msgstr "" -#: database/database.py:388 +#: database/database.py:424 msgid "The changes have been reverted." msgstr "" @@ -30664,6 +30709,10 @@ msgstr "" msgid "The fieldname you've specified in Attached To Field is invalid" msgstr "" +#: automation/doctype/assignment_rule/assignment_rule.py:61 +msgid "The following Assignment Days have been repeated: {0}" +msgstr "" + #: core/doctype/data_import/importer.py:1035 msgid "The following values are invalid: {0}. Values must be one of {1}" msgstr "" @@ -30755,7 +30804,7 @@ msgstr "" msgid "The system is being updated. Please refresh again after a few moments." msgstr "" -#: public/js/frappe/form/grid_row.js:615 +#: public/js/frappe/form/grid_row.js:635 msgid "The total column width cannot be more than 10." msgstr "" @@ -30822,7 +30871,7 @@ msgstr "" msgid "There can be only 9 Page Break fields in a Web Form" msgstr "" -#: core/doctype/doctype/doctype.py:1394 +#: core/doctype/doctype/doctype.py:1390 msgid "There can be only one Fold in a form" msgstr "" @@ -30862,7 +30911,7 @@ msgstr "" msgid "There were errors while creating the document. Please try again." msgstr "" -#: public/js/frappe/views/communication.js:728 +#: public/js/frappe/views/communication.js:770 msgid "There were errors while sending email. Please try again." msgstr "" @@ -30913,7 +30962,7 @@ msgstr "" msgid "This Kanban Board will be private" msgstr "" -#: __init__.py:917 +#: __init__.py:957 msgid "This action is only allowed for {}" msgstr "" @@ -31350,11 +31399,11 @@ msgctxt "Activity Log" msgid "Timeline Name" msgstr "" -#: core/doctype/doctype/doctype.py:1489 +#: core/doctype/doctype/doctype.py:1485 msgid "Timeline field must be a Link or Dynamic Link" msgstr "" -#: core/doctype/doctype/doctype.py:1485 +#: core/doctype/doctype/doctype.py:1481 msgid "Timeline field must be a valid fieldname" msgstr "" @@ -31398,9 +31447,9 @@ msgid "Timestamp" msgstr "" #: public/js/form_builder/store.js:89 -#: public/js/frappe/views/workspace/workspace.js:599 -#: public/js/frappe/views/workspace/workspace.js:928 -#: public/js/frappe/views/workspace/workspace.js:1175 +#: public/js/frappe/views/workspace/workspace.js:605 +#: public/js/frappe/views/workspace/workspace.js:934 +#: public/js/frappe/views/workspace/workspace.js:1181 msgid "Title" msgstr "" @@ -31542,7 +31591,7 @@ msgctxt "Website Settings" msgid "Title Prefix" msgstr "" -#: core/doctype/doctype/doctype.py:1426 +#: core/doctype/doctype/doctype.py:1422 msgid "Title field must be a valid fieldname" msgstr "" @@ -31800,7 +31849,7 @@ msgstr "" msgid "Too Many Requests" msgstr "" -#: database/database.py:387 +#: database/database.py:423 msgid "Too many changes to database in single action." msgstr "" @@ -31999,7 +32048,7 @@ msgid "" "Note: If you're sending to multiple recipients, even if 1 recipient reads the email, it'll be considered \"Opened\"" msgstr "" -#: public/js/frappe/utils/utils.js:1744 +#: public/js/frappe/utils/utils.js:1745 msgid "Tracking URL generated and copied to clipboard" msgstr "" @@ -32475,7 +32524,7 @@ msgstr "" msgid "Unhandled Email" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:556 +#: public/js/frappe/views/workspace/workspace.js:562 msgid "Unhide Workspace" msgstr "" @@ -32505,7 +32554,7 @@ msgstr "" msgid "Unknown Rounding Method: {}" msgstr "" -#: auth.py:299 +#: auth.py:301 msgid "Unknown User" msgstr "" @@ -32604,7 +32653,7 @@ msgstr "" msgid "Unzipping files..." msgstr "" -#: desk/doctype/event/event.py:258 +#: desk/doctype/event/event.py:259 msgid "Upcoming Events for Today" msgstr "" @@ -32617,7 +32666,7 @@ msgstr "" #: printing/page/print_format_builder/print_format_builder.js:670 #: printing/page/print_format_builder/print_format_builder.js:757 #: public/js/frappe/form/grid_row.js:402 -#: public/js/frappe/views/workspace/workspace.js:647 +#: public/js/frappe/views/workspace/workspace.js:653 msgid "Update" msgstr "" @@ -32633,7 +32682,7 @@ msgctxt "Document Naming Settings" msgid "Update Amendment Naming" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:596 +#: public/js/frappe/views/workspace/workspace.js:602 msgid "Update Details" msgstr "" @@ -32692,7 +32741,7 @@ msgctxt "Workflow Document State" msgid "Update Value" msgstr "" -#: public/js/frappe/list/bulk_operations.js:310 +#: public/js/frappe/list/bulk_operations.js:331 msgid "Update {0} records" msgstr "" @@ -32721,7 +32770,7 @@ msgstr "" msgid "Updated To A New Version 🎉" msgstr "" -#: public/js/frappe/list/bulk_operations.js:307 +#: public/js/frappe/list/bulk_operations.js:328 msgid "Updated successfully" msgstr "" @@ -32785,6 +32834,18 @@ msgctxt "File" msgid "Uploaded To Google Drive" msgstr "" +#: integrations/doctype/google_drive/google_drive.py:199 +msgid "Uploading backup to Google Drive." +msgstr "" + +#: integrations/doctype/google_drive/google_drive.py:204 +msgid "Uploading successful." +msgstr "" + +#: integrations/doctype/google_drive/google_drive.js:16 +msgid "Uploading to Google Drive" +msgstr "" + #. Description of the 'Value to Validate' (Data) field in DocType 'Onboarding #. Step' #: desk/doctype/onboarding_step/onboarding_step.json @@ -33230,7 +33291,7 @@ msgid "User Permissions" msgstr "" #: core/doctype/user_permission/user_permission_list.js:124 -msgid "User Permissions created sucessfully" +msgid "User Permissions created successfully" msgstr "" #. Label of a shortcut in the Users Workspace @@ -33456,8 +33517,8 @@ msgid "Validity" msgstr "" #: email/doctype/auto_email_report/auto_email_report.js:92 -#: public/js/frappe/list/bulk_operations.js:271 -#: public/js/frappe/list/bulk_operations.js:333 +#: public/js/frappe/list/bulk_operations.js:292 +#: public/js/frappe/list/bulk_operations.js:354 msgid "Value" msgstr "" @@ -33534,7 +33595,7 @@ msgctxt "Notification" msgid "Value To Be Set" msgstr "" -#: model/base_document.py:930 model/document.py:648 +#: model/base_document.py:949 model/document.py:648 msgid "Value cannot be changed for {0}" msgstr "" @@ -33554,7 +33615,7 @@ msgstr "" msgid "Value for field {0} is too long in {1}. Length should be lesser than {2} characters" msgstr "" -#: model/base_document.py:360 +#: model/base_document.py:379 msgid "Value for {0} cannot be a list" msgstr "" @@ -33565,7 +33626,7 @@ msgctxt "Assignment Rule" msgid "Value from this field will be set as the due date in the ToDo" msgstr "" -#: model/base_document.py:712 +#: model/base_document.py:731 msgid "Value missing for" msgstr "" @@ -33579,7 +33640,7 @@ msgctxt "Onboarding Step" msgid "Value to Validate" msgstr "" -#: model/base_document.py:997 +#: model/base_document.py:1016 msgid "Value too big" msgstr "" @@ -33909,7 +33970,7 @@ msgstr "" msgid "Web Page Block" msgstr "" -#: public/js/frappe/utils/utils.js:1697 +#: public/js/frappe/utils/utils.js:1698 msgid "Web Page URL" msgstr "" @@ -34122,7 +34183,7 @@ msgctxt "DocType" msgid "Website Search Field" msgstr "" -#: core/doctype/doctype/doctype.py:1473 +#: core/doctype/doctype/doctype.py:1469 msgid "Website Search Field must be a valid fieldname" msgstr "" @@ -34405,7 +34466,7 @@ msgctxt "System Settings" msgid "When uploading files, force the use of the web-based image capture. If this is unchecked, the default behavior is to use the mobile native camera when use from a mobile is detected." msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:440 +#: public/js/frappe/widgets/widget_dialog.js:446 msgid "Which view of the associated DocType should this shortcut take you to?" msgstr "" @@ -34637,7 +34698,7 @@ msgstr "" #. Name of a DocType #: desk/doctype/workspace/workspace.json -#: public/js/frappe/ui/toolbar/search_utils.js:541 +#: public/js/frappe/ui/toolbar/search_utils.js:557 #: public/js/frappe/views/workspace/workspace.js:10 msgid "Workspace" msgstr "" @@ -34694,15 +34755,19 @@ msgstr "" msgid "Workspace Shortcut" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:1265 +#: desk/doctype/workspace/workspace.py:283 +msgid "Workspace not found" +msgstr "" + +#: public/js/frappe/views/workspace/workspace.js:1271 msgid "Workspace {0} Created Successfully" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:894 +#: public/js/frappe/views/workspace/workspace.js:900 msgid "Workspace {0} Deleted Successfully" msgstr "" -#: public/js/frappe/views/workspace/workspace.js:672 +#: public/js/frappe/views/workspace/workspace.js:678 msgid "Workspace {0} Edited Successfully" msgstr "" @@ -34736,7 +34801,7 @@ msgctxt "User Document Type" msgid "Write" msgstr "" -#: model/base_document.py:840 +#: model/base_document.py:859 msgid "Wrong Fetch From value" msgstr "" @@ -34861,7 +34926,7 @@ msgstr "" #: integrations/doctype/webhook/webhook.py:127 #: integrations/doctype/webhook/webhook.py:137 #: public/js/form_builder/utils.js:336 -#: public/js/frappe/form/controls/link.js:471 +#: public/js/frappe/form/controls/link.js:472 #: public/js/frappe/list/list_sidebar_group_by.js:223 #: public/js/frappe/views/reports/query_report.js:1513 #: website/doctype/help_article/templates/help_article.html:25 @@ -34916,11 +34981,11 @@ msgstr "" msgid "You are connected to internet." msgstr "" -#: permissions.py:417 +#: permissions.py:420 msgid "You are not allowed to access this {0} record because it is linked to {1} '{2}' in field {3}" msgstr "" -#: permissions.py:406 +#: permissions.py:409 msgid "You are not allowed to access this {0} record because it is linked to {1} '{2}' in row {3}, field {4}" msgstr "" @@ -34940,7 +35005,7 @@ msgstr "" msgid "You are not allowed to edit the report." msgstr "" -#: permissions.py:614 +#: permissions.py:617 msgid "You are not allowed to export {} doctype" msgstr "" @@ -34948,7 +35013,7 @@ msgstr "" msgid "You are not allowed to print this report" msgstr "" -#: public/js/frappe/views/communication.js:673 +#: public/js/frappe/views/communication.js:715 msgid "You are not allowed to send emails related to this document" msgstr "" @@ -34968,7 +35033,7 @@ msgstr "" msgid "You are not permitted to access this page." msgstr "" -#: __init__.py:834 +#: __init__.py:874 msgid "You are not permitted to access this resource." msgstr "" @@ -35021,7 +35086,7 @@ msgstr "" msgid "You can manually remove the lock if you think it's safe: {}" msgstr "" -#: public/js/frappe/form/controls/markdown_editor.js:74 +#: public/js/frappe/form/controls/markdown_editor.js:75 msgid "You can only insert images in Markdown fields" msgstr "" @@ -35169,7 +35234,7 @@ msgstr "" msgid "You have hit the row size limit on database table: {0}" msgstr "" -#: public/js/frappe/list/bulk_operations.js:347 +#: public/js/frappe/list/bulk_operations.js:368 msgid "You have not entered a value. The field will be set to empty." msgstr "" @@ -35202,7 +35267,7 @@ msgstr "" msgid "You last edited this" msgstr "" -#: public/js/frappe/widgets/widget_dialog.js:308 +#: public/js/frappe/widgets/widget_dialog.js:314 msgid "You must add atleast one link." msgstr "" @@ -35214,7 +35279,7 @@ msgstr "" msgid "You must login to submit this form" msgstr "" -#: desk/doctype/workspace/workspace.py:69 +#: desk/doctype/workspace/workspace.py:72 msgid "You need to be Workspace Manager to edit this document" msgstr "" @@ -35310,11 +35375,11 @@ msgstr "" msgid "Your account has been deleted" msgstr "" -#: auth.py:465 +#: auth.py:476 msgid "Your account has been locked and will resume after {0} seconds" msgstr "" -#: desk/form/assign_to.py:268 +#: desk/form/assign_to.py:282 msgid "Your assignment on {0} {1} has been removed by {2}" msgstr "" @@ -35389,6 +35454,10 @@ msgctxt "Desktop Icon" msgid "_report" msgstr "" +#: database/database.py:315 +msgid "`as_iterator` only works with `as_list=True` or `as_dict=True`" +msgstr "" + #: utils/background_jobs.py:94 msgid "`job_id` paramater is required for deduplication." msgstr "" @@ -35440,7 +35509,7 @@ msgctxt "Permission Inspector" msgid "amend" msgstr "" -#: public/js/frappe/utils/utils.js:396 utils/data.py:1528 +#: public/js/frappe/utils/utils.js:396 utils/data.py:1526 msgid "and" msgstr "" @@ -35542,7 +35611,7 @@ msgctxt "Workflow State" msgid "bullhorn" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:270 +#: public/js/frappe/ui/toolbar/search_utils.js:286 msgid "calendar" msgstr "" @@ -35660,7 +35729,7 @@ msgctxt "Workspace" msgid "cyan" msgstr "" -#: public/js/frappe/utils/utils.js:1113 +#: public/js/frappe/utils/utils.js:1114 msgctxt "Days (Field: Duration)" msgid "d" msgstr "" @@ -35810,12 +35879,12 @@ msgctxt "Social Link Settings" msgid "email" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:289 +#: public/js/frappe/ui/toolbar/search_utils.js:305 msgid "email inbox" msgstr "" -#: permissions.py:411 permissions.py:422 -#: public/js/frappe/form/controls/link.js:479 +#: permissions.py:414 permissions.py:425 +#: public/js/frappe/form/controls/link.js:481 msgid "empty" msgstr "" @@ -35994,11 +36063,11 @@ msgctxt "Workspace" msgid "grey" msgstr "" -#: utils/backups.py:375 +#: utils/backups.py:379 msgid "gzip not found in PATH! This is required to take a backup." msgstr "" -#: public/js/frappe/utils/utils.js:1117 +#: public/js/frappe/utils/utils.js:1118 msgctxt "Hours (Field: Duration)" msgid "h" msgstr "" @@ -36051,7 +36120,7 @@ msgctxt "Workflow State" msgid "home" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:280 +#: public/js/frappe/ui/toolbar/search_utils.js:296 msgid "hub" msgstr "" @@ -36181,7 +36250,7 @@ msgctxt "RQ Worker" msgid "long" msgstr "" -#: public/js/frappe/utils/utils.js:1121 +#: public/js/frappe/utils/utils.js:1122 msgctxt "Minutes (Field: Duration)" msgid "m" msgstr "" @@ -36253,7 +36322,7 @@ msgctxt "Workflow State" msgid "music" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:144 +#: public/js/frappe/ui/toolbar/search_utils.js:160 msgid "new" msgstr "" @@ -36357,10 +36426,6 @@ msgstr "" msgid "one of" msgstr "" -#: utils/data.py:1535 -msgid "only." -msgstr "" - #: public/js/frappe/utils/utils.js:393 www/login.html:87 msgid "or" msgstr "" @@ -36597,7 +36662,7 @@ msgctxt "Workflow State" msgid "road" msgstr "" -#: public/js/frappe/utils/utils.js:1125 +#: public/js/frappe/utils/utils.js:1126 msgctxt "Seconds (Field: Duration)" msgid "s" msgstr "" @@ -36888,7 +36953,7 @@ msgctxt "Audit Trail" msgid "version_table" msgstr "" -#: automation/doctype/assignment_rule/assignment_rule.py:386 +#: automation/doctype/assignment_rule/assignment_rule.py:383 msgid "via Assignment Rule" msgstr "" @@ -36991,21 +37056,15 @@ msgctxt "Workflow State" msgid "zoom-out" msgstr "" -#: desk/doctype/event/event.js:83 -#: integrations/doctype/google_drive/google_drive.js:19 +#: desk/doctype/event/event.js:87 msgid "{0}" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:81 -#: public/js/frappe/ui/toolbar/search_utils.js:82 -msgid "{0} ${label}" -msgstr "" - -#: public/js/frappe/ui/toolbar/search_utils.js:177 +#: public/js/frappe/ui/toolbar/search_utils.js:193 msgid "{0} ${skip_list ? \"\" : type}" msgstr "" -#: public/js/frappe/ui/toolbar/search_utils.js:182 +#: public/js/frappe/ui/toolbar/search_utils.js:198 msgid "{0} ${type}" msgstr "" @@ -37022,8 +37081,8 @@ msgstr "" msgid "{0} ({1}) - {2}%" msgstr "" -#: public/js/frappe/ui/toolbar/awesome_bar.js:346 -#: public/js/frappe/ui/toolbar/awesome_bar.js:349 +#: public/js/frappe/ui/toolbar/awesome_bar.js:348 +#: public/js/frappe/ui/toolbar/awesome_bar.js:351 msgid "{0} = {1}" msgstr "" @@ -37036,9 +37095,9 @@ msgid "{0} Chart" msgstr "" #: core/page/dashboard_view/dashboard_view.js:67 -#: public/js/frappe/ui/toolbar/search_utils.js:331 -#: public/js/frappe/ui/toolbar/search_utils.js:332 -#: public/js/frappe/utils/utils.js:929 +#: public/js/frappe/ui/toolbar/search_utils.js:347 +#: public/js/frappe/ui/toolbar/search_utils.js:348 +#: public/js/frappe/utils/utils.js:930 #: public/js/frappe/views/dashboard/dashboard_view.js:10 msgid "{0} Dashboard" msgstr "" @@ -37061,7 +37120,9 @@ msgstr "" msgid "{0} Liked" msgstr "" -#: public/js/frappe/utils/utils.js:923 +#: public/js/frappe/ui/toolbar/search_utils.js:83 +#: public/js/frappe/ui/toolbar/search_utils.js:84 +#: public/js/frappe/utils/utils.js:924 #: public/js/frappe/widgets/chart_widget.js:317 www/list.html:4 www/list.html:8 msgid "{0} List" msgstr "" @@ -37074,7 +37135,7 @@ msgstr "" msgid "{0} Map" msgstr "" -#: public/js/frappe/utils/utils.js:926 +#: public/js/frappe/utils/utils.js:927 msgid "{0} Modules" msgstr "" @@ -37082,11 +37143,13 @@ msgstr "" msgid "{0} Name" msgstr "" -#: model/base_document.py:1027 +#: model/base_document.py:1046 msgid "{0} Not allowed to change {1} after submission from {2} to {3}" msgstr "" -#: public/js/frappe/utils/utils.js:920 +#: public/js/frappe/ui/toolbar/search_utils.js:95 +#: public/js/frappe/ui/toolbar/search_utils.js:96 +#: public/js/frappe/utils/utils.js:921 #: public/js/frappe/widgets/chart_widget.js:325 msgid "{0} Report" msgstr "" @@ -37096,6 +37159,8 @@ msgstr "" msgid "{0} Settings" msgstr "" +#: public/js/frappe/ui/toolbar/search_utils.js:87 +#: public/js/frappe/ui/toolbar/search_utils.js:88 #: public/js/frappe/views/treeview.js:139 msgid "{0} Tree" msgstr "" @@ -37109,6 +37174,11 @@ msgstr "" msgid "{0} Web page views" msgstr "" +#: public/js/frappe/ui/toolbar/search_utils.js:91 +#: public/js/frappe/ui/toolbar/search_utils.js:92 +msgid "{0} Workspace" +msgstr "" + #: public/js/frappe/form/link_selector.js:225 msgid "{0} added" msgstr "" @@ -37125,7 +37195,7 @@ msgstr "" msgid "{0} already unsubscribed for {1} {2}" msgstr "" -#: utils/data.py:1715 +#: utils/data.py:1709 msgid "{0} and {1}" msgstr "" @@ -37163,7 +37233,7 @@ msgstr "" msgid "{0} are required" msgstr "" -#: desk/form/assign_to.py:275 +#: desk/form/assign_to.py:289 msgid "{0} assigned a new task {1} {2} to you" msgstr "" @@ -37176,6 +37246,10 @@ msgctxt "Form timeline" msgid "{0} attached {1}" msgstr "" +#: core/doctype/system_settings/system_settings.py:139 +msgid "{0} can not be more than {1}" +msgstr "" + #: public/js/frappe/form/footer/version_timeline_content_builder.js:77 msgid "{0} cancelled this document" msgstr "" @@ -37310,7 +37384,7 @@ msgstr "" msgid "{0} has left the conversation in {1} {2}" msgstr "" -#: __init__.py:2373 +#: __init__.py:2427 msgid "{0} has no versions tracked." msgstr "" @@ -37327,7 +37401,7 @@ msgstr "" msgid "{0} in row {1} cannot have both URL and child items" msgstr "" -#: core/doctype/doctype/doctype.py:916 +#: core/doctype/doctype/doctype.py:912 msgid "{0} is a mandatory field" msgstr "" @@ -37335,7 +37409,7 @@ msgstr "" msgid "{0} is a not a valid zip file" msgstr "" -#: core/doctype/doctype/doctype.py:1559 +#: core/doctype/doctype/doctype.py:1555 msgid "{0} is an invalid Data field." msgstr "" @@ -37412,11 +37486,11 @@ msgstr "" msgid "{0} is not a valid Workflow State. Please update your Workflow and try again." msgstr "" -#: permissions.py:795 +#: permissions.py:798 msgid "{0} is not a valid parent DocType for {1}" msgstr "" -#: permissions.py:815 +#: permissions.py:818 msgid "{0} is not a valid parentfield for {1}" msgstr "" @@ -37506,11 +37580,11 @@ msgstr "" msgid "{0} must be one of {1}" msgstr "" -#: model/base_document.py:771 +#: model/base_document.py:790 msgid "{0} must be set first" msgstr "" -#: model/base_document.py:629 +#: model/base_document.py:648 msgid "{0} must be unique" msgstr "" @@ -37544,7 +37618,12 @@ msgstr "" msgid "{0} of {1} sent" msgstr "" -#: utils/data.py:1705 +#: utils/data.py:1529 +msgctxt "Money in words" +msgid "{0} only." +msgstr "" + +#: utils/data.py:1699 msgid "{0} or {1}" msgstr "" @@ -37612,7 +37691,7 @@ msgstr "" msgid "{0} shared this document with {1}" msgstr "" -#: core/doctype/doctype/doctype.py:320 +#: core/doctype/doctype/doctype.py:316 msgid "{0} should be indexed because it's referred in dashboard connections" msgstr "" @@ -37684,11 +37763,11 @@ msgstr "" msgid "{0} {1} added to Dashboard {2}" msgstr "" -#: model/base_document.py:562 model/rename_doc.py:112 +#: model/base_document.py:581 model/rename_doc.py:112 msgid "{0} {1} already exists" msgstr "" -#: model/base_document.py:873 +#: model/base_document.py:892 msgid "{0} {1} cannot be \"{2}\". It should be one of \"{3}\"" msgstr "" @@ -37704,7 +37783,7 @@ msgstr "" msgid "{0} {1} is linked with the following submitted documents: {2}" msgstr "" -#: model/document.py:170 permissions.py:566 +#: model/document.py:170 permissions.py:569 msgid "{0} {1} not found" msgstr "" @@ -37712,39 +37791,39 @@ msgstr "" msgid "{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first." msgstr "" -#: model/base_document.py:988 +#: model/base_document.py:1007 msgid "{0}, Row {1}" msgstr "" -#: model/base_document.py:993 +#: model/base_document.py:1012 msgid "{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}" msgstr "" -#: core/doctype/doctype/doctype.py:1741 +#: core/doctype/doctype/doctype.py:1737 msgid "{0}: Cannot set Amend without Cancel" msgstr "" -#: core/doctype/doctype/doctype.py:1759 +#: core/doctype/doctype/doctype.py:1755 msgid "{0}: Cannot set Assign Amend if not Submittable" msgstr "" -#: core/doctype/doctype/doctype.py:1757 +#: core/doctype/doctype/doctype.py:1753 msgid "{0}: Cannot set Assign Submit if not Submittable" msgstr "" -#: core/doctype/doctype/doctype.py:1736 +#: core/doctype/doctype/doctype.py:1732 msgid "{0}: Cannot set Cancel without Submit" msgstr "" -#: core/doctype/doctype/doctype.py:1743 +#: core/doctype/doctype/doctype.py:1739 msgid "{0}: Cannot set Import without Create" msgstr "" -#: core/doctype/doctype/doctype.py:1739 +#: core/doctype/doctype/doctype.py:1735 msgid "{0}: Cannot set Submit, Cancel, Amend without Write" msgstr "" -#: core/doctype/doctype/doctype.py:1763 +#: core/doctype/doctype/doctype.py:1759 msgid "{0}: Cannot set import as {1} is not importable" msgstr "" @@ -37752,47 +37831,47 @@ msgstr "" msgid "{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings" msgstr "" -#: core/doctype/doctype/doctype.py:1377 +#: core/doctype/doctype/doctype.py:1373 msgid "{0}: Field '{1}' cannot be set as Unique as it has non-unique values" msgstr "" -#: core/doctype/doctype/doctype.py:1285 +#: core/doctype/doctype/doctype.py:1281 msgid "{0}: Field {1} in row {2} cannot be hidden and mandatory without default" msgstr "" -#: core/doctype/doctype/doctype.py:1244 +#: core/doctype/doctype/doctype.py:1240 msgid "{0}: Field {1} of type {2} cannot be mandatory" msgstr "" -#: core/doctype/doctype/doctype.py:1232 +#: core/doctype/doctype/doctype.py:1228 msgid "{0}: Fieldname {1} appears multiple times in rows {2}" msgstr "" -#: core/doctype/doctype/doctype.py:1362 +#: core/doctype/doctype/doctype.py:1358 msgid "{0}: Fieldtype {1} for {2} cannot be unique" msgstr "" -#: core/doctype/doctype/doctype.py:1698 +#: core/doctype/doctype/doctype.py:1694 msgid "{0}: No basic permissions set" msgstr "" -#: core/doctype/doctype/doctype.py:1712 +#: core/doctype/doctype/doctype.py:1708 msgid "{0}: Only one rule allowed with the same Role, Level and {1}" msgstr "" -#: core/doctype/doctype/doctype.py:1266 +#: core/doctype/doctype/doctype.py:1262 msgid "{0}: Options must be a valid DocType for field {1} in row {2}" msgstr "" -#: core/doctype/doctype/doctype.py:1255 +#: core/doctype/doctype/doctype.py:1251 msgid "{0}: Options required for Link or Table type field {1} in row {2}" msgstr "" -#: core/doctype/doctype/doctype.py:1273 +#: core/doctype/doctype/doctype.py:1269 msgid "{0}: Options {1} must be the same as doctype name {2} for the field {3}" msgstr "" -#: core/doctype/doctype/doctype.py:1727 +#: core/doctype/doctype/doctype.py:1723 msgid "{0}: Permission at level 0 must be set before higher levels are set" msgstr "" @@ -37800,7 +37879,7 @@ msgstr "" msgid "{0}: You can increase the limit for the field if required via {1}" msgstr "" -#: core/doctype/doctype/doctype.py:1219 +#: core/doctype/doctype/doctype.py:1215 msgid "{0}: fieldname cannot be set to reserved keyword {1}" msgstr "" @@ -37818,7 +37897,7 @@ msgstr "" msgid "{0}: {1} vs {2}" msgstr "" -#: core/doctype/doctype/doctype.py:1385 +#: core/doctype/doctype/doctype.py:1381 msgid "{0}:Fieldtype {1} for {2} cannot be indexed" msgstr "" @@ -37838,7 +37917,7 @@ msgstr "" msgid "{count} rows selected" msgstr "" -#: core/doctype/doctype/doctype.py:1439 +#: core/doctype/doctype/doctype.py:1435 msgid "{{{0}}} is not a valid fieldname pattern. It should be {{field_name}}." msgstr "" @@ -37846,11 +37925,11 @@ msgstr "" msgid "{} Complete" msgstr "" -#: utils/data.py:2418 +#: utils/data.py:2412 msgid "{} Invalid python code on line {}" msgstr "" -#: utils/data.py:2427 +#: utils/data.py:2421 msgid "{} Possibly invalid python code.
{}" msgstr "" @@ -37871,7 +37950,7 @@ msgstr "" msgid "{} is not a valid date string." msgstr "" -#: commands/utils.py:519 +#: commands/utils.py:532 msgid "{} not found in PATH! This is required to access the console." msgstr "" @@ -37879,7 +37958,7 @@ msgstr "" msgid "{} not found in PATH! This is required to restore the database." msgstr "" -#: utils/backups.py:441 +#: utils/backups.py:445 msgid "{} not found in PATH! This is required to take a backup." msgstr "" diff --git a/frappe/migrate.py b/frappe/migrate.py index cad55d5d24..88d107a483 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -76,6 +76,7 @@ class SiteMigration: """Complete setup required for site migration""" frappe.flags.touched_tables = set() self.touched_tables_file = frappe.get_site_path("touched_tables.json") + frappe.clear_cache() add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data") clear_global_cache() diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 2dad3df4fd..18d7244179 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -134,22 +134,22 @@ log_types = ( ) std_fields = [ - {"fieldname": "name", "fieldtype": "Link", "label": _lt("ID")}, - {"fieldname": "owner", "fieldtype": "Link", "label": _lt("Created By"), "options": "User"}, - {"fieldname": "idx", "fieldtype": "Int", "label": _lt("Index")}, - {"fieldname": "creation", "fieldtype": "Datetime", "label": _lt("Created On")}, - {"fieldname": "modified", "fieldtype": "Datetime", "label": _lt("Last Updated On")}, + {"fieldname": "name", "fieldtype": "Link", "label": "ID"}, + {"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"}, + {"fieldname": "idx", "fieldtype": "Int", "label": "Index"}, + {"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"}, + {"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"}, { "fieldname": "modified_by", "fieldtype": "Link", - "label": _lt("Last Updated By"), + "label": "Last Updated By", "options": "User", }, - {"fieldname": "_user_tags", "fieldtype": "Data", "label": _lt("Tags")}, - {"fieldname": "_liked_by", "fieldtype": "Data", "label": _lt("Liked By")}, - {"fieldname": "_comments", "fieldtype": "Text", "label": _lt("Comments")}, - {"fieldname": "_assign", "fieldtype": "Text", "label": _lt("Assigned To")}, - {"fieldname": "docstatus", "fieldtype": "Int", "label": _lt("Document Status")}, + {"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"}, + {"fieldname": "_liked_by", "fieldtype": "Data", "label": "Liked By"}, + {"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"}, + {"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"}, + {"fieldname": "docstatus", "fieldtype": "Int", "label": "Document Status"}, ] @@ -230,6 +230,9 @@ def get_permitted_fields( if permission_type is None: permission_type = "select" if frappe.only_has_select_perm(doctype, user=user) else "read" + meta_fields = meta.default_fields.copy() + optional_meta_fields = [x for x in optional_fields if x in valid_columns] + if permitted_fields := meta.get_permitted_fieldnames( parenttype=parenttype, user=user, @@ -239,15 +242,12 @@ def get_permitted_fields( if permission_type == "select": return permitted_fields - meta_fields = meta.default_fields.copy() - optional_meta_fields = [x for x in optional_fields if x in valid_columns] - if meta.istable: meta_fields.extend(child_table_fields) return meta_fields + permitted_fields + optional_meta_fields - return [] + return meta_fields + optional_meta_fields def is_default_field(fieldname: str) -> bool: diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index f83a971082..943fcfae10 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -217,7 +217,7 @@ class BaseDocument: value = self.__dict__.get(key, default) - if limit and isinstance(value, (list, tuple)) and len(value) > limit: + if limit and isinstance(value, list | tuple) and len(value) > limit: value = value[:limit] return value @@ -396,7 +396,7 @@ class BaseDocument: value = None if convert_dates_to_str and isinstance( - value, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) + value, datetime.datetime | datetime.date | datetime.time | datetime.timedelta ): value = str(value) @@ -774,9 +774,7 @@ class BaseDocument: invalid_links = [] cancelled_links = [] - for df in self.meta.get_link_fields() + self.meta.get( - "fields", {"fieldtype": ("=", "Dynamic Link")} - ): + for df in self.meta.get_link_fields() + self.meta.get("fields", {"fieldtype": ("=", "Dynamic Link")}): docname = self.get(df.fieldname) if docname: @@ -806,13 +804,18 @@ class BaseDocument: # cache a single value type values = _dict(name=frappe.db.get_value(doctype, docname, "name", cache=True)) else: - values_to_fetch = ["name"] + [_df.fetch_from.split(".")[-1] for _df in fields_to_fetch] + values_to_fetch = ["name"] + [ + _df.fetch_from.split(".")[-1] for _df in fields_to_fetch + ] # fallback to dict with field_to_fetch=None if link field value is not found # (for compatibility, `values` must have same data type) empty_values = _dict({value: None for value in values_to_fetch}) # don't cache if fetching other values too - values = frappe.db.get_value(doctype, docname, values_to_fetch, as_dict=True) or empty_values + values = ( + frappe.db.get_value(doctype, docname, values_to_fetch, as_dict=True) + or empty_values + ) if getattr(frappe.get_meta(doctype), "issingle", 0): values.name = doctype @@ -838,7 +841,6 @@ class BaseDocument: and frappe.get_meta(doctype).is_submittable and cint(frappe.db.get_value(doctype, docname, "docstatus")) == DocStatus.cancelled() ): - cancelled_links.append((df.fieldname, docname, get_msg(df, docname))) return invalid_links, cancelled_links @@ -855,7 +857,9 @@ class BaseDocument: if not fetch_from_df: frappe.throw( - _('Please check the value of "Fetch From" set for field {0}').format(frappe.bold(df.label)), + _('Please check the value of "Fetch From" set for field {0}').format( + frappe.bold(df.label) + ), title=_("Wrong Fetch From value"), ) @@ -1123,9 +1127,7 @@ class BaseDocument: if self.get(fieldname) and not self.is_dummy_password(self.get(fieldname)): return self.get(fieldname) - return get_decrypted_password( - self.doctype, self.name, fieldname, raise_exception=raise_exception - ) + return get_decrypted_password(self.doctype, self.name, fieldname, raise_exception=raise_exception) def is_dummy_password(self, pwd): return "".join(set(pwd)) == "*" @@ -1187,7 +1189,7 @@ class BaseDocument: if not doc: doc = getattr(self, "parent_doc", None) or self - if (absolute_value or doc.get("absolute_value")) and isinstance(val, (int, float)): + if (absolute_value or doc.get("absolute_value")) and isinstance(val, int | float): val = abs(self.get(fieldname)) return format_value(val, df=df, doc=doc, currency=currency, format=format) @@ -1295,7 +1297,7 @@ def _filter(data, filters, limit=None): for f in filters: fval = filters[f] - if not isinstance(fval, (tuple, list)): + if not isinstance(fval, tuple | list): if fval is True: fval = ("not None", fval) elif fval is False: diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index d836fa481d..b1ab33ff29 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -32,9 +32,7 @@ def get_new_doc(doctype, parent_doc=None, parentfield=None, as_dict=False): def make_new_doc(doctype): - doc = frappe.get_doc( - {"doctype": doctype, "__islocal": 1, "owner": frappe.session.user, "docstatus": 0} - ) + doc = frappe.get_doc({"doctype": doctype, "__islocal": 1, "owner": frappe.session.user, "docstatus": 0}) set_user_and_static_default_values(doc) @@ -72,7 +70,9 @@ def set_user_and_static_default_values(doc): else: if df.fieldname != doc.meta.title_field: - static_default_value = get_static_default_value(df, doctype_user_permissions, allowed_records) + static_default_value = get_static_default_value( + df, doctype_user_permissions, allowed_records + ) if static_default_value is not None: doc.set(df.fieldname, static_default_value) @@ -118,9 +118,7 @@ def get_static_default_value(df, doctype_user_permissions, allowed_records): return df.options.split("\n", 1)[0] -def validate_value_via_user_permissions( - df, doctype_user_permissions, allowed_records, user_default=None -): +def validate_value_via_user_permissions(df, doctype_user_permissions, allowed_records, user_default=None): is_valid = True # If User Permission exists and allowed records is empty, # that means there are User Perms, but none applicable to this new doctype. @@ -173,9 +171,7 @@ def get_default_based_on_another_field(df, user_permissions, parent_doc): ref_doctype = df.default[1:] ref_fieldname = ref_doctype.lower().replace(" ", "_") - reference_name = ( - parent_doc.get(ref_fieldname) if parent_doc else frappe.db.get_default(ref_fieldname) - ) + reference_name = parent_doc.get(ref_fieldname) if parent_doc else frappe.db.get_default(ref_fieldname) default_value = frappe.db.get_value(ref_doctype, reference_name, df.fieldname) is_allowed_default_value = not user_permissions_exist(df, user_permissions.get(df.options)) or ( default_value in get_allowed_docs_for_doctype(user_permissions[df.options], df.parent) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 1957fb40f7..da4dd4f112 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -32,21 +32,13 @@ from frappe.utils import ( from frappe.utils.data import DateTimeLikeObject, get_datetime, getdate, sbool LOCATE_PATTERN = re.compile(r"locate\([^,]+,\s*[`\"]?name[`\"]?\s*\)", flags=re.IGNORECASE) -LOCATE_CAST_PATTERN = re.compile( - r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", flags=re.IGNORECASE -) -FUNC_IFNULL_PATTERN = re.compile( - r"(strpos|ifnull|coalesce)\(\s*[`\"]?name[`\"]?\s*,", flags=re.IGNORECASE -) -CAST_VARCHAR_PATTERN = re.compile( - r"([`\"]?tab[\w`\" -]+\.[`\"]?name[`\"]?)(?!\w)", flags=re.IGNORECASE -) +LOCATE_CAST_PATTERN = re.compile(r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", flags=re.IGNORECASE) +FUNC_IFNULL_PATTERN = re.compile(r"(strpos|ifnull|coalesce)\(\s*[`\"]?name[`\"]?\s*,", flags=re.IGNORECASE) +CAST_VARCHAR_PATTERN = re.compile(r"([`\"]?tab[\w`\" -]+\.[`\"]?name[`\"]?)(?!\w)", flags=re.IGNORECASE) ORDER_BY_PATTERN = re.compile(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", flags=re.IGNORECASE) SUB_QUERY_PATTERN = re.compile("^.*[,();@].*") IS_QUERY_PATTERN = re.compile(r"^(select|delete|update|drop|create)\s") -IS_QUERY_PREDICATE_PATTERN = re.compile( - r"\s*[0-9a-zA-z]*\s*( from | group by | order by | where | join )" -) +IS_QUERY_PREDICATE_PATTERN = re.compile(r"\s*[0-9a-zA-z]*\s*( from | group by | order by | where | join )") FIELD_QUOTE_PATTERN = re.compile(r"[0-9a-zA-Z]+\s*'") FIELD_COMMA_PATTERN = re.compile(r"[0-9a-zA-Z]+\s*,") STRICT_FIELD_PATTERN = re.compile(r".*/\*.*") @@ -118,15 +110,12 @@ class DatabaseQuery: *, parent_doctype=None, ) -> list: - if not ignore_permissions: self.check_read_permission(self.doctype, parent_doctype=parent_doctype) # filters and fields swappable # its hard to remember what comes first - if isinstance(fields, dict) or ( - fields and isinstance(fields, list) and isinstance(fields[0], list) - ): + if isinstance(fields, dict) or (fields and isinstance(fields, list) and isinstance(fields[0], list)): # if fields is given as dict/list of list, its probably filters filters, fields = fields, filters @@ -217,6 +206,10 @@ class DatabaseQuery: args = self.prepare_args() args.limit = self.add_limit() + if not args.fields: + # apply_fieldlevel_read_permissions has likely removed ALL the fields that user asked for + return [] + if args.conditions: args.conditions = "where " + args.conditions @@ -229,15 +222,12 @@ class DatabaseQuery: if frappe.db.db_type == "postgres" and args.order_by and args.group_by: args = self.prepare_select_args(args) - query = ( - """select %(fields)s - from %(tables)s - %(conditions)s - %(group_by)s - %(order_by)s - %(limit)s""" - % args - ) + query = """select {fields} + from {tables} + {conditions} + {group_by} + {order_by} + {limit}""".format(**args) return frappe.db.sql( query, @@ -484,7 +474,8 @@ class DatabaseQuery: 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() + table_name not in self.query_tables + and table_name not in self.linked_table_aliases.values() ): self.append_table(table_name) @@ -697,7 +688,11 @@ class DatabaseQuery: params = (x.strip() for x in _params[0].split(",")) for param in params: if not ( - not param or param in permitted_fields or param.isnumeric() or "'" in param or '"' in param + not param + or param in permitted_fields + or param.isnumeric() + or "'" in param + or '"' in param ): self.remove_field(i) break @@ -835,7 +830,6 @@ class DatabaseQuery: f.fieldname in ("creation", "modified") or (df and (df.fieldtype == "Date" or df.fieldtype == "Datetime")) ): - escape = False value = get_between_date_filter(f.value, df) fallback = f"'{FallBackDateTimeStr}'" @@ -881,9 +875,7 @@ class DatabaseQuery: # because "like" uses backslash (\) for escaping value = value.replace("\\", "\\\\").replace("%", "%%") - elif ( - f.operator == "=" and df and df.fieldtype in ["Link", "Data"] - ): # TODO: Refactor if possible + elif f.operator == "=" and df and df.fieldtype in ["Link", "Data"]: # TODO: Refactor if possible value = f.value or "''" fallback = "''" @@ -1104,7 +1096,9 @@ class DatabaseQuery: sort_field = self.doctype_meta.sort_field or "modified" sort_order = (self.doctype_meta.sort_field and self.doctype_meta.sort_order) or "desc" if self.order_by: - args.order_by = f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}" + args.order_by = ( + f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}" + ) # draft docs always on top if hasattr(self.doctype_meta, "is_submittable") and self.doctype_meta.is_submittable: @@ -1273,7 +1267,7 @@ def get_between_date_filter(value, df=None): from_date = frappe.utils.nowdate() to_date = frappe.utils.nowdate() - if value and isinstance(value, (list, tuple)): + if value and isinstance(value, list | tuple): if len(value) >= 1: from_date = value[0] if len(value) >= 2: diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index b6ab2546bf..4463188208 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -65,7 +65,6 @@ def delete_doc( doc = None if doctype == "DocType": if for_reload: - try: doc = frappe.get_doc(doctype, name) except frappe.DoesNotExistError: @@ -92,7 +91,10 @@ def delete_doc( frappe.conf.developer_mode and not doc.custom and not ( - for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_uninstall + for_reload + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_uninstall ) ): try: @@ -133,6 +135,7 @@ def delete_doc( doctype=doc.doctype, name=doc.name, now=frappe.flags.in_test, + enqueue_after_commit=True, ) # clear cache for Document @@ -161,13 +164,11 @@ def add_to_deleted_document(doc): """Add this document to Deleted Document table. Called after delete""" if doc.doctype != "Deleted Document" and frappe.flags.in_install != "frappe": frappe.get_doc( - dict( - doctype="Deleted Document", - deleted_doctype=doc.doctype, - deleted_name=doc.name, - data=doc.as_json(), - owner=frappe.session.user, - ) + doctype="Deleted Document", + deleted_doctype=doc.doctype, + deleted_name=doc.name, + data=doc.as_json(), + owner=frappe.session.user, ).db_insert() @@ -299,7 +300,6 @@ def check_if_doc_is_linked(doc, method="Delete"): def check_if_doc_is_dynamically_linked(doc, method="Delete"): """Raise `frappe.LinkExistsError` if the document is dynamically linked""" for df in get_dynamic_link_map().get(doc.doctype, []): - ignore_linked_doctypes = doc.get("ignore_linked_doctypes") or [] if df.parent in frappe.get_hooks("ignore_links_on_delete") or ( @@ -328,9 +328,7 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"): df["table"] = ", `parent`, `parenttype`, `idx`" if meta.istable else "" for refdoc in frappe.db.sql( """select `name`, `docstatus` {table} from `tab{parent}` where - {options}=%s and {fieldname}=%s""".format( - **df - ), + {options}=%s and {fieldname}=%s""".format(**df), (doc.doctype, doc.name), as_dict=True, ): @@ -354,10 +352,8 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"): def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=""): - doc_link = '{1}'.format(doc.doctype, doc.name) - reference_link = '{1}'.format( - reference_doctype, reference_docname - ) + doc_link = f'{doc.name}' + reference_link = f'{reference_docname}' # hack to display Single doctype only once in message if reference_doctype == reference_docname: @@ -409,14 +405,12 @@ def clear_references( reference_name_field="reference_name", ): frappe.db.sql( - """update - `tab{0}` + f"""update + `tab{doctype}` set - {1}=NULL, {2}=NULL + {reference_doctype_field}=NULL, {reference_name_field}=NULL where - {1}=%s and {2}=%s""".format( - doctype, reference_doctype_field, reference_name_field - ), # nosec + {reference_doctype_field}=%s and {reference_name_field}=%s""", # nosec (reference_doctype, reference_name), ) diff --git a/frappe/model/document.py b/frappe/model/document.py index 8ba9b0efd4..090008492e 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -21,15 +21,18 @@ from frappe.model.naming import set_new_name, validate_name from frappe.model.utils import is_virtual_doctype from frappe.model.workflow import set_workflow_state_on_action, validate_workflow from frappe.types import DF -from frappe.utils import compare, cstr, date_diff, file_lock, flt, get_datetime_str, now +from frappe.utils import compare, cstr, date_diff, file_lock, flt, now from frappe.utils.data import get_absolute_url -from frappe.utils.deprecations import deprecated from frappe.utils.global_search import update_global_search if TYPE_CHECKING: from frappe.core.doctype.docfield.docfield import DocField +DOCUMENT_LOCK_EXPIRTY = 12 * 60 * 60 # All locks expire in 12 hours automatically +DOCUMENT_LOCK_SOFT_EXPIRY = 60 * 60 # Let users force-unlock after 60 minutes + + def get_doc(*args, **kwargs): """Return a `frappe.model.Document` object. @@ -158,7 +161,7 @@ class Document(BaseDocument): else: get_value_kwargs = {"for_update": self.flags.for_update, "as_dict": True} - if not isinstance(self.name, (dict, list)): + if not isinstance(self.name, dict | list): get_value_kwargs["order_by"] = None d = frappe.db.get_value( @@ -318,9 +321,7 @@ class Document(BaseDocument): if hasattr(self, "__unsaved"): delattr(self, "__unsaved") - if not ( - frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard - ): + if not (frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard): if frappe.get_cached_value("User", frappe.session.user, "follow_created_documents"): follow_document(self.doctype, self.name, frappe.session.user) return self @@ -398,7 +399,6 @@ class Document(BaseDocument): # loop through attachments for attach_item in get_attachments(self.doctype, self.amended_from): - # save attachments to new doc _file = frappe.get_doc( { @@ -601,7 +601,6 @@ class Document(BaseDocument): for df in self.meta.get( "fields", {"non_negative": ("=", 1), "fieldtype": ("in", ["Int", "Float", "Currency"])} ): - if flt(self.get(df.fieldname)) < 0: msg = get_msg(df) frappe.throw(msg, frappe.NonNegativeError, title=_("Negative Value")) @@ -679,23 +678,15 @@ class Document(BaseDocument): return same def apply_fieldlevel_read_permissions(self): - """Remove values the user is not allowed to read (called when loading in desk)""" - + """Remove values the user is not allowed to read.""" if frappe.session.user == "Administrator": return - has_higher_permlevel = False - all_fields = self.meta.fields.copy() for table_field in self.meta.get_table_fields(): all_fields += frappe.get_meta(table_field.options).fields or [] - for df in all_fields: - if df.permlevel > 0: - has_higher_permlevel = True - break - - if not has_higher_permlevel: + if all(df.permlevel == 0 for df in all_fields): return has_access_to = self.get_permlevel_access("read") @@ -745,9 +736,7 @@ class Document(BaseDocument): roles = frappe.get_roles() for perm in self.get_permissions(): - if ( - perm.role in roles and perm.get(permission_type) and perm.permlevel not in allowed_permlevels - ): + if perm.role in roles and perm.get(permission_type) and perm.permlevel not in allowed_permlevels: allowed_permlevels.append(perm.permlevel) return allowed_permlevels @@ -894,7 +883,7 @@ class Document(BaseDocument): if not missing: return - for fieldname, msg in missing: + for idx, msg in missing: # noqa: B007 msgprint(msg) if frappe.flags.print_messages: @@ -1027,9 +1016,7 @@ class Document(BaseDocument): self.docstatus = DocStatus.cancelled() return self.save() - def _rename( - self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True - ): + def _rename(self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True): """Rename the document. Triggers frappe.rename_doc, then reloads.""" from frappe.model.rename_doc import rename_doc @@ -1170,11 +1157,7 @@ class Document(BaseDocument): after_commit=True, ) - if ( - not self.meta.get("read_only") - and not self.meta.get("issingle") - and not self.meta.get("istable") - ): + if not self.meta.get("read_only") and not self.meta.get("issingle") and not self.meta.get("istable"): data = {"doctype": self.doctype, "name": self.name, "user": frappe.session.user} frappe.publish_realtime("list_update", data, after_commit=True) @@ -1269,7 +1252,7 @@ class Document(BaseDocument): doc_to_compare = frappe.get_doc(self.doctype, amended_from) version = frappe.new_doc("Version") - if is_useful_diff := version.update_version_info(doc_to_compare, self): + if version.update_version_info(doc_to_compare, self): version.insert(ignore_permissions=True) if not frappe.flags.in_migrate: @@ -1418,7 +1401,9 @@ class Document(BaseDocument): if user not in _seen: _seen.append(user) - frappe.db.set_value(self.doctype, self.name, "_seen", json.dumps(_seen), update_modified=False) + frappe.db.set_value( + self.doctype, self.name, "_seen", json.dumps(_seen), update_modified=False + ) frappe.local.flags.commit = True def add_viewed(self, user=None, force=False, unique_views=False): @@ -1456,7 +1441,7 @@ class Document(BaseDocument): def get_signature(self): """Return signature (hash) for private URL.""" - return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() + return hashlib.sha224(f"{self.doctype}:{self.name}".encode(), usedforsecurity=False).hexdigest() def get_document_share_key(self, expires_on=None, no_expiry=False): if no_expiry: @@ -1514,9 +1499,25 @@ class Document(BaseDocument): try: self.lock() except frappe.DocumentLockedError: + # Allow unlocking if created more than 60 minutes ago + primary_action = None + if file_lock.lock_age(self.get_signature()) > DOCUMENT_LOCK_SOFT_EXPIRY: + primary_action = { + "label": "Force Unlock", + "server_action": "frappe.model.document.unlock_document", + "hide_on_success": True, + "args": { + "doctype": self.doctype, + "name": self.name, + }, + } + frappe.throw( - _("This document is currently queued for execution. Please try again"), + _( + "This document is currently locked and queued for execution. Please try again after some time." + ), title=_("Document Queued"), + primary_action=primary_action, ) return enqueue( @@ -1535,8 +1536,11 @@ class Document(BaseDocument): signature = self.get_signature() if file_lock.lock_exists(signature): lock_exists = True + if file_lock.lock_age(signature) > DOCUMENT_LOCK_EXPIRTY: + file_lock.delete_lock(signature) + lock_exists = False if timeout: - for i in range(timeout): + for _ in range(timeout): time.sleep(1) if not file_lock.lock_exists(signature): lock_exists = False @@ -1704,3 +1708,9 @@ def _document_values_generator( ignore_virtual=True, ) yield tuple(doc_values.get(col) for col in columns) + + +@frappe.whitelist() +def unlock_document(doctype: str, name: str): + frappe.get_doc(doctype, name).unlock() + frappe.msgprint(frappe._("Document Unlocked"), alert=True) diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index 61ed86de46..96545f9f75 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -42,7 +42,9 @@ def get_dynamic_link_map(for_delete=False): dynamic_link_map.setdefault(meta.name, []).append(df) else: try: - links = frappe.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df)) + links = frappe.db.sql_list( + """select distinct {options} from `tab{parent}`""".format(**df) + ) for doctype in links: dynamic_link_map.setdefault(doctype, []).append(df) except frappe.db.TableMissingError: # noqa: E722 diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py index d467c92958..6d0cba2044 100644 --- a/frappe/model/mapper.py +++ b/frappe/model/mapper.py @@ -63,7 +63,6 @@ def get_mapped_doc( ignore_child_tables=False, cached=False, ): - apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions") # main @@ -168,9 +167,7 @@ def get_mapped_doc( ret_doc.run_method("after_mapping", source_doc) ret_doc.set_onload("load_after_mapping", True) - if ( - apply_strict_user_permissions and not ignore_permissions and not ret_doc.has_permission("create") - ): + if apply_strict_user_permissions and not ignore_permissions and not ret_doc.has_permission("create"): ret_doc.raise_no_permission_to("create") return ret_doc @@ -272,9 +269,7 @@ def map_fetch_fields(target_doc, df, no_copy_fields): def map_child_doc(source_d, target_parent, table_map, source_parent=None): target_child_doctype = table_map["doctype"] target_parentfield = target_parent.get_parentfield_of_doctype(target_child_doctype) - target_d = frappe.new_doc( - target_child_doctype, parent_doc=target_parent, parentfield=target_parentfield - ) + target_d = frappe.new_doc(target_child_doctype, parent_doc=target_parent, parentfield=target_parentfield) map_doc(source_d, target_d, table_map, source_parent) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 7c4a1cf15e..8dca6b32bf 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -147,7 +147,7 @@ class Meta(Document): def serialize(doc): out = {} for key, value in doc.__dict__.items(): - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): if not value or not isinstance(value[0], BaseDocument): # non standard list object, skip continue @@ -155,7 +155,7 @@ class Meta(Document): value = [serialize(d) for d in value] if (not no_nulls and value is None) or isinstance( - value, (str, int, float, datetime, list, tuple) + value, str | int | float | datetime | list | tuple ): out[key] = value @@ -183,9 +183,7 @@ class Meta(Document): return self._dynamic_link_fields def get_select_fields(self): - return self.get( - "fields", {"fieldtype": "Select", "options": ["not in", ["[Select]", "Loading..."]]} - ) + return self.get("fields", {"fieldtype": "Select", "options": ["not in", ["[Select]", "Loading..."]]}) def get_image_fields(self): return self.get("fields", {"fieldtype": "Attach Image"}) @@ -536,7 +534,9 @@ class Meta(Document): def get_fieldnames_with_value(self, with_field_meta=False, with_virtual_fields=False): def is_value_field(docfield): return not ( - not with_virtual_fields and docfield.get("is_virtual") or docfield.fieldtype in no_value_fields + not with_virtual_fields + and docfield.get("is_virtual") + or docfield.fieldtype in no_value_fields ) if with_field_meta: @@ -596,6 +596,10 @@ class Meta(Document): self.get_permlevel_access(permission_type=permission_type, parenttype=parenttype, user=user) ) + if 0 not in permlevel_access and permission_type in ("read", "select"): + if frappe.share.get_shared(self.name, user, rights=[permission_type], limit=1): + permlevel_access.add(0) + permitted_fieldnames.extend( df.fieldname for df in self.get_fieldnames_with_value( @@ -709,9 +713,7 @@ class Meta(Document): module_name, "doctype", doctype, "templates", doctype + suffix + ".html" ) if os.path.exists(template_path): - return "{module_name}/doctype/{doctype_name}/templates/{doctype_name}{suffix}.html".format( - module_name=module_name, doctype_name=doctype, suffix=suffix - ) + return f"{module_name}/doctype/{doctype}/templates/{doctype}{suffix}.html" return None def is_nested_set(self): @@ -766,7 +768,6 @@ def get_field_currency(df, doc=None): and frappe.local.field_currency.get((doc.doctype, doc.parent), {}).get(df.fieldname) ) ): - ref_docname = doc.get("parent") or doc.name if ":" in cstr(df.get("options")): @@ -789,8 +790,7 @@ def get_field_currency(df, doc=None): ) return frappe.local.field_currency.get((doc.doctype, doc.name), {}).get(df.fieldname) or ( - doc.get("parent") - and frappe.local.field_currency.get((doc.doctype, doc.parent), {}).get(df.fieldname) + doc.get("parent") and frappe.local.field_currency.get((doc.doctype, doc.parent), {}).get(df.fieldname) ) @@ -843,9 +843,7 @@ def trim_tables(doctype=None, dry_run=False, quiet=False): if quiet: continue click.secho(f"Ignoring missing table for DocType: {doctype}", fg="yellow", err=True) - click.secho( - f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True - ) + click.secho(f"Consider removing record in the DocType table for {doctype}", fg="yellow", err=True) except Exception as e: if quiet: continue diff --git a/frappe/model/naming.py b/frappe/model/naming.py index e775e0573b..15e4c549e6 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -115,9 +115,7 @@ class NamingSeries: if frappe.db.get_value("Series", prefix, "name", order_by="name") is None: frappe.qb.into(Series).insert(prefix, 0).columns("name", "current").run() - ( - frappe.qb.update(Series).set(Series.current, cint(new_count)).where(Series.name == prefix) - ).run() + (frappe.qb.update(Series).set(Series.current, cint(new_count)).where(Series.name == prefix)).run() def get_current_value(self) -> int: prefix = self.get_prefix() @@ -276,7 +274,6 @@ def parse_naming_series( doc: Optional["Document"] = None, number_generator: Callable[[str, int], str] | None = None, ) -> str: - """Parse the naming series and get next name. args: @@ -411,9 +408,7 @@ def revert_series_if_last(key, name, doc=None): count = cint(name.replace(prefix, "")) series = DocType("Series") - current = ( - frappe.qb.from_(series).where(series.name == prefix).for_update().select("current") - ).run() + current = (frappe.qb.from_(series).where(series.name == prefix).for_update().select("current")).run() if current and current[0][0] == count: frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix) @@ -431,7 +426,6 @@ def get_default_naming_series(doctype: str) -> str | None: def validate_name(doctype: str, name: int | str): - if not name: frappe.throw(_("No Name Specified for {0}").format(doctype)) @@ -456,9 +450,7 @@ def validate_name(doctype: str, name: int | str): special_characters = "<>" if re.findall(f"[{special_characters}]+", name): message = ", ".join(f"'{c}'" for c in special_characters) - frappe.throw( - _("Name cannot contain special characters like {0}").format(message), frappe.NameError - ) + frappe.throw(_("Name cannot contain special characters like {0}").format(message), frappe.NameError) return name @@ -473,12 +465,10 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-" if exists: last = frappe.db.sql( - """SELECT `{fieldname}` FROM `tab{doctype}` - WHERE `{fieldname}` {regex_character} %s + f"""SELECT `{fieldname}` FROM `tab{doctype}` + WHERE `{fieldname}` {frappe.db.REGEX_CHARACTER} %s ORDER BY length({fieldname}) DESC, - `{fieldname}` DESC LIMIT 1""".format( - doctype=doctype, fieldname=fieldname, regex_character=frappe.db.REGEX_CHARACTER - ), + `{fieldname}` DESC LIMIT 1""", regex, ) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 7d5539becd..b535331c23 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -47,7 +47,7 @@ def update_document_title( # TODO: omit this after runtime type checking (ref: https://github.com/frappe/frappe/pull/14927) for obj in [docname, updated_title, updated_name]: - if not isinstance(obj, (str, NoneType)): + if not isinstance(obj, str | NoneType): frappe.throw(f"{obj=} must be of type str or None") # handle bad API usages @@ -60,9 +60,7 @@ def update_document_title( title_field = doc.meta.get_title_field() - title_updated = ( - updated_title and (title_field != "name") and (updated_title != doc.get(title_field)) - ) + title_updated = updated_title and (title_field != "name") and (updated_title != doc.get(title_field)) name_updated = updated_name and (updated_name != doc.name) queue = kwargs.get("queue") or "default" @@ -213,9 +211,7 @@ def rename_doc( if merge: new_doc.add_comment("Edit", _("merged {0} into {1}").format(frappe.bold(old), frappe.bold(new))) else: - new_doc.add_comment( - "Edit", _("renamed from {0} to {1}").format(frappe.bold(old), frappe.bold(new)) - ) + new_doc.add_comment("Edit", _("renamed from {0} to {1}").format(frappe.bold(old), frappe.bold(new))) if merge: frappe.delete_doc(doctype, old) @@ -358,9 +354,7 @@ def validate_rename( _SAVE_POINT = f"validate_rename_{frappe.generate_hash(length=8)}" frappe.db.savepoint(_SAVE_POINT) - exists = ( - frappe.qb.from_(doctype).where(Field("name") == new).for_update().select("name").run(pluck=True) - ) + exists = frappe.qb.from_(doctype).where(Field("name") == new).for_update().select("name").run(pluck=True) exists = exists[0] if exists else None if not frappe.db.exists(doctype, old): @@ -379,9 +373,7 @@ def validate_rename( if not merge and exists and not ignore_if_exists: frappe.throw(_("Another {0} with name {1} exists, select another name").format(doctype, new)) - if not ( - ignore_permissions or frappe.permissions.has_permission(doctype, "write", print_logs=False) - ): + if not (ignore_permissions or frappe.permissions.has_permission(doctype, "write", print_logs=False)): frappe.throw(_("You need write permission to rename")) if not force and not ignore_permissions and not meta.allow_rename: @@ -481,9 +473,7 @@ def get_link_fields(doctype: str) -> list[dict]: .run(as_dict=True) ) - ps_issingle = ( - frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") - ) + ps_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") property_setter_fields = ( frappe.qb.from_(ps) .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle) @@ -562,9 +552,7 @@ def get_select_fields(old: str, new: str) -> list[dict]: ) # remove fields whose options have been changed using property setter - ps_issingle = ( - frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") - ) + ps_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") property_setter_select_fields = ( frappe.qb.from_(ps) .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle) @@ -593,17 +581,13 @@ def update_select_field_values(old: str, new: str): & (DocField.options.like(f"%\n{old}%") | DocField.options.like(f"%{old}\n%")) ).run() - frappe.qb.update(CustomField).set( - CustomField.options, Replace(CustomField.options, old, new) - ).where( + frappe.qb.update(CustomField).set(CustomField.options, Replace(CustomField.options, old, new)).where( (CustomField.fieldtype == "Select") & (CustomField.dt != new) & (CustomField.options.like(f"%\n{old}%") | CustomField.options.like(f"%{old}\n%")) ).run() - frappe.qb.update(PropertySetter).set( - PropertySetter.value, Replace(PropertySetter.value, old, new) - ).where( + frappe.qb.update(PropertySetter).set(PropertySetter.value, Replace(PropertySetter.value, old, new)).where( (PropertySetter.property == "options") & (PropertySetter.field_name.notnull()) & (PropertySetter.doc_type != new) @@ -659,9 +643,7 @@ def rename_dynamic_links(doctype: str, old: str, new: str): ).run() -def bulk_rename( - doctype: str, rows: list[list] | None = None, via_console: bool = False -) -> list[str] | None: +def bulk_rename(doctype: str, rows: list[list] | None = None, via_console: bool = False) -> list[str] | None: """Bulk rename documents :param doctype: DocType to be renamed diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index 153a42ec12..7d8a389ec9 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -3,7 +3,6 @@ import re import frappe -from frappe import _ from frappe.build import html_to_js_template from frappe.utils import cstr from frappe.utils.caching import site_cache @@ -30,9 +29,8 @@ def set_default(doc, key): frappe.db.set(doc, "is_default", 1) frappe.db.sql( - """update `tab%s` set `is_default`=0 - where `%s`=%s and name!=%s""" - % (doc.doctype, key, "%s", "%s"), + """update `tab{}` set `is_default`=0 + where `{}`={} and name!={}""".format(doc.doctype, key, "%s", "%s"), (doc.get(key), doc.name), ) @@ -62,11 +60,11 @@ def render_include(content): content = cstr(content) # try 5 levels of includes - for i in range(5): + for _ in range(5): if "{% include" in content: paths = INCLUDE_DIRECTIVE_PATTERN.findall(content) if not paths: - frappe.throw(_("Invalid include path"), InvalidIncludePath) + raise InvalidIncludePath for path in paths: app, app_path = path.split("/", 1) diff --git a/frappe/model/utils/rename_doc.py b/frappe/model/utils/rename_doc.py index ae6649f057..828123283a 100644 --- a/frappe/model/utils/rename_doc.py +++ b/frappe/model/utils/rename_doc.py @@ -29,9 +29,7 @@ def update_linked_doctypes( ) -def get_fetch_fields( - doctype: str, linked_to: str, ignore_doctypes: list | None = None -) -> list[dict]: +def get_fetch_fields(doctype: str, linked_to: str, ignore_doctypes: list | None = None) -> list[dict]: """ doctype = Master DocType in which the changes are being made linked_to = DocType name of the field thats being updated in Master diff --git a/frappe/model/utils/rename_field.py b/frappe/model/utils/rename_field.py index e3b0835ddd..23c934da74 100644 --- a/frappe/model/utils/rename_field.py +++ b/frappe/model/utils/rename_field.py @@ -27,9 +27,8 @@ def rename_field(doctype, old_fieldname, new_fieldname, validate=True): if new_field.fieldtype in table_fields: # change parentfield of table mentioned in options frappe.db.sql( - """update `tab%s` set parentfield=%s - where parentfield=%s""" - % (new_field.options.split("\n", 1)[0], "%s", "%s"), + """update `tab{}` set parentfield={} + where parentfield={}""".format(new_field.options.split("\n", 1)[0], "%s", "%s"), (new_fieldname, old_fieldname), ) @@ -142,9 +141,8 @@ def update_users_report_view_settings(doctype, ref_fieldname, new_fieldname): if columns_modified: frappe.db.sql( - """update `tabDefaultValue` set defvalue=%s - where defkey=%s""" - % ("%s", "%s"), + """update `tabDefaultValue` set defvalue={} + where defkey={}""".format("%s", "%s"), (json.dumps(new_columns), key), ) diff --git a/frappe/model/utils/user_settings.py b/frappe/model/utils/user_settings.py index 02bc67b929..16922200bc 100644 --- a/frappe/model/utils/user_settings.py +++ b/frappe/model/utils/user_settings.py @@ -87,7 +87,10 @@ def update_user_settings_data( if view_settings and view_settings.get("filters"): view_filters = view_settings.get("filters") for view_filter in view_filters: - if condition_fieldname and view_filter[filter_dict[condition_fieldname]] != condition_values: + if ( + condition_fieldname + and view_filter[filter_dict[condition_fieldname]] != condition_values + ): continue if view_filter[filter_dict[fieldname]] == old: view_filter[filter_dict[fieldname]] = new diff --git a/frappe/model/virtual_doctype.py b/frappe/model/virtual_doctype.py index 6d8088ed80..6390e35cef 100644 --- a/frappe/model/virtual_doctype.py +++ b/frappe/model/virtual_doctype.py @@ -60,9 +60,7 @@ def validate_controller(doctype: str) -> None: try: controller = get_controller(doctype) except ImportError: - frappe.msgprint( - _("Failed to import virtual doctype {}, is controller file present?").format(doctype) - ) + frappe.msgprint(_("Failed to import virtual doctype {}, is controller file present?").format(doctype)) return def _as_str(method): diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index c089b8fa74..69d9a5065f 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -29,9 +29,7 @@ class WorkflowPermissionError(frappe.ValidationError): def get_workflow_name(doctype): workflow_name = frappe.cache.hget("workflow", doctype) if workflow_name is None: - workflow_name = frappe.db.get_value( - "Workflow", {"document_type": doctype, "is_active": 1}, "name" - ) + workflow_name = frappe.db.get_value("Workflow", {"document_type": doctype, "is_active": 1}, "name") frappe.cache.hset("workflow", doctype, workflow_name or "") return workflow_name @@ -94,9 +92,7 @@ def is_transition_condition_satisfied(transition, doc) -> bool: if not transition.condition: return True else: - return frappe.safe_eval( - transition.condition, get_workflow_safe_globals(), dict(doc=doc.as_dict()) - ) + return frappe.safe_eval(transition.condition, get_workflow_safe_globals(), dict(doc=doc.as_dict())) @frappe.whitelist() @@ -216,9 +212,7 @@ def get_workflow(doctype) -> "Workflow": def has_approval_access(user, doc, transition): - return ( - user == "Administrator" or transition.get("allow_self_approval") or user != doc.get("owner") - ) + return user == "Administrator" or transition.get("allow_self_approval") or user != doc.get("owner") def get_workflow_state_field(workflow_name): @@ -235,7 +229,6 @@ def get_workflow_field_value(workflow_name, field): @frappe.whitelist() def bulk_workflow_approval(docnames, doctype, action): - docnames = json.loads(docnames) if len(docnames) < 20: _bulk_workflow_action(docnames, doctype, action) @@ -259,7 +252,7 @@ def _bulk_workflow_action(docnames, doctype, action): successful_transactions = defaultdict(list) frappe.clear_messages() - for (idx, docname) in enumerate(docnames, 1): + for idx, docname in enumerate(docnames, 1): message_dict = {} try: show_progress(docnames, _("Applying: {0}").format(action), idx, docname) @@ -334,7 +327,7 @@ def get_common_transition_actions(docs, doctype): if isinstance(docs, str): docs = json.loads(docs) try: - for (i, doc) in enumerate(docs, 1): + for i, doc in enumerate(docs, 1): if not doc.get("doctype"): doc["doctype"] = doctype actions = [ diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py index b58bdf235f..52379892d0 100644 --- a/frappe/modules/import_file.py +++ b/frappe/modules/import_file.py @@ -38,7 +38,7 @@ ignore_doctypes = [""] def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False): - if type(module) is list: + if isinstance(module, list): return [ import_file( m[0], @@ -59,9 +59,7 @@ def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_ def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions=False): """Sync a file from txt if modifed, return false if not updated""" path = get_file_path(module, dt, dn) - return import_file_by_path( - path, force, pre_process=pre_process, reset_permissions=reset_permissions - ) + return import_file_by_path(path, force, pre_process=pre_process, reset_permissions=reset_permissions) def get_file_path(module, dt, dn): @@ -211,11 +209,7 @@ def import_doc( docdict["__islocal"] = 1 controller = get_controller(docdict["doctype"]) - if ( - controller - and hasattr(controller, "prepare_for_import") - and callable(getattr(controller, "prepare_for_import")) - ): + if controller and hasattr(controller, "prepare_for_import") and callable(controller.prepare_for_import): controller.prepare_for_import(docdict) doc = frappe.get_doc(docdict) diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index c9bf443248..bc15b646ef 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -82,7 +82,6 @@ def run_all(skip_failing: bool = False, patch_type: PatchType | None = None) -> def get_all_patches(patch_type: PatchType | None = None) -> list[str]: - if patch_type and not isinstance(patch_type, PatchType): frappe.throw(f"Unsupported patch type specified: {patch_type}") diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 5d4cd6deac..3fd3c83b05 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -32,9 +32,7 @@ def export_module_json(doc: "Document", is_standard: bool, module: str) -> str | from frappe.modules.export_file import export_to_files # json - export_to_files( - record_list=[[doc.doctype, doc.name]], record_module=module, create_init=is_standard - ) + export_to_files(record_list=[[doc.doctype, doc.name]], record_module=module, create_init=is_standard) return os.path.join( frappe.get_module_path(module), scrub(doc.doctype), scrub(doc.name), scrub(doc.name) @@ -75,9 +73,7 @@ def export_customizations( } if with_permissions: - custom["custom_perms"] = frappe.get_all( - "Custom DocPerm", fields="*", filters={"parent": doctype} - ) + custom["custom_perms"] = frappe.get_all("Custom DocPerm", fields="*", filters={"parent": doctype}) # also update the custom fields and property setters for all child tables for d in frappe.get_meta(doctype).get_table_fields(): @@ -246,9 +242,7 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""): return doctype_python_modules[key] -def get_module_name( - doctype: str, module: str, prefix: str = "", suffix: str = "", app: str | None = None -): +def get_module_name(doctype: str, module: str, prefix: str = "", suffix: str = "", app: str | None = None): app = scrub(app or get_module_app(module)) module = scrub(module) doctype = scrub(doctype) diff --git a/frappe/oauth.py b/frappe/oauth.py index 119e0d1771..fcd59430d0 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -15,7 +15,6 @@ from frappe.utils.data import get_system_timezone, now_datetime class OAuthWebRequestValidator(RequestValidator): - # Pre- and post-authorization. def validate_client_id(self, client_id, request, *args, **kwargs): # Simple validity check, does client exist? Not banned? @@ -73,7 +72,6 @@ class OAuthWebRequestValidator(RequestValidator): # Post-authorization def save_authorization_code(self, client_id, code, request, *args, **kwargs): - cookie_dict = get_cookie_dict_from_headers(request) oac = frappe.new_doc("OAuth Authorization Code") @@ -292,9 +290,7 @@ class OAuthWebRequestValidator(RequestValidator): - Refresh Token Grant """ - otoken = frappe.get_doc( - "OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"} - ) + otoken = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"}) if not otoken: return False diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index ebfa5d8c32..b0d3244e18 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -143,7 +143,7 @@ def split_by_weight(work, weights, chunk_count): chunk_no = 0 chunk_weight = 0 - for task, weight in zip(work, weights): + for task, weight in zip(work, weights, strict=False): if chunk_weight > expected_weight: chunk_weight = 0 chunk_no += 1 @@ -274,9 +274,7 @@ class ParallelTestWithOrchestrator(ParallelTestRunner): def register_instance(self): test_spec_list = get_all_tests(self.app) - response_data = self.call_orchestrator( - "register-instance", data={"test_spec_list": test_spec_list} - ) + response_data = self.call_orchestrator("register-instance", data={"test_spec_list": test_spec_list}) self.is_master = response_data.get("is_master") def get_next_test(self): diff --git a/frappe/patches.txt b/frappe/patches.txt index 7942fa5b53..a09008d921 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -232,4 +232,6 @@ frappe.patches.v15_0.move_event_cancelled_to_status frappe.patches.v15_0.set_file_type frappe.core.doctype.data_import.patches.remove_stale_docfields_from_legacy_version frappe.patches.v15_0.validate_newsletter_recipients -frappe.patches.v15_0.sanitize_workspace_titles \ No newline at end of file +frappe.patches.v15_0.sanitize_workspace_titles +frappe.patches.v15_0.migrate_role_profile_to_table_multi_select +frappe.core.doctype.user.patches.hash_reset_password_tokens diff --git a/frappe/patches/v10_0/refactor_social_login_keys.py b/frappe/patches/v10_0/refactor_social_login_keys.py index 7beaa4e53b..0c4e940f57 100644 --- a/frappe/patches/v10_0/refactor_social_login_keys.py +++ b/frappe/patches/v10_0/refactor_social_login_keys.py @@ -30,9 +30,7 @@ def execute(): frappe_login_key.base_url = social_login_keys.get("frappe_server_url") frappe_login_key.client_id = social_login_keys.get("frappe_client_id") frappe_login_key.client_secret = social_login_keys.get("frappe_client_secret") - if not ( - frappe_login_key.client_secret and frappe_login_key.client_id and frappe_login_key.base_url - ): + if not (frappe_login_key.client_secret and frappe_login_key.client_id and frappe_login_key.base_url): frappe_login_key.enable_social_login = 0 frappe_login_key.save() @@ -63,9 +61,7 @@ def run_patch(): frappe.reload_doc("core", "doctype", "user", force=True) frappe.reload_doc("core", "doctype", "user_social_login", force=True) - users = frappe.get_all( - "User", fields=["*"], filters={"name": ("not in", ["Administrator", "Guest"])} - ) + users = frappe.get_all("User", fields=["*"], filters={"name": ("not in", ["Administrator", "Guest"])}) for user in users: idx = 0 @@ -122,9 +118,7 @@ def insert_user_social_login(user, modified_by, provider, idx, userid=None, user query = """INSERT INTO `tabUser Social Login` (`{source_cols}`) VALUES ({values}) - """.format( - source_cols="`, `".join(source_cols), values=", ".join([frappe.db.escape(d) for d in values]) - ) + """.format(source_cols="`, `".join(source_cols), values=", ".join([frappe.db.escape(d) for d in values])) frappe.db.sql(query) diff --git a/frappe/patches/v11_0/change_email_signature_fieldtype.py b/frappe/patches/v11_0/change_email_signature_fieldtype.py index 8ca13437e6..b188c048a1 100644 --- a/frappe/patches/v11_0/change_email_signature_fieldtype.py +++ b/frappe/patches/v11_0/change_email_signature_fieldtype.py @@ -5,9 +5,7 @@ import frappe def execute(): - signatures = frappe.db.get_list( - "User", {"email_signature": ["!=", ""]}, ["name", "email_signature"] - ) + signatures = frappe.db.get_list("User", {"email_signature": ["!=", ""]}, ["name", "email_signature"]) frappe.reload_doc("core", "doctype", "user") for d in signatures: signature = d.get("email_signature") diff --git a/frappe/patches/v11_0/delete_duplicate_user_permissions.py b/frappe/patches/v11_0/delete_duplicate_user_permissions.py index f2ca6d51fe..27af1e98c7 100644 --- a/frappe/patches/v11_0/delete_duplicate_user_permissions.py +++ b/frappe/patches/v11_0/delete_duplicate_user_permissions.py @@ -12,9 +12,7 @@ def execute(): for record in duplicateRecords: frappe.db.sql( - """delete from `tabUser Permission` - where allow=%s and user=%s and for_value=%s limit {}""".format( - record.count - 1 - ), + f"""delete from `tabUser Permission` + where allow=%s and user=%s and for_value=%s limit {record.count - 1}""", (record.allow, record.user, record.for_value), ) diff --git a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py index 277235ce04..9fb9cc3fb6 100644 --- a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py +++ b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py @@ -2,10 +2,7 @@ import frappe def execute(): - if ( - frappe.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) - > 10000 - ): + if frappe.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) > 10000: frappe.db.auto_commit_on_many_writes = True files = frappe.get_all( diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py index ce0e43302a..a8e20a4e9b 100644 --- a/frappe/patches/v11_0/remove_skip_for_doctype.py +++ b/frappe/patches/v11_0/remove_skip_for_doctype.py @@ -86,6 +86,4 @@ def execute(): ).insert(*new_user_permissions_list).run() if user_permissions_to_delete: - frappe.db.delete( - "User Permission", filters=(Field("name").isin(tuple(user_permissions_to_delete))) - ) + frappe.db.delete("User Permission", filters=(Field("name").isin(tuple(user_permissions_to_delete)))) diff --git a/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py b/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py index b8fa85217a..7d3e23c9bf 100644 --- a/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py +++ b/frappe/patches/v11_0/rename_workflow_action_to_workflow_action_master.py @@ -3,8 +3,6 @@ from frappe.model.rename_doc import rename_doc def execute(): - if frappe.db.table_exists("Workflow Action") and not frappe.db.table_exists( - "Workflow Action Master" - ): + if frappe.db.table_exists("Workflow Action") and not frappe.db.table_exists("Workflow Action Master"): rename_doc("DocType", "Workflow Action", "Workflow Action Master") frappe.reload_doc("workflow", "doctype", "workflow_action_master") diff --git a/frappe/patches/v11_0/replicate_old_user_permissions.py b/frappe/patches/v11_0/replicate_old_user_permissions.py index 98ae220ff9..2b3b29b6f2 100644 --- a/frappe/patches/v11_0/replicate_old_user_permissions.py +++ b/frappe/patches/v11_0/replicate_old_user_permissions.py @@ -55,7 +55,6 @@ def get_doctypes_to_skip(doctype, user): doctypes_to_skip.append(parent_doctype) elif parent_doctype not in doctypes_to_skip: - user_permission_doctypes = get_user_permission_doctypes(perm) # "No doctypes present" indicates that user permission will be applied to each link field diff --git a/frappe/patches/v11_0/update_list_user_settings.py b/frappe/patches/v11_0/update_list_user_settings.py index 5209b9e384..f90871af27 100644 --- a/frappe/patches/v11_0/update_list_user_settings.py +++ b/frappe/patches/v11_0/update_list_user_settings.py @@ -12,10 +12,8 @@ def execute(): for user in users: # get user_settings for each user settings = frappe.db.sql( - "select * from `__UserSettings` \ - where user={}".format( - frappe.db.escape(user.user) - ), + f"select * from `__UserSettings` \ + where user={frappe.db.escape(user.user)}", as_dict=True, ) diff --git a/frappe/patches/v12_0/fix_email_id_formatting.py b/frappe/patches/v12_0/fix_email_id_formatting.py index 9cc2f49966..6b5375e058 100644 --- a/frappe/patches/v12_0/fix_email_id_formatting.py +++ b/frappe/patches/v12_0/fix_email_id_formatting.py @@ -17,7 +17,6 @@ def fix_communications(): """, as_dict=1, ): - communication["recipients"] = format_email_id(communication.recipients) communication["cc"] = format_email_id(communication.cc) communication["bcc"] = format_email_id(communication.bcc) @@ -35,10 +34,7 @@ def fix_show_as_cc_email_queue(): {"creation": [">", "2020-06-01"], "status": "Not Sent", "show_as_cc": ["like", "%<%"]}, ["name", "show_as_cc"], ): - - frappe.db.set_value( - "Email Queue", queue["name"], "show_as_cc", format_email_id(queue["show_as_cc"]) - ) + frappe.db.set_value("Email Queue", queue["name"], "show_as_cc", format_email_id(queue["show_as_cc"])) def fix_email_queue_recipients(): @@ -48,7 +44,6 @@ def fix_email_queue_recipients(): and status='Not Sent' and creation > '2020-06-01' """, as_dict=1, ): - frappe.db.set_value( "Email Queue Recipient", recipient["name"], "recipient", format_email_id(recipient["recipient"]) ) diff --git a/frappe/patches/v12_0/fix_public_private_files.py b/frappe/patches/v12_0/fix_public_private_files.py index 382e3c2db1..d33bf8fbc2 100644 --- a/frappe/patches/v12_0/fix_public_private_files.py +++ b/frappe/patches/v12_0/fix_public_private_files.py @@ -2,9 +2,7 @@ import frappe def execute(): - files = frappe.get_all( - "File", fields=["is_private", "file_url", "name"], filters={"is_folder": 0} - ) + files = frappe.get_all("File", fields=["is_private", "file_url", "name"], filters={"is_folder": 0}) for file in files: file_url = file.file_url or "" diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index 7283760c23..e1fcb72e21 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -22,7 +22,6 @@ def execute(): phone_values = [] for count, contact_detail in enumerate(contact_details): phone_counter = 1 - is_primary = 1 if contact_detail.email_id: email_values.append( ( @@ -83,9 +82,7 @@ def execute(): (`idx`, `name`, `email_id`, `parentfield`, `parenttype`, `parent`, `is_primary`, `creation`, `modified`, `modified_by`) VALUES {} - """.format( - ", ".join(["%s"] * len(email_values)) - ), + """.format(", ".join(["%s"] * len(email_values))), tuple(email_values), ) @@ -98,9 +95,7 @@ def execute(): (`idx`, `name`, `phone`, `parentfield`, `parenttype`, `parent`, `is_primary_phone`, `is_primary_mobile_no`, `creation`, `modified`, `modified_by`) VALUES {} - """.format( - ", ".join(["%s"] * len(phone_values)) - ), + """.format(", ".join(["%s"] * len(phone_values))), tuple(phone_values), ) diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 4d2061c5ac..c51a21f7eb 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -56,9 +56,7 @@ def execute(): (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, `modified`, `modified_by`) VALUES {} - """.format( - ", ".join([d for d in values]) - ) + """.format(", ".join([d for d in values])) ) values = [] diff --git a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py index 5727ab7b48..e7673ff837 100644 --- a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py +++ b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py @@ -5,8 +5,6 @@ def execute(): frappe.reload_doc("core", "doctype", "doctype_link") frappe.reload_doc("core", "doctype", "doctype_action") frappe.reload_doc("core", "doctype", "doctype") - frappe.model.delete_fields( - {"DocType": ["hide_heading", "image_view", "read_only_onload"]}, delete=1 - ) + frappe.model.delete_fields({"DocType": ["hide_heading", "image_view", "read_only_onload"]}, delete=1) frappe.db.delete("Property Setter", {"property": "read_only_onload"}) diff --git a/frappe/patches/v12_0/replace_null_values_in_tables.py b/frappe/patches/v12_0/replace_null_values_in_tables.py index 1dc8d964a1..617e9886f6 100644 --- a/frappe/patches/v12_0/replace_null_values_in_tables.py +++ b/frappe/patches/v12_0/replace_null_values_in_tables.py @@ -18,7 +18,7 @@ def execute(): update_column_table_map.setdefault(field.TABLE_NAME, []) update_column_table_map[field.TABLE_NAME].append( - "`{fieldname}`=COALESCE(`{fieldname}`, 0)".format(fieldname=field.COLUMN_NAME) + f"`{field.COLUMN_NAME}`=COALESCE(`{field.COLUMN_NAME}`, 0)" ) for table in frappe.db.get_tables(): diff --git a/frappe/patches/v12_0/set_correct_url_in_files.py b/frappe/patches/v12_0/set_correct_url_in_files.py index fee0b5d6fc..98f6e100af 100644 --- a/frappe/patches/v12_0/set_correct_url_in_files.py +++ b/frappe/patches/v12_0/set_correct_url_in_files.py @@ -30,7 +30,9 @@ def execute(): if file_is_private: public_file_url = os.path.join(public_file_path, file_name) if os.path.exists(public_file_url): - frappe.db.set_value("File", file.name, {"file_url": f"/files/{file_name}", "is_private": 0}) + frappe.db.set_value( + "File", file.name, {"file_url": f"/files/{file_name}", "is_private": 0} + ) else: private_file_url = os.path.join(private_file_path, file_name) if os.path.exists(private_file_url): diff --git a/frappe/patches/v12_0/setup_comments_from_communications.py b/frappe/patches/v12_0/setup_comments_from_communications.py index ad168f7c22..add1e4ce68 100644 --- a/frappe/patches/v12_0/setup_comments_from_communications.py +++ b/frappe/patches/v12_0/setup_comments_from_communications.py @@ -7,10 +7,7 @@ def execute(): if frappe.db.count("Communication", filters=dict(communication_type="Comment")) > 20000: frappe.db.auto_commit_on_many_writes = True - for comment in frappe.get_all( - "Communication", fields=["*"], filters=dict(communication_type="Comment") - ): - + for comment in frappe.get_all("Communication", fields=["*"], filters=dict(communication_type="Comment")): new_comment = frappe.new_doc("Comment") new_comment.comment_type = comment.comment_type new_comment.comment_email = comment.sender diff --git a/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py index 4ac6b918b5..ec96a212a4 100644 --- a/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py +++ b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py @@ -34,9 +34,7 @@ def execute(): def get_modified_docs(doctype): - return frappe.get_all( - doctype, filters={"owner": "Administrator", "modified_by": ["!=", "Administrator"]} - ) + return frappe.get_all(doctype, filters={"owner": "Administrator", "modified_by": ["!=", "Administrator"]}) def rename_modified_doc(docname, doctype): diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py index 62c7bcdfde..32d77d578c 100644 --- a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py +++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py @@ -6,9 +6,7 @@ import frappe def execute(): frappe.reload_doc("website", "doctype", "website_theme_ignore_app") - themes = frappe.get_all( - "Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")} - ) + themes = frappe.get_all("Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")}) for theme in themes: doc = frappe.get_doc("Website Theme", theme.name) try: diff --git a/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py index c7c8cbc724..20b92a1929 100644 --- a/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py +++ b/frappe/patches/v13_0/rename_list_view_setting_to_list_view_settings.py @@ -12,9 +12,7 @@ def execute(): frappe.reload_doc("desk", "doctype", "List View Settings") - existing_list_view_settings = frappe.get_all( - "List View Settings", as_list=True, order_by="modified" - ) + existing_list_view_settings = frappe.get_all("List View Settings", as_list=True, order_by="modified") for list_view_setting in frappe.get_all( "List View Setting", fields=["disable_count", "disable_sidebar_stats", "disable_auto_refresh", "name"], diff --git a/frappe/patches/v13_0/set_read_times.py b/frappe/patches/v13_0/set_read_times.py index 987ed0f1b9..bc5a1c90c8 100644 --- a/frappe/patches/v13_0/set_read_times.py +++ b/frappe/patches/v13_0/set_read_times.py @@ -9,9 +9,7 @@ def execute(): for blog in frappe.get_all("Blog Post"): blog = frappe.get_doc("Blog Post", blog.name) - frappe.db.set_value( - "Blog Post", blog.name, "read_time", get_read_time(blog), update_modified=False - ) + frappe.db.set_value("Blog Post", blog.name, "read_time", get_read_time(blog), update_modified=False) def get_read_time(blog): diff --git a/frappe/patches/v13_0/update_date_filters_in_user_settings.py b/frappe/patches/v13_0/update_date_filters_in_user_settings.py index a2ac9e94fb..030ea3936d 100644 --- a/frappe/patches/v13_0/update_date_filters_in_user_settings.py +++ b/frappe/patches/v13_0/update_date_filters_in_user_settings.py @@ -9,14 +9,12 @@ def execute(): for user in users: user_settings = frappe.db.sql( - """ + f""" select * from `__UserSettings` where - user='{user}' - """.format( - user=user.user - ), + user='{user.user}' + """, as_dict=True, ) diff --git a/frappe/patches/v13_0/update_notification_channel_if_empty.py b/frappe/patches/v13_0/update_notification_channel_if_empty.py index 5ebab68f27..5b4c79f62f 100644 --- a/frappe/patches/v13_0/update_notification_channel_if_empty.py +++ b/frappe/patches/v13_0/update_notification_channel_if_empty.py @@ -5,13 +5,10 @@ import frappe def execute(): - frappe.reload_doc("Email", "doctype", "Notification") notifications = frappe.get_all("Notification", {"is_standard": 1}, {"name", "channel"}) for notification in notifications: if not notification.channel: - frappe.db.set_value( - "Notification", notification.name, "channel", "Email", update_modified=False - ) + frappe.db.set_value("Notification", notification.name, "channel", "Email", update_modified=False) frappe.db.commit() diff --git a/frappe/patches/v14_0/copy_mail_data.py b/frappe/patches/v14_0/copy_mail_data.py index c44b7c9e92..69a9818ff9 100644 --- a/frappe/patches/v14_0/copy_mail_data.py +++ b/frappe/patches/v14_0/copy_mail_data.py @@ -3,9 +3,7 @@ import frappe def execute(): # patch for all Email Account with the flag use_imap - for email_account in frappe.get_list( - "Email Account", filters={"enable_incoming": 1, "use_imap": 1} - ): + for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}): # get all data from Email Account doc = frappe.get_doc("Email Account", email_account.name) diff --git a/frappe/patches/v14_0/disable_email_accounts_with_oauth.py b/frappe/patches/v14_0/disable_email_accounts_with_oauth.py index 2066b5f640..4ac1a1b44a 100644 --- a/frappe/patches/v14_0/disable_email_accounts_with_oauth.py +++ b/frappe/patches/v14_0/disable_email_accounts_with_oauth.py @@ -3,9 +3,7 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat def execute(): - if frappe.get_all( - "Email Account", {"auth_method": "OAuth", "connected_user": ["is", "set"]}, limit=1 - ): + if frappe.get_all("Email Account", {"auth_method": "OAuth", "connected_user": ["is", "set"]}, limit=1): return # Setting awaiting password to 1 for email accounts where Oauth is enabled. diff --git a/frappe/patches/v14_0/reset_creation_datetime.py b/frappe/patches/v14_0/reset_creation_datetime.py index 5ba1bc9529..c82e57de25 100644 --- a/frappe/patches/v14_0/reset_creation_datetime.py +++ b/frappe/patches/v14_0/reset_creation_datetime.py @@ -9,9 +9,7 @@ from frappe.query_builder import DocType as _DocType def execute(): """Resetting creation datetimes for DocTypes""" DocType = _DocType("DocType") - doctype_jsons = glob.glob( - os.path.join("..", "apps", "frappe", "frappe", "**", "doctype", "**", "*.json") - ) + doctype_jsons = glob.glob(os.path.join("..", "apps", "frappe", "frappe", "**", "doctype", "**", "*.json")) frappe_modules = frappe.get_all("Module Def", filters={"app_name": "frappe"}, pluck="name") site_doctypes = frappe.get_all( diff --git a/frappe/patches/v15_0/migrate_role_profile_to_table_multi_select.py b/frappe/patches/v15_0/migrate_role_profile_to_table_multi_select.py new file mode 100644 index 0000000000..a8f5baae80 --- /dev/null +++ b/frappe/patches/v15_0/migrate_role_profile_to_table_multi_select.py @@ -0,0 +1,27 @@ +import frappe +from frappe.model.document import bulk_insert + + +def execute(): + users = frappe.get_all( + "User", filters={"role_profile_name": ["is", "set"]}, fields=["name", "role_profile_name"] + ) + user_profiles = get_records_to_insert(users) + bulk_insert("User Role Profile", user_profiles, ignore_duplicates=True) + + +def get_records_to_insert(users): + user_profiles = [] + for user in users: + profiles = frappe.get_doc( + { + "doctype": "User Role Profile", + "role_profile": user.role_profile_name, + "parent": user.name, + "parenttype": "User", + "parentfield": "role_profiles", + } + ) + profiles.set_new_name() + user_profiles.append(profiles) + return user_profiles diff --git a/frappe/permissions.py b/frappe/permissions.py index cc36fc53c6..1452d03657 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -105,9 +105,7 @@ def has_permission( return True if ptype == "share" and frappe.get_system_settings("disable_document_sharing"): - debug and _debug_log( - "User can't share because sharing is disabled globally from system settings" - ) + debug and _debug_log("User can't share because sharing is disabled globally from system settings") return False if not doc and hasattr(doctype, "doctype"): @@ -129,7 +127,7 @@ def has_permission( meta = frappe.get_meta(doctype) if doc: - if isinstance(doc, (str, int)): + if isinstance(doc, str | int): doc = frappe.get_doc(meta.name, doc) perm = get_doc_permissions(doc, user=user, ptype=ptype, debug=debug).get(ptype) if not perm: @@ -213,9 +211,7 @@ def get_doc_permissions(doc, user=None, ptype=None, debug=False): push_perm_check_log(_("Not allowed via controller permission check"), debug=debug) return {ptype: 0} - permissions = copy.deepcopy( - get_role_permissions(meta, user=user, is_owner=is_user_owner(), debug=debug) - ) + permissions = copy.deepcopy(get_role_permissions(meta, user=user, is_owner=is_user_owner(), debug=debug)) debug and _debug_log( "User has following permissions using role permission system: " @@ -294,9 +290,7 @@ def get_role_permissions(doctype_meta, user=None, is_owner=None, debug=False): def has_permission_without_if_owner_enabled(ptype): return any(p.get(ptype, 0) and not p.get("if_owner", 0) for p in applicable_permissions) - applicable_permissions = list( - filter(is_perm_applicable, getattr(doctype_meta, "permissions", [])) - ) + applicable_permissions = list(filter(is_perm_applicable, getattr(doctype_meta, "permissions", []))) has_if_owner_enabled = any(p.get("if_owner", 0) for p in applicable_permissions) perms["has_if_owner_enabled"] = has_if_owner_enabled @@ -385,7 +379,6 @@ def has_user_permission(doc, user=None, debug=False): # check all link fields for user permissions for field in meta.get_link_fields(): - if field.ignore_user_permissions: continue @@ -514,7 +507,9 @@ def get_roles(user=None, with_standard=True): roles = ( frappe.qb.from_(table) .where( - (table.parenttype == "User") & (table.parent == user) & (table.role.notin(AUTOMATIC_ROLES)) + (table.parenttype == "User") + & (table.parent == user) + & (table.role.notin(AUTOMATIC_ROLES)) ) .select(table.role) .run(pluck=True) @@ -569,16 +564,14 @@ def add_user_permission( frappe.throw(_("{0} {1} not found").format(_(doctype), name), frappe.DoesNotExistError) frappe.get_doc( - dict( - doctype="User Permission", - user=user, - allow=doctype, - for_value=name, - is_default=is_default, - applicable_for=applicable_for, - apply_to_all_doctypes=0 if applicable_for else 1, - hide_descendants=hide_descendants, - ) + doctype="User Permission", + user=user, + allow=doctype, + for_value=name, + is_default=is_default, + applicable_for=applicable_for, + apply_to_all_doctypes=0 if applicable_for else 1, + hide_descendants=hide_descendants, ).insert(ignore_permissions=ignore_permissions) diff --git a/frappe/printing/doctype/letter_head/letter_head.js b/frappe/printing/doctype/letter_head/letter_head.js index 55d97cf37f..7055b5f78a 100644 --- a/frappe/printing/doctype/letter_head/letter_head.js +++ b/frappe/printing/doctype/letter_head/letter_head.js @@ -2,7 +2,60 @@ // For license information, please see license.txt frappe.ui.form.on("Letter Head", { + setup(frm) { + frm.get_field("instructions").html(INSTRUCTIONS); + }, + refresh: function (frm) { frm.flag_public_attachments = true; }, + + validate: (frm) => { + ["header_script", "footer_script"].forEach((field) => { + if (!frm.doc[field]) return; + + try { + eval(frm.doc[field]); + } catch (e) { + frappe.throw({ + title: __("Error in Header/Footer Script"), + indicator: "orange", + message: '
' + e.stack + "
", + }); + } + }); + }, }); + +const INSTRUCTIONS = `

${__("Letter Head Scripts")}

+

${__("Header/Footer scripts can be used to add dynamic behaviours.")}

+
+
+// ${__(
+	"The following Header Script will add the current date to an element in 'Header HTML' with class 'header-content'"
+)}
+var el = document.getElementsByClassName("header-content");
+if (el.length > 0) {
+	el[0].textContent += " " + new Date().toGMTString();
+}
+
+
+

${__("You can also access wkhtmltopdf variables (valid only in PDF print):")}

+
+
+// ${__("Get Header and Footer wkhtmltopdf variables")}
+// ${__("Snippet and more variables:  {0}", ["https://wkhtmltopdf.org/usage/wkhtmltopdf.txt"])}
+var vars = {};
+var query_strings_from_url = document.location.search.substring(1).split('&');
+for (var query_string in query_strings_from_url) {
+	if (query_strings_from_url.hasOwnProperty(query_string)) {
+		var temp_var = query_strings_from_url[query_string].split('=', 2);
+		vars[temp_var[0]] = decodeURI(temp_var[1]);
+	}
+}
+var el = document.getElementsByClassName("header-content");
+if (el.length > 0 && vars["page"] == 1) {
+	el[0].textContent += " : " + vars["date"];
+}
+
+
`; diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index 021f79ca93..4ffca134f2 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -26,7 +26,11 @@ "footer_image", "footer_image_height", "footer_image_width", - "footer_align" + "footer_align", + "scripts_section", + "header_script", + "footer_script", + "instructions" ], "fields": [ { @@ -162,13 +166,40 @@ "fieldtype": "Select", "label": "Footer Based On", "options": "Image\nHTML" + }, + { + "depends_on": "eval:!doc.__islocal && doc.source==='HTML'", + "fieldname": "header_script", + "fieldtype": "Code", + "label": "Header Script", + "options": "Javascript" + }, + { + "depends_on": "eval:!doc.__islocal && doc.footer_source==='HTML'", + "fieldname": "footer_script", + "fieldtype": "Code", + "label": "Footer Script", + "options": "Javascript" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: doc.header_script || doc.footer_script", + "fieldname": "scripts_section", + "fieldtype": "Section Break", + "label": "Scripts" + }, + { + "fieldname": "instructions", + "fieldtype": "HTML", + "label": "Instructions", + "read_only": 1 } ], "icon": "fa fa-font", "idx": 1, "links": [], "max_attachments": 3, - "modified": "2023-12-08 15:52:37.525003", + "modified": "2023-12-21 16:19:37.525003", "modified_by": "Administrator", "module": "Printing", "name": "Letter Head", diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index ca6241584d..82e9ff40ba 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -24,7 +24,9 @@ class LetterHead(Document): footer_image: DF.AttachImage | None footer_image_height: DF.Float footer_image_width: DF.Float + footer_script: DF.Code | None footer_source: DF.Literal["Image", "HTML"] + header_script: DF.Code | None image: DF.AttachImage | None image_height: DF.Float image_width: DF.Float @@ -32,6 +34,7 @@ class LetterHead(Document): letter_head_name: DF.Data source: DF.Literal["Image", "HTML"] # end: auto-generated types + def before_insert(self): # for better UX, let user set from attachment self.source = "Image" diff --git a/frappe/printing/doctype/letter_head/test_letter_head.py b/frappe/printing/doctype/letter_head/test_letter_head.py index b36daeae03..39cb0c9816 100644 --- a/frappe/printing/doctype/letter_head/test_letter_head.py +++ b/frappe/printing/doctype/letter_head/test_letter_head.py @@ -7,7 +7,7 @@ from frappe.tests.utils import FrappeTestCase class TestLetterHead(FrappeTestCase): def test_auto_image(self): letter_head = frappe.get_doc( - dict(doctype="Letter Head", letter_head_name="Test", source="Image", image="/public/test.png") + doctype="Letter Head", letter_head_name="Test", source="Image", image="/public/test.png" ).insert() # test if image is automatically set diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py index ea359820d2..71a1d9290c 100644 --- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py @@ -19,6 +19,7 @@ class NetworkPrinterSettings(Document): printer_name: DF.Literal server_ip: DF.Data # end: auto-generated types + @frappe.whitelist() def get_printers_list(self, ip="127.0.0.1", port=631): printer_list = [] diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py index 4110f849ec..7450b3882a 100644 --- a/frappe/printing/doctype/print_format/print_format.py +++ b/frappe/printing/doctype/print_format/print_format.py @@ -70,7 +70,6 @@ class PrintFormat(Document): and not frappe.flags.in_migrate and not frappe.flags.in_test ): - frappe.throw(frappe._("Standard Print Format cannot be updated")) # old_doc_type is required for clearing item cache @@ -85,9 +84,7 @@ class PrintFormat(Document): validate_template(self.html) if self.custom_format and self.raw_printing and not self.raw_commands: - frappe.throw( - _("{0} are required").format(frappe.bold(_("Raw Commands"))), frappe.MandatoryError - ) + frappe.throw(_("{0} are required").format(frappe.bold(_("Raw Commands"))), frappe.MandatoryError) if self.custom_format and not self.html and not self.raw_printing: frappe.throw(_("{0} is required").format(frappe.bold(_("HTML"))), frappe.MandatoryError) diff --git a/frappe/printing/doctype/print_format/test_print_format.py b/frappe/printing/doctype/print_format/test_print_format.py index c54e421861..212fda2b9d 100644 --- a/frappe/printing/doctype/print_format/test_print_format.py +++ b/frappe/printing/doctype/print_format/test_print_format.py @@ -18,9 +18,7 @@ class TestPrintFormat(FrappeTestCase): def test_print_user(self, style=None): print_html = frappe.get_print("User", "Administrator", style=style) self.assertTrue("" in print_html) - self.assertTrue( - re.findall(r'
[\s]*administrator[\s]*
', print_html) - ) + self.assertTrue(re.findall(r'
[\s]*administrator[\s]*
', print_html)) return print_html def test_print_user_standard(self): 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 4f3f5d53b8..abeb9a980c 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 @@ -22,6 +22,7 @@ class PrintFormatFieldTemplate(Document): template: DF.Code | None template_file: DF.Data | None # end: auto-generated types + def validate(self): 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")) diff --git a/frappe/printing/doctype/print_heading/print_heading.py b/frappe/printing/doctype/print_heading/print_heading.py index 7a652befd7..06e89393b8 100644 --- a/frappe/printing/doctype/print_heading/print_heading.py +++ b/frappe/printing/doctype/print_heading/print_heading.py @@ -17,4 +17,5 @@ class PrintHeading(Document): description: DF.SmallText | None print_heading: DF.Data # end: auto-generated types + pass diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index 555d3243d1..4ca4b3736e 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -22,9 +22,7 @@ class PrintSettings(Document): allow_print_for_draft: DF.Check enable_print_server: DF.Check enable_raw_printing: DF.Check - font: DF.Literal[ - "Default", "Helvetica Neue", "Arial", "Helvetica", "Inter", "Verdana", "Monospace" - ] + font: DF.Literal["Default", "Helvetica Neue", "Arial", "Helvetica", "Inter", "Verdana", "Monospace"] font_size: DF.Float pdf_page_height: DF.Float pdf_page_size: DF.Literal[ @@ -66,6 +64,7 @@ class PrintSettings(Document): send_print_as_pdf: DF.Check with_letterhead: DF.Check # end: auto-generated types + def validate(self): if self.pdf_page_size == "Custom" and not (self.pdf_page_height and self.pdf_page_width): frappe.throw(_("Page height and width cannot be zero")) diff --git a/frappe/printing/doctype/print_style/print_style.py b/frappe/printing/doctype/print_style/print_style.py index c1ca66242c..1c1d29a71f 100644 --- a/frappe/printing/doctype/print_style/print_style.py +++ b/frappe/printing/doctype/print_style/print_style.py @@ -20,6 +20,7 @@ class PrintStyle(Document): print_style_name: DF.Data standard: DF.Check # end: auto-generated types + def validate(self): if ( self.standard == 1 @@ -27,7 +28,6 @@ class PrintStyle(Document): 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.")) def on_update(self): diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js index 91afafa707..7c0328362a 100644 --- a/frappe/public/js/form_builder/store.js +++ b/frappe/public/js/form_builder/store.js @@ -85,8 +85,13 @@ export const useStore = defineStore("form-builder-store", () => { async function fetch() { doc.value = frm.value.doc; - if (doctype.value.startsWith("new-doctype-") && !doc.value.fields) { - doc.value.fields = [get_df("Data", "", __("Title"))]; + if (doctype.value.startsWith("new-doctype-") && !doc.value.fields?.length) { + frappe.model.with_doctype("DocType").then(() => { + frappe.listview_settings["DocType"].new_doctype_dialog(); + }); + // redirect to /doctype + frappe.set_route("List", "DocType"); + return; } if (!get_docfields.value.length) { diff --git a/frappe/public/js/frappe/assets.js b/frappe/public/js/frappe/assets.js index 98b025ebea..53883289a1 100644 --- a/frappe/public/js/frappe/assets.js +++ b/frappe/public/js/frappe/assets.js @@ -19,11 +19,22 @@ frappe.require = function (items, callback) { }); }; -frappe.assets = { - check: function () { +class AssetManager { + constructor() { + this._executed = []; + this._handlers = { + js: function (txt) { + frappe.dom.eval(txt); + }, + css: function (txt) { + frappe.dom.set_style(txt); + }, + }; + } + check() { // if version is different then clear localstorage if (window._version_number != localStorage.getItem("_version_number")) { - frappe.assets.clear_local_storage(); + this.clear_local_storage(); console.log("Cleared App Cache."); } @@ -33,160 +44,79 @@ frappe.assets = { // Evict cache if page is reloaded within 10 seconds. Which could be user trying to // refresh if things feel broken. if ((not_updated_since < 5000 && is_reload()) || not_updated_since > 2 * 86400000) { - frappe.assets.clear_local_storage(); + this.clear_local_storage(); } } else { - frappe.assets.clear_local_storage(); + this.clear_local_storage(); } - frappe.assets.init_local_storage(); - }, + this.init_local_storage(); + } - init_local_storage: function () { + init_local_storage() { localStorage._last_load = new Date(); localStorage._version_number = window._version_number; if (frappe.boot) localStorage.metadata_version = frappe.boot.metadata_version; - }, + } - clear_local_storage: function () { - $.each( - ["_last_load", "_version_number", "metadata_version", "page_info", "last_visited"], - function (i, key) { - localStorage.removeItem(key); - } + clear_local_storage() { + ["_last_load", "_version_number", "metadata_version", "page_info", "last_visited"].forEach( + (key) => localStorage.removeItem(key) ); // clear assets - for (var key in localStorage) { + for (let key in localStorage) { if ( - key.indexOf("desk_assets:") === 0 || - key.indexOf("_page:") === 0 || - key.indexOf("_doctype:") === 0 || - key.indexOf("preferred_breadcrumbs:") === 0 + key.startsWith("_page:") || + key.startsWith("_doctype:") || + key.startsWith("preferred_breadcrumbs:") ) { localStorage.removeItem(key); } } console.log("localStorage cleared"); - }, + } - // keep track of executed assets - executed_: [], + eval_assets(path, content) { + if (!this._executed.includes(path)) { + this._handlers[this.extn(path)](content); + this._executed.push(path); + } + } - // pass on to the handler to set - execute: function (items, callback) { - var to_fetch = []; - for (var i = 0, l = items.length; i < l; i++) { - if (!frappe.assets.exists(items[i])) { - to_fetch.push(items[i]); - } - } - if (to_fetch.length) { - frappe.assets.fetch(to_fetch, function () { - frappe.assets.eval_assets(items, callback); - }); - } else { - frappe.assets.eval_assets(items, callback); - } - }, - - eval_assets: function (items, callback) { - for (var i = 0, l = items.length; i < l; i++) { - // execute js/css if not already. - var path = items[i]; - if (frappe.assets.executed_.indexOf(path) === -1) { - // execute - frappe.assets.handler[frappe.assets.extn(path)](frappe.assets.get(path), path); - frappe.assets.executed_.push(path); - } - } - callback && callback(); - }, - - // check if the asset exists in - // localstorage - exists: function (src) { - if (frappe.assets.executed_.indexOf(src) !== -1) { - return true; - } - if (frappe.boot.developer_mode) { - return false; - } - if (frappe.assets.get(src)) { - return true; - } else { - return false; - } - }, - - // load an asset via - fetch: function (items, callback) { + execute(items, callback) { // this is virtual page load, only get the the source - // *without* the template - - if (items.length === 0) { - callback(); - return; - } + let me = this; const version_string = frappe.boot.developer_mode || window.dev_server ? Date.now() : window._version_number; - async function fetch_item(item) { + async function fetch_item(path) { // Add the version to the URL to bust the cache for non-bundled assets - let url = new URL(item, window.location.origin); + let url = new URL(path, window.location.origin); - if (!item.includes(".bundle.") && !url.searchParams.get("v")) { + if (!path.includes(".bundle.") && !url.searchParams.get("v")) { url.searchParams.append("v", version_string); } const response = await fetch(url.toString()); const body = await response.text(); - frappe.assets.add(item, body); + me.eval_assets(path, body); } frappe.dom.freeze(); const fetch_promises = items.map(fetch_item); Promise.all(fetch_promises).then(() => { frappe.dom.unfreeze(); - callback(); + callback?.(); }); - }, + } - add: function (src, txt) { - if ("localStorage" in window) { - try { - frappe.assets.set(src, txt); - } catch (e) { - // if quota is exceeded, clear local storage and set item - frappe.assets.clear_local_storage(); - frappe.assets.set(src, txt); - } - } - }, - - get: function (src) { - return localStorage.getItem("desk_assets:" + src); - }, - - set: function (src, txt) { - localStorage.setItem("desk_assets:" + src, txt); - }, - - extn: function (src) { + extn(src) { if (src.indexOf("?") != -1) { src = src.split("?").slice(-1)[0]; } return src.split(".").slice(-1)[0]; - }, - - handler: { - js: function (txt, src) { - frappe.dom.eval(txt); - }, - css: function (txt, src) { - frappe.dom.set_style(txt); - }, - }, + } bundled_asset(path, is_rtl = null) { if (!path.startsWith("/assets") && path.includes(".bundle.")) { @@ -197,8 +127,8 @@ frappe.assets = { return path; } return path; - }, -}; + } +} function is_reload() { try { @@ -211,3 +141,5 @@ function is_reload() { return true; } } + +frappe.assets = new AssetManager(); diff --git a/frappe/public/js/frappe/build_events/build_events.bundle.js b/frappe/public/js/frappe/build_events/build_events.bundle.js index a191156bfa..5635502507 100644 --- a/frappe/public/js/frappe/build_events/build_events.bundle.js +++ b/frappe/public/js/frappe/build_events/build_events.bundle.js @@ -17,7 +17,7 @@ frappe.realtime.on("build_event", (data) => { if (parts.length === 2) { let filename = parts[0].split("/").slice(-1)[0]; - frappe.assets.executed_ = frappe.assets.executed_.filter( + frappe.assets._executed = frappe.assets._executed.filter( (asset) => !asset.includes(`${filename}.bundle`) ); } diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 4b0f18e3e1..2dcd22acf3 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -34,7 +34,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
-
+
diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 05e8fde29c..4efd92bd8d 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -153,6 +153,7 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex JS: "ace/mode/javascript", Python: "ace/mode/python", Py: "ace/mode/python", + PythonExpression: "ace/mode/python", HTML: "ace/mode/html", CSS: "ace/mode/css", Markdown: "ace/mode/markdown", diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 5fafd39c5d..d5485fab37 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -87,10 +87,10 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat return this.is_translatable() ? __(value) : value; } is_translatable() { - return frappe.boot?.translated_doctypes || [].includes(this.get_options()); + return (frappe.boot?.translated_doctypes || []).includes(this.get_options()); } is_title_link() { - return frappe.boot?.link_title_doctypes || [].includes(this.get_options()); + return (frappe.boot?.link_title_doctypes || []).includes(this.get_options()); } async set_link_title(value) { const doctype = this.get_options(); diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 8e98c1042c..c068502fc7 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -49,6 +49,10 @@ frappe.form.formatters = { return __(frappe.form.formatters["Data"](value, df)); }, Float: function (value, docfield, options, doc) { + if (value === null) { + return ""; + } + // don't allow 0 precision for Floats, hence or'ing with null var precision = docfield.precision || @@ -73,12 +77,20 @@ frappe.form.formatters = { } }, Int: function (value, docfield, options) { + if (value === null) { + return ""; + } + if (cstr(docfield.options).trim() === "File Size") { return frappe.form.formatters.FileSize(value); } return frappe.form.formatters._right(value == null ? "" : cint(value), options); }, Percent: function (value, docfield, options) { + if (value === null) { + return ""; + } + const precision = docfield.precision || cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) || @@ -105,6 +117,10 @@ frappe.form.formatters = { `; }, Currency: function (value, docfield, options, doc) { + if (value === null) { + return ""; + } + var currency = frappe.meta.get_field_currency(docfield, doc); let precision; diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index eacefe8d26..36025fec85 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -834,7 +834,11 @@ export default class Grid { if (!this.df.data) { this.df.data = this.get_data() || []; } - this.df.data.push({ idx: this.df.data.length + 1, __islocal: true }); + const defaults = this.docfields.reduce((acc, d) => { + acc[d.fieldname] = d.default; + return acc; + }, {}); + this.df.data.push({ idx: this.df.data.length + 1, __islocal: true, ...defaults }); this.refresh(); } diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index da76488b9c..5be3778972 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -132,7 +132,9 @@ frappe.views.BaseList = class BaseList { frappe.meta.has_field(doctype, fieldname) || fieldname === "_seen"; - if (!is_valid_field) { + let is_virtual = this.meta.fields.find((df) => df.fieldname == fieldname)?.is_virtual; + + if (!is_valid_field || is_virtual) { return; } @@ -180,7 +182,7 @@ frappe.views.BaseList = class BaseList { } set_title() { - this.page.set_title(this.page_title); + this.page.set_title(this.page_title, null, true, "", this.meta?.description); } setup_view_menu() { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 7ba1898fa4..8b37c37836 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -374,7 +374,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (frappe.has_indicator(this.doctype) && df.fieldname === "status") { return false; } - if (!df.in_list_view) { + if (!df.in_list_view || df.is_virtual) { return false; } return df.fieldname !== this.meta.title_field; @@ -465,7 +465,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ? __("No {0} found with matching filters. Clear filters to see all {0}.", [ __(this.doctype), ]) + : this.meta.description + ? __(this.meta.description) : __("You haven't created a {0} yet", [__(this.doctype)]); + let new_button_label = has_filters_set ? __("Create a new {0}", [__(this.doctype)], "Create a new document from list view") : __( @@ -1517,7 +1520,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } avoid_realtime_update() { - if (this.filter_area.is_being_edited()) { + if (this.filter_area?.is_being_edited()) { return true; } // this is set when a bulk operation is called from a list view which might update the list view diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 4064ce1fee..5f5d534813 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -382,6 +382,11 @@ $.extend(frappe.model, { return frappe.boot.user.can_delete.indexOf(doctype) !== -1; }, + can_submit: function (doctype) { + if (!doctype) return false; + return frappe.boot.user.can_submit.indexOf(doctype) !== -1; + }, + can_cancel: function (doctype) { if (!doctype) return false; return frappe.boot.user.can_cancel.indexOf(doctype) !== -1; diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 77b92b046e..3358d67e55 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -203,7 +203,7 @@ $.extend(frappe.perm, { // permission if (p) { - if (p.write && !df.disabled) { + if (p.write && !df.disabled && !df.is_virtual) { status = "Write"; } else if (p.read) { status = "Read"; diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 691acb035d..e80b4164f2 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -40,6 +40,7 @@ frappe.RoleEditor = class { role && this.show_permissions(role); e.preventDefault(); }); + this.set_enable_disable(); }; } set_enable_disable() { diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index c3fae2c9af..999fcc5125 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -201,9 +201,7 @@ frappe.msgprint = function (msg, title, is_minimizable) { data.primary_action.action = () => { frappe.call({ method: data.primary_action.server_action, - args: { - args: data.primary_action.args, - }, + args: data.primary_action.args, callback() { if (data.primary_action.hide_on_success) { frappe.hide_msgprint(); diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index 7803f47911..f07483f6d1 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -757,7 +757,7 @@ frappe.ui.Page = class Page { return this.$title_area; } - set_title(title, icon = null, strip = true, tab_title = "") { + set_title(title, icon = null, strip = true, tab_title = "", tooltip_label = "") { if (!title) title = ""; if (strip) { title = strip_html(title); @@ -769,7 +769,11 @@ frappe.ui.Page = class Page { } let title_wrapper = this.$title_area.find(".title-text"); title_wrapper.html(title); - title_wrapper.attr("title", this.title); + title_wrapper.attr("title", tooltip_label || this.title); + + if (tooltip_label) { + title_wrapper.tooltip({ delay: { show: 600, hide: 100 }, trigger: "hover" }); + } } set_title_sub(txt) { diff --git a/frappe/public/js/frappe/utils/diffview.js b/frappe/public/js/frappe/utils/diffview.js index 210e582b7a..7912ffb80d 100644 --- a/frappe/public/js/frappe/utils/diffview.js +++ b/frappe/public/js/frappe/utils/diffview.js @@ -20,6 +20,7 @@ frappe.ui.DiffView = class DiffView { docname: this.docname, ref_doctype: this.doctype, fieldname: this.fieldname, + page_len: 100, }, }); const onchange = () => this.compute_diff(); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 18f93ab187..15785d514f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1705,6 +1705,8 @@ Object.assign(frappe.utils, { fieldname: "source", label: __("Source"), fieldtype: "Data", + reqd: 1, + description: "The referrer (e.g. google, newsletter)", default: localStorage.getItem("tracker_url:source"), }, { @@ -1719,25 +1721,35 @@ Object.assign(frappe.utils, { fieldname: "medium", label: __("Medium"), fieldtype: "Data", + description: "Marketing medium (e.g. cpc, banner, email)", default: localStorage.getItem("tracker_url:medium"), }, + { + fieldname: "content", + label: __("Content"), + fieldtype: "Data", + description: "Use to differentiate ad variants (e.g. A/B testing)", + default: localStorage.getItem("tracker_url:content"), + }, ], function (data) { let url = data.url; localStorage.setItem("tracker_url:url", data.url); - if (data.source) { - url += "?source=" + data.source; - localStorage.setItem("tracker_url:source", data.source); - } + url += "?utm_source=" + encodeURIComponent(data.source); + localStorage.setItem("tracker_url:source", data.source); if (data.campaign) { - url += "&campaign=" + data.campaign; + url += "&utm_campaign=" + encodeURIComponent(data.campaign); localStorage.setItem("tracker_url:campaign", data.campaign); } if (data.medium) { - url += "&medium=" + data.medium.toLowerCase(); + url += "&utm_medium=" + encodeURIComponent(data.medium); localStorage.setItem("tracker_url:medium", data.medium); } + if (data.medium) { + url += "&utm_content=" + encodeURIComponent(data.content); + localStorage.setItem("tracker_url:content", data.content); + } frappe.utils.copy_to_clipboard(url); diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js b/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js index d3690571dd..bca9889859 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js @@ -377,8 +377,13 @@ frappe.provide("frappe.views"); } function bind_add_column() { - if (!self.board_perms.write) { + let doctype = self.cur_list.doctype; + let fieldname = self.cur_list.board.field_name; + const is_custom_field = frappe.meta.get_docfield(doctype, fieldname)?.is_custom_field; + + if (!self.board_perms.write || !is_custom_field) { // If no write access to board, editing board (by adding column) should be blocked + // If standard field then users can't add options self.$kanban_board.find(".add-new-column").remove(); return; } diff --git a/frappe/public/js/frappe/widgets/links_widget.js b/frappe/public/js/frappe/widgets/links_widget.js index 2af4564ffa..b83cf04954 100644 --- a/frappe/public/js/frappe/widgets/links_widget.js +++ b/frappe/public/js/frappe/widgets/links_widget.js @@ -60,9 +60,7 @@ export default class LinksWidget extends Widget { const get_link_for_item = (item) => { if (is_link_disabled(item)) { - return `${ - item.label ? item.label : item.name - } + return `${item.link_title}