Merge branch 'develop' into copy-config-to-new-app
This commit is contained in:
commit
1aae576b1a
601 changed files with 4406 additions and 4521 deletions
75
.flake8
75
.flake8
|
|
@ -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
|
||||
|
|
@ -43,3 +43,9 @@ fa6dc03cc87ad74e11609e7373078366fdcb3e1b
|
|||
|
||||
# Bulk refactor with sourcery
|
||||
c35476256f85271fb57584eb0a26f4d9def3caf4
|
||||
|
||||
# black+isort -> ruff
|
||||
de9ac897482013f5464a05f3c171da0072619c3a
|
||||
|
||||
# flake8 -> ruff + ruff config update
|
||||
26ae0f3460f29116e0c083d57eee9f33763237ea
|
||||
|
|
|
|||
4
.github/helper/ci.py
vendored
4
.github/helper/ci.py
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
15
.github/helper/documentation.py
vendored
15
.github/helper/documentation.py
vendored
|
|
@ -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):
|
||||
|
|
|
|||
8
.github/helper/roulette.py
vendored
8
.github/helper/roulette.py
vendored
|
|
@ -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):
|
||||
|
|
|
|||
34
.github/helper/translation.py
vendored
34
.github/helper/translation.py
vendored
|
|
@ -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!")
|
||||
|
|
|
|||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
9
.github/workflows/server-tests.yml
vendored
9
.github/workflows/server-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
12
.github/workflows/ui-tests.yml
vendored
12
.github/workflows/ui-tests.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
21
.mergify.yml
21
.mergify.yml
|
|
@ -21,27 +21,6 @@ pull_request_rules:
|
|||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: Automatic merge on CI success and review
|
||||
conditions:
|
||||
- label!=dont-merge
|
||||
- label!=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
actions:
|
||||
merge:
|
||||
method: merge
|
||||
- name: Automatic squash on CI success and review
|
||||
conditions:
|
||||
- label!=dont-merge
|
||||
- label=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
commit_message_template: |
|
||||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
|
||||
- name: backport to develop
|
||||
conditions:
|
||||
- label="backport develop"
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ class AssignmentRuleDay(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ class AssignmentRuleUser(Document):
|
|||
parenttype: DF.Data
|
||||
user: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ class AutoRepeatDay(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class Milestone(Document):
|
|||
track_field: DF.Data
|
||||
value: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)"""
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@ class ContactEmail(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@ class ContactPhone(Document):
|
|||
parenttype: DF.Data
|
||||
phone: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ class Gender(Document):
|
|||
|
||||
gender: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ class Salutation(Document):
|
|||
|
||||
salutation: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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", [])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,4 +20,5 @@ class AmendedDocumentNamingSettings(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ class BlockModule(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class CommunicationLink(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ class DataExport(Document):
|
|||
file_type: DF.Literal["Excel", "CSV"]
|
||||
reference_doctype: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ class DataImportLog(Document):
|
|||
row_indexes: DF.Code | None
|
||||
success: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class DefaultValue(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class DeletedDocument(Document):
|
|||
new_name: DF.ReadOnly | None
|
||||
restored: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -33,4 +33,5 @@ class DocPerm(Document):
|
|||
submit: DF.Check
|
||||
write: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ class DocTypeAction(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ class DocTypeLink(Document):
|
|||
parenttype: DF.Data
|
||||
table_fieldname: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ class DocTypeState(Document):
|
|||
parenttype: DF.Data
|
||||
title: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -21,4 +21,5 @@ class DocumentNamingRuleCondition(Document):
|
|||
parenttype: DF.Data
|
||||
value: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class DynamicLink(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}"'
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ class HasDomain(Document):
|
|||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue