diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 33da595b6f3..407501a6d83 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -635,7 +635,7 @@ def console(context, autoreload=False): all_apps = frappe.get_installed_apps() failed_to_import = [] - for app in all_apps: + for app in list(all_apps): try: locals()[app] = __import__(app) except ModuleNotFoundError: diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 9cad86ece13..423f2272cb3 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -53,7 +53,7 @@ def notify_change(self, action): def remove_comment_from_cache(self): _comments = get_comments_from_parent(self) - for c in _comments: + for c in list(_comments): if c.get("name") == self.name: _comments.remove(c) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 80814219307..6efa8615b46 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -407,8 +407,7 @@ def has_permission(doc, ptype, user): return if doc.reference_doctype and doc.reference_name: - if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): - return True + return frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name) def get_permission_query_conditions_for_communication(user): diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 7b56f98ffed..3461f50dd50 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -458,7 +458,7 @@ "read_only": 1 }, { - "default": "1", + "default": "2", "fieldname": "simultaneous_sessions", "fieldtype": "Int", "label": "Simultaneous Sessions" diff --git a/frappe/database/database.py b/frappe/database/database.py index 982807cf596..cd45eead2fa 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -757,7 +757,7 @@ def set_single_value( Example: # Update the `deny_multiple_sessions` field in System Settings DocType. - company = frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True) + frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True) """ return self.set_value(doctype, doctype, fieldname, value, *args, **kwargs) diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 63fa12b8fb7..96b5034f1bc 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -243,7 +243,7 @@ def set_desktop_icons(visible_list, ignore_duplicate=True): frappe.db.sql("update `tabDesktop Icon` set blocked=0, hidden=1 where standard=1") # set as visible if present, or add icon - for module_name in visible_list: + for module_name in list(visible_list): name = frappe.db.get_value("Desktop Icon", {"module_name": module_name}) if name: frappe.db.set_value("Desktop Icon", name, "hidden", 0) diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index e3257e25bec..2ee4ad6581a 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -220,7 +220,7 @@ def update_column_order(board_name, order): new_columns = [] for col in order: - for column in old_columns: + for column in list(old_columns): if col == column.column_name: new_columns.append(column) old_columns.remove(column) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 453990f0912..e0f7d4d8f7d 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -209,6 +209,7 @@ def search_widget( and has_permission( doctype, ptype="select" if frappe.only_has_select_perm(doctype) else "read", + parent_doctype=reference_doctype, ) ) ) diff --git a/frappe/integrations/doctype/google_drive/google_drive.js b/frappe/integrations/doctype/google_drive/google_drive.js index 208c1e5e1a4..ec5833cf594 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.js +++ b/frappe/integrations/doctype/google_drive/google_drive.js @@ -13,13 +13,14 @@ frappe.ui.form.on("Google Drive", { frappe.realtime.on("upload_to_google_drive", (data) => { if (data.progress) { + const progress_title = __("Uploading to Google Drive"); frm.dashboard.show_progress( - "Uploading to Google Drive", + progress_title, (data.progress / data.total) * 100, - __("{0}", [data.message]) + data.message ); if (data.progress === data.total) { - frm.dashboard.hide_progress("Uploading to Google Drive"); + frm.dashboard.hide_progress(progress_title); } } }); diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index fbb970de463..f9cd1143a79 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -153,7 +153,7 @@ def upload_system_backup_to_google_drive(): validate_file_size() if frappe.flags.create_new_backup: - set_progress(1, "Backing up Data.") + set_progress(1, _("Backing up Data.")) backup = new_backup() file_urls = [] file_urls.append(backup.backup_path_db) @@ -179,12 +179,12 @@ def upload_system_backup_to_google_drive(): frappe.throw(_("Google Drive - Could not locate - {0}").format(e)) try: - set_progress(2, "Uploading backup to Google Drive.") + set_progress(2, _("Uploading backup to Google Drive.")) google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute() except HttpError as e: send_email(False, "Google Drive", "Google Drive", "email", error_status=e) - set_progress(3, "Uploading successful.") + set_progress(3, _("Uploading successful.")) frappe.db.set_single_value("Google Drive", "last_backup_on", frappe.utils.now_datetime()) send_email(True, "Google Drive", "Google Drive", "email") return _("Google Drive Backup Successful.") diff --git a/frappe/integrations/doctype/google_settings/google_settings.json b/frappe/integrations/doctype/google_settings/google_settings.json index 6f25fa4bf67..ee48fde97ba 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.json +++ b/frappe/integrations/doctype/google_settings/google_settings.json @@ -39,8 +39,7 @@ "description": "The browser API key obtained from the Google Cloud Console under \n\"APIs & Services\" > \"Credentials\"\n", "fieldname": "api_key", "fieldtype": "Data", - "label": "API Key", - "mandatory_depends_on": "google_drive_picker_enabled" + "label": "API Key" }, { "depends_on": "enable", @@ -76,7 +75,7 @@ ], "issingle": 1, "links": [], - "modified": "2021-06-29 18:26:07.094851", + "modified": "2024-01-16 13:19:22.365362", "modified_by": "Administrator", "module": "Integrations", "name": "Google Settings", @@ -96,5 +95,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/google_settings/google_settings.py b/frappe/integrations/doctype/google_settings/google_settings.py index e464e0d0901..6b44a53098f 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.py +++ b/frappe/integrations/doctype/google_settings/google_settings.py @@ -19,6 +19,5 @@ def get_file_picker_settings(): return { "enabled": True, "appId": google_settings.app_id, - "developerKey": google_settings.api_key, "clientId": google_settings.client_id, } diff --git a/frappe/integrations/doctype/google_settings/test_google_settings.py b/frappe/integrations/doctype/google_settings/test_google_settings.py index d4bb8307792..4b705a67f1c 100644 --- a/frappe/integrations/doctype/google_settings/test_google_settings.py +++ b/frappe/integrations/doctype/google_settings/test_google_settings.py @@ -40,4 +40,3 @@ def test_picker_enabled(self): self.assertEqual(True, settings.get("enabled", False)) self.assertEqual("test_client_id", settings.get("clientId", "")) self.assertEqual("test_app_id", settings.get("appId", "")) - self.assertEqual("test_api_key", settings.get("developerKey", "")) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index a6009b64028..2f3b87ace08 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -756,7 +756,7 @@ def prepare_filter_condition(self, f): ref_doctype = field.options if field else f.doctype lft, rgt = "", "" if f.value: - lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"]) + lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"]) or (0, 0) # Get descendants elements of a DocType with a tree structure if f.operator.lower() in ("descendants of", "not descendants of"): diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index de0b023332c..425ea69a026 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -156,7 +156,7 @@ def _insert(data): 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)) + print(_("Skipping fixture syncing for doctype {0} from file {1} ").format(doctype, filename)) return if data["custom_fields"]: diff --git a/frappe/permissions.py b/frappe/permissions.py index 355a85ca5ad..180dda91dce 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -300,7 +300,10 @@ def has_user_permission(doc, user=None): if get_role_permissions("User Permission", user=user).get("write"): return True - apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions") + # don't apply strict user permissions for single doctypes since they contain empty link fields + apply_strict_user_permissions = ( + False if doc.meta.issingle else frappe.get_system_settings("apply_strict_user_permissions") + ) doctype = doc.get("doctype") docname = doc.get("name") diff --git a/frappe/public/js/frappe/form/controls/select.js b/frappe/public/js/frappe/form/controls/select.js index de96cd902c9..9ce73168a10 100644 --- a/frappe/public/js/frappe/form/controls/select.js +++ b/frappe/public/js/frappe/form/controls/select.js @@ -110,7 +110,7 @@ frappe.ui.form.add_options = function (input, options_list, sort) { let options = options_list.map((raw_option) => parse_option(raw_option)); if (sort) { - options = options.sort((a, b) => a.label.localeCompare(b.label)); + options = options.sort((a, b) => cstr(a.label).localeCompare(cstr(b.label))); } options diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index 7e8f3f57e57..13fb2f87fdf 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -527,11 +527,34 @@ class FormTimeline extends BaseTimeline { title: communication_doc ? __("Reply") : null, last_email: communication_doc, subject: communication_doc && communication_doc.subject, + reply_all: reply_all, }; - if (communication_doc && reply_all) { - args.cc = communication_doc.cc; - args.bcc = communication_doc.bcc; + const email_accounts = frappe.boot.email_accounts + .filter((account) => { + return ( + !["All Accounts", "Sent", "Spam", "Trash"].includes(account.email_account) && + account.enable_outgoing + ); + }) + .map((e) => e.email_id); + + if (communication_doc && args.is_a_reply) { + args.cc = ""; + if ( + email_accounts.includes(frappe.session.user_email) && + communication_doc.sender != frappe.session.user_email + ) { + // add recipients to cc if replying sender is different from last email + const recipients = communication_doc.recipients.split(",").map((r) => r.trim()); + args.cc = + recipients.filter((r) => r != frappe.session.user_email).join(", ") + ", "; + } + if (reply_all) { + // if reply_all then add cc and bcc as well. + args.cc += communication_doc.cc; + args.bcc = communication_doc.bcc; + } } if (this.frm.doctype === "Communication") { diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index d6d5fb2a42e..3a4d6c13304 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -733,15 +733,17 @@ class FilterArea { this.standard_filters_wrapper = this.list_view.page.page_form.find( ".standard-filter-section" ); - let fields = [ - { + let fields = []; + + if (!this.list_view.settings.hide_name_filter) { + fields.push({ fieldtype: "Data", label: "ID", condition: "like", fieldname: "name", onchange: () => this.refresh_list_view(), - }, - ]; + }); + } if (this.list_view.custom_filter_configs) { this.list_view.custom_filter_configs.forEach((config) => { diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index c2e44496627..89e3a8b93ca 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -250,7 +250,7 @@ frappe.router = { standard_route = ["Tree", doctype_route.doctype]; } else { let new_route = this.list_views_route[_route.toLowerCase()]; - let re_route = route[2].toLowerCase() !== new_route.toLowerCase(); + let re_route = route[2].toLowerCase() !== new_route?.toLowerCase(); if (re_route) { /** diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 6351456922b..08b06cfd5b6 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -172,10 +172,15 @@ frappe.views.CommunicationComposer = class { reqd: 1, fieldname: "sender", options: this.user_email_accounts, + onchange: () => { + this.setup_recipients_if_reply(); + }, }); //Preselect email senders if there is only one if (this.user_email_accounts.length == 1) { this["sender"] = this.user_email_accounts; + } else if (this.user_email_accounts.includes(frappe.session.user_email)) { + this["sender"] = frappe.session.user_email; } } @@ -226,6 +231,43 @@ frappe.views.CommunicationComposer = class { }); } + setup_recipients_if_reply() { + if (!this.is_a_reply || !this.last_email) return; + let sender = this.dialog.get_value("sender"); + if (!sender) return; + const fields = { + recipients: this.dialog.fields_dict.recipients, + cc: this.dialog.fields_dict.cc, + bcc: this.dialog.fields_dict.bcc, + }; + // If same user replies to their own email, set recipients to last email recipients + if (this.last_email.sender == sender) { + fields.recipients.set_value(this.last_email.recipients); + if (this.reply_all) { + fields.cc.set_value(this.last_email.cc); + fields.bcc.set_value(this.last_email.bcc); + } + } else { + fields.recipients.set_value(this.last_email.sender); + if (this.reply_all) { + // if sending reply add ( last email's recipients - sender's email_id ) to cc. + const recipients = this.last_email.recipients.split(",").map((r) => r.trim()); + if (!this.cc) { + this.cc = ""; + } + const cc_array = this.cc.split(",").map((r) => r.trim()); + if (this.cc && !this.cc.endsWith(", ")) { + this.cc += ", "; + } + this.cc += recipients + .filter((r) => !cc_array.includes(r) && r != sender) + .join(", "); + this.cc = this.cc.replace(sender + ", ", ""); + fields.cc.set_value(this.cc); + } + } + } + setup_subject_and_recipients() { this.subject = this.subject || ""; diff --git a/frappe/public/js/integrations/google_drive_picker.js b/frappe/public/js/integrations/google_drive_picker.js index 4564e98de96..ba556514fa4 100644 --- a/frappe/public/js/integrations/google_drive_picker.js +++ b/frappe/public/js/integrations/google_drive_picker.js @@ -1,12 +1,11 @@ /* global gapi:false, google:false */ export default class GoogleDrivePicker { - constructor({ pickerCallback, enabled, appId, developerKey, clientId } = {}) { + constructor({ pickerCallback, enabled, appId, clientId } = {}) { this.scope = "https://www.googleapis.com/auth/drive.file"; this.pickerApiLoaded = false; this.enabled = enabled; this.appId = appId; this.pickerCallback = pickerCallback; - this.developerKey = developerKey; this.clientId = clientId; } @@ -45,7 +44,6 @@ export default class GoogleDrivePicker { createPicker(access_token) { this.view = new google.picker.View(google.picker.ViewId.DOCS); this.picker = new google.picker.PickerBuilder() - .setDeveloperKey(this.developerKey) .setAppId(this.appId) .setOAuthToken(access_token) .addView(this.view) diff --git a/frappe/sessions.py b/frappe/sessions.py index cac934eef60..53783f9872a 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -67,13 +67,14 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None): offset = 0 if user == frappe.session.user: simultaneous_sessions = frappe.db.get_value("User", user, "simultaneous_sessions") or 1 - offset = simultaneous_sessions - 1 + offset = simultaneous_sessions session = frappe.qb.DocType("Sessions") session_id = frappe.qb.from_(session).where( (session.user == user) & (session.device.isin(device)) ) if keep_current: + offset = max(0, offset - 1) session_id = session_id.where(session.sid != frappe.session.sid) query = ( diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py index 47a092cb54f..d039835fc21 100644 --- a/frappe/tests/test_auth.py +++ b/frappe/tests/test_auth.py @@ -22,6 +22,7 @@ def add_user(email, password, username=None, mobile_no=None): dict(doctype="User", email=email, first_name=first_name, username=username, mobile_no=mobile_no) ).insert() user.new_password = password + user.simultaneous_sessions = 1 user.add_roles("System Manager") frappe.db.commit() @@ -212,12 +213,12 @@ def test_session_expires(self): seconds_elapsed = expiry_in * step / 100 time_now = add_to_date(session_created, seconds=seconds_elapsed, as_string=True) - with patch("frappe.utils.now", return_value=time_now): + with self.freeze_time(time_now): data = s.get_session_data_from_db() self.assertEqual(data.user, "Administrator") # 1% higher should immediately expire - time_now = add_to_date(session_created, seconds=expiry_in * 1.01, as_string=True) - with patch("frappe.utils.now", return_value=time_now): + time_of_expiry = add_to_date(session_created, seconds=expiry_in * 1.01, as_string=True) + with self.freeze_time(time_of_expiry): self.assertIn(sid, get_expired_sessions()) self.assertFalse(s.get_session_data_from_db()) diff --git a/frappe/translate.py b/frappe/translate.py index 278f9731e3f..f8d5cea4638 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -296,7 +296,6 @@ def _merge_translations(): except Exception: # People mistakenly call translation function on global variables # where locals are not initalized, translations dont make much sense there - frappe.logger().error("Unable to load translations", exc_info=True) return {} diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 84d055e3dde..4a3a578b292 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -263,12 +263,12 @@ def has_gravatar(email): gravatar_url = f"https://secure.gravatar.com/avatar/{hexdigest}?d=404&s=200" try: - res = requests.get(gravatar_url) + res = requests.get(gravatar_url, timeout=5) if res.status_code == 200: return gravatar_url else: return "" - except requests.exceptions.ConnectionError: + except requests.exceptions.RequestException: return ""