Merge branch 'develop' into copy-config-to-new-app

This commit is contained in:
barredterra 2024-02-16 14:36:58 +01:00
commit 1aae576b1a
601 changed files with 4406 additions and 4521 deletions

75
.flake8
View file

@ -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

View file

@ -43,3 +43,9 @@ fa6dc03cc87ad74e11609e7373078366fdcb3e1b
# Bulk refactor with sourcery
c35476256f85271fb57584eb0a26f4d9def3caf4
# black+isort -> ruff
de9ac897482013f5464a05f3c171da0072619c3a
# flake8 -> ruff + ruff config update
26ae0f3460f29116e0c083d57eee9f33763237ea

View file

@ -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

View file

@ -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):

View file

@ -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):

View file

@ -2,7 +2,9 @@ import re
import sys
errors_encounter = 0
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
pattern = re.compile(
r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\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!")

View file

@ -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

View file

@ -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

View file

@ -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 }}

View file

@ -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"

View file

@ -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: []

View file

@ -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
**.html,frappe.gettext.extractors.html_template.extract
1 hooks.py frappe.gettext.extractors.navbar.extract
6 **/report/*/*.json frappe.gettext.extractors.report.extract
7 **.py frappe.gettext.extractors.python.extract
8 **.js frappe.gettext.extractors.javascript.extract
9 **.html frappe.gettext.extractors.jinja2.extract frappe.gettext.extractors.html_template.extract

View file

@ -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();

View file

@ -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"<details><summary>{summary}</summary>{detail}</details>"
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 = (
'<a href="/app/Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
)
html = '<a href="/app/Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
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.

View file

@ -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))})

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -19,4 +19,5 @@ class AssignmentRuleDay(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -19,4 +19,5 @@ class AssignmentRuleUser(Document):
parenttype: DF.Data
user: DF.Link
# end: auto-generated types
pass

View file

@ -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()

View file

@ -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",

View file

@ -19,4 +19,5 @@ class AutoRepeatDay(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -20,6 +20,7 @@ class Milestone(Document):
track_field: DF.Data
value: DF.Data
# end: auto-generated types
pass

View file

@ -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)

View file

@ -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",

View file

@ -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

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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",
)

View file

@ -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

View file

@ -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):

View file

@ -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")

View file

@ -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)"""

View file

@ -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():

View file

@ -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

View file

@ -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):

View file

@ -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}

View file

@ -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

View file

@ -20,4 +20,5 @@ class ContactEmail(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -21,4 +21,5 @@ class ContactPhone(Document):
parenttype: DF.Data
phone: DF.Data
# end: auto-generated types
pass

View file

@ -15,4 +15,5 @@ class Gender(Document):
gender: DF.Data | None
# end: auto-generated types
pass

View file

@ -15,4 +15,5 @@ class Salutation(Document):
salutation: DF.Data | None
# end: auto-generated types
pass

View file

@ -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", [])

View file

@ -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

View file

@ -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):

View file

@ -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(
{

View file

@ -20,4 +20,5 @@ class AmendedDocumentNamingSettings(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -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):

View file

@ -18,4 +18,5 @@ class BlockModule(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -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")

View file

@ -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(

View file

@ -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:

View file

@ -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";

View file

@ -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 {}

View file

@ -21,6 +21,7 @@ class CommunicationLink(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -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)

View file

@ -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")

View file

@ -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,

View file

@ -17,4 +17,5 @@ class DataExport(Document):
file_type: DF.Literal["Excel", "CSV"]
reference_doctype: DF.Link
# end: auto-generated types
pass

View file

@ -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,

View file

@ -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 +=
"<br/>" +
__(
"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;

View file

@ -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",

View file

@ -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"):

View file

@ -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,

View file

@ -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):

View file

@ -22,4 +22,5 @@ class DataImportLog(Document):
row_indexes: DF.Code | None
success: DF.Check
# end: auto-generated types
pass

View file

@ -20,6 +20,7 @@ class DefaultValue(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -25,6 +25,7 @@ class DeletedDocument(Document):
new_name: DF.ReadOnly | None
restored: DF.Check
# end: auto-generated types
pass
@staticmethod

View file

@ -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",

View file

@ -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).

View file

@ -33,4 +33,5 @@ class DocPerm(Document):
submit: DF.Check
write: DF.Check
# end: auto-generated types
pass

View file

@ -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)

View file

@ -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)

View file

@ -1,7 +1,7 @@
{{% extends "templates/web.html" %}}
{{% block page_content %}}
<h1>{{{{ title }}}}</h1>
<h1>{{{{ title |e }}}}</h1>
{{% endblock %}}
<!-- this is a sample default web page template -->

View file

@ -1,4 +1,4 @@
<div>
<a href="{{{{ doc.route }}}}">{{{{ doc.title or doc.name }}}}</a>
<a href="/{{{{ doc.route |e }}}}">{{{{ (doc.title or doc.name) |e }}}}</a>
</div>
<!-- this is a sample default list template -->

View file

@ -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 + <Doctype Name>) 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:

View file

@ -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"]
)

View file

@ -24,4 +24,5 @@ class DocTypeAction(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -26,4 +26,5 @@ class DocTypeLink(Document):
parenttype: DF.Data
table_fieldname: DF.Data | None
# end: auto-generated types
pass

View file

@ -23,4 +23,5 @@ class DocTypeState(Document):
parenttype: DF.Data
title: DF.Data
# end: auto-generated types
pass

View file

@ -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()

View file

@ -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:

View file

@ -21,4 +21,5 @@ class DocumentNamingRuleCondition(Document):
parenttype: DF.Data
value: DF.Data
# end: auto-generated types
pass

View file

@ -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),

View file

@ -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)

View file

@ -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:

View file

@ -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))
)

View file

@ -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:

View file

@ -21,6 +21,7 @@ class DynamicLink(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -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)

View file

@ -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)

View file

@ -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 = ' <i class="fa fa-lock text-warning"></i>' 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()

View file

@ -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)

View file

@ -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'<img src="{file_url}"'

View file

@ -18,4 +18,5 @@ class HasDomain(Document):
parentfield: DF.Data
parenttype: DF.Data
# end: auto-generated types
pass

View file

@ -19,6 +19,7 @@ class HasRole(Document):
parenttype: DF.Data
role: DF.Link | None
# end: auto-generated types
def before_insert(self):
if frappe.db.exists("Has Role", {"parent": self.parent, "role": self.role}):
frappe.throw(frappe._("User '{0}' already has the role '{1}'").format(self.parent, self.role))

Some files were not shown because too many files have changed in this diff Show more