Merge branch 'develop' into form-builder-vue3
This commit is contained in:
commit
2a718b7468
44 changed files with 285 additions and 164 deletions
55
.flake8
55
.flake8
|
|
@ -1,37 +1,74 @@
|
|||
[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,
|
||||
F403,
|
||||
B007,
|
||||
B950,
|
||||
W191,
|
||||
E124, # closing bracket, irritating while writing QB code
|
||||
E131, # continuation line unaligned for hanging indent
|
||||
E123, # closing bracket does not match indentation of opening bracket's line
|
||||
E101, # ensured by use of black
|
||||
E711,
|
||||
E129,
|
||||
F841,
|
||||
E713,
|
||||
E712,
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
exclude=,test_*.py
|
||||
|
|
|
|||
75
.github/helper/flake8.conf
vendored
75
.github/helper/flake8.conf
vendored
|
|
@ -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,
|
||||
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules,test_*.py
|
||||
|
|
@ -59,7 +59,6 @@ repos:
|
|||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: ['flake8-bugbear',]
|
||||
args: ['--config', '.github/helper/flake8.conf']
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"extends": ["stylelint-config-recommended"],
|
||||
"plugins": ["stylelint-scss"],
|
||||
"rules": {
|
||||
"at-rule-no-unknown": null,
|
||||
"scss/at-rule-no-unknown": true,
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
skips: ['E0203', 'B605', 'B404', 'B603', 'B607']
|
||||
|
|
@ -1018,19 +1018,15 @@ def get_precision(
|
|||
return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency)
|
||||
|
||||
|
||||
def generate_hash(txt: str | None = None, length: int | None = None) -> str:
|
||||
"""Generates random hash for given text + current timestamp + random string."""
|
||||
import hashlib
|
||||
import time
|
||||
def generate_hash(txt: str | None = None, length: int = 56) -> str:
|
||||
"""Generate random hash using best available randomness source."""
|
||||
import math
|
||||
import secrets
|
||||
|
||||
from .utils import random_string
|
||||
if not length:
|
||||
length = 56
|
||||
|
||||
digest = hashlib.sha224(
|
||||
((txt or "") + repr(time.time()) + repr(random_string(8))).encode()
|
||||
).hexdigest()
|
||||
if length:
|
||||
digest = digest[:length]
|
||||
return digest
|
||||
return secrets.token_hex(math.ceil(length / 2))[:length]
|
||||
|
||||
|
||||
def reset_metadata_version():
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ class DataExporter:
|
|||
row[_column_start_end.start + i + 1] = value
|
||||
|
||||
def build_response_as_excel(self):
|
||||
filename = frappe.generate_hash("", 10)
|
||||
filename = frappe.generate_hash(length=10)
|
||||
with open(filename, "wb") as f:
|
||||
f.write(cstr(self.writer.getvalue()).encode("utf-8"))
|
||||
f = open(filename)
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ class TestImporter(FrappeTestCase):
|
|||
def test_data_import_update(self):
|
||||
existing_doc = frappe.get_doc(
|
||||
doctype=doctype_name,
|
||||
title=frappe.generate_hash(doctype_name, 8),
|
||||
title=frappe.generate_hash(length=8),
|
||||
table_field_1=[{"child_title": "child title to update"}],
|
||||
)
|
||||
existing_doc.save()
|
||||
|
|
|
|||
|
|
@ -670,6 +670,9 @@ class TestDocType(FrappeTestCase):
|
|||
|
||||
self.assertEqual(test_json.test_json_field["hello"], "world")
|
||||
|
||||
def test_no_delete_doc(self):
|
||||
self.assertRaises(frappe.ValidationError, frappe.delete_doc, "DocType", "Address")
|
||||
|
||||
@patch.dict(frappe.conf, {"developer_mode": 1})
|
||||
def test_custom_field_deletion(self):
|
||||
"""Custom child tables whose doctype doesn't exist should be auto deleted."""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import random
|
|||
import re
|
||||
import string
|
||||
import traceback
|
||||
from contextlib import contextmanager
|
||||
from contextlib import contextmanager, suppress
|
||||
from time import time
|
||||
|
||||
from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder
|
||||
|
|
@ -29,7 +29,8 @@ from frappe.exceptions import DoesNotExistError, ImplicitCommitError
|
|||
from frappe.model.utils.link_count import flush_local_link_count
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cast as cast_fieldtype
|
||||
from frappe.utils import get_datetime, get_table_name, getdate, now, sbool
|
||||
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
|
||||
from frappe.utils.deprecations import deprecated, deprecation_warning
|
||||
|
||||
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
|
||||
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
|
||||
|
|
@ -114,6 +115,17 @@ class Database:
|
|||
self._cursor = self._conn.cursor()
|
||||
frappe.local.rollback_observers = []
|
||||
|
||||
try:
|
||||
if execution_timeout := get_query_execution_timeout():
|
||||
self.set_execution_timeout(execution_timeout)
|
||||
except Exception as e:
|
||||
frappe.logger("database").warning(f"Couldn't set execution timeout {e}")
|
||||
|
||||
def set_execution_timeout(self, seconds: int):
|
||||
"""Set session speicifc timeout on exeuction of statements.
|
||||
If any statement takes more time it will be killed along with entire transaction."""
|
||||
raise NotImplementedError
|
||||
|
||||
def use(self, db_name):
|
||||
"""`USE` db_name."""
|
||||
self._conn.select_db(db_name)
|
||||
|
|
@ -262,6 +274,11 @@ class Database:
|
|||
if pluck:
|
||||
return [r[0] for r in self.last_result]
|
||||
|
||||
if as_utf8:
|
||||
deprecation_warning("as_utf8 parameter is deprecated and will be removed in version 15.")
|
||||
if formatted:
|
||||
deprecation_warning("formatted parameter is deprecated and will be removed in version 15.")
|
||||
|
||||
# scrub output if required
|
||||
if as_dict:
|
||||
ret = self.fetch_as_dict(formatted, as_utf8)
|
||||
|
|
@ -380,10 +397,13 @@ class Database:
|
|||
def fetch_as_dict(self, formatted=0, as_utf8=0) -> list[frappe._dict]:
|
||||
"""Internal. Converts results to dict."""
|
||||
result = self.last_result
|
||||
ret = []
|
||||
if result:
|
||||
keys = [column[0] for column in self._cursor.description]
|
||||
|
||||
if not as_utf8:
|
||||
return [frappe._dict(zip(keys, row)) for row in result]
|
||||
|
||||
ret = []
|
||||
for r in result:
|
||||
values = []
|
||||
for value in r:
|
||||
|
|
@ -418,6 +438,9 @@ class Database:
|
|||
@staticmethod
|
||||
def convert_to_lists(res, formatted=0, as_utf8=0):
|
||||
"""Convert tuple output to lists (internal)."""
|
||||
if not as_utf8:
|
||||
return [[value for value in row] for row in res]
|
||||
|
||||
nres = []
|
||||
for r in res:
|
||||
nr = []
|
||||
|
|
@ -826,6 +849,7 @@ class Database:
|
|||
).run(debug=debug, run=run, as_dict=as_dict)
|
||||
return {}
|
||||
|
||||
@deprecated
|
||||
def update(self, *args, **kwargs):
|
||||
"""Update multiple values. Alias for `set_value`."""
|
||||
return self.set_value(*args, **kwargs)
|
||||
|
|
@ -865,6 +889,9 @@ class Database:
|
|||
modified_by = modified_by or frappe.session.user
|
||||
to_update.update({"modified": modified, "modified_by": modified_by})
|
||||
|
||||
if for_update:
|
||||
deprecation_warning("for_update parameter is deprecated and will be removed in v15.")
|
||||
|
||||
if is_single_doctype:
|
||||
frappe.db.delete(
|
||||
"Singles", filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug
|
||||
|
|
@ -896,10 +923,12 @@ class Database:
|
|||
del self.value_cache[dt]
|
||||
|
||||
@staticmethod
|
||||
@deprecated
|
||||
def set(doc, field, val):
|
||||
"""Set value in document. **Avoid**"""
|
||||
doc.db_set(field, val)
|
||||
|
||||
@deprecated
|
||||
def touch(self, doctype, docname):
|
||||
"""Update the modified timestamp of this document."""
|
||||
modified = now()
|
||||
|
|
@ -1222,6 +1251,7 @@ class Database:
|
|||
"""
|
||||
return self.sql_ddl(f"truncate `{get_table_name(doctype)}`")
|
||||
|
||||
@deprecated
|
||||
def clear_table(self, doctype):
|
||||
return self.truncate(doctype)
|
||||
|
||||
|
|
@ -1340,3 +1370,28 @@ def savepoint(catch: type | tuple[type, ...] = Exception):
|
|||
frappe.db.rollback(save_point=savepoint)
|
||||
else:
|
||||
frappe.db.release_savepoint(savepoint)
|
||||
|
||||
|
||||
def get_query_execution_timeout() -> int:
|
||||
"""Get execution timeout based on current timeout in different contexts.
|
||||
|
||||
HTTP requests: HTTP timeout or a default (300)
|
||||
Background jobs: Job timeout
|
||||
Console/Commands: No timeout = 0.
|
||||
|
||||
Note: Timeout adds 1.5x as "safety factor"
|
||||
"""
|
||||
from rq import get_current_job
|
||||
|
||||
if not frappe.conf.get("enable_db_statement_timeout"):
|
||||
return 0
|
||||
|
||||
# Zero means no timeout, which is the default value in db.
|
||||
timeout = 0
|
||||
with suppress(Exception):
|
||||
if getattr(frappe.local, "request", None):
|
||||
timeout = frappe.conf.http_timeout or 300
|
||||
elif job := get_current_job():
|
||||
timeout = job.timeout
|
||||
|
||||
return int(cint(timeout) * 1.5)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ class MariaDBExceptionUtil:
|
|||
def is_syntax_error(e: pymysql.Error) -> bool:
|
||||
return e.args[0] == ER.PARSE_ERROR
|
||||
|
||||
@staticmethod
|
||||
def is_statement_timeout(e: pymysql.Error) -> bool:
|
||||
return e.args[0] == 1969
|
||||
|
||||
@staticmethod
|
||||
def is_data_too_long(e: pymysql.Error) -> bool:
|
||||
return e.args[0] == ER.DATA_TOO_LONG
|
||||
|
|
@ -102,6 +106,9 @@ class MariaDBConnectionUtil:
|
|||
def create_connection(self):
|
||||
return pymysql.connect(**self.get_connection_settings())
|
||||
|
||||
def set_execution_timeout(self, seconds: int):
|
||||
self.sql("set session max_statement_time = %s", int(seconds))
|
||||
|
||||
def get_connection_settings(self) -> dict:
|
||||
conn_settings = {
|
||||
"host": self.host,
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ class PostgresExceptionUtil:
|
|||
def is_duplicate_fieldname(e):
|
||||
return getattr(e, "pgcode", None) == DUPLICATE_COLUMN
|
||||
|
||||
@staticmethod
|
||||
def is_statement_timeout(e):
|
||||
return PostgresDatabase.is_timedout(e) or isinstance(e, frappe.QueryTimeoutError)
|
||||
|
||||
@staticmethod
|
||||
def is_data_too_long(e):
|
||||
return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION
|
||||
|
|
@ -161,6 +165,10 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
|
|||
|
||||
return conn
|
||||
|
||||
def set_execution_timeout(self, seconds: int):
|
||||
# Postgres expects milliseconds as input
|
||||
self.sql("set local statement_timeout = %s", int(seconds) * 1000)
|
||||
|
||||
def escape(self, s, percent=True):
|
||||
"""Escape quotes and percent in given string."""
|
||||
if isinstance(s, bytes):
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ def validate_args(data):
|
|||
def validate_fields(data):
|
||||
wildcard = update_wildcard_field_param(data)
|
||||
|
||||
for field in data.fields or []:
|
||||
for field in list(data.fields or []):
|
||||
fieldname = extract_fieldname(field)
|
||||
if is_standard(fieldname):
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ def _delete_modules(modules: list[str], dry_run: bool) -> list[str]:
|
|||
|
||||
if not dry_run:
|
||||
if doctype.issingle:
|
||||
frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
|
||||
frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True, force=True)
|
||||
else:
|
||||
drop_doctypes.append(doctype.name)
|
||||
|
||||
|
|
@ -460,7 +460,7 @@ def _delete_doctypes(doctypes: list[str], dry_run: bool) -> None:
|
|||
for doctype in set(doctypes):
|
||||
print(f"* dropping Table for '{doctype}'...")
|
||||
if not dry_run:
|
||||
frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
|
||||
frappe.delete_doc("DocType", doctype, ignore_on_trash=True, force=True)
|
||||
frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ def delete_doc(
|
|||
|
||||
else:
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
if not (doc.custom or frappe.conf.developer_mode or frappe.flags.in_patch or force):
|
||||
frappe.throw(_("Standard DocType can not be deleted."))
|
||||
|
||||
update_flags(doc, flags, ignore_permissions)
|
||||
check_permission_and_not_submitted(doc)
|
||||
|
|
|
|||
|
|
@ -142,9 +142,14 @@ class Document(BaseDocument):
|
|||
self._fix_numeric_types()
|
||||
|
||||
else:
|
||||
get_value_kwargs = {"for_update": self.flags.for_update, "as_dict": True}
|
||||
if not isinstance(self.name, (dict, list)):
|
||||
get_value_kwargs["order_by"] = None
|
||||
|
||||
d = frappe.db.get_value(
|
||||
self.doctype, self.name, "*", as_dict=1, for_update=self.flags.for_update
|
||||
doctype=self.doctype, filters=self.name, fieldname="*", **get_value_kwargs
|
||||
)
|
||||
|
||||
if not d:
|
||||
frappe.throw(
|
||||
_("{0} {1} not found").format(_(self.doctype), self.name), frappe.DoesNotExistError
|
||||
|
|
@ -744,12 +749,13 @@ class Document(BaseDocument):
|
|||
Will also validate document transitions (Save > Submit > Cancel) calling
|
||||
`self.check_docstatus_transition`."""
|
||||
|
||||
self.load_doc_before_save()
|
||||
self.load_doc_before_save(raise_exception=True)
|
||||
|
||||
self._action = "save"
|
||||
previous = self.get_doc_before_save()
|
||||
previous = self._doc_before_save
|
||||
|
||||
if not previous or self.meta.get("is_virtual"):
|
||||
# previous is None for new document insert
|
||||
if not previous:
|
||||
self.check_docstatus_transition(0)
|
||||
return
|
||||
|
||||
|
|
@ -1048,7 +1054,7 @@ class Document(BaseDocument):
|
|||
|
||||
self.set_title_field()
|
||||
|
||||
def load_doc_before_save(self):
|
||||
def load_doc_before_save(self, *, raise_exception: bool = False):
|
||||
"""load existing document from db before saving"""
|
||||
|
||||
self._doc_before_save = None
|
||||
|
|
@ -1059,6 +1065,9 @@ class Document(BaseDocument):
|
|||
try:
|
||||
self._doc_before_save = frappe.get_doc(self.doctype, self.name, for_update=True)
|
||||
except frappe.DoesNotExistError:
|
||||
if raise_exception:
|
||||
raise
|
||||
|
||||
frappe.clear_last_message()
|
||||
|
||||
def run_post_save_methods(self):
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ def make_autoname(key="", doctype="", doc=""):
|
|||
DE/09/01/00001 where 09 is the year, 01 is the month and 00001 is the series
|
||||
"""
|
||||
if key == "hash":
|
||||
return frappe.generate_hash(doctype, 10)
|
||||
return frappe.generate_hash(length=10)
|
||||
|
||||
series = NamingSeries(key)
|
||||
return series.generate_next_name(doc)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.utils import cint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.model.document import Document
|
||||
from frappe.workflow.doctype.workflow.workflow import Workflow
|
||||
|
||||
|
||||
class WorkflowStateError(frappe.ValidationError):
|
||||
pass
|
||||
|
|
@ -32,20 +37,22 @@ def get_workflow_name(doctype):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_transitions(doc, workflow=None, raise_exception=False):
|
||||
def get_transitions(
|
||||
doc: Union["Document", str, dict], workflow: "Workflow" = None, raise_exception: bool = False
|
||||
) -> list[dict]:
|
||||
"""Return list of possible transitions for the given doc"""
|
||||
doc = frappe.get_doc(frappe.parse_json(doc))
|
||||
from frappe.model.document import Document
|
||||
|
||||
if not isinstance(doc, Document):
|
||||
doc = frappe.get_doc(frappe.parse_json(doc))
|
||||
doc.load_from_db()
|
||||
|
||||
if doc.is_new():
|
||||
return []
|
||||
|
||||
doc.load_from_db()
|
||||
doc.check_permission("read")
|
||||
|
||||
frappe.has_permission(doc, "read", throw=True)
|
||||
roles = frappe.get_roles()
|
||||
|
||||
if not workflow:
|
||||
workflow = get_workflow(doc.doctype)
|
||||
workflow = workflow or get_workflow(doc.doctype)
|
||||
current_state = doc.get(workflow.workflow_state_field)
|
||||
|
||||
if not current_state:
|
||||
|
|
@ -55,11 +62,14 @@ def get_transitions(doc, workflow=None, raise_exception=False):
|
|||
frappe.throw(_("Workflow State not set"), WorkflowStateError)
|
||||
|
||||
transitions = []
|
||||
roles = frappe.get_roles()
|
||||
|
||||
for transition in workflow.transitions:
|
||||
if transition.state == current_state and transition.allowed in roles:
|
||||
if not is_transition_condition_satisfied(transition, doc):
|
||||
continue
|
||||
transitions.append(transition.as_dict())
|
||||
|
||||
return transitions
|
||||
|
||||
|
||||
|
|
@ -79,7 +89,7 @@ def get_workflow_safe_globals():
|
|||
)
|
||||
|
||||
|
||||
def is_transition_condition_satisfied(transition, doc):
|
||||
def is_transition_condition_satisfied(transition, doc) -> bool:
|
||||
if not transition.condition:
|
||||
return True
|
||||
else:
|
||||
|
|
@ -198,7 +208,7 @@ def validate_workflow(doc):
|
|||
)
|
||||
|
||||
|
||||
def get_workflow(doctype):
|
||||
def get_workflow(doctype) -> "Workflow":
|
||||
return frappe.get_doc("Workflow", get_workflow_name(doctype))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ def execute():
|
|||
# Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes, creation, modified)
|
||||
new_user_permissions_list.append(
|
||||
(
|
||||
frappe.generate_hash("", 10),
|
||||
frappe.generate_hash(length=10),
|
||||
user_permission.user,
|
||||
user_permission.allow,
|
||||
user_permission.for_value,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def execute():
|
|||
email_values.append(
|
||||
(
|
||||
1,
|
||||
frappe.generate_hash(contact_detail.email_id, 10),
|
||||
frappe.generate_hash(length=10),
|
||||
contact_detail.email_id,
|
||||
"email_ids",
|
||||
"Contact",
|
||||
|
|
@ -44,7 +44,7 @@ def execute():
|
|||
phone_values.append(
|
||||
(
|
||||
phone_counter,
|
||||
frappe.generate_hash(contact_detail.email_id, 10),
|
||||
frappe.generate_hash(length=10),
|
||||
contact_detail.phone,
|
||||
"phone_nos",
|
||||
"Contact",
|
||||
|
|
@ -63,7 +63,7 @@ def execute():
|
|||
phone_values.append(
|
||||
(
|
||||
phone_counter,
|
||||
frappe.generate_hash(contact_detail.email_id, 10),
|
||||
frappe.generate_hash(length=10),
|
||||
contact_detail.mobile_no,
|
||||
"phone_nos",
|
||||
"Contact",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def execute():
|
|||
|
||||
tag_list.append((tag.strip(), time, time, "Administrator"))
|
||||
|
||||
tag_link_name = frappe.generate_hash(_user_tags.name + tag.strip() + doctype.name, 10)
|
||||
tag_link_name = frappe.generate_hash(length=10)
|
||||
tag_links.append(
|
||||
(tag_link_name, doctype.name, _user_tags.name, tag.strip(), time, time, "Administrator")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1179,7 +1179,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => {
|
||||
const $target = $(e.target);
|
||||
// tick checkbox if Ctrl/Meta key is pressed
|
||||
if (e.ctrlKey || (e.metaKey && !$target.is("a"))) {
|
||||
if ((e.ctrlKey || e.metaKey) && !$target.is("a")) {
|
||||
const $list_row = $(e.currentTarget);
|
||||
const $check = $list_row.find(".list-row-checkbox");
|
||||
$check.prop("checked", !$check.prop("checked"));
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
accounts.forEach((account) => {
|
||||
let email_account =
|
||||
account.email_id == "All Accounts" ? "All Accounts" : account.email_account;
|
||||
let route = `/app/communication/inbox/${email_account}`;
|
||||
let route = `/app/communication/view/inbox/${email_account}`;
|
||||
let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(
|
||||
account.email_id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -327,13 +327,14 @@ frappe.ui.Page = class Page {
|
|||
|
||||
//--- Menu --//
|
||||
|
||||
add_menu_item(label, click, standard, shortcut) {
|
||||
add_menu_item(label, click, standard, shortcut, show_parent) {
|
||||
return this.add_dropdown_item({
|
||||
label,
|
||||
click,
|
||||
standard,
|
||||
parent: this.menu,
|
||||
shortcut,
|
||||
show_parent,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -424,7 +425,7 @@ frappe.ui.Page = class Page {
|
|||
icon = null,
|
||||
}) {
|
||||
if (show_parent) {
|
||||
parent.parent().removeClass("hide");
|
||||
parent.parent().removeClass("hide hidden-xl");
|
||||
}
|
||||
|
||||
let $link = this.is_in_group_button_dropdown(parent, "li > a.grey-link > span", label);
|
||||
|
|
@ -602,8 +603,11 @@ frappe.ui.Page = class Page {
|
|||
};
|
||||
// Add actions as menu item in Mobile View
|
||||
let menu_item_label = group ? `${group} > ${label}` : label;
|
||||
let menu_item = this.add_menu_item(menu_item_label, _action, false);
|
||||
let menu_item = this.add_menu_item(menu_item_label, _action, false, false, false);
|
||||
menu_item.parent().addClass("hidden-xl");
|
||||
if (this.menu_btn_group.hasClass("hide")) {
|
||||
this.menu_btn_group.removeClass("hide").addClass("hidden-xl");
|
||||
}
|
||||
|
||||
if (group) {
|
||||
var $group = this.get_or_add_inner_group_button(group);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ frappe.ui.misc.about = function () {
|
|||
Facebook: <a href='https://facebook.com/erpnext' target='_blank'>https://facebook.com/erpnext</a></p>
|
||||
<p><i class='fa fa-twitter fa-fw'></i>
|
||||
Twitter: <a href='https://twitter.com/erpnext' target='_blank'>https://twitter.com/erpnext</a></p>
|
||||
<p><i class='fa fa-youtube fa-fw'></i>
|
||||
YouTube: <a href='https://www.youtube.com/@erpnextofficial' target='_blank'>https://www.youtube.com/@erpnextofficial</a></p>
|
||||
<hr>
|
||||
<h4>${__("Installed Apps")}</h4>
|
||||
<div id='about-app-versions'>${__("Loading versions...")}</div>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ frappe.breadcrumbs = {
|
|||
type: type,
|
||||
};
|
||||
}
|
||||
|
||||
this.all[frappe.breadcrumbs.current_page()] = obj;
|
||||
this.update();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% if parent %}
|
||||
|
||||
{%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%}
|
||||
{%- set dropdown_id = 'id-' + frappe.utils.generate_hash(length=12) -%}
|
||||
<li class="nav-item dropdown {% if submenu %} dropdown-submenu {% endif %}">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="{{ dropdown_id }}" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
{%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%}
|
||||
{%- set dropdown_id = 'id-' + frappe.utils.generate_hash(length=12) -%}
|
||||
<li class="dropdown {% if submenu %} dropdown-submenu {% endif %}">
|
||||
<a class="dropdown-item dropdown-toggle" href="#" id="{{ dropdown_id }}" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ import frappe
|
|||
from frappe.core.utils import find
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.database import savepoint
|
||||
from frappe.database.database import Database
|
||||
from frappe.database.database import Database, get_query_execution_timeout
|
||||
from frappe.database.utils import FallBackDateTimeStr
|
||||
from frappe.query_builder import Field
|
||||
from frappe.query_builder.functions import Concat_ws
|
||||
from frappe.tests.test_query_builder import db_type_is, run_only_if
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, cint, now, random_string
|
||||
from frappe.utils import add_days, cint, now, random_string, set_request
|
||||
from frappe.utils.testutils import clear_custom_fields
|
||||
|
||||
|
||||
|
|
@ -36,6 +36,34 @@ class TestDB(FrappeTestCase):
|
|||
def test_get_database_size(self):
|
||||
self.assertIsInstance(frappe.db.get_database_size(), (float, int))
|
||||
|
||||
def test_db_statement_execution_timeout(self):
|
||||
frappe.db.set_execution_timeout(2)
|
||||
# Setting 0 means no timeout.
|
||||
self.addCleanup(frappe.db.set_execution_timeout, 0)
|
||||
|
||||
try:
|
||||
savepoint = "statement_timeout"
|
||||
frappe.db.savepoint(savepoint)
|
||||
frappe.db.multisql(
|
||||
{
|
||||
"mariadb": "select sleep(10)",
|
||||
"postgres": "select pg_sleep(10)",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
self.assertTrue(frappe.db.is_statement_timeout(e), f"exepcted {e} to be timeout error")
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
else:
|
||||
frappe.db.rollback(save_point=savepoint)
|
||||
self.fail("Long running queries not timing out")
|
||||
|
||||
@patch.dict(frappe.conf, {"http_timeout": 20, "enable_db_statement_timeout": 1})
|
||||
def test_db_timeout_computation(self):
|
||||
set_request(method="GET", path="/")
|
||||
self.assertEqual(get_query_execution_timeout(), 30)
|
||||
frappe.local.request = None
|
||||
self.assertEqual(get_query_execution_timeout(), 0)
|
||||
|
||||
def test_get_value(self):
|
||||
self.assertEqual(frappe.db.get_value("User", {"name": ["=", "Administrator"]}), "Administrator")
|
||||
self.assertEqual(frappe.db.get_value("User", {"name": ["like", "Admin%"]}), "Administrator")
|
||||
|
|
|
|||
|
|
@ -411,6 +411,19 @@ class TestDocument(FrappeTestCase):
|
|||
todo.save()
|
||||
self.assertEqual(todo.notify_update.call_count, 1)
|
||||
|
||||
def test_error_on_saving_new_doc_with_name(self):
|
||||
"""Trying to save a new doc with name should raise DoesNotExistError"""
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"description": "this should raise frappe.DoesNotExistError",
|
||||
"name": "lets-trick-doc-save",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.DoesNotExistError, doc.save)
|
||||
|
||||
|
||||
class TestDocumentWebView(FrappeTestCase):
|
||||
def get(self, path, user="Guest"):
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class TestUtils(FrappeTestCase):
|
|||
|
||||
if self._testMethodName == "test_export_doc":
|
||||
self.note = frappe.new_doc("Note")
|
||||
self.note.title = frappe.generate_hash("Note", length=10)
|
||||
self.note.title = frappe.generate_hash(length=10)
|
||||
self.note.save()
|
||||
|
||||
if self._testMethodName == "test_make_boilerplate":
|
||||
|
|
@ -69,7 +69,7 @@ class TestUtils(FrappeTestCase):
|
|||
delattr(self, "note")
|
||||
|
||||
if self._testMethodName == "test_make_boilerplate":
|
||||
self.doctype.delete()
|
||||
self.doctype.delete(force=True)
|
||||
scrubbed = frappe.scrub(self.doctype.name)
|
||||
self.addCleanup(
|
||||
delete_path,
|
||||
|
|
|
|||
|
|
@ -493,6 +493,10 @@ class TestDateUtils(FrappeTestCase):
|
|||
frappe.utils.get_last_day_of_week("2020-12-28"), frappe.utils.getdate("2021-01-02")
|
||||
)
|
||||
|
||||
def test_is_last_day_of_the_month(self):
|
||||
self.assertEqual(frappe.utils.is_last_day_of_the_month("2020-12-24"), False)
|
||||
self.assertEqual(frappe.utils.is_last_day_of_the_month("2020-12-31"), True)
|
||||
|
||||
def test_get_time(self):
|
||||
datetime_input = now_datetime()
|
||||
timedelta_input = get_timedelta()
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class TestVirtualDoctypes(FrappeTestCase):
|
|||
cls.addClassCleanup(frappe.flags.pop, "allow_doctype_export", None)
|
||||
|
||||
vdt = new_doctype(name=TEST_DOCTYPE_NAME, is_virtual=1, custom=0).insert()
|
||||
cls.addClassCleanup(vdt.delete)
|
||||
cls.addClassCleanup(vdt.delete, force=True)
|
||||
|
||||
patch_virtual_doc = patch(
|
||||
"frappe.controllers", new={frappe.local.site: {TEST_DOCTYPE_NAME: VirtualDoctypeTest}}
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ def create_test_user():
|
|||
|
||||
@frappe.whitelist()
|
||||
def setup_tree_doctype():
|
||||
frappe.delete_doc_if_exists("DocType", "Custom Tree")
|
||||
frappe.delete_doc_if_exists("DocType", "Custom Tree", force=True)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
|
|
@ -475,7 +475,7 @@ def setup_tree_doctype():
|
|||
|
||||
@frappe.whitelist()
|
||||
def setup_image_doctype():
|
||||
frappe.delete_doc_if_exists("DocType", "Custom Image")
|
||||
frappe.delete_doc_if_exists("DocType", "Custom Image", force=True)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from uuid import uuid4
|
|||
import redis
|
||||
from redis.exceptions import BusyLoadingError, ConnectionError
|
||||
from rq import Connection, Queue, Worker
|
||||
from rq.command import send_stop_job_command
|
||||
from rq.logutils import setup_loghandlers
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -471,6 +471,12 @@ def get_last_day(dt):
|
|||
return get_first_day(dt, 0, 1) + datetime.timedelta(-1)
|
||||
|
||||
|
||||
def is_last_day_of_the_month(dt):
|
||||
last_day_of_the_month = get_last_day(dt)
|
||||
|
||||
return getdate(dt) == getdate(last_day_of_the_month)
|
||||
|
||||
|
||||
def get_quarter_ending(date):
|
||||
date = getdate(date)
|
||||
|
||||
|
|
|
|||
27
frappe/utils/deprecations.py
Normal file
27
frappe/utils/deprecations.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
""" Utils for deprecating functionality in Framework.
|
||||
|
||||
WARNING: This file is internal, instead of depending just copy the code or use deprecation
|
||||
libraries.
|
||||
"""
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
|
||||
def deprecated(func):
|
||||
"""Decorator to wrap a function/method as deprecated."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
deprecation_warning(
|
||||
f"{func.__name__} is deprecated and will be removed in next major version.",
|
||||
stacklevel=1,
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def deprecation_warning(message, category=DeprecationWarning, stacklevel=1):
|
||||
"""like warnings.warn but with auto incremented sane stacklevel."""
|
||||
|
||||
warnings.warn(message=message, category=category, stacklevel=stacklevel + 2)
|
||||
|
|
@ -92,9 +92,7 @@ def web_blocks(blocks):
|
|||
def get_dom_id(seed=None):
|
||||
from frappe import generate_hash
|
||||
|
||||
if not seed:
|
||||
seed = "DOM"
|
||||
return "id-" + generate_hash(seed, 12)
|
||||
return "id-" + generate_hash(12)
|
||||
|
||||
|
||||
def include_script(path, preload=True):
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ def update_oauth_user(user, data, provider):
|
|||
"email": get_email(data),
|
||||
"gender": gender,
|
||||
"enabled": 1,
|
||||
"new_password": frappe.generate_hash(get_email(data)),
|
||||
"new_password": frappe.generate_hash(),
|
||||
"location": data.get("location"),
|
||||
"user_type": "Website User",
|
||||
"user_image": data.get("picture") or data.get("avatar_url"),
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class WebsiteTheme(Document):
|
|||
self.delete_old_theme_files(folder_path)
|
||||
|
||||
# add a random suffix
|
||||
suffix = frappe.generate_hash("Website Theme", 8) if self.custom else "style"
|
||||
suffix = frappe.generate_hash(length=8) if self.custom else "style"
|
||||
file_name = frappe.scrub(self.name) + "_" + suffix + ".css"
|
||||
output_path = join_path(folder_path, file_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<div class="collapsible-items">
|
||||
{%- for item in items -%}
|
||||
<div class="collapsible-item">
|
||||
{%- set collapse_id = 'id-' + frappe.utils.generate_hash('Collapse', 12) -%}
|
||||
{%- set collapse_id = 'id-' + frappe.utils.generate_hash(length=12) -%}
|
||||
<a class="collapsible-title" data-toggle="collapse" href="#{{ collapse_id }}" role="button"
|
||||
aria-expanded="false" aria-controls="{{ collapse_id }}">
|
||||
<div class="collapsible-item-title">{{ _(item.title) }}</div>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6'] -%}
|
||||
|
||||
{%- set buttonid = 'id-' + frappe.utils.generate_hash('TabButton', 12) -%}
|
||||
{%- set panelid = 'id-' + frappe.utils.generate_hash('TabPanel', 12) -%}
|
||||
{%- set buttonid = 'id-' + frappe.utils.generate_hash(length=12) -%}
|
||||
{%- set panelid = 'id-' + frappe.utils.generate_hash(length=12) -%}
|
||||
|
||||
{%- set tab = {
|
||||
'title': values['tab_' + index + '_title'],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{%- set slideshow = frappe.get_doc('Website Slideshow', website_slideshow) -%}
|
||||
{%- set slides = slideshow.slideshow_items -%}
|
||||
{%- set slideshow_id = 'id-' + frappe.utils.generate_hash('Slideshow', 12) -%}
|
||||
{%- set slideshow_id = 'id-' + frappe.utils.generate_hash(length=12) -%}
|
||||
|
||||
{{ slideshow.header or '' }}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ ensure_newline_before_comments = true
|
|||
indent = "\t"
|
||||
|
||||
[tool.bench.dev-dependencies]
|
||||
coverage = "~=6.4.1"
|
||||
coverage = "~=6.5.0"
|
||||
Faker = "~=13.12.1"
|
||||
pyngrok = "~=5.0.5"
|
||||
unittest-xml-reporting = "~=3.0.4"
|
||||
|
|
|
|||
|
|
@ -3178,9 +3178,9 @@ socket.io-client@^4.5.1:
|
|||
socket.io-parser "~4.2.0"
|
||||
|
||||
socket.io-parser@~4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
|
||||
integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.5.tgz#cb404382c32324cc962f27f3a44058cf6e0552df"
|
||||
integrity sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==
|
||||
dependencies:
|
||||
"@types/component-emitter" "^1.2.10"
|
||||
component-emitter "~1.3.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue