diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js index cfd6b1a1b6..3c5c305665 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -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"), diff --git a/frappe/__init__.py b/frappe/__init__.py index 9d7befe2d1..4d67afe492 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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 = [] diff --git a/frappe/app.py b/frappe/app.py index a647b251c8..fab8facd3f 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -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 diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 4c09cb01bd..92fff13bed 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -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() diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 2754da879f..969c68fbb8 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -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()) diff --git a/frappe/boot.py b/frappe/boot.py index 0cab7a060c..83c9902020 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -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") diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py index f40f1659b3..ee2d473210 100644 --- a/frappe/core/doctype/comment/test_comment.py +++ b/frappe/core/doctype/comment/test_comment.py @@ -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) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 24b6a8fafb..73e94fad09 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -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): diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 04e57f10cf..7f2d36d60a 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -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 ", "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 "]) - 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_list=["cc+1@test.com", "cc ", "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 = [ diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 1066b8bed9..2c9bfd95fd 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -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, diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 6cc0adcc87..9a0613e6ca 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -345,6 +345,7 @@ class DocType(Document): "name", "parent", "creation", + "owner", "modified", "modified_by", "parentfield", diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 86bd69eb5f..51e065f710 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -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") diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index 17a092e340..1d0d145303 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -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 diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json index 7e9bbb1038..c9110bb998 100644 --- a/frappe/core/doctype/language/language.json +++ b/frappe/core/doctype/language/language.json @@ -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", diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index 37dce73dda..9b6b04afcc 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -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", diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index ef38387e57..ca1e7724c1 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -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 diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 0e1ed80eda..670b6b7410 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -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") diff --git a/frappe/core/doctype/translation/test_translation.py b/frappe/core/doctype/translation/test_translation.py index 5602fa2c2d..a64715a32e 100644 --- a/frappe/core/doctype/translation/test_translation.py +++ b/frappe/core/doctype/translation/test_translation.py @@ -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) diff --git a/frappe/defaults.py b/frappe/defaults.py index fcfef0b2fc..edbf784200 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -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) diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index af5f9c4184..6412c626cb 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -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 +} \ No newline at end of file diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index f7d9e8ac3e..0866795538 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -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 = [] diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 7abd6657e5..3f906d8f12 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -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", diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 00ae27d145..b450b734e9 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -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 ( ">", "<", ">=", diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 59d41b543f..27b8867f84 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -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() diff --git a/frappe/hooks.py b/frappe/hooks.py index 429640d6d5..47753ba524 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -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", diff --git a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json index 083f1c9c54..2060c48fb9 100644 --- a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json +++ b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.json @@ -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": [] } \ No newline at end of file diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.json b/frappe/integrations/doctype/oauth_client/oauth_client.json index f4ccde8174..8b863f62ad 100644 --- a/frappe/integrations/doctype/oauth_client/oauth_client.json +++ b/frappe/integrations/doctype/oauth_client/oauth_client.json @@ -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
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
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", diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json index ed5201df1f..676df48f3b 100644 --- a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json +++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json @@ -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": [] } \ No newline at end of file diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 93fddcf686..811ba5894c 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -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) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 0e124ee3aa..8488328da4 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -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"): diff --git a/frappe/monitor.py b/frappe/monitor.py index 8db1e25d32..b93ba1d3bb 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -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 diff --git a/frappe/oauth.py b/frappe/oauth.py index 8099bdab45..2d25b5dfb5 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -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( { diff --git a/frappe/patches.txt b/frappe/patches.txt index da83094961..fa9d884386 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -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 \ No newline at end of file diff --git a/frappe/patches/v15_0/remove_background_jobs_from_dropdown.py b/frappe/patches/v15_0/remove_background_jobs_from_dropdown.py new file mode 100644 index 0000000000..b070e0805c --- /dev/null +++ b/frappe/patches/v15_0/remove_background_jobs_from_dropdown.py @@ -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) diff --git a/frappe/public/js/frappe/defaults.js b/frappe/public/js/frappe/defaults.js index 03258f4691..a96f6c3167 100644 --- a/frappe/public/js/frappe/defaults.js +++ b/frappe/public/js/frappe/defaults.js @@ -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]; diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 7d224f9881..69378d3c30 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -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(); } } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 27ca3dba0c..d4fdd47a11 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -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 ( diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 8852a0df5d..ab0ca87b0d 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -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; diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 6944c4df7c..63aff539e7 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -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()); diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 8ed7d6b028..948a31b1fc 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -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]; }, diff --git a/frappe/public/js/frappe/ui/toolbar/toolbar.js b/frappe/public/js/frappe/ui/toolbar/toolbar.js index 419a22d764..bef0c19b4e 100644 --- a/frappe/public/js/frappe/ui/toolbar/toolbar.js +++ b/frappe/public/js/frappe/ui/toolbar/toolbar.js @@ -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")); + } } } diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 74560cebbc..9b1107de1c 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -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) - ) { - $( - `
  • ${__( - breadcrumbs.workspace - )}
  • ` - ).appendTo(this.$breadcrumbs); - } + if (!breadcrumbs.workspace) { + return; } + + if ( + breadcrumbs.module_info && + (breadcrumbs.module_info.blocked || + !frappe.visible_modules.includes(breadcrumbs.module_info.module)) + ) { + return; + } + + $( + `
  • ${__( + breadcrumbs.workspace + )}
  • ` + ).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]; } } }, diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index d6c50380f2..ee255032bb 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -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; diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 86d5705011..83411f0ddf 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -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; diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index f22b587405..ae53e99518 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -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); diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index 775bf90704..99ecfff932 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -362,6 +362,10 @@ } } +#freeze.grid-form { + z-index: 1020; +} + .recorder-form-in-grid { z-index: 0; @include base-grid(); diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index e1f210c440..ced95d7f69 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -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); diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss index 5294779990..765e51cab9 100644 --- a/frappe/public/scss/desk/global.scss +++ b/frappe/public/scss/desk/global.scss @@ -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); diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss index 31d1661abb..30bf1d6499 100644 --- a/frappe/public/scss/desk/list.scss +++ b/frappe/public/scss/desk/list.scss @@ -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 { diff --git a/frappe/public/scss/desk/sidebar.scss b/frappe/public/scss/desk/sidebar.scss index 25dcceec5b..b3772f47b1 100644 --- a/frappe/public/scss/desk/sidebar.scss +++ b/frappe/public/scss/desk/sidebar.scss @@ -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%; } diff --git a/frappe/templates/includes/comments/comments.py b/frappe/templates/includes/comments/comments.py index 3a056761f3..d76b6d1a48 100644 --- a/frappe/templates/includes/comments/comments.py +++ b/frappe/templates/includes/comments/comments.py @@ -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()) diff --git a/frappe/tests/test_caching.py b/frappe/tests/test_caching.py index 4faade331c..37f1583097 100644 --- a/frappe/tests/test_caching.py +++ b/frappe/tests/test_caching.py @@ -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) diff --git a/frappe/tests/test_defaults.py b/frappe/tests/test_defaults.py index 3c04f16ec8..a8c8ed2697 100644 --- a/frappe/tests/test_defaults.py +++ b/frappe/tests/test_defaults.py @@ -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") diff --git a/frappe/tests/test_monitor.py b/frappe/tests/test_monitor.py index 7536c6a75a..e59ebcde31 100644 --- a/frappe/tests/test_monitor.py +++ b/frappe/tests/test_monitor.py @@ -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 diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py index a5e2876241..c605810837 100644 --- a/frappe/tests/test_translate.py +++ b/frappe/tests/test_translate.py @@ -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" diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index dce2a159ac..4b362e7b47 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -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"), diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 7af2bfda8e..01f6e4f7cc 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -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('