Merge branch 'develop' into fix-document-signature-2

This commit is contained in:
Suraj Shetty 2022-06-01 18:01:12 +05:30 committed by GitHub
commit 0cd41fbf0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1131 additions and 235 deletions

View file

@ -2,7 +2,7 @@ name: Generate Semantic Release
on:
push:
branches:
- version-13
- version-14-beta
jobs:
release:
name: Release

View file

@ -1,11 +1,10 @@
{
"branches": ["version-13"],
"branches": [
{"name": "version-14-beta", "channel": "beta", "prerelease": true}
],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
"releaseRules": [
{"breaking": true, "release": false}
]
"preset": "angular"
},
"@semantic-release/release-notes-generator",
[

View file

@ -1258,8 +1258,10 @@ def get_module_path(module, *joins):
:param module: Module name.
:param *joins: Join additional path elements using `os.path.join`."""
module = scrub(module)
return get_pymodule_path(local.module_app[module] + "." + module, *joins)
from frappe.modules.utils import get_module_app
app = get_module_app(module)
return get_pymodule_path(app + "." + scrub(module), *joins)
def get_app_path(app_name, *joins):
@ -1501,18 +1503,26 @@ def call(fn, *args, **kwargs):
def get_newargs(fn, kwargs):
# if function has any **kwargs parameter that capture arbitrary keyword arguments
# Ref: https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind
varkw_exist = False
if hasattr(fn, "fnargs"):
fnargs = fn.fnargs
else:
signature = inspect.signature(fn)
fnargs = list(signature.parameters)
varkw = "kwargs" in fnargs
if varkw:
fnargs.pop(-1)
for param_name, parameter in signature.parameters.items():
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
varkw_exist = True
fnargs.remove(param_name)
break
newargs = {}
for a in kwargs:
if (a in fnargs) or varkw:
if (a in fnargs) or varkw_exist:
newargs[a] = kwargs.get(a)
newargs.pop("ignore_permissions", None)
@ -1808,18 +1818,21 @@ def get_value(*args, **kwargs):
return db.get_value(*args, **kwargs)
def as_json(obj: Union[Dict, List], indent=1) -> str:
def as_json(obj: Union[Dict, List], indent=1, separators=None) -> str:
from frappe.utils.response import json_handler
if separators is None:
separators = (",", ": ")
try:
return json.dumps(
obj, indent=indent, sort_keys=True, default=json_handler, separators=(",", ": ")
obj, indent=indent, sort_keys=True, default=json_handler, separators=separators
)
except TypeError:
# this would break in case the keys are not all os "str" type - as defined in the JSON
# adding this to ensure keys are sorted (expected behaviour)
sorted_obj = dict(sorted(obj.items(), key=lambda kv: str(kv[0])))
return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=(",", ": "))
return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=separators)
def are_emails_muted():

View file

@ -165,7 +165,7 @@ class LoginManager:
self.set_user_info()
def get_user_info(self):
self.info = frappe.db.get_value(
self.info = frappe.get_cached_value(
"User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1
)
@ -412,10 +412,16 @@ def clear_cookies():
def validate_ip_address(user):
"""check if IP Address is valid"""
user = (
frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
from frappe.core.doctype.user.user import get_restricted_ip_list
# Only fetch required fields - for perf
user_fields = ["restrict_ip", "bypass_restrict_ip_check_if_2fa_enabled"]
user_info = (
frappe.get_cached_value("User", user, user_fields, as_dict=True)
if not frappe.flags.in_test
else frappe.db.get_value("User", user, user_fields, as_dict=True)
)
ip_list = user.get_restricted_ip_list()
ip_list = get_restricted_ip_list(user_info)
if not ip_list:
return
@ -430,7 +436,7 @@ def validate_ip_address(user):
# check if two factor auth is enabled
if system_settings.enable_two_factor_auth and not bypass_restrict_ip_check:
# check if bypass restrict ip is enabled for login user
bypass_restrict_ip_check = user.bypass_restrict_ip_check_if_2fa_enabled
bypass_restrict_ip_check = user_info.bypass_restrict_ip_check_if_2fa_enabled
for ip in ip_list:
if frappe.local.request_ip.startswith(ip) or bypass_restrict_ip_check:

View file

@ -15,7 +15,7 @@ from frappe.geo.country_info import get_all
from frappe.model.base_document import get_controller
from frappe.query_builder import DocType
from frappe.query_builder.functions import Count
from frappe.query_builder.terms import subqry
from frappe.query_builder.terms import SubQuery
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
@ -211,7 +211,7 @@ def get_user_pages_or_reports(parent, cache=False):
if parent == "Report":
has_role[p.name].update({"ref_doctype": p.ref_doctype})
no_of_roles = (
no_of_roles = SubQuery(
frappe.qb.from_(hasRole).select(Count("*")).where(hasRole.parent == parentTable.name)
)
@ -221,7 +221,7 @@ def get_user_pages_or_reports(parent, cache=False):
pages_with_no_roles = (
frappe.qb.from_(parentTable)
.select(parentTable.name, parentTable.modified, *columns)
.where(subqry(no_of_roles) == 0)
.where(no_of_roles == 0)
).run(as_dict=True)
for p in pages_with_no_roles:
@ -327,7 +327,7 @@ def get_unseen_notes():
(note.notify_on_every_login == 1)
& (note.expire_notification_on > frappe.utils.now())
& (
subqry(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin(
SubQuery(frappe.qb.from_(nsb).select(nsb.user).where(nsb.parent == note.name)).notin(
[frappe.session.user]
)
)

View file

@ -12,6 +12,7 @@ from frappe.utils import (
cint,
get_datetime,
get_formatted_email,
get_string_between,
list_to_str,
split_emails,
validate_email_address,
@ -152,7 +153,7 @@ def _make(
"reference_doctype": doctype,
"reference_name": name,
"email_template": email_template,
"message_id": get_message_id().strip(" <>"),
"message_id": get_string_between("<", get_message_id(), ">"),
"read_receipt": read_receipt,
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type,

View file

@ -1,10 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import random
import string
import unittest
from typing import Dict, List, Optional
from unittest.mock import patch
import frappe
from frappe.cache_manager import clear_doctype_cache
from frappe.core.doctype.doctype.doctype import (
CannotIndexedError,
DoctypeLinkError,
@ -15,8 +19,8 @@ from frappe.core.doctype.doctype.doctype import (
WrongOptionsDoctypeLinkError,
validate_links_table_fieldnames,
)
# test_records = frappe.get_test_records('DocType')
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.form.load import getdoc
class TestDocType(unittest.TestCase):
@ -628,10 +632,55 @@ class TestDocType(unittest.TestCase):
self.assertEqual(test_json.test_json_field["hello"], "world")
@patch.dict(frappe.conf, {"developer_mode": 1})
def test_delete_doctype_with_customization(self):
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
custom_field = "customfield"
doctype = new_doctype(custom=0).insert().name
# Create property setter and custom field
field = "some_fieldname"
make_property_setter(doctype, field, "default", "DELETETHIS", "Data")
create_custom_fields({doctype: [{"fieldname": custom_field, "fieldtype": "Data"}]})
# Create 1 record
original_doc = frappe.get_doc(doctype=doctype, custom_field_name="wat").insert()
self.assertEqual(original_doc.some_fieldname, "DELETETHIS")
# delete doctype
frappe.delete_doc("DocType", doctype)
clear_doctype_cache(doctype)
# "restore" doctype by inserting doctype with same schema again
new_doctype(doctype, custom=0).insert()
# Ensure basically same doctype getting "restored"
restored_doc = frappe.get_last_doc(doctype)
verify_fields = ["doctype", field, custom_field]
for f in verify_fields:
self.assertEqual(original_doc.get(f), restored_doc.get(f))
# Check form load of restored doctype
getdoc(doctype, restored_doc.name)
# ensure meta - property setter
self.assertEqual(frappe.get_meta(doctype).get_field(field).default, "DELETETHIS")
frappe.delete_doc("DocType", doctype)
def new_doctype(
name, unique: bool = False, depends_on: str = "", fields: Optional[List[Dict]] = None, **kwargs
name: Optional[str] = None,
unique: bool = False,
depends_on: str = "",
fields: Optional[List[Dict]] = None,
**kwargs,
):
if not name:
# Test prefix is required to avoid coverage
name = "Test " + "".join(random.sample(string.ascii_lowercase, 10))
doc = frappe.get_doc(
{
"doctype": "DocType",

View file

@ -0,0 +1,73 @@
// Copyright (c) 2022, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Document Naming Settings", {
refresh: function(frm) {
frm.trigger("setup_transaction_autocomplete");
frm.disable_save();
},
setup_transaction_autocomplete: function(frm) {
frappe.call({
method: "get_transactions_and_prefixes",
doc: frm.doc,
callback: function(r) {
frm.fields_dict.transaction_type.set_data(r.message.transactions);
frm.fields_dict.prefix.set_data(r.message.prefixes);
},
});
},
transaction_type: function(frm) {
frm.set_value("user_must_always_select", 0);
frappe.call({
method: "get_options",
doc: frm.doc,
callback: function(r) {
frm.set_value("naming_series_options", r.message);
if (r.message && r.message.split("\n")[0] == "")
frm.set_value("user_must_always_select", 1);
},
});
},
prefix: function(frm) {
frappe.call({
method: "get_current",
doc: frm.doc,
callback: function(r) {
frm.refresh_field("current_value");
},
});
},
update: function(frm) {
frappe.call({
method: "update_series",
doc: frm.doc,
freeze: true,
freeze_msg: __("Updating naming series options"),
callback: function(r) {
frm.trigger("setup_transaction_autocomplete");
frm.trigger("transaction_type");
},
});
},
try_naming_series(frm) {
frappe.call({
method: "preview_series",
doc: frm.doc,
callback: function(r) {
if (!r.exc) {
frm.set_value("series_preview", r.message);
} else {
frm.set_value(
"series_preview",
__("Failed to generate preview of series")
);
}
},
});
},
});

View file

@ -0,0 +1,133 @@
{
"actions": [],
"creation": "2022-05-30 07:24:07.736646",
"description": "Configure various aspects of how document naming works like naming series, current counter.",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"naming_series_tab",
"setup_series",
"transaction_type",
"naming_series_options",
"user_must_always_select",
"update",
"column_break_9",
"try_naming_series",
"series_preview",
"help_html",
"update_series",
"prefix",
"current_value",
"update_series_start"
],
"fields": [
{
"collapsible": 1,
"description": "Set Naming Series options on your transactions.",
"fieldname": "setup_series",
"fieldtype": "Section Break",
"label": "Setup Series for transactions"
},
{
"depends_on": "transaction_type",
"fieldname": "help_html",
"fieldtype": "HTML",
"label": "Help HTML",
"options": "<div class=\"well\">\n Edit list of Series in the box. Rules:\n <ul>\n <li>Each Series Prefix on a new line.</li>\n <li>Allowed special characters are \"/\" and \"-\"</li>\n <li>\n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n </li>\n <li>\n You can also use variables in the series name by putting them\n between (.) dots\n <br>\n Supported Variables:\n <ul>\n <li><code>.YYYY.</code> - Year in 4 digits</li>\n <li><code>.YY.</code> - Year in 2 digits</li>\n <li><code>.MM.</code> - Month</li>\n <li><code>.DD.</code> - Day of month</li>\n <li><code>.WW.</code> - Week of the year</li>\n <li><code>.FY.</code> - Fiscal Year</li>\n <li>\n <code>.{fieldname}.</code> - fieldname on the document e.g.\n <code>branch</code>\n </li>\n </ul>\n </li>\n </ul>\n Examples:\n <ul>\n <li>INV-</li>\n <li>INV-10-</li>\n <li>INVK-</li>\n <li>INV-.YYYY.-.{branch}.-.MM.-.####</li>\n </ul>\n</div>\n<br>\n"
},
{
"default": "0",
"depends_on": "transaction_type",
"description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
"fieldname": "user_must_always_select",
"fieldtype": "Check",
"label": "User must always select"
},
{
"depends_on": "transaction_type",
"fieldname": "update",
"fieldtype": "Button",
"label": "Update"
},
{
"collapsible": 1,
"description": "Change the starting / current sequence number of an existing series. <br>\n\nWarning: Incorrectly updating counters can prevent documents from getting created. ",
"fieldname": "update_series",
"fieldtype": "Section Break",
"label": "Update Series Counter"
},
{
"fieldname": "prefix",
"fieldtype": "Autocomplete",
"label": "Prefix"
},
{
"description": "This is the number of the last created transaction with this prefix",
"fieldname": "current_value",
"fieldtype": "Int",
"label": "Current Value"
},
{
"fieldname": "update_series_start",
"fieldtype": "Button",
"label": "Update Series Number",
"options": "update_series_start"
},
{
"depends_on": "transaction_type",
"fieldname": "naming_series_options",
"fieldtype": "Text",
"label": "Series List for this Transaction"
},
{
"depends_on": "transaction_type",
"description": "Generate 3 preview of names generate by any valid series.",
"fieldname": "try_naming_series",
"fieldtype": "Data",
"label": "Try a naming Series"
},
{
"fieldname": "transaction_type",
"fieldtype": "Autocomplete",
"label": "Select Transaction"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "naming_series_tab",
"fieldtype": "Tab Break",
"label": "Naming Series"
},
{
"fieldname": "series_preview",
"fieldtype": "Text",
"label": "Preview of generated names",
"read_only": 1
}
],
"hide_toolbar": 1,
"icon": "fa fa-sort-by-order",
"issingle": 1,
"links": [],
"modified": "2022-05-30 23:51:36.136535",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,218 @@
# Copyright (c) 2022, Frappe Technologies and contributors
# For license information, please see license.txt
from typing import List, Set
import frappe
from frappe import _
from frappe.core.doctype.doctype.doctype import validate_series
from frappe.model.document import Document
from frappe.model.naming import NamingSeries
from frappe.permissions import get_doctypes_with_read
from frappe.utils import cint
class NamingSeriesNotSetError(frappe.ValidationError):
pass
class DocumentNamingSettings(Document):
@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")
custom = frappe.get_all("Custom Field", {"fieldname": "naming_series"}, "dt", pluck="dt")
return sorted(readable_doctypes.intersection(standard + custom))
def _get_prefixes(self, doctypes) -> List[str]:
"""Get all prefixes for naming series.
- For all templates prefix is evaluated considering today's date
- All existing prefix in DB are shared as is.
"""
series_templates = set()
for d in doctypes:
try:
options = frappe.get_meta(d).get_naming_series_options()
series_templates.update(options)
except frappe.DoesNotExistError:
frappe.msgprint(_("Unable to find DocType {0}").format(d))
continue
custom_templates = frappe.get_all(
"DocType",
fields=["autoname"],
filters={
"name": ("not in", doctypes),
"autoname": ("like", "%.#%"),
"module": ("not in", ["Core"]),
},
)
if custom_templates:
series_templates.update([d.autoname.rsplit(".", 1)[0] for d in custom_templates])
return self._evaluate_and_clean_templates(series_templates)
def _evaluate_and_clean_templates(self, series_templates: Set[str]) -> List[str]:
evalauted_prefix = set()
series = frappe.qb.DocType("Series")
prefixes_from_db = frappe.qb.from_(series).select(series.name).run(pluck=True)
evalauted_prefix.update(prefixes_from_db)
for series_template in series_templates:
prefix = NamingSeries(series_template).get_prefix()
if "{" in prefix:
# fieldnames can't be evalauted, rely on data in DB instead
continue
evalauted_prefix.add(prefix)
return sorted(evalauted_prefix)
def get_options_list(self, options: str) -> List[str]:
return [op.strip() for op in options.split("\n") if op.strip()]
@frappe.whitelist()
def update_series(self):
"""update series list"""
self.validate_set_series()
self.check_duplicate()
self.set_series_options_in_meta(self.transaction_type, self.naming_series_options)
frappe.msgprint(
_("Series Updated for {}").format(self.transaction_type), alert=True, indicator="green"
)
def validate_set_series(self):
if self.transaction_type and not self.naming_series_options:
frappe.throw(_("Please set the series to be used."))
def set_series_options_in_meta(self, doctype: str, options: str) -> None:
options = self.get_options_list(options)
# validate names
for series in options:
self.validate_series_name(series)
if options and self.user_must_always_select:
options = [""] + options
default = options[0] if options else ""
option_string = "\n".join(options)
self.update_naming_series_property_setter(doctype, "options", option_string)
self.update_naming_series_property_setter(doctype, "default", default)
self.naming_series_options = option_string
frappe.clear_cache(doctype=doctype)
def update_naming_series_property_setter(self, doctype, property, value):
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
make_property_setter(doctype, "naming_series", property, value, "Text")
def check_duplicate(self):
def stripped_series(s: str) -> str:
return s.strip().rstrip("#")
standard = frappe.get_all("DocField", {"fieldname": "naming_series"}, "parent", pluck="parent")
custom = frappe.get_all("Custom Field", {"fieldname": "naming_series"}, "dt", pluck="dt")
all_doctypes_with_naming_series = set(standard + custom)
all_doctypes_with_naming_series.remove(self.transaction_type)
existing_series = {}
for doctype in all_doctypes_with_naming_series:
for series in frappe.get_meta(doctype).get_naming_series_options():
existing_series[stripped_series(series)] = doctype
dt = frappe.get_doc("DocType", self.transaction_type)
options = self.get_options_list(self.naming_series_options)
for series in options:
if stripped_series(series) in existing_series:
frappe.throw(_("Series {0} already used in {1}").format(series, existing_series[series]))
validate_series(dt, series)
def validate_series_name(self, series):
NamingSeries(series).validate()
@frappe.whitelist()
def get_options(self, doctype=None):
doctype = doctype or self.transaction_type
if not doctype:
return
if frappe.get_meta(doctype or self.transaction_type).get_field("naming_series"):
return frappe.get_meta(doctype or self.transaction_type).get_field("naming_series").options
@frappe.whitelist()
def get_current(self):
"""get series current"""
if self.prefix:
self.current_value = NamingSeries(self.prefix).get_current_value()
return self.current_value
@frappe.whitelist()
def update_series_start(self):
frappe.only_for("System Manager")
if not self.prefix:
frappe.throw(_("Please select prefix first"))
naming_series = NamingSeries(self.prefix)
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
)
frappe.msgprint(
_("Series counter for {} updated to {} successfully").format(self.prefix, self.current_value),
alert=True,
indicator="green",
)
def create_version_log_for_change(self, series, old, new):
version = frappe.new_doc("Version")
version.ref_doctype = "Series"
version.docname = series
version.data = frappe.as_json({"changed": [["current", old, new]]})
version.flags.ignore_links = True # series is not a "real" doctype
version.flags.ignore_permissions = True
version.insert()
@frappe.whitelist()
def preview_series(self) -> str:
"""Preview what the naming series will generate."""
series = self.try_naming_series
if not series:
return ""
try:
doc = self._fetch_last_doc_if_available()
return "\n".join(NamingSeries(series).get_preview(doc=doc))
except Exception as e:
if frappe.message_log:
frappe.message_log.pop()
return _("Failed to generate names from the series") + f"\n{str(e)}"
def _fetch_last_doc_if_available(self):
"""Fetch last doc for evaluating naming series with fields."""
try:
return frappe.get_last_doc(self.transaction_type)
except Exception:
return None

View file

@ -0,0 +1,65 @@
# Copyright (c) 2022, Frappe Technologies and Contributors
# See license.txt
import frappe
from frappe.core.doctype.document_naming_settings.document_naming_settings import (
DocumentNamingSettings,
)
from frappe.model.naming import NamingSeries, get_default_naming_series
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cint
class TestNamingSeries(FrappeTestCase):
def setUp(self):
self.dns: DocumentNamingSettings = frappe.get_doc("Document Naming Settings")
def tearDown(self):
frappe.db.rollback()
def get_valid_serieses(self):
VALID_SERIES = ["SINV-", "SI-.{field}.", "SI-#.###", ""]
exisiting_series = self.dns.get_transactions_and_prefixes()["prefixes"]
return VALID_SERIES + exisiting_series
def test_naming_preview(self):
self.dns.transaction_type = "Webhook"
self.dns.try_naming_series = "AXBZ.####"
serieses = self.dns.preview_series().split("\n")
self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses)
self.dns.try_naming_series = "AXBZ-.{currency}.-"
serieses = self.dns.preview_series().split("\n")
def test_get_transactions(self):
naming_info = self.dns.get_transactions_and_prefixes()
self.assertIn("Webhook", naming_info["transactions"])
existing_naming_series = frappe.get_meta("Webhook").get_field("naming_series").options
for series in existing_naming_series.split("\n"):
self.assertIn(NamingSeries(series).get_prefix(), naming_info["prefixes"])
def test_default_naming_series(self):
self.assertIn("HOOK", get_default_naming_series("Webhook"))
self.assertIsNone(get_default_naming_series("DocType"))
def test_updates_naming_options(self):
self.dns.transaction_type = "Webhook"
test_series = "KOOHBEW.###"
self.dns.naming_series_options = self.dns.get_options() + "\n" + test_series
self.dns.update_series()
self.assertIn(test_series, frappe.get_meta("Webhook").get_naming_series_options())
def test_update_series_counter(self):
for series in self.get_valid_serieses():
if not series:
continue
self.dns.prefix = series
current_count = cint(self.dns.get_current())
new_count = self.dns.current_value = current_count + 1
self.dns.update_series_start()
self.assertEqual(self.dns.get_current(), new_count, f"Incorrect update for {series}")

View file

@ -89,7 +89,9 @@ class Report(Document):
]
custom_roles = get_custom_allowed_roles("report", self.name)
allowed.extend(custom_roles)
if custom_roles:
allowed = custom_roles
if not allowed:
return True

View file

@ -186,6 +186,38 @@ class TestReport(FrappeTestCase):
self.assertNotEqual(report.is_permitted(), True)
frappe.set_user("Administrator")
def test_report_custom_permissions(self):
frappe.set_user("test@example.com")
frappe.db.delete("Custom Role", {"report": "Test Custom Role Report"})
frappe.db.commit() # nosemgrep
if not frappe.db.exists("Report", "Test Custom Role Report"):
report = frappe.get_doc(
{
"doctype": "Report",
"ref_doctype": "User",
"report_name": "Test Custom Role Report",
"report_type": "Query Report",
"is_standard": "No",
"roles": [{"role": "_Test Role"}, {"role": "System Manager"}],
}
).insert(ignore_permissions=True)
else:
report = frappe.get_doc("Report", "Test Custom Role Report")
self.assertEqual(report.is_permitted(), True)
frappe.get_doc(
{
"doctype": "Custom Role",
"report": "Test Custom Role Report",
"roles": [{"role": "_Test Role 2"}],
"ref_doctype": "User",
}
).insert(ignore_permissions=True)
self.assertNotEqual(report.is_permitted(), True)
frappe.set_user("Administrator")
# test for the `_format` method if report data doesn't have sort_by parameter
def test_format_method(self):
if frappe.db.exists("Report", "User Activity Report Without Sort"):

View file

@ -194,14 +194,14 @@ frappe.ui.form.on('User', {
}
}
}
if (frm.doc.user_emails){
var found =0;
for (var i = 0;i<frm.doc.user_emails.length;i++){
if (frm.doc.email==frm.doc.user_emails[i].email_id){
if (frm.doc.user_emails && frappe.model.can_create("Email Account")) {
var found = 0;
for (var i = 0; i < frm.doc.user_emails.length; i++) {
if (frm.doc.email == frm.doc.user_emails[i].email_id) {
found = 1;
}
}
if (!found){
if (!found) {
frm.add_custom_button(__("Create User Email"), function() {
frm.events.create_user_email(frm);
});

View file

@ -424,6 +424,9 @@ class User(Document):
frappe.cache().delete_key("enabled_users")
# delete user permissions
frappe.db.delete("User Permission", {"user": self.name})
def before_rename(self, old_name, new_name, merge=False):
frappe.clear_cache(user=old_name)
self.validate_rename(old_name, new_name)
@ -586,10 +589,7 @@ class User(Document):
self.append("social_logins", social_logins)
def get_restricted_ip_list(self):
if not self.restrict_ip:
return
return [i.strip() for i in self.restrict_ip.split(",")]
return get_restricted_ip_list(self)
@classmethod
def find_by_credentials(cls, user_name: str, password: str, validate_password: bool = True):
@ -1156,6 +1156,13 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
contact.save(ignore_permissions=True)
def get_restricted_ip_list(user):
if not user.restrict_ip:
return
return [i.strip() for i in user.restrict_ip.split(",")]
@frappe.whitelist()
def generate_keys(user):
"""

View file

@ -15,7 +15,7 @@ class Version(Document):
if diff:
self.ref_doctype = new.doctype
self.docname = new.name
self.data = frappe.as_json(diff)
self.data = frappe.as_json(diff, indent=None, separators=(",", ":"))
return True
else:
return False

View file

@ -148,6 +148,6 @@ def trigger_indicator_hide():
def set_notifications_as_unseen(user):
try:
frappe.db.set_value("Notification Settings", user, "seen", 0)
frappe.db.set_value("Notification Settings", user, "seen", 0, update_modified=False)
except frappe.DoesNotExistError:
return

View file

@ -48,9 +48,15 @@ def create_notification_settings(user):
_doc.insert(ignore_permissions=True)
def toggle_notifications(user, enable=False):
if frappe.db.exists("Notification Settings", user):
frappe.db.set_value("Notification Settings", user, "enabled", enable)
def toggle_notifications(user: str, enable: bool = False):
try:
settings = frappe.get_doc("Notification Settings", user)
except frappe.DoesNotExistError:
return
if settings.enabled != enable:
settings.enabled = enable
settings.save()
@frappe.whitelist()

View file

@ -171,7 +171,7 @@ def raise_invalid_field(fieldname):
def is_standard(fieldname):
if "." in fieldname:
parenttype, fieldname = get_parenttype_and_fieldname(fieldname, None)
fieldname = fieldname.split(".")[1].strip("`")
return (
fieldname in default_fields or fieldname in optional_fields or fieldname in child_table_fields
)
@ -235,7 +235,16 @@ def parse_json(data):
def get_parenttype_and_fieldname(field, data):
if "." in field:
parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`")
parts = field.split(".")
parenttype = parts[0]
fieldname = parts[1]
if parenttype.startswith("`tab"):
# `tabChild DocType`.`fieldname`
parenttype = parenttype[4:-1]
fieldname = fieldname.strip("`")
else:
# tablefield.fieldname
parenttype = frappe.get_meta(data.doctype).get_field(parenttype).options
else:
parenttype = data.doctype
fieldname = field.strip("`")

View file

@ -284,7 +284,7 @@ class TestEmailAccount(unittest.TestCase):
messages = {
# append_to = ToDo
'"INBOX"': {
"latest_messages": [f.read().replace("{{ message_id }}", last_mail.message_id)],
"latest_messages": [f.read().replace("{{ message_id }}", "<" + last_mail.message_id + ">")],
"seen_status": {2: "UNSEEN"},
"uid_list": [2],
}

View file

@ -19,7 +19,15 @@ from frappe.email.email_body import add_attachment, get_email, get_formatted_htm
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
from frappe.model.document import Document
from frappe.query_builder.utils import DocType
from frappe.utils import add_days, cint, cstr, get_hook_method, nowdate, split_emails
from frappe.utils import (
add_days,
cint,
cstr,
get_hook_method,
get_string_between,
nowdate,
split_emails,
)
MAX_RETRY_COUNT = 3
@ -635,7 +643,7 @@ class QueueBuilder:
d = {
"priority": self.send_priority,
"attachments": json.dumps(self.get_attachments()),
"message_id": mail.msg_root["Message-Id"].strip(" <>"),
"message_id": get_string_between("<", mail.msg_root["Message-Id"], ">"),
"message": mail_to_string,
"sender": self.sender,
"reference_doctype": self.reference_doctype,

View file

@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See LICENSE
import unittest
from random import choice
from typing import Union
from unittest.mock import MagicMock, PropertyMock, patch
@ -17,9 +16,9 @@ from frappe.email.doctype.newsletter.newsletter import (
send_scheduled_email,
)
from frappe.email.queue import flush
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate
test_dependencies = ["Email Group"]
emails = [
"test_subscriber1@example.com",
"test_subscriber2@example.com",
@ -63,6 +62,10 @@ class TestNewsletterMixin:
for email in emails:
doctype = "Email Group Member"
email_filters = {"email": email, "email_group": "_Test Email Group"}
savepoint = "setup_email_group"
frappe.db.savepoint(savepoint)
try:
frappe.get_doc(
{
@ -71,8 +74,11 @@ class TestNewsletterMixin:
}
).insert()
except Exception:
frappe.db.rollback(save_point=savepoint)
frappe.db.update(doctype, email_filters, "unsubscribed", 0)
frappe.db.release_savepoint(savepoint)
def send_newsletter(self, published=0, schedule_send=None) -> Union[str, None]:
frappe.db.delete("Email Queue")
frappe.db.delete("Email Queue Recipient")
@ -127,7 +133,7 @@ class TestNewsletterMixin:
return newsletter
class TestNewsletter(TestNewsletterMixin, unittest.TestCase):
class TestNewsletter(TestNewsletterMixin, FrappeTestCase):
def test_send(self):
self.send_newsletter()

View file

@ -25,6 +25,7 @@ from frappe.utils import (
cstr,
extract_email_id,
get_datetime,
get_string_between,
markdown,
now,
parse_addr,
@ -425,7 +426,9 @@ class Email:
self.set_content_and_type()
self.set_subject()
self.set_from()
self.message_id = (self.mail.get("Message-ID") or "").strip(" <>")
message_id = self.mail.get("Message-ID") or ""
self.message_id = get_string_between("<", message_id, ">")
if self.mail["Date"]:
try:
@ -441,7 +444,8 @@ class Email:
@property
def in_reply_to(self):
return (self.mail.get("In-Reply-To") or "").strip(" <>")
in_reply_to = self.mail.get("In-Reply-To") or ""
return get_string_between("<", in_reply_to, ">")
def parse(self):
"""Walk and process multi-part email."""

View file

@ -2412,7 +2412,7 @@
"Singapore": {
"code": "sg",
"currency": "SGD",
"currency_fraction": "Sen",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"currency_name": "Singapore Dollar",
"currency_symbol": "$",

View file

@ -473,13 +473,20 @@ def set_all_patches_as_completed(app):
def init_singles():
singles = [single["name"] for single in frappe.get_all("DocType", filters={"issingle": True})]
singles = frappe.get_all("DocType", filters={"issingle": True}, pluck="name")
for single in singles:
if not frappe.db.get_singles_dict(single):
if frappe.db.get_singles_dict(single):
continue
try:
doc = frappe.new_doc(single)
doc.flags.ignore_mandatory = True
doc.flags.ignore_validate = True
doc.save()
except ImportError:
# The doctype exists, but controller is deleted,
# no need to attempt to init such single, ref: #16917
continue
def make_conf(

View file

@ -34,6 +34,7 @@ class DatabaseQuery(object):
def __init__(self, doctype, user=None):
self.doctype = doctype
self.tables = []
self.link_tables = []
self.conditions = []
self.or_conditions = []
self.fields = None
@ -216,6 +217,10 @@ class DatabaseQuery(object):
parent_name = cast_name(f"{self.tables[0]}.name")
args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})"
# left join link tables
for link in self.link_tables:
args.tables += f" {self.join} `tab{link.doctype}` on (`tab{link.doctype}`.`name` = {self.tables[0]}.`{link.fieldname}`)"
if self.grouped_or_conditions:
self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
@ -287,6 +292,29 @@ class DatabaseQuery(object):
# remove empty strings / nulls in fields
self.fields = [f for f in self.fields if f]
# convert child_table.fieldname to `tabChild DocType`.`fieldname`
for field in self.fields:
if "." in field and "tab" not in field:
original_field = field
alias = None
if " as " in field:
field, alias = field.split(" as ")
linked_fieldname, fieldname = field.split(".")
linked_field = frappe.get_meta(self.doctype).get_field(linked_fieldname)
linked_doctype = linked_field.options
if linked_field.fieldtype == "Link":
self.link_tables.append(
frappe._dict(
doctype=linked_doctype, fieldname=linked_fieldname, table_name=f"`tab{linked_doctype}`"
)
)
field = field.replace(linked_fieldname, f"`tab{linked_doctype}`")
field = field.replace(fieldname, f"`{fieldname}`")
if alias:
field = f"{field} as {alias}"
self.fields[self.fields.index(original_field)] = field
for filter_name in ["filters", "or_filters"]:
filters = getattr(self, filter_name)
if isinstance(filters, str):
@ -396,7 +424,9 @@ class DatabaseQuery(object):
table_name = table_name[13:]
if not table_name[0] == "`":
table_name = f"`{table_name}`"
if table_name not in self.tables:
if table_name not in self.tables and table_name not in (
d.table_name for d in self.link_tables
):
self.append_table(table_name)
def append_table(self, table_name):
@ -418,7 +448,7 @@ class DatabaseQuery(object):
methods = ("count(", "avg(", "sum(", "extract(", "dayofyear(")
return field.lower().startswith(methods)
if len(self.tables) > 1:
if len(self.tables) > 1 or len(self.link_tables) > 0:
for idx, field in enumerate(self.fields):
if "." not in field and not _in_standard_sql_methods(field):
self.fields[idx] = f"{self.tables[0]}.{field}"

View file

@ -3,6 +3,7 @@
import os
import shutil
from typing import List
import frappe
import frappe.defaults
@ -89,10 +90,6 @@ def delete_doc(
update_flags(doc, flags, ignore_permissions)
check_permission_and_not_submitted(doc)
frappe.db.delete("Custom Field", {"dt": name})
frappe.db.delete("Client Script", {"dt": name})
frappe.db.delete("Property Setter", {"doc_type": name})
frappe.db.delete("Report", {"ref_doctype": name})
frappe.db.delete("Custom DocPerm", {"parent": name})
frappe.db.delete("__global_search", {"doctype": name})
@ -193,39 +190,24 @@ def update_naming_series(doc):
revert_series_if_last(doc.meta.autoname, doc.name, doc)
def delete_from_table(doctype, name, ignore_doctypes, doc):
def delete_from_table(doctype: str, name: str, ignore_doctypes: List[str], doc):
if doctype != "DocType" and doctype == name:
frappe.db.delete("Singles", {"doctype": name})
else:
frappe.db.delete(doctype, {"name": name})
# get child tables
if doc:
tables = [d.options for d in doc.meta.get_table_fields()]
child_doctypes = [d.options for d in doc.meta.get_table_fields()]
else:
child_doctypes = frappe.get_all(
"DocField",
fields="options",
filters={"fieldtype": ["in", frappe.model.table_fields], "parent": doctype},
pluck="options",
)
def get_table_fields(field_doctype):
if field_doctype == "Custom Field":
return []
return [
r[0]
for r in frappe.get_all(
field_doctype,
fields="options",
filters={"fieldtype": ["in", frappe.model.table_fields], "parent": doctype},
as_list=1,
)
]
tables = get_table_fields("DocField")
if not frappe.flags.in_install == "frappe":
tables += get_table_fields("Custom Field")
# delete from child tables
for t in list(set(tables)):
if t not in ignore_doctypes:
frappe.db.delete(t, {"parenttype": doctype, "parent": name})
child_doctypes_to_delete = set(child_doctypes) - set(ignore_doctypes)
for child_doctype in child_doctypes_to_delete:
frappe.db.delete(child_doctype, {"parenttype": doctype, "parent": name})
def update_flags(doc, flags=None, ignore_permissions=False):

View file

@ -17,6 +17,7 @@ Example:
import json
import os
from datetime import datetime
from typing import List
import click
@ -341,6 +342,16 @@ class Meta(Document):
def get_workflow(self):
return get_workflow_name(self.name)
def get_naming_series_options(self) -> List[str]:
"""Get list naming series options."""
field = self.get_field("naming_series")
if field:
options = field.options or ""
return options.split("\n")
return []
def add_custom_fields(self):
if not frappe.db.table_exists("Custom Field"):
return

View file

@ -2,7 +2,7 @@
# License: MIT. See LICENSE
import re
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Callable, List, Optional, Union
import frappe
from frappe import _
@ -11,6 +11,7 @@ from frappe.query_builder import DocType
from frappe.utils import cint, cstr, now_datetime
if TYPE_CHECKING:
from frappe.model.document import Document
from frappe.model.meta import Meta
@ -18,6 +19,95 @@ if TYPE_CHECKING:
# whether `log_types` have autoincremented naming set for the site or not.
autoincremented_site_status_map = {}
NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE)
class InvalidNamingSeriesError(frappe.ValidationError):
pass
class NamingSeries:
__slots__ = ("series",)
def __init__(self, series: str):
self.series = series
# Add default number part if missing
if "#" not in self.series:
self.series += ".#####"
def validate(self):
if "." not in self.series:
frappe.throw(
_("Invalid naming series {}: dot (.) missing").format(frappe.bold(self.series)),
exc=InvalidNamingSeriesError,
)
if not NAMING_SERIES_PATTERN.match(self.series):
frappe.throw(
_(
'Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series',
),
exc=InvalidNamingSeriesError,
)
def generate_next_name(self, doc: "Document") -> str:
self.validate()
parts = self.series.split(".")
return parse_naming_series(parts, doc=doc)
def get_prefix(self) -> str:
"""Naming series stores prefix to maintain a counter in DB. This prefix can be used to update counter or validations.
e.g. `SINV-.YY.-.####` has prefix of `SINV-22-` in database for year 2022.
"""
prefix = None
def fake_counter_backend(partial_series, digits):
nonlocal prefix
prefix = partial_series
return "#" * digits
# This function evaluates all parts till we hit numerical parts and then
# sends prefix + digits to DB to find next number.
# Instead of reimplementing the whole parsing logic in multiple places we
# can just ask this function to give us the prefix.
parse_naming_series(self.series, number_generator=fake_counter_backend)
if prefix is None:
frappe.throw(_("Invalid Naming Series"))
return prefix
def get_preview(self, doc=None) -> List[str]:
"""Generate preview of naming series without using DB counters"""
generated_names = []
for count in range(1, 4):
def fake_counter(_prefix, digits):
return str(count).zfill(digits)
generated_names.append(parse_naming_series(self.series, doc=doc, number_generator=fake_counter))
return generated_names
def update_counter(self, new_count: int) -> None:
"""Warning: Incorrectly updating series can result in unusable transactions"""
Series = frappe.qb.DocType("Series")
prefix = self.get_prefix()
# Initialize if not present in DB
if frappe.db.get_value("Series", prefix, "name", order_by="name") is None:
frappe.qb.into(Series).insert(prefix, 0).columns("name", "current").run()
(
frappe.qb.update(Series).set(Series.current, cint(new_count)).where(Series.name == prefix)
).run()
def get_current_value(self) -> int:
prefix = self.get_prefix()
return cint(frappe.db.get_value("Series", prefix, "current", order_by="name"))
def set_new_name(doc):
"""
@ -175,24 +265,32 @@ def make_autoname(key="", doctype="", doc=""):
if key == "hash":
return frappe.generate_hash(doctype, 10)
if "#" not in key:
key = key + ".#####"
elif "." not in key:
error_message = _("Invalid naming series (. missing)")
if doctype:
error_message = _("Invalid naming series (. missing) for {0}").format(doctype)
frappe.throw(error_message)
parts = key.split(".")
n = parse_naming_series(parts, doctype, doc)
return n
series = NamingSeries(key)
return series.generate_next_name(doc)
def parse_naming_series(parts, doctype="", doc=""):
n = ""
def parse_naming_series(
parts: Union[List[str], str],
doctype=None,
doc: Optional["Document"] = None,
number_generator: Optional[Callable[[str, int], str]] = None,
) -> str:
"""Parse the naming series and get next name.
args:
parts: naming series parts (split by `.`)
doc: document to use for series that have parts using fieldnames
number_generator: Use different counter backend other than `tabSeries`. Primarily used for testing.
"""
name = ""
if isinstance(parts, str):
parts = parts.split(".")
if not number_generator:
number_generator = getseries
series_set = False
today = now_datetime()
for e in parts:
@ -200,7 +298,7 @@ def parse_naming_series(parts, doctype="", doc=""):
if e.startswith("#"):
if not series_set:
digits = len(e)
part = getseries(n, digits)
part = number_generator(name, digits)
series_set = True
elif e == "YY":
part = today.strftime("%y")
@ -225,9 +323,9 @@ def parse_naming_series(parts, doctype="", doc=""):
part = e
if isinstance(part, str):
n += part
name += part
return n
return name
def determine_consecutive_week_number(datetime):
@ -311,14 +409,15 @@ def revert_series_if_last(key, name, doc=None):
frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix)
def get_default_naming_series(doctype):
def get_default_naming_series(doctype: str) -> Optional[str]:
"""get default value for `naming_series` property"""
naming_series = frappe.get_meta(doctype).get_field("naming_series").options or ""
if naming_series:
naming_series = naming_series.split("\n")
return naming_series[0] or naming_series[1]
else:
return None
naming_series_options = frappe.get_meta(doctype).get_naming_series_options()
# Return first truthy options
# Empty strings are used to avoid populating forms by default
for option in naming_series_options:
if option:
return option
def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None):

View file

@ -207,13 +207,18 @@ def export_doc(doctype, name, module=None):
write_document_file(frappe.get_doc(doctype, name), module)
def get_doctype_module(doctype):
def get_doctype_module(doctype) -> str:
"""Returns **Module Def** name of given doctype."""
def make_modules_dict():
return dict(frappe.db.sql("select name, module from tabDocType"))
return frappe.cache().get_value("doctype_modules", make_modules_dict)[doctype]
doctype_module_map = frappe.cache().get_value("doctype_modules", make_modules_dict)
if module_name := doctype_module_map.get(doctype):
return module_name
else:
frappe.throw(_("DocType {} not found").format(doctype), exc=frappe.DoesNotExistError)
doctype_python_modules = {}
@ -234,9 +239,9 @@ def load_doctype_module(doctype, module=None, prefix="", suffix=""):
if key not in doctype_python_modules:
doctype_python_modules[key] = frappe.get_module(module_name)
except ImportError as e:
raise ImportError(
"Module import failed for {0} ({1})".format(doctype, module_name + " Error: " + str(e))
)
msg = f"Module import failed for {doctype}, the DocType you're trying to open might be deleted."
msg += f"<br> Error: {e}"
raise ImportError(msg) from e
return doctype_python_modules[key]
@ -251,12 +256,15 @@ def get_module_name(doctype, module, prefix="", suffix="", app=None):
)
def get_module_app(module):
return frappe.local.module_app[scrub(module)]
def get_module_app(module: str) -> str:
app = frappe.local.module_app.get(scrub(module))
if app is None:
frappe.throw(_("Module {} not found").format(module), exc=frappe.DoesNotExistError)
return app
def get_app_publisher(module):
app = frappe.local.module_app[scrub(module)]
def get_app_publisher(module: str) -> str:
app = get_module_app(module)
if not app:
frappe.throw(_("App not found"))
app_publisher = frappe.get_hooks(hook="app_publisher", app_name=app)[0]
@ -321,7 +329,7 @@ def make_boilerplate(template, doc, opts=None):
base_class=base_class,
doctype=doc.name,
**opts,
custom_controller=custom_controller
custom_controller=custom_controller,
)
)
)

View file

@ -441,7 +441,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
const field = $(e.currentTarget).parent();
// new dialog
var d = new frappe.ui.Dialog({
title: "Set Properties",
title: __("Set Properties"),
fields: [
{
label: __("Label"),
@ -452,7 +452,8 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
label: __("Align Value"),
fieldname: "align",
fieldtype: "Select",
options: [{'label': __('Left'), 'value': 'left'}, {'label': __('Right'), 'value': 'right'}]
options: [{'label': __('Left', null, 'alignment'), 'value': 'left'},
{'label': __('Right', null, 'alignment'), 'value': 'right'}]
},
{
label: __("Remove Field"),

View file

@ -28,5 +28,6 @@ frappe.ui.form.ControlHTML = class ControlHTML extends frappe.ui.form.Control {
this.df.options = html;
this.html(html);
}
return Promise.resolve();
}
};

View file

@ -132,7 +132,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
var f = this.fields_dict[key];
if (f) {
f.set_value(val).then(() => {
f.set_input(val);
f.set_input?.(val);
this.refresh_dependency();
resolve();
});

View file

@ -181,8 +181,8 @@ export default {
return [
{ label: __("Top"), fieldname: "margin_top" },
{ label: __("Bottom"), fieldname: "margin_bottom" },
{ label: __("Left"), fieldname: "margin_left" },
{ label: __("Right"), fieldname: "margin_right" }
{ label: __("Left", null, 'alignment'), fieldname: "margin_left" },
{ label: __("Right", null, 'alignment'), fieldname: "margin_right" }
];
},
fields() {

View file

@ -104,12 +104,12 @@
}
.group-by-button.btn-primary-light {
color: var(--blue-500);
color: var(--text-on-blue);
}
.group-by-icon.active {
use {
stroke: var(--blue-500);
stroke: var(--text-on-blue);
}
}

View file

@ -125,6 +125,10 @@
align-items: center;
}
.page_content {
min-height: 50vh;
}
.breadcrumb-container {
margin-top: 1rem;
padding-top: 0.25rem;

View file

@ -100,7 +100,7 @@ class ParameterizedFunction(Function):
return function_sql
class subqry(Criterion):
class SubQuery(Criterion):
def __init__(
self,
subq: QueryBuilder,
@ -112,3 +112,6 @@ class subqry(Criterion):
def get_sql(self, **kwg: Any) -> str:
kwg["subquery"] = True
return self.subq.get_sql(**kwg)
subqry = SubQuery

View file

@ -93,7 +93,7 @@ def set_permission(doctype, name, user, permission_to, value=1, everyone=0):
if not (share.read or share.write or share.submit or share.share):
share.delete()
share = {}
share = None
return share

View file

@ -3,6 +3,7 @@ $(document).ready(function(){
var options = {
"key": "{{ api_key }}",
"amount": cint({{ amount }} * 100), // 2000 paise = INR 20
"currency": "{{ currency }}",
"name": "{{ title }}",
"description": "{{ description }}",
"subscription_id": "{{ subscription_id }}",

View file

@ -17,6 +17,7 @@ expected_keys = (
"payer_name",
"payer_email",
"order_id",
"currency",
)

View file

@ -2,33 +2,32 @@
background-color: var(--bg-color);
}
.page-card {
max-width: 360px;
padding: 15px;
margin: 70px auto;
border-radius: 4px;
background-color: var(--fg-color);
/* box-shadow: var(--shadow-base); */
max-width: 360px;
padding: 15px;
margin: 70px auto;
border-radius: 4px;
background-color: var(--fg-color);
box-shadow: var(--shadow-base);
}
.for-reset-password {
margin: 80px 0;
margin: 80px 0;
}
.for-reset-password .page-card {
border: 0;
max-width: 450px;
margin: auto;
border-radius: 10px;
border: 0;
max-width: 450px;
margin: auto;
border-radius: var(--border-radius-md);
padding: 40px 60px;
}
@media (min-width: 567px) {
@media (max-width: 425px) {
.for-reset-password .page-card {
box-shadow: var(--shadow-base);
padding: 40px 60px;
box-shadow: none;
background: none;
padding: 0px;
}
}

View file

@ -35,6 +35,35 @@ class TestReportview(unittest.TestCase):
clear_custom_fields("DocType")
def test_child_table_field_syntax(self):
note = frappe.get_doc(
doctype="Note",
title=f"Test {frappe.utils.random_string(8)}",
content="test",
seen_by=[{"user": "Administrator"}],
).insert()
result = frappe.db.get_all(
"Note",
filters={"name": note.name},
fields=["name", "seen_by.user as seen_by"],
limit=1,
)
self.assertEqual(result[0].seen_by, "Administrator")
note.delete()
def test_link_field_syntax(self):
todo = frappe.get_doc(
doctype="ToDo", description="Test ToDo", allocated_to="Administrator"
).insert()
result = frappe.db.get_all(
"ToDo",
filters={"name": todo.name},
fields=["name", "allocated_to.email as allocated_user_email"],
limit=1,
)
self.assertEqual(result[0].allocated_user_email, "admin@example.com")
todo.delete()
def test_build_match_conditions(self):
clear_user_permissions_for_doctype("Blog Post", "test2@example.com")

View file

@ -1,20 +1,21 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import unittest
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.model.naming import (
InvalidNamingSeriesError,
NamingSeries,
append_number_if_name_exists,
determine_consecutive_week_number,
getseries,
revert_series_if_last,
)
from frappe.tests.utils import FrappeTestCase
from frappe.utils import now_datetime
class TestNaming(unittest.TestCase):
class TestNaming(FrappeTestCase):
def setUp(self):
frappe.db.delete("Note")
@ -52,16 +53,13 @@ class TestNaming(unittest.TestCase):
self.assertEqual(country.name, country.country_name)
def test_child_table_naming(self):
child_dt_with_naming = new_doctype(
"childtable_with_autonaming", istable=1, autoname="field:some_fieldname"
).insert()
child_dt_with_naming = new_doctype(istable=1, autoname="field:some_fieldname").insert()
dt_with_child_autoname = new_doctype(
"dt_with_childtable_naming",
fields=[
{
"label": "table with naming",
"fieldname": "table_with_naming",
"options": "childtable_with_autonaming",
"options": child_dt_with_naming.name,
"fieldtype": "Table",
}
],
@ -69,7 +67,7 @@ class TestNaming(unittest.TestCase):
name = frappe.generate_hash(length=10)
doc = frappe.new_doc("dt_with_childtable_naming")
doc = frappe.new_doc(dt_with_child_autoname.name)
doc.append("table_with_naming", {"some_fieldname": name})
doc.save()
self.assertEqual(doc.table_with_naming[0].name, name)
@ -89,31 +87,18 @@ class TestNaming(unittest.TestCase):
"""
Test if braced params are replaced in format autoname
"""
doctype = "ToDo"
todo_doctype = frappe.get_doc("DocType", doctype)
todo_doctype.autoname = "format:TODO-{MM}-{status}-{##}"
todo_doctype.save()
doctype = new_doctype(autoname="format:TODO-{MM}-{some_fieldname}-{##}").insert()
description = "Format"
todo = frappe.new_doc(doctype)
todo.description = description
todo.insert()
doc = frappe.new_doc(doctype.name)
doc.some_fieldname = description
doc.insert()
series = getseries("", 2)
series = int(series) - 1
series = str(int(series) - 1)
if len(series) < 2:
series = "0" + series
self.assertEqual(
todo.name,
"TODO-{month}-{status}-{series}".format(
month=now_datetime().strftime("%m"), status=todo.status, series=series
),
)
self.assertEqual(doc.name, f"TODO-{now_datetime().strftime('%m')}-{description}-{series:02}")
def test_format_autoname_for_consecutive_week_number(self):
"""
@ -303,6 +288,46 @@ class TestNaming(unittest.TestCase):
dt.delete(ignore_permissions=True)
def test_naming_series_prefix(self):
today = now_datetime()
year = today.strftime("%y")
month = today.strftime("%m")
prefix_test_cases = {
"SINV-.YY.-.####": f"SINV-{year}-",
"SINV-.YY.-.MM.-.####": f"SINV-{year}-{month}-",
"SINV": "SINV",
"SINV-.": "SINV-",
}
for series, prefix in prefix_test_cases.items():
self.assertEqual(prefix, NamingSeries(series).get_prefix())
def test_naming_series_validation(self):
dns = frappe.get_doc("Document Naming Settings")
exisiting_series = dns.get_transactions_and_prefixes()["prefixes"]
valid = ["SINV-", "SI-.{field}.", "SI-#.###", ""] + exisiting_series
invalid = ["$INV-", r"WINDOWS\NAMING"]
for series in valid:
if series.strip():
try:
NamingSeries(series).validate()
except Exception as e:
self.fail(f"{series} should be valid\n{e}")
for series in invalid:
self.assertRaises(InvalidNamingSeriesError, NamingSeries(series).validate)
def test_naming_using_fields(self):
webhook = frappe.new_doc("Webhook")
webhook.webhook_docevent = "on_update"
name = NamingSeries("KOOH-.{webhook_docevent}.").generate_next_name(webhook)
self.assertTrue(
name.startswith("KOOH-on_update"), f"incorrect name generated {name}, missing field value"
)
def make_invalid_todo():
frappe.get_doc({"doctype": "ToDo", "description": "Test"}).insert(set_name="ToDo")

View file

@ -615,3 +615,29 @@ class TestAppParser(unittest.TestCase):
self.assertEqual("healthcare", parse_app_name("https://github.com/frappe/healthcare.git"))
self.assertEqual("healthcare", parse_app_name("git@github.com:frappe/healthcare.git"))
self.assertEqual("healthcare", parse_app_name("frappe/healthcare@develop"))
class TestIntrospectionMagic(unittest.TestCase):
"""Test utils that inspect live objects"""
def test_get_newargs(self):
# `kwargs` is just convention any **varname should work.
def f(a, b=2, **args):
pass
safe_kwargs = {"company": "Wind Power", "b": 1}
self.assertEqual(frappe.get_newargs(f, safe_kwargs), safe_kwargs)
unsafe_args = dict(safe_kwargs)
unsafe_args.update({"ignore_permissions": True, "flags": {"ignore_mandatory": True}})
self.assertEqual(frappe.get_newargs(f, unsafe_args), safe_kwargs)
def test_strip_off_kwargs_when_not_supported(self):
def f(a, b=2):
pass
args = {"company": "Wind Power", "b": 1}
self.assertEqual(frappe.get_newargs(f, args), {"b": 1})
# No args
self.assertEqual(frappe.get_newargs(lambda: None, args), {})

View file

@ -118,7 +118,7 @@ class TestWebsite(unittest.TestCase):
def test_error_page(self):
set_request(method="GET", path="/_test/problematic_page")
response = get_response()
self.assertEqual(response.status_code, 500)
self.assertEqual(response.status_code, 417)
def test_login(self):
set_request(method="GET", path="/login")

View file

@ -82,6 +82,8 @@ def _restore_thread_locals(flags):
frappe.local.realtime_log = []
frappe.local.conf = frappe._dict(frappe.get_site_config())
frappe.local.cache = {}
frappe.local.lang = "en"
frappe.local.lang_full_dict = None
@contextmanager

View file

@ -4717,3 +4717,6 @@ Document has been cancelled,Document annulé
Document is in draft state,Document au statut brouillon
Copy to Clipboard,Copier vers le presse-papiers
Don't have an account?,Vous n&#39;avez pas de compte?
Left:alignment,Gauche
Right:alignment,Droite
Set Properties,Gérer les proriétés

Can't render this file because it has a wrong number of fields in line 421.

View file

@ -840,7 +840,7 @@ Default Sending and Inbox,По умолчанию отправка и получ
Default Sort Field,Поле сортировки по умолчанию,
Default Sort Order,Порядок сортировки по умолчанию,
Default Value,Значение по умолчанию,
"Default: ""Contact Us""","По умолчанию: ""Обратная связь""",
"Default: ""Contact Us""","По умолчанию: ""Contact Us""",
DefaultValue,DefaultValue,
Define workflows for forms.,Определите рабочие процессы для форм.,
Defines actions on states and the next step and allowed roles.,"Определяет действия на статусах, следующий шаг и роли, обладающие правами перевода статусов.",
@ -849,7 +849,7 @@ Delayed,Задерживается,
Delete Data,Удалить данные,
Delete comment?,Удалить комментарий?,
Delete this record to allow sending to this email address,"Удалить эту запись, чтобы разрешить отправку на этот адрес электронной почты",
Delete {0} items permanently?,Удалить {0} продуктов навсегда?,
Delete {0} items permanently?,Удалить {0} объектов навсегда?,
Deleted,Удаленный,
Deleted DocType,Удаленный DocType,
Deleted Document,Удаленный документ,
@ -914,7 +914,7 @@ Document can't saved.,Документ не может быть сохранен
Document {0} has been set to state {1} by {2},Документ {0} установлен в состояние {1} на {2},
Documents,Документы,
Documents assigned to you and by you.,"Документы, назначенные вам и вами.",
Domain Settings,Настройки домена,
Domain Settings,Настройка сфер деятельности,
Domains HTML,Домены HTML,
"Don't HTML Encode HTML tags like &lt;script&gt; or just characters like &lt; or &gt;, as they could be intentionally used in this field","Не HTML Кодировать HTML-теги, такие как &lt;скрипт&gt; или просто символы, такие как &lt;или&gt;, так как они могут быть преднамеренно использованы в этой области",
Don't Override Status,Не переопределять статус,
@ -990,7 +990,7 @@ Enable Auto Reply,Включить автоматический ответ,
Enable Automatic Backup,Включить автоматическое резервное копирование,
Enable Chat,Включить чат,
Enable Comments,Включить комментарии,
Enable Incoming,Включение входящей,
Enable Incoming,Включить входящие,
Enable Outgoing,Включить исходящие,
Enable Password Policy,Включить политику паролей,
Enable Print Server,Включить сервер печати,
@ -1000,7 +1000,7 @@ Enable Scheduled Jobs,Включить запланированных задан
Enable Social Login,Включить социальный вход,
Enable Two Factor Auth,Включить двухфакторный аут,
Enabled email inbox for user {0},Включен почтовый ящик для пользователя {0},
"Encryption key is invalid, Please check site_config.json","Ключ шифрования недействителен, проверьте сайт_config.json",
"Encryption key is invalid, Please check site_config.json","Ключ шифрования недействителен, проверьте site_config.json",
End Date Field,Поле конечной даты,
End Date cannot be before Start Date!,Дата окончания не может быть до даты начала!,
Endpoint URL,URL конечной точки,
@ -1029,8 +1029,8 @@ Error in Notification: {},Ошибка в уведомлении: {},
Error while connecting to email account {0},Ошибка при подключении к учетной записи электронной почты {0},
Error while evaluating Notification {0}. Please fix your template.,Ошибка при оценке уведомления {0}. Исправьте шаблон.,
Error: Document has been modified after you have opened it,"Ошибка: документ был изменен после того, как вы открыли его",
Error: Value missing for {0}: {1},Ошибка: значение отсутствует для {0}: {1},
Errors in Background Events,Ошибки в фоновых событий,
Error: Value missing for {0}: {1},Ошибка: отсутствует значение для {0}: {1},
Errors in Background Events,Ошибки в фоновых событиях,
Event Category,Категория события,
Event Participants,Участники мероприятия,
Event Type,Тип события,
@ -1184,7 +1184,7 @@ Get Contacts,Получить контакты,
Get Fields,Получить поля,
Get your globally recognized avatar from Gravatar.com,Получить всемирно признанный аватара из Gravatar.com,
GitHub,GitHub,
Give Review Points,Дайте очки обзора,
Give Review Points,Дайте баллы обзора,
Global Unsubscribe,Глобальная отписка,
Go to the document,Перейти к документу,
Go to this URL after completing the form (only for Guest users),Перейдите по этому URL-адресу после заполнения формы (только для гостевых пользователей),
@ -1461,8 +1461,8 @@ Letter Head Name,Название заголовка письма,
Letter Head in HTML,Заголовок письма в HTML,
Level Name,Название уровня,
Liked,Понравилось,
Liked By,В избранное К,
Liked by {0},В избранное {0},
Liked By,Нравится,
Liked by {0},Нравится {0},
Likes,Понравившееся,
Limit Number of DB Backups,Ограничение количества резервных копий БД,
Line,Линия,
@ -1470,7 +1470,7 @@ Link DocType,Ссылка DocType,
Link Expired,Срок действия ссылки,
Link Name,Имя ссылки,
Link Title,Название ссылки,
"Link that is the website home page. Standard Links (index, login, products, blog, about, contact)","Ссылка, которая является стартовой страницей сайта. Стандартные ссылки (индекс, логин, продукты, блог, о, контакт)",
"Link that is the website home page. Standard Links (index, login, products, blog, about, contact)","Ссылка, которая является стартовой страницей сайта. Стандартные ссылки (index, login, products, blog, about, contact)",
Link to the page you want to open. Leave blank if you want to make it a group parent.,"Ссылка на страницу, которую вы хотите открыть. Оставьте пустым, если хотите сделать его родительским элементом группы.",
Linked,Связанный,
Linked With,Связанные с,
@ -2096,7 +2096,7 @@ Revert Of,Вернуть из,
Reverted,Отменено,
Review Level,Уровень обзора,
Review Levels,Уровни обзора,
Review Points,Очки обзора,
Review Points,Баллы обзора,
Reviews,Отзывы,
Revoke,Аннулировать,
Revoked,Аннулировано,
@ -2141,10 +2141,10 @@ SMS sent to following numbers: {0},SMS отправлено следующим
SMTP Server,SMTP-сервер,
SMTP Settings for outgoing emails,Настройки SMTP для исходящих писем,
"SQL Conditions. Example: status=""Open""",SQL условия. Пример: статус = &quot;Открыть&quot;,
SSL/TLS Mode,Режим SSL / TLS,
SSL/TLS Mode,Режим SSL/TLS,
Salesforce,Salesforce,
Same Field is entered more than once,Одно и то же поле вводится не один раз,
Save API Secret: ,Сохранить API-интерфейс:,
Save API Secret: ,Сохранить API секрет: ,
Save As,Сохранить как,
Save Filter,Сохранить фильтр,
Save Report,Сохранить отчет,
@ -2258,7 +2258,7 @@ Set Property After Alert,Задать свойство после оповеще
Set Quantity,Установите Количество,
Set Role For,Установить роль для,
Set User Permissions,Задание разрешений пользователя,
Set Value,Задать значение,
Set Value,Установить значение,
Set custom roles for page and report,Набор пользовательских ролей для страницы и отчета,
"Set default format, page size, print style etc.","Установить форму, размер страницы, стиль печати и т.д., используюмых по умолчанию",
Set non-standard precision for a Float or Currency field,Установите нестандартные точность для поплавка или валютной области,
@ -2337,7 +2337,7 @@ Slideshow like display for the website,"Слайд-шоу, как дисплей
Small Text,Маленьикий текст,
Smallest Currency Fraction Value,Минимальное дробное значение,
Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01,"Минимальная разменная денежная единица (монета). Например, для доллара — 1 цент, и его нужно ввести как 0,01",
Snapshot View,Снимок Посмотреть,
Snapshot View,Просмотр снимка,
Social,Сообщество,
Social Login Key,Ключ социального входа,
Social Login Provider,Социальный провайдер,
@ -2418,7 +2418,7 @@ Suspend Sending,Приостановить Отправка,
Switch To Desk,Переключение на рабочий стол,
Symbol,Символ,
Sync,Синхронизация,
Sync on Migrate,Синхронизация по Migrate,
Sync on Migrate,Синхронизировать при переносе,
Syntax error in template,Синтаксическая ошибка в шаблоне,
System,Система,
System Page,Страница системы,
@ -2450,7 +2450,7 @@ Thank you for your interest in subscribing to our updates,Спасибо за в
Thank you for your message,Спасибо за ваше сообщение,
The CSV format is case sensitive,Формат CSV чувствителен к регистру,
The Condition '{0}' is invalid,Условие '{0}' является недействительным,
The First User: You,Первый пользователя: Вы,
The First User: You,Первый пользователь: Вы,
"The application has been updated to a new version, please refresh this page","Приложение был обновлен до новой версии, пожалуйста, обновите эту страницу",
The attachments could not be correctly linked to the new document,Вложения не могут быть правильно связаны с новым документом,
The document could not be correctly assigned,Документ не может быть правильно назначен,
@ -2653,7 +2653,7 @@ User Field,Поле пользователя,
User ID of a Blogger,ID пользователя-блоггера,
User Image,Изображение пользователя,
User Name,Имя пользователя,
User Permission,Пользователь Введено,
User Permission,Разрешения пользователя,
User Permissions,Разрешения пользователей,
User Permissions are used to limit users to specific records.,Пользовательские разрешения используются для ограничения пользователей конкретными записями.,
User Permissions created sucessfully,Пользовательские разрешения созданы успешно,
@ -3068,8 +3068,8 @@ zoom-out,отдалить,
{0} or {1},{0} или {1},
{0} record deleted,{0} запись удалена,
{0} records deleted,{0} записей удалено,
{0} reverted your point on {1},{0} вернул вашу точку на {1},
{0} reverted your points on {1},{0} вернул ваши очки на {1},
{0} reverted your point on {1},{0} вернул ваш балл на {1},
{0} reverted your points on {1},{0} вернул ваши баллы на {1},
{0} reverted {1},{0} вернул {1},
{0} room must have atmost one user.,{0} номер должен иметь самого одного пользователя.,
{0} rows for {1},{0} строк для {1},
@ -3148,7 +3148,7 @@ Access not allowed from this IP Address,Доступ с этого IP-адрес
Action Type,Тип действия,
Activity Log by ,Активность Журнал по,
Add Fields,Добавить поля,
Administration,Администрация,
Administration,Администрирование,
After Cancel,После отмены,
After Delete,После удаления,
After Save,После сохранения,
@ -3157,7 +3157,7 @@ After Submit,После отправки,
Aggregate Function Based On,"Агрегатная функция, основанная на",
Aggregate Function field is required to create a dashboard chart,Поле Aggregate Function необходимо для создания диаграммы панели мониторинга.,
All Records,Все записи,
Allot Points To Assigned Users,Выделить очки назначенным пользователям,
Allot Points To Assigned Users,Выделить баллы назначенным пользователям,
Allow Auto Repeat,Разрешить автоматическое повторение,
Allow Google Calendar Access,Разрешить доступ к Календарю Google,
Allow Google Contacts Access,Разрешить доступ к контактам Google,
@ -3380,7 +3380,7 @@ Invalid field name: {0},Неверное имя поля: {0},
Invalid file URL. Please contact System Administrator.,"Неверный URL файла. Пожалуйста, свяжитесь с системным администратором.",
Invalid include path,Неверный путь включения,
Invalid username or password,неправильное имя пользователя или пароль,
Is Primary,Первичный,
Is Primary,Основной,
Is Primary Mobile,Основной мобильный,
Is Primary Phone,Основной телефон,
Is Tree,Дерево,
@ -3667,7 +3667,7 @@ via Data Import,через импорт данных,
{0} are mandatory fields,{0} обязательные поля,
{0} are required,{0} требуется,
{0} assigned a new task {1} {2} to you,{0} назначил вам новое задание {1} {2},
{0} gained {1} point for {2} {3},{0} набрал {1} очко за {2} {3},
{0} gained {1} point for {2} {3},{0} получил {1} балл за {2} {3},
{0} gained {1} points for {2} {3},{0} набрал {1} баллов за {2} {3},
{0} has no versions tracked.,{0} не отслеживает версии.,
{0} is not a valid report format. Report format should one of the following {1},{0} не является допустимым форматом отчета. Формат отчета должен быть одним из следующих {1},
@ -4045,7 +4045,7 @@ No Permitted Charts on this Dashboard,На этой панели инструм
No Permitted Charts,Нет разрешенных графиков,
Reset Chart,Сбросить график,
via {0},через {0},
{0} is not a valid Phone Number,{0} не является действительным номером телефона,
{0} is not a valid Phone Number,{0} недействительный номер телефона,
Failed Transactions,Неудачные транзакции,
Value for field {0} is too long in {1}. Length should be lesser than {2} characters,Значение поля {0} слишком длинное в {1}. Длина должна быть меньше {2} симв.,
Data Too Long,Данные слишком длинные,
@ -4121,8 +4121,8 @@ Using this console may allow attackers to impersonate you and steal your informa
{0} w,{0} н,
{0} M,{0} М,
{0} y,{0} г,
yesterday,вчерашний день,
{0} years ago,{0} лет назад,
yesterday,вчера,
{0} years ago,{0} год назад,
New Chart,Новый график,
New Shortcut,Новый ярлык,
Edit Chart,Изменить диаграмму,
@ -4700,3 +4700,11 @@ Value cannot be negative for {0}: {1},Значение не может быть
Negative Value,Отрицательное значение,
Authentication failed while receiving emails from Email Account: {0}.,Ошибка аутентификации при получении писем из учетной записи электронной почты: {0}.,
Message from server: {0},Сообщение с сервера: {0},
Documentation,Документация,
User Forum,Форум пользователей,
Report an issue,Сообщить об ошибке,
My Profile,Мой профиль,
My Settings,Мои настройки,
Toggle Full Width,Переключить ширину,
Toggle Theme,Переключить тему,
Modules,Модули,

Can't render this file because it contains an unexpected character in line 1588 and column 67.

View file

@ -277,7 +277,9 @@ def get_email_subject_for_qr_code(kwargs_dict):
def get_email_body_for_qr_code(kwargs_dict):
"""Get QRCode email body."""
body_template = "Please click on the following link and follow the instructions on the page.<br><br> {{qrcode_link}}"
body_template = _(
"Please click on the following link and follow the instructions on the page. {0}"
).format("<br><br> {{qrcode_link}}")
body = frappe.render_template(body_template, kwargs_dict)
return body

View file

@ -1887,6 +1887,16 @@ def strip(val: str, chars: Optional[str] = None) -> str:
return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars)
def get_string_between(start: str, string: str, end: str) -> str:
if not string:
return ""
regex = "{0}(.*){1}".format(start, end)
out = re.search(regex, string)
return out.group(1) if out else string
def to_markdown(html: str) -> str:
from html.parser import HTMLParser

View file

@ -17,6 +17,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder import Order
from frappe.query_builder.functions import Coalesce, Max
from frappe.query_builder.terms import SubQuery
from frappe.query_builder.utils import DocType
@ -336,14 +337,15 @@ class NestedSet(Document):
def get_root_of(doctype):
"""Get root element of a DocType with a tree structure"""
from frappe.query_builder.functions import Count
from frappe.query_builder.terms import subqry
Table = DocType(doctype)
t1 = Table.as_("t1")
t2 = Table.as_("t2")
subq = frappe.qb.from_(t2).select(Count("*")).where((t2.lft < t1.lft) & (t2.rgt > t1.rgt))
result = frappe.qb.from_(t1).select(t1.name).where((subqry(subq) == 0) & (t1.rgt > t1.lft)).run()
node_query = SubQuery(
frappe.qb.from_(t2).select(Count("*")).where((t2.lft < t1.lft) & (t2.rgt > t1.rgt))
)
result = frappe.qb.from_(t1).select(t1.name).where((node_query == 0) & (t1.rgt > t1.lft)).run()
return result[0][0] if result else None

View file

@ -2,17 +2,17 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import json
import unittest
import frappe
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.core.doctype.user.user import create_contact
from frappe.tests.utils import FrappeTestCase
from frappe.website.doctype.personal_data_download_request.personal_data_download_request import (
get_user_data,
)
class TestRequestPersonalData(unittest.TestCase):
class TestRequestPersonalData(FrappeTestCase):
def setUp(self):
create_user_if_not_exists(email="test_privacy@example.com")
@ -48,7 +48,7 @@ class TestRequestPersonalData(unittest.TestCase):
email_queue = frappe.get_all(
"Email Queue", fields=["message"], order_by="creation DESC", limit=1
)
self.assertTrue("Subject: Download Your Data" in email_queue[0].message)
self.assertIn(frappe._("Download Your Data"), email_queue[0].message)
frappe.db.delete("Email Queue")

View file

@ -21,6 +21,7 @@
"website_theme_image_link",
"brand",
"banner_image",
"splash_image",
"brand_html",
"set_banner_from_image",
"favicon",
@ -413,6 +414,11 @@
"fieldname": "footer_powered",
"fieldtype": "Small Text",
"label": "Footer \"Powered By\""
},
{
"fieldname": "splash_image",
"fieldtype": "Attach Image",
"label": "Splash Image"
}
],
"icon": "fa fa-cog",
@ -420,7 +426,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-03-09 01:47:31.094462",
"modified": "2022-05-27 12:33:29.019998",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",
@ -445,4 +451,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View file

@ -136,7 +136,7 @@ def get_website_settings(context=None):
}
)
settings = frappe.get_single("Website Settings")
settings: "WebsiteSettings" = frappe.get_single("Website Settings")
for k in [
"banner_html",
"banner_image",
@ -203,6 +203,9 @@ def get_website_settings(context=None):
context["hide_login"] = settings.hide_login
if splash_image := settings.splash_image or context.get("splash_image"):
context["splash_image"] = splash_image
return context

View file

@ -5,7 +5,13 @@ class ErrorPage(TemplatePage):
def __init__(self, path=None, http_status_code=None, exception=None):
path = "error"
super().__init__(path=path, http_status_code=http_status_code)
self.http_status_code = getattr(exception, "http_status_code", None) or http_status_code or 500
self.exception = exception
def can_render(self):
return True
def init_context(self):
super().init_context()
self.context.http_status_code = getattr(self.exception, "http_status_code", None) or 500
self.context.error_title = getattr(self.exception, "title", None)
self.context.error_message = getattr(self.exception, "message", None)

View file

@ -212,19 +212,13 @@ class TemplatePage(BaseTemplatePage):
def run_pymodule_method(self, method_name):
if hasattr(self.pymodule, method_name):
try:
import inspect
import inspect
method = getattr(self.pymodule, method_name)
if inspect.getfullargspec(method).args:
return method(self.context)
else:
return method()
except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect):
raise
except Exception:
if not frappe.flags.in_migrate:
frappe.errprint(frappe.utils.get_traceback())
method = getattr(self.pymodule, method_name)
if inspect.getfullargspec(method).args:
return method(self.context)
else:
return method()
def render_template(self):
if self.template_path.endswith("min.js"):

