Merge branch 'develop' into fix-note-2

This commit is contained in:
Suraj Shetty 2023-04-17 13:19:48 +05:30 committed by GitHub
commit 3028918f98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 495 additions and 261 deletions

View file

@ -183,7 +183,7 @@ function get_all_files_to_build(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
include_patterns.push(
path.resolve(public_path, "**", "*.bundle.{js,ts,css,sass,scss,less,styl}")
path.resolve(public_path, "**", "*.bundle.{js,ts,css,sass,scss,less,styl,jsx}")
);
ignore_patterns.push(
path.resolve(public_path, "node_modules"),

View file

@ -182,9 +182,9 @@ if TYPE_CHECKING:
# end: static analysis hack
def init(site: str, sites_path: str = ".", new_site: bool = False) -> None:
def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) -> None:
"""Initialize frappe for the current site. Reset thread locals `frappe.local`"""
if getattr(local, "initialised", None):
if getattr(local, "initialised", None) and not force:
return
local.error_log = []

View file

@ -74,12 +74,18 @@ def application(request: Request):
rollback = sync_database(rollback)
finally:
# Important note:
# this function *must* always return a response, hence any exception thrown outside of
# try..catch block like this finally block needs to be handled appropriately.
if request.method in UNSAFE_HTTP_METHODS and frappe.db and rollback:
frappe.db.rollback()
if getattr(frappe.local, "initialised", False):
for after_request_task in frappe.get_hooks("after_request"):
frappe.call(after_request_task, response=response, request=request)
try:
run_after_request_hooks(request, response)
except Exception as e:
# We can not handle exceptions safely here.
frappe.logger().error("Failed to run after request hook", exc_info=True)
log_request(request, response)
process_response(response)
@ -89,12 +95,20 @@ def application(request: Request):
return response
def run_after_request_hooks(request, response):
if not getattr(frappe.local, "initialised", False):
return
for after_request_task in frappe.get_hooks("after_request"):
frappe.call(after_request_task, response=response, request=request)
def init_request(request):
frappe.local.request = request
frappe.local.is_ajax = frappe.get_request_header("X-Requested-With") == "XMLHttpRequest"
site = _site or request.headers.get("X-Frappe-Site-Name") or get_site_name(request.host)
frappe.init(site=site, sites_path=_sites_path)
frappe.init(site=site, sites_path=_sites_path, force=True)
if not (frappe.local.conf and frappe.local.conf.db_name):
# site does not exist

View file

@ -12,8 +12,6 @@ from frappe.contacts.doctype.contact.contact import (
get_contacts_linked_from,
get_contacts_linking_to,
)
from frappe.core.doctype.communication.email import make
from frappe.desk.form import assign_to
from frappe.model.document import Document
from frappe.utils import (
add_days,
@ -365,7 +363,7 @@ class AutoRepeat(Document):
error_string += _(
"{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings"
).format(frappe.bold(_("Note")), frappe.bold(_("Allow Print for Draft")))
attachments = "[]"
attachments = None
if error_string:
message = error_string
@ -376,14 +374,14 @@ class AutoRepeat(Document):
recipients = self.recipients.split("\n")
make(
doctype=new_doc.doctype,
name=new_doc.name,
frappe.sendmail(
reference_doctype=new_doc.doctype,
reference_name=new_doc.name,
recipients=recipients,
subject=subject,
content=message,
attachments=attachments,
send_email=1,
expose_recipients="header",
)
@frappe.whitelist()

View file

@ -163,7 +163,7 @@ class TestAutoRepeat(FrappeTestCase):
docnames = frappe.get_all(doc.reference_doctype, {"auto_repeat": doc.name})
self.assertEqual(len(docnames), months)
def test_notification_is_attached(self):
def test_email_notification(self):
todo = frappe.get_doc(
dict(
doctype="ToDo",
@ -187,10 +187,10 @@ class TestAutoRepeat(FrappeTestCase):
"ToDo", {"auto_repeat": doc.name, "name": ("!=", todo.name)}, "name"
)
linked_comm = frappe.db.exists(
"Communication", dict(reference_doctype="ToDo", reference_name=new_todo)
email_queue = frappe.db.exists(
"Email Queue", dict(reference_doctype="ToDo", reference_name=new_todo)
)
self.assertTrue(linked_comm)
self.assertTrue(email_queue)
def test_next_schedule_date(self):
current_date = getdate(today())

View file

@ -130,7 +130,7 @@ def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_workspace_sidebar_items
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages")
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces()
bootinfo.dashboards = frappe.get_all("Dashboard")

View file

@ -3,7 +3,10 @@
import json
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.templates.includes.comments.comments import add_comment
from frappe.tests.test_model_utils import set_user
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
class TestComment(FrappeTestCase):
@ -39,14 +42,10 @@ class TestComment(FrappeTestCase):
# test via blog
def test_public_comment(self):
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
test_blog = make_test_blog()
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
from frappe.templates.includes.comments.comments import add_comment
frappe.form_dict.comment = "Good comment with 10 chars"
frappe.form_dict.comment_email = "test@test.com"
frappe.form_dict.comment_by = "Good Tester"
@ -102,3 +101,32 @@ class TestComment(FrappeTestCase):
)
test_blog.delete()
@change_settings("Blog Settings", {"allow_guest_to_comment": 0})
def test_guest_cannot_comment(self):
test_blog = make_test_blog()
with set_user("Guest"):
frappe.form_dict.comment = "Good comment with 10 chars"
frappe.form_dict.comment_email = "mail@example.org"
frappe.form_dict.comment_by = "Good Tester"
frappe.form_dict.reference_doctype = "Blog Post"
frappe.form_dict.reference_name = test_blog.name
frappe.form_dict.route = test_blog.route
frappe.local.request_ip = "127.0.0.1"
self.assertEqual(add_comment(), None)
def test_user_not_logged_in(self):
some_system_user = frappe.db.get_value("User", {})
test_blog = make_test_blog()
with set_user("Guest"):
frappe.form_dict.comment = "Good comment with 10 chars"
frappe.form_dict.comment_email = some_system_user
frappe.form_dict.comment_by = "Good Tester"
frappe.form_dict.reference_doctype = "Blog Post"
frappe.form_dict.reference_name = test_blog.name
frappe.form_dict.route = test_blog.route
frappe.local.request_ip = "127.0.0.1"
self.assertRaises(frappe.ValidationError, add_comment)

View file

@ -66,11 +66,16 @@ class CommunicationEmailMixin:
cc = self.cc_list()
# Need to inform parent document owner incase communication is created through inbound mail
if include_sender:
cc.append(self.sender_mailid)
sender = self.sender_mailid
# if user has selected send_me_a_copy, use their email as sender
if frappe.session.user not in frappe.STANDARD_USERS:
sender = frappe.db.get_value("User", frappe.session.user, "email")
cc.append(sender)
if is_inbound_mail_communcation:
if (doc_owner := self.get_owner()) and (doc_owner not in frappe.STANDARD_USERS):
# inform parent document owner incase communication is created through inbound mail
if doc_owner := self.get_owner():
cc.append(doc_owner)
cc = set(cc) - {self.sender_mailid}
cc.update(self.get_assignees())
@ -82,7 +87,7 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation:
cc = cc - set(self.cc_list() + self.to_list())
self._final_cc = [m for m in cc if m not in frappe.STANDARD_USERS]
self._final_cc = [m for m in cc if m and m not in frappe.STANDARD_USERS]
return self._final_cc
def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False):

View file

@ -308,6 +308,7 @@ class TestCommunicationEmailMixin(FrappeTestCase):
"recipients": recipients,
"cc": cc,
"bcc": bcc,
"sender": "sender@test.com",
}
).insert(ignore_permissions=True)
@ -327,14 +328,26 @@ class TestCommunicationEmailMixin(FrappeTestCase):
comm.delete()
def test_cc(self):
to_list = ["to@test.com"]
cc_list = ["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"]
user = self.new_user(email="cc+1@test.com", thread_notify=0)
comm = self.new_communication(recipients=to_list, cc=cc_list)
res = comm.get_mail_cc_with_displayname()
self.assertCountEqual(res, ["cc <cc+2@test.com>"])
user.delete()
comm.delete()
def test(assertion, cc_list=None, set_user_as=None, include_sender=False, thread_notify=False):
if set_user_as:
frappe.set_user(set_user_as)
user = self.new_user(email="cc+1@test.com", thread_notify=thread_notify)
comm = self.new_communication(recipients=["to@test.com"], cc=cc_list)
res = comm.get_mail_cc_with_displayname(include_sender=include_sender)
frappe.set_user("Administrator")
user.delete()
comm.delete()
self.assertEqual(res, assertion)
# test filter_thread_notification_disbled_users and filter_mail_recipients
test(["cc <cc+2@test.com>"], cc_list=["cc+1@test.com", "cc <cc+2@test.com>", "to@test.com"])
# test include_sender
test(["sender@test.com"], include_sender=True, thread_notify=True)
test(["cc+1@test.com"], include_sender=True, thread_notify=True, set_user_as="cc+1@test.com")
def test_bcc(self):
bcc_list = [

View file

@ -205,9 +205,11 @@ class Exporter:
for df in self.fields:
is_parent = not df.is_child_table_field
if is_parent:
label = _(df.label)
label = _(df.label or df.fieldname)
else:
label = f"{_(df.label)} ({_(df.child_table_df.label)})"
label = (
f"{_(df.label or df.fieldname)} ({_(df.child_table_df.label or df.child_table_df.fieldname)})"
)
if label in header:
# this label is already in the header,

View file

@ -345,6 +345,7 @@ class DocType(Document):
"name",
"parent",
"creation",
"owner",
"modified",
"modified_by",
"parentfield",

View file

@ -17,6 +17,7 @@ from frappe.core.api.file import (
move_file,
unzip_file,
)
from frappe.core.doctype.file.utils import get_extension
from frappe.exceptions import ValidationError
from frappe.tests.utils import FrappeTestCase
from frappe.utils import get_files_path
@ -461,7 +462,7 @@ class TestFile(FrappeTestCase):
).insert(ignore_permissions=True)
test_file.make_thumbnail()
self.assertTrue(test_file.thumbnail_url.endswith("_small.jpeg"))
self.assertTrue(test_file.thumbnail_url.endswith("_small.jpg"))
# test local image
test_file.db_set("thumbnail_url", None)
@ -739,3 +740,10 @@ class TestFileOptimization(FrappeTestCase):
size_after_rollback = os.stat(image_path).st_size
self.assertEqual(size_before_optimization, size_after_rollback)
def test_image_header_guessing(self):
file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg")
with open(file_path, "rb") as f:
file_content = f.read()
self.assertEqual(get_extension("", None, file_content), "jpg")

View file

@ -1,5 +1,4 @@
import hashlib
import imghdr
import mimetypes
import os
import re
@ -7,6 +6,7 @@ from io import BytesIO
from typing import TYPE_CHECKING, Optional
from urllib.parse import unquote
import filetype
import requests
import requests.exceptions
from PIL import Image
@ -76,9 +76,11 @@ def get_extension(
mimetype = mimetypes.guess_type(filename + "." + extn)[0]
if mimetype is None or not mimetype.startswith("image/") and content:
# detect file extension by reading image header properties
extn = imghdr.what(filename + "." + (extn or ""), h=content)
if mimetype is None and extn is None and content:
# detect file extension by using filetype matchers
_type_info = filetype.match(content)
if _type_info:
extn = _type_info.extension
return extn

View file

@ -51,7 +51,7 @@
"icon": "fa fa-globe",
"in_create": 1,
"links": [],
"modified": "2022-08-14 18:54:03.490836",
"modified": "2023-04-13 13:48:38.127995",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",
@ -66,13 +66,8 @@
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
"role": "All",
"read": 1
}
],
"search_fields": "language_name",

