diff --git a/frappe/app.py b/frappe/app.py index dc194a7559..e2b42ab2a0 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -413,13 +413,7 @@ def sync_database(rollback: bool) -> bool: def serve( - host=None, - port=8000, - profile=False, - no_reload=False, - no_threading=False, - site=None, - sites_path=".", + port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="." ): global application, _site, _sites_path _site = site @@ -444,7 +438,7 @@ def serve( log.setLevel(logging.ERROR) run_simple( - host, + "0.0.0.0", int(port), application, exclude_patterns=["test_*"], diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 263c5438bf..a833431142 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -481,43 +481,35 @@ def install_app(context, apps, force=False): @click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text") @pass_context def list_apps(context, format): - "List apps in site" + """ + List apps in site. + """ summary_dict = {} - def fix_whitespaces(text): - if site == context.sites[-1]: - text = text.rstrip() - if len(context.sites) == 1: - text = text.lstrip() - return text + def format_app(app): + name_len = max(len(app.app_name) for app in apps) + ver_len = max(len(app.app_version) for app in apps) + template = f"{{0:{name_len}}} {{1:{ver_len}}} {{2}}" + return template.format(app.app_name, app.app_version, app.git_branch) for site in context.sites: frappe.init(site=site) frappe.connect() site_title = click.style(f"{site}", fg="green") if len(context.sites) > 1 else "" + installed_apps_info = [] + apps = frappe.get_single("Installed Applications").installed_applications - if apps: - name_len, ver_len = (max(len(x.get(y)) for x in apps) for y in ["app_name", "app_version"]) - template = f"{{0:{name_len}}} {{1:{ver_len}}} {{2}}" - - installed_applications = [ - template.format(app.app_name, app.app_version, app.git_branch) for app in apps - ] - applications_summary = "\n".join(installed_applications) - summary = f"{site_title}\n{applications_summary}\n" - summary_dict[site] = [app.app_name for app in apps] - + installed_apps_info.extend(format_app(app) for app in apps) else: - installed_applications = frappe.get_installed_apps() - applications_summary = "\n".join(installed_applications) - summary = f"{site_title}\n{applications_summary}\n" - summary_dict[site] = installed_applications + installed_apps_info.extend(frappe.get_installed_apps()) - summary = fix_whitespaces(summary) + installed_apps_info_str = "\n".join(installed_apps_info) + summary = f"{site_title}\n{installed_apps_info_str}\n" + summary_dict[site] = [app.app_name for app in apps] - if format == "text" and applications_summary and summary: + if format == "text" and installed_apps_info and summary: print(summary) frappe.destroy() diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 3af7a935dd..6de2885b58 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -922,7 +922,6 @@ def run_ui_tests( @click.command("serve") -@click.option("--host", default="127.0.0.1") @click.option("--port", default=8000) @click.option("--profile", is_flag=True, default=False) @click.option("--noreload", "no_reload", is_flag=True, default=False) @@ -931,7 +930,6 @@ def run_ui_tests( @pass_context def serve( context, - host="127.0.0.1", port=None, profile=False, no_reload=False, @@ -953,7 +951,6 @@ def serve( no_threading = True no_reload = True frappe.app.serve( - host=host, port=port, profile=profile, no_reload=no_reload, diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index cd89a57dfe..f168ad920f 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -1,7 +1,7 @@ frappe.ui.form.on("User", { before_load: function (frm) { - var update_tz_select = function (user_language) { - frm.set_df_property("time_zone", "options", [""].concat(frappe.all_timezones)); + let update_tz_options = function () { + frm.fields_dict.time_zone.set_data(frappe.all_timezones); }; if (!frappe.all_timezones) { @@ -9,11 +9,11 @@ frappe.ui.form.on("User", { method: "frappe.core.doctype.user.user.get_timezones", callback: function (r) { frappe.all_timezones = r.message.timezones; - update_tz_select(); + update_tz_options(); }, }); } else { - update_tz_select(); + update_tz_options(); } }, diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 0396776183..5ed990a794 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -191,7 +191,7 @@ }, { "fieldname": "time_zone", - "fieldtype": "Select", + "fieldtype": "Autocomplete", "label": "Time Zone" }, { @@ -762,7 +762,7 @@ "link_fieldname": "user" } ], - "modified": "2023-06-05 17:26:04.127555", + "modified": "2023-09-18 22:19:49.933972", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 33b1cd3bf5..1af7af72e5 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -104,7 +104,7 @@ class User(Document): simultaneous_sessions: DF.Int social_logins: DF.Table[UserSocialLogin] thread_notify: DF.Check - time_zone: DF.Literal + time_zone: DF.Autocomplete | None unsubscribed: DF.Check user_emails: DF.Table[UserEmail] user_image: DF.AttachImage | None diff --git a/frappe/database/database.py b/frappe/database/database.py index f790aa6c9b..77fcfe73fe 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -879,7 +879,9 @@ class Database: """ from frappe.model.utils import is_single_doctype - if (dn is None or dt == dn) and is_single_doctype(dt): + if dn is None or dt == dn: + if not is_single_doctype(dt): + return deprecation_warning( "Calling db.set_value on single doctype is deprecated. This behaviour will be removed in future. Use db.set_single_value instead." ) diff --git a/frappe/desk/doctype/console_log/console_log.json b/frappe/desk/doctype/console_log/console_log.json index 7531d97991..3653a26eab 100644 --- a/frappe/desk/doctype/console_log/console_log.json +++ b/frappe/desk/doctype/console_log/console_log.json @@ -7,7 +7,8 @@ "engine": "InnoDB", "field_order": [ "script", - "type" + "type", + "committed" ], "fields": [ { @@ -23,11 +24,18 @@ "hidden": 1, "label": "Type", "read_only": 1 + }, + { + "default": "0", + "fieldname": "committed", + "fieldtype": "Check", + "label": "Committed", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-07-27 22:52:37.239039", + "modified": "2023-09-19 13:02:56.332137", "modified_by": "Administrator", "module": "Desk", "name": "Console Log", diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py index bed829c5b8..cd004745a3 100644 --- a/frappe/desk/doctype/console_log/console_log.py +++ b/frappe/desk/doctype/console_log/console_log.py @@ -14,6 +14,7 @@ class ConsoleLog(Document): if TYPE_CHECKING: from frappe.types import DF + committed: DF.Check script: DF.Code | None type: DF.Data | None # end: auto-generated types diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 6d4a86d535..a649633049 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -280,6 +280,7 @@ "options": "DocType" }, { + "description": "If set, only user with these roles can access this chart. If not set, DocType or Report permissions will be used.", "fieldname": "roles", "fieldtype": "Table", "label": "Roles", @@ -287,7 +288,7 @@ } ], "links": [], - "modified": "2023-08-28 20:20:54.186299", + "modified": "2023-09-18 13:41:05.263676", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 9fe135d4e1..1c331a7fe8 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -79,7 +79,11 @@ def has_permission(doc, ptype, user): if "System Manager" in roles: return True - if doc.chart_type == "Report": + if doc.roles: + allowed = [d.role for d in doc.roles] + if has_common(roles, allowed): + return True + elif doc.chart_type == "Report": if doc.report_name in get_allowed_report_names(): return True else: @@ -87,11 +91,6 @@ def has_permission(doc, ptype, user): if doc.document_type in allowed_doctypes: return True - if doc.roles: - allowed = [d.role for d in doc.roles] - if has_common(roles, allowed): - return True - return False diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 068457c2c6..8201a5d0aa 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -108,8 +108,8 @@ let add_custom_button = (frm) => { tour_name: frm.doc.name, }, }); - }, - delete frappe.boot.user.onboarding_status[frm.doc.name] + delete frappe.boot.user.onboarding_status[frm.doc.name]; + } ); }); } else { diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py index 14576d3860..4969f5a04a 100644 --- a/frappe/desk/doctype/system_console/system_console.py +++ b/frappe/desk/doctype/system_console/system_console.py @@ -40,7 +40,9 @@ class SystemConsole(Document): frappe.db.commit() else: frappe.db.rollback() - frappe.get_doc(dict(doctype="Console Log", script=self.console, type=self.type)).insert() + frappe.get_doc( + dict(doctype="Console Log", script=self.console, type=self.type, committed=self.commit) + ).insert() frappe.db.commit() diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index f0470566ac..4c728bdee9 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -311,7 +311,7 @@ def get_internal_links(doc, link, link_doctype): elif isinstance(link, list): # get internal links in child documents table_fieldname, link_fieldname = link - for row in doc.get(table_fieldname): + for row in doc.get(table_fieldname) or []: value = row.get(link_fieldname) if value and value not in names: names.append(value) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 9504a30df6..3a2b369a23 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -108,6 +108,7 @@ def update_global_settings(args): update_system_settings(args) update_user_name(args) + set_timezone(args) def run_post_setup_complete(args): @@ -248,6 +249,12 @@ def update_user_name(args): add_all_roles_to(args.get("name")) +def set_timezone(args): + if args.get("timezone"): + for name in frappe.STANDARD_USERS: + frappe.db.set_value("User", name, "time_zone", args.get("timezone")) + + def parse_args(args): if not args: args = frappe.local.form_dict diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index a1740b0b0a..b9ba3a2bc1 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -606,6 +606,20 @@ class TestInboundMail(FrappeTestCase): reference_doc = inbound_mail.reference_document() self.assertEqual(todo.name, reference_doc.name) + def test_reference_document_by_subject_match_with_accents(self): + subject = "Nouvelle tâche à faire 😃" + todo = self.new_todo(sender="test_sender@example.com", description=subject) + + mail_content = ( + self.get_test_mail(fname="incoming-subject-placeholder.raw") + .replace("{{ subject }}", f"RE: {subject}") + .encode("utf-8") + ) # note: encode to bytes because that's what triggered the error + email_account = frappe.get_doc("Email Account", "_Test Email Account 1") + inbound_mail = InboundMail(mail_content, email_account, 12345, 1) + reference_doc = inbound_mail.reference_document() + self.assertEqual(todo.name, reference_doc.name) + def test_create_communication_from_mail(self): # Create email queue record mail_content = self.get_test_mail(fname="incoming-2.raw") diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 39a5537ebf..d51d9e6be9 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -402,11 +402,19 @@ class Email: """Parse and decode `Subject` header.""" _subject = decode_header(self.mail.get("Subject", "No Subject")) self.subject = _subject[0][0] or "" + if _subject[0][1]: + # Encoding is known by decode_header (might also be unknown-8bit) self.subject = safe_decode(self.subject, _subject[0][1]) - else: - # assume that the encoding is utf-8 - self.subject = safe_decode(self.subject)[:140] + + if isinstance(self.subject, bytes): + # Fall back to utf-8 if the charset is unknown or decoding fails + # Replace invalid characters with '>' + self.subject = self.subject.decode("utf-8", "replace") + + # Convert non-string (e.g. None) + # Truncate to 140 chars (can be used as a document name) + self.subject = str(self.subject).strip()[:140] if not self.subject: self.subject = "No Subject" diff --git a/frappe/hooks.py b/frappe/hooks.py index 28ba377793..070e7f5ba0 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -35,6 +35,7 @@ app_include_css = [ "desk.bundle.css", "report.bundle.css", ] +app_include_icons = ["frappe/public/icons/timeless/icons.svg"] doctype_js = { "Web Page": "public/js/frappe/utils/web_template.js", diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 5ae8965c83..0f94d2ee29 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -10,14 +10,16 @@ from frappe import _ from frappe.utils import get_request_session -def make_request(method, url, auth=None, headers=None, data=None): +def make_request(method, url, auth=None, headers=None, data=None, json=None): auth = auth or "" data = data or {} headers = headers or {} try: s = get_request_session() - frappe.flags.integration_request = s.request(method, url, data=data, auth=auth, headers=headers) + frappe.flags.integration_request = s.request( + method, url, data=data, auth=auth, headers=headers, json=json + ) frappe.flags.integration_request.raise_for_status() if frappe.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8": diff --git a/frappe/public/icons/timeless/test.svg b/frappe/public/icons/timeless/test.svg new file mode 100644 index 0000000000..9563c827fd --- /dev/null +++ b/frappe/public/icons/timeless/test.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/frappe/public/js/frappe/form/controls/icon.js b/frappe/public/js/frappe/form/controls/icon.js index 89abb82114..f3386ed813 100644 --- a/frappe/public/js/frappe/form/controls/icon.js +++ b/frappe/public/js/frappe/form/controls/icon.js @@ -10,7 +10,7 @@ frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlDat get_all_icons() { frappe.symbols = []; - $("#frappe-symbols > symbol[id]").each(function () { + $("#all-symbols > svg > symbol[id]").each(function () { this.id.includes("icon-") && frappe.symbols.push(this.id.replace("icon-", "")); }); } diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 2dc180ed8c..57c443ba9d 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -176,6 +176,11 @@ frappe.ui.form.Form = class FrappeForm { page: this.page, description: __("Redo last action"), }); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+p", + action: () => this.print_doc(), + description: __("Print document"), + }); let grid_shortcut_keys = [ { diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 5e38c4c25b..a0271967b4 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -235,7 +235,11 @@ export default class BulkOperations { } edit(docnames, field_mappings, done) { - let field_options = Object.keys(field_mappings).sort(); + let field_options = Object.keys(field_mappings).sort(function (a, b) { + return __(cstr(field_mappings[a].label)).localeCompare( + cstr(__(field_mappings[b].label)) + ); + }); const status_regex = /status/i; const default_field = field_options.find((value) => status_regex.test(value)); diff --git a/frappe/public/js/frappe/ui/group_by/group_by.html b/frappe/public/js/frappe/ui/group_by/group_by.html index 8522b46da8..35f354a129 100644 --- a/frappe/public/js/frappe/ui/group_by/group_by.html +++ b/frappe/public/js/frappe/ui/group_by/group_by.html @@ -10,22 +10,12 @@ {% for (var parent_doctype in group_by_conditions) { %} {% for (var val in group_by_conditions[parent_doctype]) { %} - {% if (parent_doctype !== doctype) { %} - {% } else { %} - - {% } %} {% } %} {% } %} diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js index 31ddb707a7..5b006db4a6 100644 --- a/frappe/public/js/frappe/ui/group_by/group_by.js +++ b/frappe/public/js/frappe/ui/group_by/group_by.js @@ -369,7 +369,7 @@ frappe.ui.GroupBy = class { const tag_field = { fieldname: "_user_tags", fieldtype: "Data", label: __("Tags") }; this.group_by_fields[this.doctype] = fields .concat(tag_field) - .sort((a, b) => __(a.label).localeCompare(__(b.label))); + .sort((a, b) => __(cstr(a.label)).localeCompare(cstr(__(b.label)))); this.all_fields[this.doctype] = this.report_view.meta.fields; const standard_fields_filter = (df) => @@ -407,8 +407,9 @@ frappe.ui.GroupBy = class { } get_group_by_field_label() { - return this.group_by_fields[this.group_by_doctype].find( + let field = this.group_by_fields[this.group_by_doctype].find( (field) => field.fieldname == this.group_by_field - ).label; + ); + return field.label || field.fieldname; } }; diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index 83a6766841..870d5b7182 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -332,10 +332,39 @@ frappe.utils.sanitise_redirect = (url) => { }; })(); + /* + * Strips out url containing the text `javascript` with or without any HTML Entities in it + **/ const sanitise_javascript = (url) => { - // please do not ask how or why - const REGEX_SCRIPT = - /j[\s]*(.{1,7})?a[\s]*(.{1,7})?v[\s]*(.{1,7})?a[\s]*(.{1,7})?s[\s]*(.{1,7})?c[\s]*(.{1,7})?r[\s]*(.{1,7})?i[\s]*(.{1,7})?p[\s]*(.{1,7})?t/gi; + /* + * Written below split into parts, but actual is in one line regardless of whitespaces + * / + * j + * \s*(.{1,7})? + * a + * \s*(.{1,7})? + * v + * \s*(.{1,7})? + * a + * \s*(.{1,7})? + * s + * \s*(.{1,7})? + * c + * \s*(.{1,7})? + * r + * \s*(.{1,7})? + * i + * \s*(.{1,7})? + * p + * \s*(.{1,7})? + * t + * /gi + * */ + const REGEX_ESC_UNIT = /\s*(.{1,7})?/; + const REGEX_SCRIPT = new RegExp( + Array.from("javascript").join(REGEX_ESC_UNIT.source), + "gi" + ); return url.replace(REGEX_SCRIPT, ""); }; diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 3acb6e99f3..655777b39f 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -693,6 +693,12 @@ class TestDBSetValue(FrappeTestCase): current_value = frappe.db.get_single_value("System Settings", "deny_multiple_sessions") self.assertEqual(current_value, changed_value) + def test_none_no_set_value(self): + frappe.db.set_value("User", None, "middle_name", "test") + with self.assertQueryCount(0): + frappe.db.set_value("User", None, "middle_name", "test") + frappe.db.set_value("User", "User", "middle_name", "test") + def test_update_single_row_single_column(self): frappe.db.set_value("ToDo", self.todo1.name, "description", "test_set_value change 1") updated_value = frappe.db.get_value("ToDo", self.todo1.name, "description") diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index b7b33b3531..279252865c 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -391,6 +391,27 @@ class TestWebsite(FrappeTestCase): delattr(frappe.local, "request") frappe.set_user("Guest") + def test_include_icons(self): + from frappe import get_hooks + + TEST_ICONS_PATH = "frappe/public/icons/timeless/test.svg" + + def patched_get_hooks(*args, **kwargs): + return_value = get_hooks(*args, **kwargs) + if isinstance(return_value, dict) and "app_include_icons" in return_value: + return_value.app_include_icons.append(TEST_ICONS_PATH) + return return_value + + with patch.object(frappe, "get_hooks", patched_get_hooks): + frappe.set_user("Administrator") + + set_request(method="GET", path="/app") + content = get_response_content("/app") + # icon is available in a symbol tag + self.assertIn('id="icon-TEST-ONLY"', content) + delattr(frappe.local, "request") + frappe.set_user("Guest") + def patched_get_hooks(hook, value): def wrapper(*args, **kwargs): diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 85bf3d6f8c..f3a9d39190 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -365,6 +365,11 @@ app_license = "{app_license}" # doctype_tree_js = {{"doctype" : "public/js/doctype_tree.js"}} # doctype_calendar_js = {{"doctype" : "public/js/doctype_calendar.js"}} +# Svg Icons +# ------------------ +# include app icons in desk +# app_include_icons = "{app_name}/public/icons.svg" + # Home Pages # ---------- diff --git a/frappe/utils/data.py b/frappe/utils/data.py index ee092a6a1f..d3cb996c9d 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -309,7 +309,7 @@ def get_eta(from_time, percent_complete): def _get_system_timezone(): - return frappe.db.get_system_setting("time_zone") or "Asia/Kolkata" # Default to India ?! + return frappe.get_system_settings("time_zone") or "Asia/Kolkata" # Default to India ?! def get_system_timezone(): diff --git a/frappe/www/app.html b/frappe/www/app.html index 844eb79ea4..8d69e092a0 100644 --- a/frappe/www/app.html +++ b/frappe/www/app.html @@ -27,6 +27,11 @@
{% include "public/icons/timeless/icons.svg" %} {% include "public/icons/espresso/icons.svg" %} + {% include "templates/includes/splash_screen.html" %}