View file

@ -23,15 +23,15 @@
<script></script>
<div class="page-card">
<div class="page-card-head">
<span class="indicator red">{{_("Uncaught Server Exception")}}</span>
<span class="indicator red">{{ error_title }}</span>
</div>
<p>{{_("There was an error building this page")}}</p>
<p>{{ error_message }}</p>
<div>
<a href="/" class="btn btn-primary btn-sm">{{ _("Home") }}</a>
</div>
</div>
<p class="text-muted text-center small" style="margin-top: -20px;">
{{ _("Error Code: {0}").format('500') }}
{{ _("Error Code: {0}").format(http_status_code) }}
</p>
<div class="text-center mt-3">
<button class="btn btn-xs btn-link text-muted small view-error" >{{ _("Show Traceback") if not dev_server else _("Hide Traceback") }}</a>

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe import _
no_cache = 1
@ -8,7 +9,8 @@ no_cache = 1
def get_context(context):
if frappe.flags.in_migrate:
return
context.http_status_code = 500
print(frappe.get_traceback())
context.error_title = context.error_title or _("Uncaught Server Exception")
context.error_message = context.error_message or _("There was an error building this page")
return {"error": frappe.get_traceback().replace("<", "&lt;").replace(">", "&gt;")}

View file

@ -44,7 +44,7 @@ PyMySQL~=1.0.2
pyOpenSSL~=20.0.1
pyotp~=2.6.0
PyPDF2~=1.26.0
PyPika~=0.48.6
PyPika~=0.48.9
pypng~=0.0.20
PyQRCode~=1.2.1
python-dateutil~=2.8.1