View file

@ -148,11 +148,13 @@
{
"collapsible": 1,
"collapsible_depends_on": "filters",
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "filters",
"fieldtype": "Table",
"label": "Filters",
@ -161,11 +163,13 @@
{
"collapsible": 1,
"collapsible_depends_on": "columns",
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "columns_section",
"fieldtype": "Section Break",
"label": "Columns"
},
{
"depends_on": "eval:doc.report_type != \"Custom Report\"",
"fieldname": "columns",
"fieldtype": "Table",
"label": "Columns",
@ -182,7 +186,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-20 14:56:36.578412",
"modified": "2023-04-07 18:18:11.782178",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",

View file

@ -169,7 +169,7 @@ class Report(Document):
return columns, result
def run_query_report(self, filters, user, ignore_prepared_report=False):
def run_query_report(self, filters=None, user=None, ignore_prepared_report=False):
columns, result = [], []
data = frappe.desk.query_report.run(
self.name, filters=filters, user=user, ignore_prepared_report=ignore_prepared_report

View file

@ -118,11 +118,10 @@ class TestReport(FrappeTestCase):
}
]
),
json.dumps({"user": "Administrator", "doctype": "User"}),
)
custom_report = frappe.get_doc("Report", custom_report_name)
columns, result = custom_report.run_query_report(
filters={"user": "Administrator", "doctype": "User"}, user=frappe.session.user
)
columns, result = custom_report.run_query_report(user=frappe.session.user)
self.assertListEqual(["email"], [column.get("fieldname") for column in columns])
admin_dict = frappe.core.utils.find(result, lambda d: d["name"] == "Administrator")

View file

@ -37,20 +37,16 @@ class TestTranslation(FrappeTestCase):
frappe.local.lang = "es"
clear_translation_cache()
self.assertTrue(_(data[0][0]), data[0][1])
clear_translation_cache()
self.assertTrue(_(data[1][0]), data[1][1])
frappe.local.lang = "es-MX"
# different translation for es-MX
clear_translation_cache()
self.assertTrue(_(data[2][0]), data[2][1])
# from spanish (general)
clear_translation_cache()
self.assertTrue(_(data[1][0]), data[1][1])
def test_multi_language_translations(self):
@ -112,7 +108,3 @@ def create_translation(key, val):
translation.translated_text = val[1]
translation.save()
return translation
def clear_translation_cache():
frappe.cache().delete_key("translations_from_apps", shared=True)

View file

@ -25,9 +25,12 @@ def get_user_default(key, user=None):
if d and isinstance(d, (list, tuple)) and len(d) == 1:
# Use User Permission value when only when it has a single value
d = d[0]
else:
d = user_defaults.get(frappe.scrub(key), None)
user_permission_default = get_user_permission_default(key, user_defaults)
if not d:
# If no default value is found, use the User Permission value
d = user_permission_default
value = isinstance(d, (list, tuple)) and d[0] or d
if not_in_user_permission(key, value, user):
@ -36,6 +39,24 @@ def get_user_default(key, user=None):
return value
def get_user_permission_default(key, defaults):
permissions = get_user_permissions()
user_default = ""
if permissions.get(key):
# global default in user permission
for item in permissions.get(key):
doc = item.get("doc")
if defaults.get(key) == doc:
user_default = doc
for item in permissions.get(key):
if item.get("is_default"):
user_default = item.get("doc")
break
return user_default
def get_user_default_as_list(key, user=None):
user_defaults = get_defaults(user or frappe.session.user)
d = user_defaults.get(key, None)

View file

@ -198,7 +198,7 @@
],
"in_create": 1,
"links": [],
"modified": "2023-02-15 01:16:56.035205",
"modified": "2023-04-11 14:34:24.829366",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
@ -220,5 +220,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"states": [],
"track_changes": 1
}

View file

@ -1,6 +1,7 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# License: MIT. See LICENSE
from collections import defaultdict
from json import loads
import frappe
@ -49,12 +50,22 @@ class Workspace(Document):
delete_folder(self.module, "Workspace", self.title)
@staticmethod
def get_module_page_map():
pages = frappe.get_all(
"Workspace", fields=["name", "module"], filters={"for_user": ""}, as_list=1
def get_module_wise_workspaces():
workspaces = frappe.get_all(
"Workspace",
fields=["name", "module"],
filters={"for_user": "", "public": 1},
order_by="creation",
)
return {page[1]: page[0] for page in pages if page[1]}
module_workspaces = defaultdict(list)
for workspace in workspaces:
if not workspace.module:
continue
module_workspaces[workspace.module].append(workspace.name)
return module_workspaces
def get_link_groups(self):
cards = []

View file

@ -15,12 +15,13 @@ from frappe.model.utils import render_include
from frappe.modules import get_module_path, scrub
from frappe.monitor import add_data_to_monitor
from frappe.permissions import get_role_permissions
from frappe.utils import cint, cstr, flt, format_duration, get_html_format
from frappe.utils import cint, cstr, flt, format_duration, get_html_format, sbool
def get_report_doc(report_name):
doc = frappe.get_doc("Report", report_name)
doc.custom_columns = []
doc.custom_filters = []
if doc.report_type == "Custom Report":
custom_report_doc = doc
@ -30,7 +31,8 @@ def get_report_doc(report_name):
if custom_report_doc.json:
data = json.loads(custom_report_doc.json)
if data:
doc.custom_columns = data["columns"]
doc.custom_columns = data.get("columns")
doc.custom_filters = data.get("filters")
doc.is_custom_report = True
if not doc.is_permitted():
@ -182,6 +184,7 @@ def run(
custom_columns=None,
is_tree=False,
parent_field=None,
are_default_filters=True,
):
report = get_report_doc(report_name)
if not user:
@ -194,6 +197,9 @@ def run(
result = None
if sbool(are_default_filters) and report.custom_filters:
filters = report.custom_filters
if report.prepared_report and not ignore_prepared_report and not custom_columns:
if filters:
if isinstance(filters, str):
@ -209,6 +215,9 @@ def run(
result["add_total_row"] = report.add_total_row and not result.get("skip_total_row", False)
if sbool(are_default_filters) and report.custom_filters:
result["custom_filters"] = report.custom_filters
return result
@ -443,7 +452,7 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
def get_data_for_custom_field(doctype, field):
if not frappe.has_permission(doctype, "read"):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
frappe.throw(_("Not Permitted to read {0}").format(doctype), frappe.PermissionError)
value_map = frappe._dict(frappe.get_all(doctype, fields=["name", field], as_list=1))
@ -463,7 +472,7 @@ def get_data_for_custom_report(columns):
@frappe.whitelist()
def save_report(reference_report, report_name, columns):
def save_report(reference_report, report_name, columns, filters):
report_doc = get_report_doc(reference_report)
docname = frappe.db.exists(
@ -479,6 +488,7 @@ def save_report(reference_report, report_name, columns):
report = frappe.get_doc("Report", docname)
existing_jd = json.loads(report.json)
existing_jd["columns"] = json.loads(columns)
existing_jd["filters"] = json.loads(filters)
report.update({"json": json.dumps(existing_jd, separators=(",", ":"))})
report.save()
frappe.msgprint(_("Report updated successfully"))
@ -489,7 +499,7 @@ def save_report(reference_report, report_name, columns):
{
"doctype": "Report",
"report_name": report_name,
"json": f'{{"columns":{columns}}}',
"json": f'{{"columns":{columns},"filters":{filters}}}',
"ref_doctype": report_doc.ref_doctype,
"is_standard": "No",
"report_type": "Custom Report",

View file

@ -678,7 +678,7 @@ def get_filters_cond(
for f in filters:
if isinstance(f[1], str) and f[1][0] == "!":
flt.append([doctype, f[0], "!=", f[1][1:]])
elif isinstance(f[1], (list, tuple)) and f[1][0] in (
elif isinstance(f[1], (list, tuple)) and f[1][0].lower() in (
">",
"<",
">=",

View file

@ -151,7 +151,7 @@ class EmailServer:
except _socket.error:
# log performs rollback and logs error in Error Log
self.log_error("POP: Unable to connect")
frappe.log_error("POP: Unable to connect")
# Invalid mail server -- due to refusing connection
frappe.msgprint(_("Invalid Mail Server. Please rectify and try again."))
@ -332,7 +332,7 @@ class EmailServer:
else:
# log performs rollback and logs error in Error Log
self.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
frappe.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
self.errors = True
frappe.db.rollback()

View file

@ -184,11 +184,13 @@ scheduler_events = {
"frappe.oauth.delete_oauth2_data",
"frappe.website.doctype.web_page.web_page.check_publish_status",
"frappe.twofactor.delete_all_barcodes_for_users",
]
],
"0/10 * * * *": [
"frappe.email.doctype.email_account.email_account.pull",
],
},
"all": [
"frappe.email.queue.flush",
"frappe.email.doctype.email_account.email_account.pull",
"frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.utils.global_search.sync_global_search",
"frappe.monitor.flush",

View file

@ -67,6 +67,7 @@
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Active\nRevoked",
@ -74,10 +75,11 @@
}
],
"links": [],
"modified": "2021-04-26 06:40:34.922441",
"modified": "2023-04-07 07:08:00.249740",
"modified_by": "Administrator",
"module": "Integrations",
"name": "OAuth Bearer Token",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@ -92,5 +94,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View file

@ -76,7 +76,7 @@
"fieldtype": "Column Break"
},
{
"description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n<br>e.g. http://hostname//api/method/frappe.www.login.login_via_facebook",
"description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n<br>e.g. http://hostname/api/method/frappe.www.login.login_via_facebook",
"fieldname": "redirect_uris",
"fieldtype": "Text",
"label": "Redirect URIs"
@ -117,7 +117,7 @@
}
],
"links": [],
"modified": "2022-08-03 12:21:52.062755",
"modified": "2023-04-07 07:06:35.765981",
"modified_by": "Administrator",
"module": "Integrations",
"name": "OAuth Client",

View file

@ -79,7 +79,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-02-24 14:59:24.743552",
"modified": "2023-04-12 11:50:01.702862",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook Request Log",
@ -101,6 +101,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"states": []
}

View file

@ -304,7 +304,9 @@ class BaseDocument:
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
) -> dict:
d = _dict()
permitted_fields = get_permitted_fields(doctype=self.doctype)
permitted_fields = get_permitted_fields(
doctype=self.doctype, parenttype=getattr(self, "parenttype", None)
)
for fieldname in self.meta.get_valid_columns():
field_value = getattr(self, fieldname, None)

View file

@ -114,10 +114,10 @@ def sync_customizations(app=None):
with open(os.path.join(folder, fname)) as f:
data = json.loads(f.read())
if data.get("sync_on_migrate"):
sync_customizations_for_doctype(data, folder)
sync_customizations_for_doctype(data, folder, fname)
def sync_customizations_for_doctype(data: dict, folder: str):
def sync_customizations_for_doctype(data: dict, folder: str, filename: str = ""):
"""Sync doctype customzations for a particular data set"""
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
@ -158,6 +158,11 @@ def sync_customizations_for_doctype(data: dict, folder: str):
if doc_type == doctype or not os.path.exists(os.path.join(folder, scrub(doc_type) + ".json")):
sync_single_doctype(doc_type)
if not frappe.db.exists("DocType", doctype):
print(_("DocType {0} does not exist.").format(doctype))
print(_("Skipping fixture syncing for doctyoe {0} from file {1} ").format(doctype, filename))
return
if data["custom_fields"]:
sync("custom_fields", "Custom Field", "dt")
update_schema = True
@ -165,10 +170,10 @@ def sync_customizations_for_doctype(data: dict, folder: str):
if data["property_setters"]:
sync("property_setters", "Property Setter", "doc_type")
print(f"Updating customizations for {doctype}")
if data.get("custom_perms"):
sync("custom_perms", "Custom DocPerm", "parent")
print(f"Updating customizations for {doctype}")
validate_fields_for_doctype(doctype)
if update_schema and not frappe.db.get_value("DocType", doctype, "issingle"):

View file

@ -89,8 +89,11 @@ class Monitor:
self.data.duration = int(timediff.total_seconds() * 1000000)
if self.data.transaction_type == "request":
self.data.request.status_code = response.status_code
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
if response:
self.data.request.status_code = response.status_code
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
else:
self.data.request.status_code = 500
if hasattr(frappe.local, "rate_limiter"):
limiter = frappe.local.rate_limiter

View file

@ -3,7 +3,7 @@ import datetime
import hashlib
import re
from http import cookies
from urllib.parse import unquote, urlparse
from urllib.parse import unquote, urljoin, urlparse
import jwt
import pytz
@ -575,7 +575,7 @@ def get_userinfo(user):
if frappe.utils.validate_url(user.user_image, valid_schemes=valid_url_schemes):
picture = user.user_image
else:
picture = frappe_server_url + "/" + user.user_image
picture = urljoin(frappe_server_url, user.user_image)
userinfo = frappe._dict(
{

View file

@ -168,7 +168,6 @@ execute:frappe.db.set_default('desktop:home_page', 'space')
execute:frappe.delete_doc_if_exists('Page', 'workspace')
execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1)
frappe.core.doctype.page.patches.drop_unused_pages
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
frappe.patches.v13_0.remove_chat
frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021
frappe.patches.v13_0.delete_package_publish_tool
@ -199,6 +198,7 @@ frappe.patches.v15_0.remove_event_streaming
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
[post_model_sync]
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
frappe.core.doctype.role.patches.v13_set_default_desk_properties
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.copy_mail_data #08.03.21
@ -223,3 +223,4 @@ frappe.patches.v14_0.disable_email_accounts_with_oauth
execute:frappe.delete_doc("Page", "translation-tool", force=1)
frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
frappe.patches.v14_0.remove_manage_subscriptions_from_navbar
frappe.patches.v15_0.remove_background_jobs_from_dropdown

View file

@ -0,0 +1,9 @@
import frappe
def execute():
item = frappe.db.exists("Navbar Item", {"item_label": "Background Jobs"})
if not item:
return
frappe.delete_doc("Navbar Item", item)

View file

@ -3,10 +3,14 @@
frappe.defaults = {
get_user_default: function (key) {
var defaults = frappe.boot.user.defaults;
var d = defaults[key];
if (!d && frappe.defaults.is_a_user_permission_key(key))
let defaults = frappe.boot.user.defaults;
let d = defaults[key];
if (!d && frappe.defaults.is_a_user_permission_key(key)) {
d = defaults[frappe.model.scrub(key)];
// Check for default user permission values
user_default = this.get_user_permission_default(key, defaults);
if (user_default) d = user_default;
}
if ($.isArray(d)) d = d[0];
if (!frappe.defaults.in_user_permission(key, d)) {
@ -15,6 +19,27 @@ frappe.defaults = {
return d;
},
get_user_permission_default: function (key, defaults) {
let permissions = this.get_user_permissions();
let user_default = null;
if (permissions[key]) {
permissions[key].forEach((item) => {
if (defaults[key] == item.doc) {
user_default = item.doc;
}
});
permissions[key].forEach((item) => {
if (item.is_default) {
user_default = item.doc;
}
});
}
return user_default;
},
get_user_defaults: function (key) {
var defaults = frappe.boot.user.defaults;
var d = defaults[key];

View file

@ -1188,6 +1188,13 @@ export default class Grid {
// update the parent too (for new rows)
this.docfields.find((d) => d.fieldname === fieldname)[property] = value;
if (this.user_defined_columns && this.user_defined_columns.length > 0) {
let field = this.user_defined_columns.find((d) => d.fieldname === fieldname);
if (field && Object.keys(field).includes(property)) {
field[property] = value;
}
}
this.debounced_refresh();
}
}

View file

@ -1296,7 +1296,7 @@ export default class GridRow {
.find(".grid-delete-row")
.toggle(!(this.grid.df && this.grid.df.cannot_delete_rows));
frappe.dom.freeze("", "dark");
frappe.dom.freeze("", "dark grid-form");
if (cur_frm) cur_frm.cur_grid = this;
this.wrapper.addClass("grid-row-open");
if (

View file

@ -53,8 +53,8 @@ frappe.views.BaseList = class BaseList {
this.fields = [];
this.filters = [];
this.sort_by = "modified";
this.sort_order = "desc";
this.sort_by = this.meta.sort_field || "modified";
this.sort_order = this.meta.sort_order || "desc";
// Setup buttons
this.primary_action = null;

View file

@ -80,8 +80,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
this.view = "List";
// initialize with saved order by
this.sort_by = this.view_user_settings.sort_by || "modified";
this.sort_order = this.view_user_settings.sort_order || "desc";
this.sort_by = this.view_user_settings.sort_by || this.sort_by || "modified";
this.sort_order = this.view_user_settings.sort_order || this.sort_order || "desc";
// build menu items
this.menu_items = this.menu_items.concat(this.get_menu_items());

View file

@ -34,9 +34,7 @@ $.extend(frappe.perm, {
doctype_perm: {},
has_perm: (doctype, permlevel, ptype, doc) => {
if (!permlevel) permlevel = 0;
has_perm: (doctype, permlevel = 0, ptype = "read", doc) => {
const perms = frappe.perm.get_perm(doctype, doc);
return !!perms?.[permlevel]?.[ptype];
},

View file

@ -13,6 +13,9 @@ frappe.ui.toolbar.Toolbar = class {
})
);
$(".dropdown-toggle").dropdown();
$("#toolbar-user a[href]").click(function () {
$(this).closest(".dropdown-menu").prev().dropdown("toggle");
});
this.setup_awesomebar();
this.setup_notifications();
@ -133,6 +136,12 @@ frappe.ui.toolbar.Toolbar = class {
frappe.utils.generate_tracking_url,
__("Generate Tracking URL")
);
if (frappe.perm.has_perm("RQ Job")) {
frappe.search.utils.make_function_searchable(function () {
frappe.set_route("List", "RQ Job");
}, __("Background Jobs"));
}
}
}

View file

@ -82,25 +82,33 @@ frappe.breadcrumbs = {
this.$breadcrumbs.append(html);
},
get last_route() {
return frappe.route_history.slice(-2)[0];
},
set_workspace_breadcrumb(breadcrumbs) {
// get preferred module for breadcrumbs, based on sent via module
// get preferred module for breadcrumbs, based on history and module
if (!breadcrumbs.workspace) {
this.set_workspace(breadcrumbs);
}
if (breadcrumbs.workspace) {
if (
!breadcrumbs.module_info.blocked &&
frappe.visible_modules.includes(breadcrumbs.module_info.module)
) {
$(
`<li><a href="/app/${frappe.router.slug(breadcrumbs.workspace)}">${__(
breadcrumbs.workspace
)}</a></li>`
).appendTo(this.$breadcrumbs);
}
if (!breadcrumbs.workspace) {
return;
}
if (
breadcrumbs.module_info &&
(breadcrumbs.module_info.blocked ||
!frappe.visible_modules.includes(breadcrumbs.module_info.module))
) {
return;
}
$(
`<li><a href="/app/${frappe.router.slug(breadcrumbs.workspace)}">${__(
breadcrumbs.workspace
)}</a></li>`
).appendTo(this.$breadcrumbs);
},
set_workspace(breadcrumbs) {
@ -117,6 +125,19 @@ frappe.breadcrumbs = {
breadcrumbs.module = this.preferred[breadcrumbs.doctype];
}
// guess from last route
if (this.last_route?.[0] == "Workspaces") {
let last_workspace = this.last_route[1];
if (
breadcrumbs.module &&
frappe.boot.module_wise_workspaces[breadcrumbs.module]?.includes(last_workspace)
) {
breadcrumbs.workspace = last_workspace;
return;
}
}
if (breadcrumbs.module) {
if (this.module_map[breadcrumbs.module]) {
breadcrumbs.module = this.module_map[breadcrumbs.module];
@ -125,8 +146,11 @@ frappe.breadcrumbs = {
breadcrumbs.module_info = frappe.get_module(breadcrumbs.module);
// set workspace
if (breadcrumbs.module_info && frappe.boot.module_page_map[breadcrumbs.module]) {
breadcrumbs.workspace = frappe.boot.module_page_map[breadcrumbs.module];
if (
breadcrumbs.module_info &&
frappe.boot.module_wise_workspaces[breadcrumbs.module]
) {
breadcrumbs.workspace = frappe.boot.module_wise_workspaces[breadcrumbs.module][0];
}
}
},

View file

@ -2,6 +2,9 @@
// MIT License. See license.txt
import DataTable from "frappe-datatable";
// Expose DataTable globally to allow customizations.
window.DataTable = DataTable;
frappe.provide("frappe.widget.utils");
frappe.provide("frappe.views");
frappe.provide("frappe.query_reports");
@ -539,7 +542,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (this.prepared_report) {
this.reset_report_view();
} else if (!this._no_refresh) {
this.refresh();
this.refresh(true);
}
}
};
@ -595,10 +598,25 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.page.clear_fields();
}
refresh() {
refresh(have_filters_changed) {
this.toggle_message(true);
this.toggle_report(false);
let filters = this.get_filter_values(true);
// for custom reports,
// are_default_filters is true if the filters haven't been modified and for all filters,
// the filter value is the default value or there's no default value for the filter and the current value is empty.
// are_default_filters is false otherwise.
let are_default_filters = this.filters
.map((filter) => {
return (
!have_filters_changed &&
(filter.default === filter.value || (!filter.default && !filter.value))
);
})
.every((res) => res === true);
this.show_loading_screen();
// only one refresh at a time
@ -621,6 +639,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
filters: filters,
is_tree: this.report_settings.tree,
parent_field: this.report_settings.parent_field,
are_default_filters: are_default_filters,
},
callback: resolve,
always: () => this.page.btn_secondary.prop("disabled", false),
@ -633,6 +652,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.execution_time = data.execution_time || 0.1;
if (data.custom_filters) {
this.set_filters(data.custom_filters);
this.previous_filters = data.custom_filters;
}
if (data.prepared_report) {
this.prepared_report = true;
this.prepared_report_document = data.doc;
@ -933,7 +957,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (this.report_settings.get_datatable_options) {
datatable_options = this.report_settings.get_datatable_options(datatable_options);
}
this.datatable = new DataTable(this.$report[0], datatable_options);
this.datatable = new window.DataTable(this.$report[0], datatable_options);
}
if (typeof this.report_settings.initial_depth == "number") {
@ -1712,6 +1736,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
reference_report: this.report_name,
report_name: values.report_name,
columns: this.get_visible_columns(),
filters: this.get_filter_values(),
},
callback: function (r) {
this.show_save = false;

View file

@ -33,7 +33,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.filters = this.report_doc.json.filters;
this.order_by = this.report_doc.json.order_by;
this.add_totals_row = this.report_doc.json.add_totals_row;
this.page_title = this.report_name;
this.page_title = __(this.report_name);
this.page_length = this.report_doc.json.page_length || 20;
this.order_by = this.report_doc.json.order_by || "modified desc";
this.chart_args = this.report_doc.json.chart_args;

View file

@ -243,6 +243,8 @@ $input-height: 28px !default;
--highlight-color: var(--gray-50);
--yellow-highlight-color: var(--yellow-50);
--btn-group-border-color: var(--gray-300);
--field-placeholder-color: var(--gray-50);
--highlight-shadow: 1px 1px 10px var(--blue-50), 0px 0px 4px var(--blue-600);

View file

@ -362,6 +362,10 @@
}
}
#freeze.grid-form {
z-index: 1020;
}
.recorder-form-in-grid {
z-index: 0;
@include base-grid();

View file

@ -100,6 +100,8 @@
--highlight-color: var(--gray-700);
--yellow-highlight-color: var(--yellow-700);
--btn-group-border-color: var(--gray-800);
--field-placeholder-color: var(--gray-700);
--highlight-shadow: 1px 1px 10px var(--blue-900), 0px 0px 4px var(--blue-500);

View file

@ -234,6 +234,21 @@ h2 {
font-size: var(--text-md);
}
.btn-group {
.btn {
box-shadow: none;
outline: 1px solid var(--btn-group-border-color);
&:not(:first-child) {
margin-left: 1px;
}
&:focus {
outline: 2px solid var(--dark-border-color);
}
}
}
.btn-xs {
@extend .btn-sm;
line-height: 1.2;
@ -418,7 +433,7 @@ kbd {
// freeze backdrop text
#freeze {
z-index: 1020;
z-index: 1055;
bottom: 0;
opacity: 0;
background-color: var(--bg-color);

View file

@ -189,29 +189,12 @@ $level-margin-right: 8px;
.list-paging-area, .footnote-area {
border-top: 1px solid var(--border-color);
.btn-group {
box-shadow: var(--drop-shadow);
border-radius: var(--border-radius-md);
&> .btn:nth-child(2) {
border-left: none;
border-right: none;
}
.btn-paging {
box-shadow: none;
margin-left: 0px !important;
border: 1px solid var(--dark-border-color);
&.btn-info {
background-color: var(--gray-600);
border-color: var(--gray-600);
color: var(--white);
font-weight: var(--text-bold);
}
}
.btn-group .btn-paging.btn-info {
background-color: var(--gray-600);
border-color: var(--gray-600);
color: var(--white);
font-weight: var(--text-bold);
}
}
.frappe-card {

View file

@ -242,7 +242,7 @@ body[data-route^="Module"] .main-menu {
right: 0;
opacity: 0.3;
background: #000;
z-index: 1041;
z-index: 9998;
height: 100%;
width: 100%;
}

View file

@ -18,10 +18,17 @@ EMAIL_PATTERN = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
@frappe.whitelist(allow_guest=True)
@rate_limit(key="reference_name", limit=get_comment_limit, seconds=60 * 60)
def add_comment(comment, comment_email, comment_by, reference_doctype, reference_name, route):
doc = frappe.get_doc(reference_doctype, reference_name)
if frappe.session.user == "Guest":
if reference_doctype not in ("Blog Post", "Web Page"):
return
if frappe.session.user == "Guest" and doc.doctype not in ["Blog Post", "Web Page"]:
return
if reference_doctype == "Blog Post" and not frappe.db.get_single_value(
"Blog Settings", "allow_guest_to_comment"
):
return
if frappe.db.exists("User", comment_email):
frappe.throw(_("Please login to post a comment."))
if not comment.strip():
frappe.msgprint(_("The comment cannot be empty"))
@ -31,6 +38,7 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
frappe.msgprint(_("Comments cannot have links or email addresses"))
return False
doc = frappe.get_doc(reference_doctype, reference_name)
comment = doc.add_comment(
text=clean_html(comment), comment_email=comment_email, comment_by=comment_by
)
@ -50,9 +58,7 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference
url, _("View Comment")
)
if doc.doctype == "Blog Post" and not doc.enable_email_notification:
pass
else:
if doc.doctype != "Blog Post" or doc.enable_email_notification:
# notify creator
creator_email = frappe.db.get_value("User", doc.owner, "email") or doc.owner
subject = _("New Comment on {0}: {1}").format(doc.doctype, doc.get_title())

View file

@ -107,7 +107,7 @@ class TestRedisCache(FrappeAPITestCase):
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 1)
time.sleep(CACHE_TTL)
time.sleep(CACHE_TTL * 1.5)
self.assertEqual(calculate_area(10), 314)
self.assertEqual(function_call_count, 2)

View file

@ -1,7 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.defaults import *
from frappe.query_builder.utils import db_type_is
from frappe.tests.test_query_builder import run_only_if
from frappe.tests.utils import FrappeTestCase
@ -71,3 +74,39 @@ class TestDefaults(FrappeTestCase):
frappe.delete_doc("User Permission", perm_doc.name)
frappe.set_user(old_user)
@run_only_if(db_type_is.MARIADB)
def test_user_permission_defaults(self):
# Create user permission
create_user("user_default_test@example.com", "Blogger")
frappe.set_user("user_default_test@example.com")
set_global_default("Country", "")
clear_user_default("Country")
perm_doc = frappe.get_doc(
dict(
doctype="User Permission",
user=frappe.session.user,
allow="Country",
for_value="India",
)
).insert(ignore_permissions=True)
frappe.db.set_value("User Permission", perm_doc.name, "is_default", 1)
set_global_default("Country", "United States")
self.assertEqual(get_user_default("Country"), "India")
frappe.db.set_value("User Permission", perm_doc.name, "is_default", 0)
clear_user_default("Country")
self.assertEqual(get_user_default("Country"), None)
perm_doc = frappe.get_doc(
dict(
doctype="User Permission",
user=frappe.session.user,
allow="Country",
for_value="United States",
)
).insert(ignore_permissions=True)
self.assertEqual(get_user_default("Country"), "United States")

View file

@ -33,6 +33,20 @@ class TestMonitor(FrappeTestCase):
self.assertEqual(log.transaction_type, "request")
self.assertEqual(log.request["method"], "GET")
def test_no_response(self):
set_request(method="GET", path="/api/method/frappe.ping")
frappe.monitor.start()
frappe.monitor.stop(response=None)
logs = frappe.cache().lrange(MONITOR_REDIS_KEY, 0, -1)
self.assertEqual(len(logs), 1)
log = frappe.parse_json(logs[0].decode())
self.assertEqual(log.request["status_code"], 500)
self.assertEqual(log.transaction_type, "request")
self.assertEqual(log.request["method"], "GET")
def test_job(self):
frappe.utils.background_jobs.execute_job(
frappe.local.site, "frappe.ping", None, None, {}, is_async=False

View file

@ -8,7 +8,6 @@ from unittest.mock import patch
import frappe
import frappe.translate
from frappe import _
from frappe.core.doctype.translation.test_translation import clear_translation_cache
from frappe.tests.utils import FrappeTestCase
from frappe.translate import (
extract_javascript,
@ -39,15 +38,11 @@ class TestTranslate(FrappeTestCase):
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Guest")
clear_translation_cache()
def tearDown(self):
frappe.form_dict.pop("_lang", None)
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Administrator")
clear_translation_cache()
def test_extract_message_from_file(self):
data = frappe.translate.get_messages_from_file(translation_string_file)
exp_filename = "apps/frappe/frappe/tests/translation_test_file.txt"

View file

@ -611,12 +611,12 @@ class TestDateUtils(FrappeTestCase):
now = get_datetime()
test_cases = {
now: _("just now"),
now: _("1 second ago"),
add_to_date(now, minutes=-1): _("1 minute ago"),
add_to_date(now, minutes=-3): _("3 minutes ago"),
add_to_date(now, hours=-1): _("1 hour ago"),
add_to_date(now, hours=-2): _("2 hours ago"),
add_to_date(now, days=-1): _("Yesterday"),
add_to_date(now, days=-1): _("1 day ago"),
add_to_date(now, days=-5): _("5 days ago"),
add_to_date(now, days=-8): _("1 week ago"),
add_to_date(now, days=-14): _("2 weeks ago"),

View file

@ -236,6 +236,7 @@ class TestWebsite(FrappeTestCase):
def test_printview_page(self):
frappe.db.value_cache[("DocType", "Language", "name")] = (("Language",),)
frappe.set_user("Administrator")
content = get_response_content("/Language/ru")
self.assertIn('<div class="print-format">', content)
self.assertIn("<div>Language</div>", content)

View file

@ -56,7 +56,6 @@ CSV_STRIP_WHITESPACE_PATTERN = re.compile(r"{\s?([0-9]+)\s?}")
# Cache keys
MERGED_TRANSLATION_KEY = "merged_translations"
APP_TRANSLATION_KEY = "translations_from_apps"
USER_TRANSLATION_KEY = "lang_user_translations"
@ -171,7 +170,7 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]:
fortype = fortype.lower()
cache = frappe.cache()
asset_key = fortype + ":" + (name or "-")
translation_assets = cache.hget("translation_assets", frappe.local.lang, shared=True) or {}
translation_assets = cache.hget("translation_assets", frappe.local.lang) or {}
if asset_key not in translation_assets:
messages = []
@ -211,7 +210,7 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]:
# remove untranslated
message_dict = {k: v for k, v in message_dict.items() if k != v}
translation_assets[asset_key] = message_dict
cache.hset("translation_assets", frappe.local.lang, translation_assets, shared=True)
cache.hset("translation_assets", frappe.local.lang, translation_assets)
translation_map: dict = translation_assets[asset_key]
@ -308,20 +307,17 @@ def get_translations_from_apps(lang, apps=None):
if lang == "en":
return {}
def _get_from_disk():
translations = {}
for app in apps or frappe.get_all_apps(True):
path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
translations.update(get_translation_dict_from_file(path, lang, app) or {})
if "-" in lang:
parent = lang.split("-", 1)[0]
parent_translations = get_translations_from_apps(parent)
parent_translations.update(translations)
return parent_translations
translations = {}
for app in apps or frappe.get_installed_apps(_ensure_on_bench=True):
path = os.path.join(frappe.get_pymodule_path(app), "translations", lang + ".csv")
translations.update(get_translation_dict_from_file(path, lang, app) or {})
if "-" in lang:
parent = lang.split("-", 1)[0]
parent_translations = get_translations_from_apps(parent)
parent_translations.update(translations)
return parent_translations
return translations
return frappe.cache().hget(APP_TRANSLATION_KEY, lang, shared=True, generator=_get_from_disk)
return translations
def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, str]:
@ -375,8 +371,7 @@ def clear_cache():
# clear translations saved in boot cache
cache.delete_key("bootinfo")
cache.delete_key("translation_assets", shared=True)
cache.delete_key(APP_TRANSLATION_KEY, shared=True)
cache.delete_key("translation_assets")
cache.delete_key(USER_TRANSLATION_KEY)
cache.delete_key(MERGED_TRANSLATION_KEY)
@ -687,7 +682,7 @@ def get_messages_from_include_files(app_name=None):
def get_all_messages_from_js_files(app_name=None):
"""Extracts all translatable strings from app `.js` files"""
messages = []
for app in [app_name] if app_name else frappe.get_installed_apps():
for app in [app_name] if app_name else frappe.get_installed_apps(_ensure_on_bench=True):
if os.path.exists(frappe.get_app_path(app, "public")):
for basepath, folders, files in os.walk(frappe.get_app_path(app, "public")):
if "frappe/public/js/lib" in basepath:

View file

@ -383,7 +383,7 @@ Align Labels to the Right,Etiketten rechts ausrichten,
Align Value,Wert anordnen,
All Images attached to Website Slideshow should be public,"Alle Bilder, die an die Website-Slideshow angehängt werden, sollten öffentlich sein.",
All customizations will be removed. Please confirm.,Alle Anpassungen werden entfernt. Bitte bestätigen.,
"All possible Workflow States and roles of the workflow. Docstatus Options: 0 is""Saved"", 1 is ""Submitted"" and 2 is ""Cancelled""","Alle möglichen Zustände und Rollen des Workflows. Dokumentenstatus-Optionen sind: 0 ist ""Gespeichert"", 1 ist ""Übertragen"" und 2 ist ""Abgebrochen""",
"All possible Workflow States and roles of the workflow. Docstatus Options: 0 is""Saved"", 1 is ""Submitted"" and 2 is ""Cancelled""","Alle möglichen Stati und Rollen des Workflows. Dokumentenstatus-Optionen sind: 0 ist ""Gespeichert"", 1 ist ""Übertragen"" und 2 ist ""Abgebrochen""",
All-uppercase is almost as easy to guess as all-lowercase.,Ausschließlich Großbuchstaben sind fast so einfach zu erraten wie ausschließlich Kleinbuchstaben.,
Allocated To,Zugewiesen zu,
Allow,Zulassen,
@ -851,7 +851,7 @@ Default Value,Standardwert,
DefaultValue,Standardwert,
Define workflows for forms.,Workflows für Formulare definieren,
Defines actions on states and the next step and allowed roles.,"Definiert Maßnahmen bei bestimmten Zuständen, den nächsten Schritt und erlaubte Rollen.",
Defines workflow states and rules for a document.,Definiert Workflow-Zustände und Regeln für ein Dokument.,
Defines workflow states and rules for a document.,Definiert Workflow-Stati und Regeln für ein Dokument.,
Delayed,Verzögert,
Delete Data,Daten löschen,
Delete comment?,Kommentar löschen?,
@ -1080,7 +1080,7 @@ Fetch attached images from document,Holen Sie angehängte Bilder aus dem Dokumen
Field Description,Feldbeschreibung,
Field Maps,Feldkarten,
Field Type,Feldtyp,
"Field that represents the Workflow State of the transaction (if field is not present, a new hidden Custom Field will be created)","Feld, das den Status des Workflows der einzelnen Transaktionen wiedergibt (wenn das Feld nicht vorhanden ist, wird ein neues verstecktes, benutzerdefiniertes Feld erstellt)",
"Field that represents the Workflow State of the transaction (if field is not present, a new hidden Custom Field will be created)","Feldc für den Workflow-Status der einzelnen Transaktionen (wenn das Feld nicht vorhanden ist, wird ein neues verstecktes, benutzerdefiniertes Feld erstellt)",
Field to Track,Zu verfolgendes Feld,
Field type cannot be changed for {0},Feldtyp kann nicht für {0} geändert werden,
Field {0} not found.,Feld {0} nicht gefunden,
@ -2777,11 +2777,11 @@ Workflow Action Master,Stammdaten zu Workflow-Aktionen,
Workflow Action Name,Workflow-Aktionsname,
Workflow Document State,Workflow-Dokumentenstatus,
Workflow Name,Workflow-Name,
Workflow State,Workflow-Zustand,
Workflow State Field,Workflow-Zustandsfeld,
Workflow State,Workflow-Status,
Workflow State Field,Workflow-Status-Feld,
Workflow State not set,Workflow-Status nicht festgelegt,
Workflow Transition,Workflow-Übergang,
Workflow state represents the current state of a document.,Workflow-Zustand stellt den aktuellen Status eines Dokuments dar.,
Workflow state represents the current state of a document.,Der Workflow-Status steht für den aktuellen Zustand eines Dokuments.,
Write,Schreiben,
Wrong fieldname <b>{0}</b> in add_fetch configuration of custom script,Falscher Feldname <b>{0}</b> in der add_fetch-Konfiguration des benutzerdefinierten Skripts,
X Axis Field,X-Achsenfeld,
@ -3070,7 +3070,7 @@ zoom-out,verkleinern,
{0} is an invalid email address in 'Recipients',"{0} ist eine ungültige E-Mail-Adresse in ""Empfänger""",
{0} is not a raw printing format.,{0} ist kein unformatiertes Druckformat.,
{0} is not a valid Email Address,{0} ist keine gültige E-Mail-Adresse,
{0} is not a valid Workflow State. Please update your Workflow and try again.,{0} ist kein gültiger Workflow-Zustand. Bitte aktualisieren Sie Ihren Workflow und versuchen Sie es erneut.,
{0} is not a valid Workflow State. Please update your Workflow and try again.,{0} ist kein gültiger Workflow-Status. Bitte aktualisieren Sie Ihren Workflow und versuchen Sie es erneut.,
{0} is now default print format for {1} doctype,{0} ist jetzt das Standard-Druckformat für den DocType {1},
{0} is saved,{0} ist gespeichert,
{0} items selected,{0} Elemente ausgewählt,
@ -3131,7 +3131,7 @@ Force User to Reset Password,Benutzer zum Zurücksetzen des Kennworts zwingen,
In Days,In Tagen,
Last Password Reset Date,Datum der letzten Kennwortrücksetzung,
The password of your account has expired.,Das Passwort Ihres Kontos ist abgelaufen.,
Workflow State transition not allowed from {0} to {1},Workflow-Statusübergang von {0} nach {1} nicht zulässig,
Workflow State transition not allowed from {0} to {1},Eine Veränderung des Workflow-Status von {0} nach {1} ist nicht zulässig,
{0} must be after {1},{0} muss nach {1} liegen,
{0}: Field '{1}' cannot be set as Unique as it has non-unique values,"{0}: Feld &#39;{1}&#39; kann nicht als eindeutig festgelegt werden, da es nicht eindeutige Werte enthält",
{0}: Field {1} in row {2} cannot be hidden and mandatory without default,{0}: Das Feld {1} in Zeile {2} kann ohne Vorgabe nicht ausgeblendet und obligatorisch sein,
@ -4645,8 +4645,8 @@ Hide Traceback,Traceback ausblenden,
Value from this field will be set as the due date in the ToDo,Der Wert aus diesem Feld wird im Fälligkeitsdatum als Fälligkeitsdatum festgelegt,
New module created {0},Neues Modul erstellt {0},
"Report has no numeric fields, please change the Report Name",Der Bericht enthält keine numerischen Felder. Bitte ändern Sie den Berichtsnamen,
There are documents which have workflow states that do not exist in this Workflow. It is recommended that you add these states to the Workflow and change their states before removing these states.,"Es gibt Dokumente mit Workflow-Status, die in diesem Workflow nicht vorhanden sind. Es wird empfohlen, diese Status zum Workflow hinzuzufügen und ihre Status zu ändern, bevor Sie diese Status entfernen.",
Worflow States Don't Exist,Worflow-Zustände existieren nicht,
There are documents which have workflow states that do not exist in this Workflow. It is recommended that you add these states to the Workflow and change their states before removing these states.,"Es gibt Dokumente mit Workflow-Status, die in diesem Workflow nicht vorhanden sind. Es wird empfohlen, diese Stati zum Workflow hinzuzufügen oder den Status der Dokumente ändern, bevor Sie einen Status entfernen.",
Worflow States Don't Exist,Worflow-Stati existieren nicht,
Save Anyway,Auf jeden Fall speichern,
Energy Points:,Energiepunkte:,
Review Points:,Bewertungspunkte:,

1 A4 A4
383 Align Value Wert anordnen
384 All Images attached to Website Slideshow should be public Alle Bilder, die an die Website-Slideshow angehängt werden, sollten öffentlich sein.
385 All customizations will be removed. Please confirm. Alle Anpassungen werden entfernt. Bitte bestätigen.
386 All possible Workflow States and roles of the workflow. Docstatus Options: 0 is"Saved", 1 is "Submitted" and 2 is "Cancelled" Alle möglichen Zustände und Rollen des Workflows. Dokumentenstatus-Optionen sind: 0 ist "Gespeichert", 1 ist "Übertragen" und 2 ist "Abgebrochen" Alle möglichen Stati und Rollen des Workflows. Dokumentenstatus-Optionen sind: 0 ist "Gespeichert", 1 ist "Übertragen" und 2 ist "Abgebrochen"
387 All-uppercase is almost as easy to guess as all-lowercase. Ausschließlich Großbuchstaben sind fast so einfach zu erraten wie ausschließlich Kleinbuchstaben.
388 Allocated To Zugewiesen zu
389 Allow Zulassen
851 DefaultValue Standardwert
852 Define workflows for forms. Workflows für Formulare definieren
853 Defines actions on states and the next step and allowed roles. Definiert Maßnahmen bei bestimmten Zuständen, den nächsten Schritt und erlaubte Rollen.
854 Defines workflow states and rules for a document. Definiert Workflow-Zustände und Regeln für ein Dokument. Definiert Workflow-Stati und Regeln für ein Dokument.
855 Delayed Verzögert
856 Delete Data Daten löschen
857 Delete comment? Kommentar löschen?
1080 Field Description Feldbeschreibung
1081 Field Maps Feldkarten
1082 Field Type Feldtyp
1083 Field that represents the Workflow State of the transaction (if field is not present, a new hidden Custom Field will be created) Feld, das den Status des Workflows der einzelnen Transaktionen wiedergibt (wenn das Feld nicht vorhanden ist, wird ein neues verstecktes, benutzerdefiniertes Feld erstellt) Feldc für den Workflow-Status der einzelnen Transaktionen (wenn das Feld nicht vorhanden ist, wird ein neues verstecktes, benutzerdefiniertes Feld erstellt)
1084 Field to Track Zu verfolgendes Feld
1085 Field type cannot be changed for {0} Feldtyp kann nicht für {0} geändert werden
1086 Field {0} not found. Feld {0} nicht gefunden
2777 Workflow Action Name Workflow-Aktionsname
2778 Workflow Document State Workflow-Dokumentenstatus
2779 Workflow Name Workflow-Name
2780 Workflow State Workflow-Zustand Workflow-Status
2781 Workflow State Field Workflow-Zustandsfeld Workflow-Status-Feld
2782 Workflow State not set Workflow-Status nicht festgelegt
2783 Workflow Transition Workflow-Übergang
2784 Workflow state represents the current state of a document. Workflow-Zustand stellt den aktuellen Status eines Dokuments dar. Der Workflow-Status steht für den aktuellen Zustand eines Dokuments.
2785 Write Schreiben
2786 Wrong fieldname <b>{0}</b> in add_fetch configuration of custom script Falscher Feldname <b>{0}</b> in der add_fetch-Konfiguration des benutzerdefinierten Skripts
2787 X Axis Field X-Achsenfeld
3070 {0} is an invalid email address in 'Recipients' {0} ist eine ungültige E-Mail-Adresse in "Empfänger"
3071 {0} is not a raw printing format. {0} ist kein unformatiertes Druckformat.
3072 {0} is not a valid Email Address {0} ist keine gültige E-Mail-Adresse
3073 {0} is not a valid Workflow State. Please update your Workflow and try again. {0} ist kein gültiger Workflow-Zustand. Bitte aktualisieren Sie Ihren Workflow und versuchen Sie es erneut. {0} ist kein gültiger Workflow-Status. Bitte aktualisieren Sie Ihren Workflow und versuchen Sie es erneut.
3074 {0} is now default print format for {1} doctype {0} ist jetzt das Standard-Druckformat für den DocType {1}
3075 {0} is saved {0} ist gespeichert
3076 {0} items selected {0} Elemente ausgewählt
3131 In Days In Tagen
3132 Last Password Reset Date Datum der letzten Kennwortrücksetzung
3133 The password of your account has expired. Das Passwort Ihres Kontos ist abgelaufen.
3134 Workflow State transition not allowed from {0} to {1} Workflow-Statusübergang von {0} nach {1} nicht zulässig Eine Veränderung des Workflow-Status von {0} nach {1} ist nicht zulässig
3135 {0} must be after {1} {0} muss nach {1} liegen
3136 {0}: Field '{1}' cannot be set as Unique as it has non-unique values {0}: Feld &#39;{1}&#39; kann nicht als eindeutig festgelegt werden, da es nicht eindeutige Werte enthält
3137 {0}: Field {1} in row {2} cannot be hidden and mandatory without default {0}: Das Feld {1} in Zeile {2} kann ohne Vorgabe nicht ausgeblendet und obligatorisch sein
4645 Value from this field will be set as the due date in the ToDo Der Wert aus diesem Feld wird im Fälligkeitsdatum als Fälligkeitsdatum festgelegt
4646 New module created {0} Neues Modul erstellt {0}
4647 Report has no numeric fields, please change the Report Name Der Bericht enthält keine numerischen Felder. Bitte ändern Sie den Berichtsnamen
4648 There are documents which have workflow states that do not exist in this Workflow. It is recommended that you add these states to the Workflow and change their states before removing these states. Es gibt Dokumente mit Workflow-Status, die in diesem Workflow nicht vorhanden sind. Es wird empfohlen, diese Status zum Workflow hinzuzufügen und ihre Status zu ändern, bevor Sie diese Status entfernen. Es gibt Dokumente mit Workflow-Status, die in diesem Workflow nicht vorhanden sind. Es wird empfohlen, diese Stati zum Workflow hinzuzufügen oder den Status der Dokumente ändern, bevor Sie einen Status entfernen.
4649 Worflow States Don't Exist Worflow-Zustände existieren nicht Worflow-Stati existieren nicht
4650 Save Anyway Auf jeden Fall speichern
4651 Energy Points: Energiepunkte:
4652 Review Points: Bewertungspunkte:

View file

@ -1514,55 +1514,20 @@ def escape_html(text: str) -> str:
def pretty_date(iso_datetime: datetime.datetime | str) -> str:
"""
Takes an ISO time and returns a string representing how
long ago the date represents.
Ported from PrettyDate by John Resig
"""
from frappe import _
Return a localized string representation of the delta to the current system time.
For example, "1 hour ago", "2 days ago", "in 5 seconds", etc.
"""
if not iso_datetime:
return ""
import math
from babel.dates import format_timedelta
if isinstance(iso_datetime, str):
iso_datetime = datetime.datetime.strptime(iso_datetime, DATETIME_FORMAT)
now_dt = datetime.datetime.strptime(now(), DATETIME_FORMAT)
dt_diff = now_dt - iso_datetime
# available only in python 2.7+
# dt_diff_seconds = dt_diff.total_seconds()
dt_diff_seconds = dt_diff.days * 86400.0 + dt_diff.seconds
dt_diff_days = math.floor(dt_diff_seconds / 86400.0)
# differnt cases
if dt_diff_seconds < 60.0:
return _("just now")
elif dt_diff_seconds < 120.0:
return _("1 minute ago")
elif dt_diff_seconds < 3600.0:
return _("{0} minutes ago").format(cint(math.floor(dt_diff_seconds / 60.0)))
elif dt_diff_seconds < 7200.0:
return _("1 hour ago")
elif dt_diff_seconds < 86400.0:
return _("{0} hours ago").format(cint(math.floor(dt_diff_seconds / 3600.0)))
elif dt_diff_days == 1.0:
return _("Yesterday")
elif dt_diff_days < 7.0:
return _("{0} days ago").format(cint(dt_diff_days))
elif dt_diff_days < 12:
return _("1 week ago")
elif dt_diff_days < 31.0:
return _("{0} weeks ago").format(dt_diff_days // 7)
elif dt_diff_days < 46:
return _("1 month ago")
elif dt_diff_days < 365.0:
return _("{0} months ago").format(dt_diff_days // 30)
elif dt_diff_days < 550.0:
return _("1 year ago")
else:
return _("{0} years ago").format(dt_diff_days // 365)
locale = frappe.local.lang.replace("-", "_") if frappe.local.lang else None
return format_timedelta(iso_datetime - now_dt, add_direction=True, locale=locale)
def comma_or(some_list, add_quotes=True):

View file

@ -137,11 +137,8 @@
{% if success_url %}
<div class="success_url_message">
<p>
<span>Click on this </span>
<a href="{{ success_url }}">{{_("URL")}}</a>
<span> if you are not redirected within </span>
<span class="time">5</span>
<span> seconds.</span>
{% set success_link = "<a href='{0}'>link</a>".format(success_url) %}
<span>{{ _("Click on this {0} if you are not redirected within 5 seconds").format(success_link) }} </span>
</p>
</div>
{% else %}

View file

@ -40,17 +40,21 @@ frappe.ui.form.on("Workflow", {
},
update_field_options: function (frm) {
var doc = frm.doc;
if (doc.document_type) {
const get_field_method =
"frappe.workflow.doctype.workflow.workflow.get_fieldnames_for";
frappe.xcall(get_field_method, { doctype: doc.document_type }).then((resp) => {
frm.fields_dict.states.grid.update_docfield_property(
"update_field",
"options",
[""].concat(resp)
);
});
if (!doc.document_type) {
return;
}
frappe.model.with_doctype(doc.document_type, () => {
const fieldnames = frappe
.get_meta(doc.document_type)
.fields.filter((field) => !frappe.model.no_value_type.includes(field.fieldtype))
.map((field) => field.fieldname);
frm.fields_dict.states.grid.update_docfield_property(
"update_field",
"options",
[""].concat(fieldnames)
);
});
},
create_warning_dialog: function (frm) {
const warning_html = `<p class="bold">

View file

@ -124,15 +124,9 @@ class Workflow(Document):
)
@frappe.whitelist()
def get_fieldnames_for(doctype):
return [
f.fieldname for f in frappe.get_meta(doctype).fields if f.fieldname not in no_value_fields
]
@frappe.whitelist()
def get_workflow_state_count(doctype, workflow_state_field, states):
frappe.has_permission(doctype=doctype, ptype="read", throw=True)
states = frappe.parse_json(states)
result = frappe.get_all(
doctype,

View file

@ -1,6 +1,7 @@
{
"actions": [],
"autoname": "field:workflow_action_name",
"allow_rename": 1,
"creation": "2012-12-28 10:49:56",
"description": "Workflow Action Master",
"doctype": "DocType",
@ -21,7 +22,7 @@
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2022-08-03 12:20:52.449982",
"modified": "2023-04-14 12:20:52.449982",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow Action Master",
@ -43,4 +44,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -12,6 +12,7 @@ dependencies = [
"Babel~=2.12.1",
"Click~=8.1.3",
"filelock~=3.8.0",
"filetype~=1.2.0",
"GitPython~=3.1.30",
"Jinja2~=3.1.2",
"Pillow~=9.3.0",