From 2af64893a31e3f6df2cfaeda7cf45208952dab72 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Thu, 16 Feb 2023 18:32:34 +0530 Subject: [PATCH 01/62] feat: allow number cards in workspaces --- frappe/desk/desktop.py | 28 +++++++-- .../desk/doctype/number_card/number_card.py | 4 +- frappe/desk/doctype/workspace/workspace.json | 17 ++++- .../doctype/workspace_number_card/__init__.py | 0 .../workspace_number_card.json | 40 ++++++++++++ .../workspace_number_card.py | 9 +++ .../js/frappe/views/workspace/blocks/index.js | 2 + .../views/workspace/blocks/number_card.js | 62 +++++++++++++++++++ .../js/frappe/views/workspace/workspace.js | 8 +++ .../js/frappe/widgets/number_card_widget.js | 4 +- .../public/js/frappe/widgets/widget_dialog.js | 2 +- 11 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 frappe/desk/doctype/workspace_number_card/__init__.py create mode 100644 frappe/desk/doctype/workspace_number_card/workspace_number_card.json create mode 100644 frappe/desk/doctype/workspace_number_card/workspace_number_card.py create mode 100644 frappe/public/js/frappe/views/workspace/blocks/number_card.js diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index f2243c2e56..824e144272 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -157,14 +157,11 @@ class Workspace: return False def build_workspace(self): + self.number_cards = {"items": self.get_number_cards()} self.cards = {"items": self.get_links()} - self.charts = {"items": self.get_charts()} - self.shortcuts = {"items": self.get_shortcuts()} - self.onboardings = {"items": self.get_onboardings()} - self.quick_lists = {"items": self.get_quick_lists()} def _doctype_contains_a_record(self, name): @@ -204,6 +201,22 @@ class Workspace: return item + @handle_not_exist + def get_number_cards(self): + all_number_cards = [] + if frappe.has_permission("Number Card", throw=False): + number_cards = self.doc.number_cards + for number_card in number_cards: + if frappe.has_permission("Number Card", doc=number_card.number_card_name): + # Translate label + number_card.label = ( + _(number_card.label) if number_card.label else _(number_card.number_card_name) + ) + + all_number_cards.append(number_card) + + return all_number_cards + @handle_not_exist def get_links(self): cards = self.doc.get_link_groups() @@ -349,6 +362,7 @@ def get_desktop_page(page): workspace = Workspace(loads(page)) workspace.build_workspace() return { + "number_cards": workspace.number_cards, "charts": workspace.charts, "shortcuts": workspace.shortcuts, "cards": workspace.cards, @@ -476,6 +490,10 @@ def save_new_widget(doc, page, blocks, new_widgets): if loads(new_widgets): widgets = _dict(loads(new_widgets)) + if widgets.number_card: + doc.number_cards.extend( + new_widget(widgets.number_card, "Workspace Number Card", "number_cards") + ) if widgets.chart: doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts")) if widgets.shortcut: @@ -511,7 +529,7 @@ def save_new_widget(doc, page, blocks, new_widgets): def clean_up(original_page, blocks): page_widgets = {} - for wid in ["shortcut", "card", "chart", "quick_list"]: + for wid in ["number_card", "shortcut", "card", "chart", "quick_list"]: # get list of widget's name from blocks page_widgets[wid] = [x["data"][wid + "_name"] for x in loads(blocks) if x["type"] == wid] diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index d940448cb1..0783a4ab29 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -124,10 +124,10 @@ def get_result(doc, filters, to_date=None): ) ] - filters = frappe.parse_json(filters) - if not filters: filters = [] + elif isinstance(filters, str): + filters = frappe.parse_json(filters) if to_date: filters.append([doc.document_type, "creation", "<", to_date]) diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index edd5c32e99..af5f9c4184 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -21,6 +21,8 @@ "public", "is_hidden", "content", + "number_cards_tab", + "number_cards", "tab_break_2", "charts", "tab_break_15", @@ -181,11 +183,22 @@ "fieldname": "is_hidden", "fieldtype": "Check", "label": "Is Hidden" + }, + { + "fieldname": "number_cards_tab", + "fieldtype": "Tab Break", + "label": "Number Cards" + }, + { + "fieldname": "number_cards", + "fieldtype": "Table", + "label": "Number Cards", + "options": "Workspace Number Card" } ], "in_create": 1, "links": [], - "modified": "2023-01-07 19:37:39.512482", + "modified": "2023-02-15 01:16:56.035205", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", @@ -208,4 +221,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/frappe/desk/doctype/workspace_number_card/__init__.py b/frappe/desk/doctype/workspace_number_card/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/workspace_number_card/workspace_number_card.json b/frappe/desk/doctype/workspace_number_card/workspace_number_card.json new file mode 100644 index 0000000000..f9e3865d74 --- /dev/null +++ b/frappe/desk/doctype/workspace_number_card/workspace_number_card.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-02-15 01:16:26.216201", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "number_card_name", + "label" + ], + "fields": [ + { + "fieldname": "number_card_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Number Card Name", + "options": "Number Card", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-02-15 01:16:26.216201", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Number Card", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/desk/doctype/workspace_number_card/workspace_number_card.py b/frappe/desk/doctype/workspace_number_card/workspace_number_card.py new file mode 100644 index 0000000000..e972f3f525 --- /dev/null +++ b/frappe/desk/doctype/workspace_number_card/workspace_number_card.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WorkspaceNumberCard(Document): + pass diff --git a/frappe/public/js/frappe/views/workspace/blocks/index.js b/frappe/public/js/frappe/views/workspace/blocks/index.js index d15635fba9..afee5ab98a 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/index.js +++ b/frappe/public/js/frappe/views/workspace/blocks/index.js @@ -1,6 +1,7 @@ // import blocks import Header from "./header"; import Paragraph from "./paragraph"; +import NumberCard from "./number_card"; import Card from "./card"; import Chart from "./chart"; import Shortcut from "./shortcut"; @@ -16,6 +17,7 @@ frappe.provide("frappe.workspace_block"); frappe.workspace_block.blocks = { header: Header, paragraph: Paragraph, + number_card: NumberCard, card: Card, chart: Chart, shortcut: Shortcut, diff --git a/frappe/public/js/frappe/views/workspace/blocks/number_card.js b/frappe/public/js/frappe/views/workspace/blocks/number_card.js new file mode 100644 index 0000000000..a952d7666b --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/number_card.js @@ -0,0 +1,62 @@ +import Block from "./block.js"; +export default class NumberCard extends Block { + static get toolbox() { + return { + title: "Number Card", + icon: frappe.utils.icon("income", "sm"), + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly, block }) { + super({ data, api, config, readOnly, block }); + this.sections = {}; + this.col = this.data.col ? this.data.col : "4"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true, + allow_resize: true, + }; + } + + render() { + this.wrapper = document.createElement("div"); + this.new("number_card"); + + if (this.data && this.data.number_card_name) { + let has_data = this.make("number_card", this.data.number_card_name); + if (!has_data) return; + } + + if (!this.readOnly) { + $(this.wrapper).find(".widget").addClass("number_card edit-mode"); + this.add_settings_button(); + this.add_new_block_button(); + } + + return this.wrapper; + } + + validate(savedData) { + if (!savedData.number_card_name) { + return false; + } + + return true; + } + + save() { + return { + number_card_name: this.wrapper.getAttribute("number_card_name"), + col: this.get_col(), + new: this.new_block_widget, + }; + } +} diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index 76f528dad7..fca6f35786 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -387,6 +387,7 @@ frappe.views.Workspace = class Workspace { this.editor.isReady.then(() => { this.editor.configuration.tools.chart.config.page_data = this.page_data; this.editor.configuration.tools.shortcut.config.page_data = this.page_data; + this.editor.configuration.tools.number_card.config.page_data = this.page_data; this.editor.configuration.tools.card.config.page_data = this.page_data; this.editor.configuration.tools.onboarding.config.page_data = this.page_data; this.editor.configuration.tools.quick_list.config.page_data = this.page_data; @@ -1334,9 +1335,16 @@ frappe.views.Workspace = class Workspace { page_data: this.page_data || [], }, }, + number_card: { + class: this.blocks["number_card"], + config: { + page_data: this.page_data || [], + }, + }, spacer: this.blocks["spacer"], HeaderSize: frappe.workspace_block.tunes["header_size"], }; + this.editor = new EditorJS({ data: { blocks: blocks || [], diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 0874f825a6..03502cfd72 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -11,6 +11,7 @@ export default class NumberCardWidget extends Widget { get_config() { return { name: this.name, + number_card_name: this.number_card_name || this.name, label: this.label, color: this.color, hidden: this.hidden, @@ -31,7 +32,7 @@ export default class NumberCardWidget extends Widget { } make_card() { - frappe.model.with_doc("Number Card", this.name).then((card) => { + frappe.model.with_doc("Number Card", this.number_card_name || this.name).then((card) => { if (!card) { if (this.document_type) { frappe.run_serially([ @@ -144,7 +145,6 @@ export default class NumberCardWidget extends Widget { } render_card() { - this.prepare_actions(); this.set_title(); this.set_loading_state(); diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 1696927cd8..56234071e3 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -643,7 +643,7 @@ class NumberCardDialog extends WidgetDialog { } data.stats_filter = this.filter_group && JSON.stringify(this.filter_group.get_filters()); data.document_type = this.document_type; - + data.label = data.label ? data.label : data.card; return data; } } From 71d3448200e34a5d6452e609546b9b2453d0aba4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 23 Feb 2023 10:34:05 +0530 Subject: [PATCH 02/62] fix(UX): Freeze form while applying workflow --- frappe/public/js/frappe/form/workflow.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js index 667f6f5290..cc4ab419db 100644 --- a/frappe/public/js/frappe/form/workflow.js +++ b/frappe/public/js/frappe/form/workflow.js @@ -102,6 +102,7 @@ frappe.ui.form.States = class FormStates { added = true; me.frm.page.add_action_item(__(d.action), function () { // set the workflow_action for use in form scripts + frappe.dom.freeze(); me.frm.selected_workflow_action = d.action; me.frm.script_manager.trigger("before_workflow_action").then(() => { frappe @@ -111,6 +112,7 @@ frappe.ui.form.States = class FormStates { }) .then((doc) => { frappe.model.sync(doc); + frappe.dom.unfreeze(); me.frm.refresh(); me.frm.selected_workflow_action = null; me.frm.script_manager.trigger("after_workflow_action"); From 4bda4bd8c31b3a087522bac867a62e9be2ed1386 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 23 Feb 2023 10:36:01 +0530 Subject: [PATCH 03/62] fix: Reload form on "doc_update" trigger to avoid timestamp conflict --- frappe/public/js/frappe/model/model.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index e4ca3f40b8..ebf1ff3eac 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -149,9 +149,13 @@ $.extend(frappe.model, { cur_frm.doc.doctype === doc.doctype && cur_frm.doc.name === doc.name ) { - if (!frappe.ui.form.is_saving && data.modified != cur_frm.doc.modified) { - doc.__needs_refresh = true; - cur_frm.show_conflict_message(); + if (data.modified !== cur_frm.doc.modified) { + if (!cur_frm.is_dirty()) { + cur_frm.reload_doc(); + } else if (!frappe.ui.form.is_saving) { + doc.__needs_refresh = true; + cur_frm.show_conflict_message(); + } } } else { if (!doc.__unsaved) { From b397ec3858a570e49d3ec424932ab8271c708af8 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 23 Feb 2023 13:45:53 +0530 Subject: [PATCH 04/62] feat: Track count of views on newsletter emails --- frappe/__init__.py | 2 + frappe/core/doctype/communication/email.py | 14 +------ .../email/doctype/email_queue/email_queue.py | 24 ++++++++++-- .../email/doctype/newsletter/newsletter.json | 15 +++++++- frappe/email/doctype/newsletter/newsletter.py | 37 +++++++++++++++++++ frappe/model/document.py | 13 +++++-- frappe/utils/data.py | 13 +++++++ 7 files changed, 97 insertions(+), 21 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index f7208035e5..d570948ca0 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -622,6 +622,7 @@ def sendmail( header=None, print_letterhead=False, with_container=False, + email_read_tracker_method=None, ): """Send email using user's default **Email Account** or global default **Email Account**. @@ -703,6 +704,7 @@ def sendmail( header=header, print_letterhead=print_letterhead, with_container=with_container, + email_read_tracker_method=email_read_tracker_method, ) # build email queue and send the email if send_now is True. diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index fbb34bc7e6..2e199e014d 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -12,6 +12,7 @@ from frappe.utils import ( cint, get_datetime, get_formatted_email, + get_imaginary_pixel_response, get_string_between, list_to_str, split_emails, @@ -249,18 +250,7 @@ def mark_email_as_seen(name: str = None): frappe.log_error("Unable to mark as seen", None, "Communication", name) finally: - frappe.response.update( - { - "type": "binary", - "filename": "imaginary_pixel.png", - "filecontent": ( - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" - b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" - b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" - b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" - ), - } - ) + frappe.response.update(frappe.utils.get_imaginary_pixel_response()) def update_communication_as_read(name): diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 41740281a8..2928d0c6bd 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -283,7 +283,9 @@ class SendMailContext: if not message: return "" - message = message.replace(self.message_placeholder("tracker"), self.get_tracker_str()) + message = message.replace( + self.message_placeholder("tracker"), self.get_tracker_str(recipient_email) + ) message = message.replace( self.message_placeholder("unsubscribe_url"), self.get_unsubscribe_str(recipient_email) ) @@ -294,10 +296,20 @@ class SendMailContext: message = self.include_attachments(message) return message - def get_tracker_str(self) -> str: - if frappe.conf.use_ssl and self.email_account_doc.track_email_status: - tracker_url_html = f'' + def get_tracker_str(self, recipient_email) -> str: + tracker_url = "" + if self.queue_doc.get("email_read_tracker_method"): + tracker_url = frappe.call( + self.queue_doc.email_read_tracker_method, recipient_email=recipient_email + ) + + elif frappe.conf.use_ssl and self.email_account_doc.track_email_status: + tracker_url = f"{get_url()}/api/method/frappe.core.doctype.communication.email.mark_email_as_seen?name={self.queue_doc.communication}" + + if tracker_url: + tracker_url_html = f'' return quopri.encodestring(tracker_url_html.encode()).decode() + return "" def get_unsubscribe_str(self, recipient_email: str) -> str: @@ -428,6 +440,7 @@ class QueueBuilder: header=None, print_letterhead=False, with_container=False, + email_read_tracker_method=None, ): """Add email to sending queue (Email Queue) @@ -452,6 +465,7 @@ class QueueBuilder: :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id :param header: Append header in email (boolean) :param with_container: Wraps email inside styled container + :param email_read_tracker_method: A method that returns URL for tracking whether an email is read by the recipient. """ self._unsubscribe_method = unsubscribe_method @@ -486,6 +500,7 @@ class QueueBuilder: self.is_notification = is_notification self.inline_images = inline_images self.print_letterhead = print_letterhead + self.email_read_tracker_method = email_read_tracker_method @property def unsubscribe_method(self): @@ -723,6 +738,7 @@ class QueueBuilder: "show_as_cc": ",".join(self.final_cc()), "show_as_bcc": ",".join(self.bcc), "email_account": email_account_name or None, + "email_read_tracker_method": self.email_read_tracker_method, } if include_recipients: diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index b42f4755cb..93e96732bd 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -12,6 +12,7 @@ "column_break_3", "total_recipients", "column_break_12", + "total_views", "email_sent", "from_section", "sender_name", @@ -228,6 +229,14 @@ { "fieldname": "column_break_3", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "total_views", + "fieldtype": "Int", + "label": "Total Views", + "no_copy": 1, + "read_only": 1 } ], "has_web_view": 1, @@ -236,7 +245,7 @@ "index_web_pages_for_search": 1, "is_published_field": "published", "links": [], - "modified": "2022-03-09 01:48:16.741603", + "modified": "2023-02-23 12:53:18.478018", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", @@ -259,6 +268,8 @@ "route": "newsletters", "sort_field": "modified", "sort_order": "ASC", + "states": [], "title_field": "subject", - "track_changes": 1 + "track_changes": 1, + "track_seen": 1 } \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 4745a8f1ca..da6a2d7f90 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -186,6 +186,7 @@ class Newsletter(WebsiteGenerator): queue_separately=True, send_priority=0, args=args, + email_read_tracker_method=get_newsletter_read_tracker_method(self.name), ) frappe.db.auto_commit_on_many_writes = is_auto_commit_set @@ -347,3 +348,39 @@ def send_scheduled_email(): if not frappe.flags.in_test: frappe.db.commit() + + +@frappe.whitelist(allow_guest=True) +def newsletter_email_read(recipient_email, newsletter_name): + verify_request() + try: + doc = frappe.get_doc("Newsletter", newsletter_name) + if doc.add_viewed(recipient_email, force=True, unique_views=True): + doc.db_set("total_views", doc.total_views + 1) + + except Exception: + frappe.log_error( + f"Unable to mark as viewed for {recipient_email}", None, "Newsletter", newsletter_name + ) + + finally: + frappe.response.update(frappe.utils.get_imaginary_pixel_response()) + + +def get_newsletter_read_tracker_method(newsletter_name): + """Returns the read url for the newsletter.""" + + def tracker_method(recipient_email): + from frappe.utils import get_url + from frappe.utils.verified_command import get_signed_params + + params = { + "recipient_email": recipient_email, + "newsletter_name": newsletter_name, + } + unsubscribe_method = ( + "/api/method/frappe.email.doctype.newsletter.newsletter.newsletter_email_read" + ) + return get_url(f"{unsubscribe_method}?{get_signed_params(params)}") + + return tracker_method diff --git a/frappe/model/document.py b/frappe/model/document.py index 8a99676b60..7c45b9dd1d 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1382,16 +1382,21 @@ class Document(BaseDocument): frappe.db.set_value(self.doctype, self.name, "_seen", json.dumps(_seen), update_modified=False) frappe.local.flags.commit = True - def add_viewed(self, user=None): + def add_viewed(self, user=None, force=False, unique_views=False): """add log to communication when a user views a document""" if not user: user = frappe.session.user - if hasattr(self.meta, "track_views") and self.meta.track_views: + if unique_views and frappe.db.exists( + "View Log", {"reference_doctype": self.doctype, "reference_name": self.name, "viewed_by": user} + ): + return + + if (hasattr(self.meta, "track_views") and self.meta.track_views) or force: view_log = frappe.get_doc( { "doctype": "View Log", - "viewed_by": frappe.session.user, + "viewed_by": user, "reference_doctype": self.doctype, "reference_name": self.name, } @@ -1402,6 +1407,8 @@ class Document(BaseDocument): view_log.insert(ignore_permissions=True) frappe.local.flags.commit = True + return view_log + def log_error(self, title=None, message=None): """Helper function to create an Error Log""" return frappe.log_error( diff --git a/frappe/utils/data.py b/frappe/utils/data.py index f17a6e59d0..b2ef5920a6 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -2166,3 +2166,16 @@ def get_job_name(key: str, doctype: str = None, doc_name: str = None) -> str: if doc_name: job_name += f"_{doc_name}" return job_name + + +def get_imaginary_pixel_response(): + return { + "type": "binary", + "filename": "imaginary_pixel.png", + "filecontent": ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" + b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r" + b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0" + b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" + ), + } From 3a130abfc5729244538ca24bbcd1c1329aef5baf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Feb 2023 15:03:11 +0530 Subject: [PATCH 05/62] fix: extension extracted if at least one dot, because if not dot (#19946) (#20142) (cherry picked from commit 53c41e0ce6bf9985786a570c43fd32c79332c44c) Co-authored-by: Jaime --- frappe/website/page_renderers/static_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/page_renderers/static_page.py b/frappe/website/page_renderers/static_page.py index 1f26de1514..04e58ff217 100644 --- a/frappe/website/page_renderers/static_page.py +++ b/frappe/website/page_renderers/static_page.py @@ -31,7 +31,7 @@ class StaticPage(BaseRenderer): return self.is_valid_file_path() and self.file_path def is_valid_file_path(self): - extension = self.path.rsplit(".", 1)[-1] + extension = self.path.rsplit(".", 1)[-1] if "." in self.path else "" if extension in UNSUPPORTED_STATIC_PAGE_TYPES: return False return True From 66ef29907f3f4ba6072b23e0927f00e243909be9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 20 Feb 2023 14:36:45 +0530 Subject: [PATCH 06/62] chore: Better webhook loging --- .../integrations/doctype/webhook/webhook.py | 19 +++++++++--- .../webhook_request_log.json | 31 +++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 6d2773a29c..242468624f 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -128,16 +128,16 @@ def enqueue_webhook(doc, webhook) -> None: ) r.raise_for_status() frappe.logger().debug({"webhook_success": r.text}) - log_request(webhook.request_url, headers, data, r) + log_request(doc.doctype, doc.name, webhook.request_url, headers, data, r) break except requests.exceptions.ReadTimeout as e: frappe.logger().debug({"webhook_error": e, "try": i + 1}) - log_request(webhook.request_url, headers, data) + log_request(doc.doctype, doc.name, webhook.request_url, headers, data) except Exception as e: frappe.logger().debug({"webhook_error": e, "try": i + 1}) - log_request(webhook.request_url, headers, data, r) + log_request(doc.doctype, doc.name, webhook.request_url, headers, data, r, e) sleep(3 * i + 1) if i != 2: continue @@ -145,15 +145,26 @@ def enqueue_webhook(doc, webhook) -> None: webhook.log_error("Webhook failed") -def log_request(url: str, headers: dict, data: dict, res: requests.Response | None = None): +def log_request( + document_type: str, + docname: str, + url: str, + headers: dict, + data: dict, + res: requests.Response | None = None, + error: str | None = None, +): request_log = frappe.get_doc( { "doctype": "Webhook Request Log", + "reference_document_type": document_type, + "reference_document": docname, "user": frappe.session.user if frappe.session.user else None, "url": url, "headers": frappe.as_json(headers) if headers else None, "data": frappe.as_json(data) if data else None, "response": frappe.as_json(res.json()) if res else None, + "error": error, } ) 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 d9410a2f82..0a4a9d32b4 100644 --- a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json +++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json @@ -4,14 +4,18 @@ "creation": "2021-05-24 21:35:59.104776", "doctype": "DocType", "editable_grid": 1, - "engine": "InnoDB", + "document_type": "System", + "engine": "MyISAM", "field_order": [ "user", + "reference_document_type", + "reference_document", "headers", "data", "column_break_4", "url", - "response" + "response", + "error" ], "fields": [ { @@ -51,12 +55,33 @@ "label": "User", "options": "User", "read_only": 1 + }, + { + "fieldname": "reference_document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Document Type", + "options": "DocType" + }, + { + "fieldname": "reference_document", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference Document", + "options": "reference_document_type" + }, + { + "fieldname": "error", + "fieldtype": "Text", + "label": "Error", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-05-03 09:33:49.240777", + "modified": "2023-02-20 13:45:16.662079", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook Request Log", From e6d5f1fe2e0d534e6f9a7d4e8bba7c57206ccbbd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Feb 2023 15:29:09 +0530 Subject: [PATCH 07/62] revert: engine change --- .../doctype/webhook_request_log/webhook_request_log.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 0a4a9d32b4..8f38878f1f 100644 --- a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json +++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json @@ -4,8 +4,7 @@ "creation": "2021-05-24 21:35:59.104776", "doctype": "DocType", "editable_grid": 1, - "document_type": "System", - "engine": "MyISAM", + "engine": "InnoDB", "field_order": [ "user", "reference_document_type", From 1fbd6badd1eecfcf9ba828f9f234e231b60b8ed0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Feb 2023 15:41:33 +0530 Subject: [PATCH 08/62] fix: log full traceback --- frappe/integrations/doctype/webhook/webhook.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 242468624f..e3740f9d6b 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -137,12 +137,10 @@ def enqueue_webhook(doc, webhook) -> None: except Exception as e: frappe.logger().debug({"webhook_error": e, "try": i + 1}) - log_request(doc.doctype, doc.name, webhook.request_url, headers, data, r, e) + log_request(doc.doctype, doc.name, webhook.request_url, headers, data, r) sleep(3 * i + 1) if i != 2: continue - else: - webhook.log_error("Webhook failed") def log_request( @@ -152,7 +150,6 @@ def log_request( headers: dict, data: dict, res: requests.Response | None = None, - error: str | None = None, ): request_log = frappe.get_doc( { @@ -164,7 +161,7 @@ def log_request( "headers": frappe.as_json(headers) if headers else None, "data": frappe.as_json(data) if data else None, "response": frappe.as_json(res.json()) if res else None, - "error": error, + "error": frappe.get_traceback(), } ) From 22f5a60ebbc0155c68b783a75513b46ef33feb1d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 23 Feb 2023 15:49:17 +0530 Subject: [PATCH 09/62] feat: make webhook requests log clearable --- frappe/core/doctype/log_settings/log_settings.py | 1 + .../doctype/webhook_request_log/webhook_request_log.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index eaa55f4bca..d0e639dfa1 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -19,6 +19,7 @@ DEFAULT_LOGTYPES_RETENTION = { "Route History": 90, "Submission Queue": 30, "Prepared Report": 30, + "Webhook Request Log": 30, } diff --git a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py index 8fbc73f5e5..175215f4d4 100644 --- a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py +++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.py @@ -1,9 +1,15 @@ # Copyright (c) 2021, Frappe Technologies and contributors # License: MIT. See LICENSE -# import frappe +import frappe from frappe.model.document import Document class WebhookRequestLog(Document): - pass + @staticmethod + def clear_old_logs(days=30): + from frappe.query_builder import Interval + from frappe.query_builder.functions import Now + + table = frappe.qb.DocType("Webhook Request Log") + frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) From 7d5c98c20621408a5ec79291741d89fcee7cc044 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 23 Feb 2023 16:19:07 +0530 Subject: [PATCH 10/62] feat: Add UTM tracking parameters to internal links in newsletters - Also, extend web page view to store UTM tracking data. --- frappe/email/doctype/newsletter/newsletter.py | 23 +++++++++- frappe/utils/data.py | 29 +++++++++++- .../doctype/web_page_view/web_page_view.json | 45 ++++++++++++++++++- .../doctype/web_page_view/web_page_view.py | 19 +++++++- frappe/www/website_script.js | 8 +++- 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index da6a2d7f90..55a3b03473 100644 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -198,7 +198,28 @@ class Newsletter(WebsiteGenerator): if self.content_type == "HTML": message = self.message_html - return frappe.render_template(message, {"doc": self.as_dict()}) + html = frappe.render_template(message, {"doc": self.as_dict()}) + + return self.add_utm(html) + + def add_utm(self, html: str) -> str: + """Add UTM parameters to internal links in the newsletter.""" + from bs4 import BeautifulSoup + + soup = BeautifulSoup(html, "html.parser") + + links = soup.find_all("a") + for link in links: + href = link.get("href") + if href and not href.startswith("#"): + if not frappe.utils.is_internal_link(href): + continue + new_href = frappe.utils.add_utm_to_url( + href, source="Newsletter", medium="Email", campaign=self.name + ) + link["href"] = new_href + + return str(soup) def get_recipients(self) -> list[str]: """Get recipients from Email Group""" diff --git a/frappe/utils/data.py b/frappe/utils/data.py index b2ef5920a6..bf8e697c67 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -12,7 +12,7 @@ import typing from code import compile_command from enum import Enum from typing import Any, Literal, Optional, TypeVar, Union -from urllib.parse import quote, urljoin +from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlparse, urlunparse from click import secho @@ -2179,3 +2179,30 @@ def get_imaginary_pixel_response(): b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82" ), } + + +def is_internal_link(link: str) -> bool: + if link.startswith("/"): + return True + return urlparse(link).netloc == urlparse(frappe.utils.get_url()).netloc + + +def add_utm_to_url(url: str, source: str, medium: str, campaign: str) -> str: + """Add utm parameters to url. + + Args: + url (str): URL to add utm parameters to. + utm (dict[str, str]): Dictionary of utm parameters. + + Returns: + str: URL with utm parameters added. + """ + url_parts = list(urlparse(url)) + query = dict(parse_qsl(url_parts[4])) | { + "utm_source": source, + "utm_medium": medium, + "utm_campaign": campaign, + } + + url_parts[4] = urlencode(query) + return urlunparse(url_parts) diff --git a/frappe/website/doctype/web_page_view/web_page_view.json b/frappe/website/doctype/web_page_view/web_page_view.json index 7548ed5f83..d6783aec8f 100644 --- a/frappe/website/doctype/web_page_view/web_page_view.json +++ b/frappe/website/doctype/web_page_view/web_page_view.json @@ -12,7 +12,13 @@ "browser_version", "is_unique", "time_zone", - "user_agent" + "user_agent", + "utm_trackers_section", + "utm_source", + "utm_medium", + "utm_campaign", + "utm_content", + "utm_term" ], "fields": [ { @@ -53,11 +59,46 @@ "fieldname": "user_agent", "fieldtype": "Data", "label": "User Agent" + }, + { + "fieldname": "utm_trackers_section", + "fieldtype": "Section Break", + "label": "UTM Trackers" + }, + { + "fieldname": "utm_source", + "fieldtype": "Data", + "label": "Source", + "read_only": 1 + }, + { + "fieldname": "utm_medium", + "fieldtype": "Data", + "label": "Medium", + "read_only": 1 + }, + { + "fieldname": "utm_campaign", + "fieldtype": "Data", + "label": "Campaign", + "read_only": 1 + }, + { + "fieldname": "utm_content", + "fieldtype": "Data", + "label": "Content", + "read_only": 1 + }, + { + "fieldname": "utm_term", + "fieldtype": "Data", + "label": "Term", + "read_only": 1 } ], "in_create": 1, "links": [], - "modified": "2022-09-13 15:38:25.401797", + "modified": "2023-02-23 15:10:14.134789", "modified_by": "Administrator", "module": "Website", "name": "Web Page View", diff --git a/frappe/website/doctype/web_page_view/web_page_view.py b/frappe/website/doctype/web_page_view/web_page_view.py index 40c11782f5..e60ff0134b 100644 --- a/frappe/website/doctype/web_page_view/web_page_view.py +++ b/frappe/website/doctype/web_page_view/web_page_view.py @@ -10,7 +10,19 @@ class WebPageView(Document): @frappe.whitelist(allow_guest=True) -def make_view_log(path, referrer=None, browser=None, version=None, url=None, user_tz=None): +def make_view_log( + path, + referrer=None, + browser=None, + version=None, + url=None, + user_tz=None, + utm_source=None, + utm_medium=None, + utm_campaign=None, + utm_term=None, + utm_content=None, +): if not is_tracking_enabled(): return @@ -35,6 +47,11 @@ def make_view_log(path, referrer=None, browser=None, version=None, url=None, use view.time_zone = user_tz view.user_agent = user_agent view.is_unique = is_unique + view.utm_source = utm_source + view.utm_medium = utm_medium + view.utm_campaign = utm_campaign + view.utm_term = utm_term + view.utm_content = utm_content try: if frappe.flags.read_only: diff --git a/frappe/www/website_script.js b/frappe/www/website_script.js index ce9c28e9d9..391862e685 100644 --- a/frappe/www/website_script.js +++ b/frappe/www/website_script.js @@ -20,13 +20,19 @@ ga('send', 'pageview'); if (navigator.doNotTrack != 1 && !window.is_404) { frappe.ready(() => { let browser = frappe.utils.get_browser(); + let query_params = frappe.utils.get_query_params(); frappe.call("frappe.website.doctype.web_page_view.web_page_view.make_view_log", { path: location.pathname, referrer: document.referrer, browser: browser.name, version: browser.version, url: location.origin, - user_tz: Intl.DateTimeFormat().resolvedOptions().timeZone + user_tz: Intl.DateTimeFormat().resolvedOptions().timeZone, + utm_source: query_params.utm_source, + utm_medium: query_params.utm_medium, + utm_campaign: query_params.utm_campaign, + utm_term: query_params.utm_term, + utm_content: query_params.utm_content, }) }) } From 19249aec4e2620167ac66ffa6e4532662815a6a2 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 23 Feb 2023 18:10:08 +0530 Subject: [PATCH 11/62] fix: send updates from google calendar events (#20075) --- .../integrations/doctype/google_calendar/google_calendar.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 534e3c1ac7..5056f536fc 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -422,7 +422,10 @@ def insert_event_in_google_calendar(doc, method=None): event = ( google_calendar.events() .insert( - calendarId=doc.google_calendar_id, body=event, conferenceDataVersion=conference_data_version + calendarId=doc.google_calendar_id, + body=event, + conferenceDataVersion=conference_data_version, + sendUpdates="all", ) .execute() ) @@ -504,6 +507,7 @@ def update_event_in_google_calendar(doc, method=None): eventId=doc.google_calendar_event_id, body=event, conferenceDataVersion=conference_data_version, + sendUpdates="all", ) .execute() ) From d217c94a084477ea631e88f6ff111a75e5c2eb91 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 24 Feb 2023 11:04:19 +0530 Subject: [PATCH 12/62] fix(print): Overflow wrap anywhere if a long word is found overflow-wrap: anywhere; breaks anywhere in the string intead of just word end. This is helpful to avoid breaking of print grid when an unexpectedly long word is passed. --- frappe/public/js/frappe/views/reports/print_grid.html | 7 +++---- frappe/public/scss/print.bundle.scss | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html index 6f693bc932..a1e629fb0c 100644 --- a/frappe/public/js/frappe/views/reports/print_grid.html +++ b/frappe/public/js/frappe/views/reports/print_grid.html @@ -36,10 +36,9 @@ {% for col in columns %} {% if col.name && col._id !== "_check" %} - - {% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %} - - + {% var value = col.fieldname ? row[col.fieldname] : row[col.id] || "" %} + {% var longest_word = value.split(' ').reduce((longest, word) => word.length > longest.length ? word : longest, ''); %} + 45 %} class="overflow-wrap-anywhere" {% endif %}> {% format_data = row.is_total_row && ["Currency", "Float"].includes(col.fieldtype) ? data[0] : row %} {% if (row.is_total_row && col._index == 0) { %} diff --git a/frappe/public/scss/print.bundle.scss b/frappe/public/scss/print.bundle.scss index 3e8baddcb6..d018145755 100644 --- a/frappe/public/scss/print.bundle.scss +++ b/frappe/public/scss/print.bundle.scss @@ -34,4 +34,10 @@ img { margin: auto; } +} + +.overflow-wrap-anywhere { + * { + overflow-wrap: anywhere; + } } \ No newline at end of file From 23bfd5d1b6fe79f7edc3ba61b27e4e040d6a0724 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 22 Feb 2023 11:25:05 +0530 Subject: [PATCH 13/62] fix: doctype editing message --- frappe/core/doctype/doctype/doctype.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index d9c31d312e..d92277152c 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -55,7 +55,7 @@ frappe.ui.form.on("DocType", { msg += __("If you just want to customize for your site, use {0} instead.", [ customize_form_link, ]); - frm.dashboard.add_comment(msg, "yellow"); + frm.dashboard.add_comment(msg, "yellow", true); } if (frm.is_new()) { From 0925453adb80266812ae81c5411c5d9209512b9e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 22 Feb 2023 11:29:37 +0530 Subject: [PATCH 14/62] chore: format doctype controller boilerplate --- frappe/core/doctype/doctype/boilerplate/controller._py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/doctype/boilerplate/controller._py b/frappe/core/doctype/doctype/boilerplate/controller._py index 6db99def55..d8f02bf09c 100644 --- a/frappe/core/doctype/doctype/boilerplate/controller._py +++ b/frappe/core/doctype/doctype/boilerplate/controller._py @@ -4,5 +4,6 @@ # import frappe {base_class_import} + class {classname}({base_class}): {custom_controller} From 73cd823a0ae2bbe3e3344e5f297aaaa7386f83f9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 22 Feb 2023 11:01:52 +0530 Subject: [PATCH 15/62] feat: Document Reminders There is `Notification` doctype but it doesn't work so well for small one-off reminders. Imagine these scenarios: 1. Remind me to follow up on this lead in 5 days. 2. Remind me to revoke temporary access I am giving to this user. For such scenarios, I am proposing a simple reminder system built into framework. All it does is: 1. For any document you can set a reminder with time and message. 2. When time comes you'll get a system notification with message and link to the document. Permissions: 1. Users can only see their own set reminders. --- .../doctype/docreminder/__init__.py | 0 .../doctype/docreminder/docreminder.js | 8 ++ .../doctype/docreminder/docreminder.json | 85 +++++++++++++++++ .../doctype/docreminder/docreminder.py | 78 ++++++++++++++++ .../doctype/docreminder/test_docreminder.py | 28 ++++++ .../core/doctype/log_settings/log_settings.py | 1 + frappe/hooks.py | 1 + frappe/public/js/frappe/form/reminders.js | 93 +++++++++++++++++++ frappe/public/js/frappe/form/toolbar.js | 14 +++ 9 files changed, 308 insertions(+) create mode 100644 frappe/automation/doctype/docreminder/__init__.py create mode 100644 frappe/automation/doctype/docreminder/docreminder.js create mode 100644 frappe/automation/doctype/docreminder/docreminder.json create mode 100644 frappe/automation/doctype/docreminder/docreminder.py create mode 100644 frappe/automation/doctype/docreminder/test_docreminder.py create mode 100644 frappe/public/js/frappe/form/reminders.js diff --git a/frappe/automation/doctype/docreminder/__init__.py b/frappe/automation/doctype/docreminder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/docreminder/docreminder.js b/frappe/automation/doctype/docreminder/docreminder.js new file mode 100644 index 0000000000..a3f08c2c5e --- /dev/null +++ b/frappe/automation/doctype/docreminder/docreminder.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("DocReminder", { +// refresh(frm) { + +// }, +// }); diff --git a/frappe/automation/doctype/docreminder/docreminder.json b/frappe/automation/doctype/docreminder/docreminder.json new file mode 100644 index 0000000000..38b0f681ca --- /dev/null +++ b/frappe/automation/doctype/docreminder/docreminder.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2023-02-22 11:23:58.183276", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "reminder_doctype", + "reminder_docname", + "remind_at", + "description", + "notified" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "reminder_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType" + }, + { + "fieldname": "reminder_docname", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Document Name", + "options": "reminder_doctype" + }, + { + "fieldname": "remind_at", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Remind At", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "description", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "notified", + "fieldtype": "Check", + "label": "notified" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-02-24 12:28:00.642813", + "modified_by": "Administrator", + "module": "Automation", + "name": "DocReminder", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "if_owner": 1, + "read": 1, + "role": "All", + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "description" +} \ No newline at end of file diff --git a/frappe/automation/doctype/docreminder/docreminder.py b/frappe/automation/doctype/docreminder/docreminder.py new file mode 100644 index 0000000000..61832fa2a9 --- /dev/null +++ b/frappe/automation/doctype/docreminder/docreminder.py @@ -0,0 +1,78 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import cint +from frappe.utils.data import add_to_date, get_datetime, now_datetime + + +class DocReminder(Document): + @staticmethod + def clear_old_logs(days=30): + from frappe.query_builder import Interval + from frappe.query_builder.functions import Now + + table = frappe.qb.DocType("DocReminder") + frappe.db.delete(table, filters=(table.remind_at < (Now() - Interval(days=days)))) + + def validate(self): + self.user = frappe.session.user + if get_datetime(self.remind_at) < now_datetime(): + frappe.throw(_("Reminder cannot be created in past.")) + + def send_reminder(self): + if self.notified: + return + + self.db_set("notified", 1, update_modified=False) + + try: + notification = frappe.new_doc("Notification Log") + notification.for_user = self.user + notification.set("type", "Alert") + notification.document_type = self.reminder_doctype + notification.document_name = self.reminder_docname + notification.subject = self.description + notification.insert() + except Exception: + self.log_error("Failed to send reminder") + + +@frappe.whitelist() +def create_new_reminder( + remind_at: str, + description: str, + reminder_doctype: str | None = None, + reminder_docname: str | None = None, +): + reminder = frappe.new_doc("DocReminder") + + reminder.description = description + reminder.remind_at = remind_at + reminder.reminder_doctype = reminder_doctype + reminder.reminder_docname = reminder_docname + + return reminder.insert() + + +def send_reminders(): + # Ensure that we send all reminders that might be before next job execution. + job_freq = cint(frappe.get_conf().scheduler_interval) or 240 + upper_threshold = add_to_date(now_datetime(), seconds=job_freq, as_string=True, as_datetime=True) + + lower_threshold = add_to_date(now_datetime(), hours=-8, as_string=True, as_datetime=True) + + pending_reminders = frappe.get_all( + "DocReminder", + filters=[ + ("remind_at", "<=", upper_threshold), + ("remind_at", ">=", lower_threshold), # dont send too old reminders if failed to send + ("notified", "=", 0), + ], + pluck="name", + ) + + for reminder in pending_reminders: + frappe.get_doc("DocReminder", reminder).send_reminder() diff --git a/frappe/automation/doctype/docreminder/test_docreminder.py b/frappe/automation/doctype/docreminder/test_docreminder.py new file mode 100644 index 0000000000..54cf47fc2e --- /dev/null +++ b/frappe/automation/doctype/docreminder/test_docreminder.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See license.txt + +import frappe +from frappe.automation.doctype.docreminder.docreminder import create_new_reminder, send_reminders +from frappe.desk.doctype.notification_log.notification_log import get_notification_logs +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_to_date, now_datetime + + +class TestDocReminder(FrappeTestCase): + def test_reminder(self): + + description = "TEST_REMINDER" + + create_new_reminder( + remind_at=add_to_date(now_datetime(), minutes=1, as_datetime=True, as_string=True), + description=description, + ) + + send_reminders() + + notifications = get_notification_logs()["notification_logs"] + self.assertIn( + description, + [n.subject for n in notifications], + msg=f"Failed to find reminder notification \n{notifications}", + ) diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index d0e639dfa1..eb617d2ee2 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -20,6 +20,7 @@ DEFAULT_LOGTYPES_RETENTION = { "Submission Queue": 30, "Prepared Report": 30, "Webhook Request Log": 30, + "DocReminder": 30, } diff --git a/frappe/hooks.py b/frappe/hooks.py index 317439c358..aaed06124f 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -194,6 +194,7 @@ scheduler_events = { "frappe.email.doctype.email_account.email_account.notify_unreplied", "frappe.utils.global_search.sync_global_search", "frappe.monitor.flush", + "frappe.automation.doctype.docreminder.docreminder.send_reminders", ], "hourly": [ "frappe.model.utils.link_count.update_link_count", diff --git a/frappe/public/js/frappe/form/reminders.js b/frappe/public/js/frappe/form/reminders.js new file mode 100644 index 0000000000..17499ecfe9 --- /dev/null +++ b/frappe/public/js/frappe/form/reminders.js @@ -0,0 +1,93 @@ +export class ReminderManager { + constructor({ frm }) { + this.frm = frm; // can be optional if not setting for document. + } + + show() { + let me = this; + this.dialog = new frappe.ui.Dialog({ + title: __("Create a Reminder"), + fields: [ + { + fieldtype: "Select", + label: __("Remind Me In"), + fieldname: "remind_me_in", + options: [ + { label: __("30 minutes"), value: "30_minutes" }, + { label: __("1 hour"), value: "1_hour" }, + { label: __("4 hours"), value: "4_hours" }, + { label: __("1 Day"), value: "1_day" }, + ], + onchange: () => { + me.convert_period_to_absolute_time(); + }, + }, + { + fieldtype: "Column Break", + fieldname: "col_break_1", + }, + { + fieldtype: "Datetime", + label: __("Remind At"), + fieldname: "remind_at", + reqd: 1, + onchange: () => { + // TODO: reset remind_me_in only if user modified time. + // me.dialog.set_value("remind_me_in", ""); + }, + }, + { + fieldtype: "Section Break", + fieldname: "divider_between_message_and_time", + }, + { + fieldtype: "Small Text", + label: __("Description"), + fieldname: "description", + reqd: 1, + }, + ], + primary_action_label: __("Create"), + primary_action: () => { + this.create_reminder(); + this.dialog.hide(); + }, + secondary_action_label: __("Cancel"), + secondary_action: () => { + this.dialog.hide(); + }, + }); + + this.dialog.fields_dict.remind_at.datepicker?.update({ + minDate: frappe.datetime.str_to_obj(frappe.datetime.now_datetime()), + }); + + this.dialog.show(); + } + + convert_period_to_absolute_time() { + const period = this.dialog.get_value("remind_me_in"); + if (!period) return; + + const now_time = frappe.datetime.str_to_obj(frappe.datetime.now_datetime()); + let [magnitude, unit] = period.split("_"); + + let time_to_set = moment(now_time) + .add(magnitude, unit) + .format(frappe.defaultDatetimeFormat); + this.dialog.set_value("remind_at", time_to_set); + } + + create_reminder() { + frappe + .xcall("frappe.automation.doctype.docreminder.docreminder.create_new_reminder", { + remind_at: this.dialog.get_value("remind_at"), + description: this.dialog.get_value("description"), + reminder_doctype: this.frm?.doc.doctype, + reminder_docname: this.frm?.doc.name, + }) + .then((reminder) => { + frappe.show_alert(__("Reminder set at {0}", [reminder.remind_at])); + }); + } +} diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 0052ccf5c2..497ca68cd3 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -2,6 +2,7 @@ // MIT License. See license.txt import "./linked_with"; import "./form_viewers"; +import { ReminderManager } from "./reminders"; frappe.ui.form.Toolbar = class Toolbar { constructor(opts) { @@ -436,6 +437,19 @@ frappe.ui.form.Toolbar = class Toolbar { ); } + this.page.add_menu_item( + __("Remind Me"), + () => { + let reminder_maanger = new ReminderManager({ frm: this.frm }); + reminder_maanger.show(); + }, + true, + { + shortcut: "Shift+R", + condition: () => !this.frm.is_new(), + } + ); + this.make_customize_buttons(); // Auto Repeat From 9a3a16f25bbd85e6f0e5a2228009fd9daf71e6e6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 24 Feb 2023 11:50:08 +0530 Subject: [PATCH 16/62] fix(UX): enable datetime selection on custom period only --- frappe/public/js/frappe/form/reminders.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/reminders.js b/frappe/public/js/frappe/form/reminders.js index 17499ecfe9..69b792d79a 100644 --- a/frappe/public/js/frappe/form/reminders.js +++ b/frappe/public/js/frappe/form/reminders.js @@ -17,9 +17,13 @@ export class ReminderManager { { label: __("1 hour"), value: "1_hour" }, { label: __("4 hours"), value: "4_hours" }, { label: __("1 Day"), value: "1_day" }, + { label: __("Custom"), value: "custom" }, ], onchange: () => { me.convert_period_to_absolute_time(); + me.dialog.fields_dict.remind_at.df.read_only = + me.dialog.get_value("remind_me_in") != "custom"; + me.dialog.fields_dict.remind_at.refresh(); }, }, { @@ -31,6 +35,7 @@ export class ReminderManager { label: __("Remind At"), fieldname: "remind_at", reqd: 1, + read_only: 1, onchange: () => { // TODO: reset remind_me_in only if user modified time. // me.dialog.set_value("remind_me_in", ""); @@ -67,7 +72,7 @@ export class ReminderManager { convert_period_to_absolute_time() { const period = this.dialog.get_value("remind_me_in"); - if (!period) return; + if (!period || period == "custom") return; const now_time = frappe.datetime.str_to_obj(frappe.datetime.now_datetime()); let [magnitude, unit] = period.split("_"); From c1740955f7bc8dc8fb558cc1b36d0b63747393ce Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 24 Feb 2023 11:56:04 +0530 Subject: [PATCH 17/62] fix(UX): Set default time to 1hr and update selector --- frappe/public/js/frappe/form/reminders.js | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/frappe/public/js/frappe/form/reminders.js b/frappe/public/js/frappe/form/reminders.js index 69b792d79a..f417a709a0 100644 --- a/frappe/public/js/frappe/form/reminders.js +++ b/frappe/public/js/frappe/form/reminders.js @@ -19,11 +19,9 @@ export class ReminderManager { { label: __("1 Day"), value: "1_day" }, { label: __("Custom"), value: "custom" }, ], + default: "1_hour", onchange: () => { - me.convert_period_to_absolute_time(); - me.dialog.fields_dict.remind_at.df.read_only = - me.dialog.get_value("remind_me_in") != "custom"; - me.dialog.fields_dict.remind_at.refresh(); + me._update_datetime_selector(); }, }, { @@ -36,10 +34,6 @@ export class ReminderManager { fieldname: "remind_at", reqd: 1, read_only: 1, - onchange: () => { - // TODO: reset remind_me_in only if user modified time. - // me.dialog.set_value("remind_me_in", ""); - }, }, { fieldtype: "Section Break", @@ -62,15 +56,21 @@ export class ReminderManager { this.dialog.hide(); }, }); - - this.dialog.fields_dict.remind_at.datepicker?.update({ - minDate: frappe.datetime.str_to_obj(frappe.datetime.now_datetime()), - }); - + this._update_datetime_selector(); this.dialog.show(); } - convert_period_to_absolute_time() { + _update_datetime_selector() { + this._convert_period_to_absolute_time(); + this.dialog.fields_dict.remind_at.df.read_only = + this.dialog.get_value("remind_me_in") != "custom"; + this.dialog.fields_dict.remind_at.refresh(); + this.dialog.fields_dict.remind_at.datepicker?.update({ + minDate: frappe.datetime.str_to_obj(frappe.datetime.now_datetime()), + }); + } + + _convert_period_to_absolute_time() { const period = this.dialog.get_value("remind_me_in"); if (!period || period == "custom") return; From 2b32dc054c721f27447091e63ce769cecf8d0acf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 24 Feb 2023 12:20:22 +0530 Subject: [PATCH 18/62] chore: rename "field" -> "notification_log" field makes no sense here. --- .../frappe/ui/notifications/notifications.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index dab436acc7..a0c0257f1d 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -237,18 +237,18 @@ class NotificationsView extends BaseNotificationsView { this.change_activity_status(); } - get_dropdown_item_html(field) { - let doc_link = this.get_item_link(field); + get_dropdown_item_html(notification_log) { + let doc_link = this.get_item_link(notification_log); - let read_class = field.read ? "" : "unread"; - let message = field.subject; + let read_class = notification_log.read ? "" : "unread"; + let message = notification_log.subject; let title = message.match(/(.*?)<\/b>/); message = title ? message.replace(title[1], frappe.ellipsis(strip_html(title[1]), 100)) : message; - let timestamp = frappe.datetime.comment_when(field.creation); + let timestamp = frappe.datetime.comment_when(notification_log.creation); let message_html = `
${message}
@@ -256,12 +256,12 @@ class NotificationsView extends BaseNotificationsView {
`; - let user = field.from_user; + let user = notification_log.from_user; let user_avatar = frappe.avatar(user, "avatar-medium user-avatar"); let item_html = $(`
${user_avatar} @@ -271,18 +271,18 @@ class NotificationsView extends BaseNotificationsView {
`); - if (!field.read) { + if (!notification_log.read) { let mark_btn = item_html.find(".mark-as-read"); mark_btn.tooltip({ delay: { show: 600, hide: 100 }, trigger: "hover" }); mark_btn.on("click", (e) => { e.preventDefault(); e.stopImmediatePropagation(); - this.mark_as_read(field.name, item_html); + this.mark_as_read(notification_log.name, item_html); }); } item_html.on("click", () => { - !field.read && this.mark_as_read(field.name, item_html); + !notification_log.read && this.mark_as_read(notification_log.name, item_html); this.notifications_icon.trigger("click"); }); @@ -298,8 +298,8 @@ class NotificationsView extends BaseNotificationsView { } else { if (this.dropdown_items.length) { this.container.empty(); - this.dropdown_items.forEach((field) => { - this.container.append(this.get_dropdown_item_html(field)); + this.dropdown_items.forEach((notification_log) => { + this.container.append(this.get_dropdown_item_html(notification_log)); }); this.container.append(` From e7f604ef488de094475e5ded812d2f846fc9cff6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 24 Feb 2023 13:45:32 +0530 Subject: [PATCH 19/62] refactor: rename DocReminder -> Reminder --- .../{docreminder => reminder}/__init__.py | 0 .../docreminder.js => reminder/reminder.js} | 2 +- .../reminder.json} | 23 +++++++++++-------- .../docreminder.py => reminder/reminder.py} | 10 ++++---- .../test_reminder.py} | 4 ++-- .../core/doctype/log_settings/log_settings.py | 2 +- frappe/hooks.py | 2 +- frappe/public/js/frappe/form/reminders.js | 2 +- 8 files changed, 25 insertions(+), 20 deletions(-) rename frappe/automation/doctype/{docreminder => reminder}/__init__.py (100%) rename frappe/automation/doctype/{docreminder/docreminder.js => reminder/reminder.js} (79%) rename frappe/automation/doctype/{docreminder/docreminder.json => reminder/reminder.json} (83%) rename frappe/automation/doctype/{docreminder/docreminder.py => reminder/reminder.py} (91%) rename frappe/automation/doctype/{docreminder/test_docreminder.py => reminder/test_reminder.py} (83%) diff --git a/frappe/automation/doctype/docreminder/__init__.py b/frappe/automation/doctype/reminder/__init__.py similarity index 100% rename from frappe/automation/doctype/docreminder/__init__.py rename to frappe/automation/doctype/reminder/__init__.py diff --git a/frappe/automation/doctype/docreminder/docreminder.js b/frappe/automation/doctype/reminder/reminder.js similarity index 79% rename from frappe/automation/doctype/docreminder/docreminder.js rename to frappe/automation/doctype/reminder/reminder.js index a3f08c2c5e..6d1a72bab2 100644 --- a/frappe/automation/doctype/docreminder/docreminder.js +++ b/frappe/automation/doctype/reminder/reminder.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies and contributors // For license information, please see license.txt -// frappe.ui.form.on("DocReminder", { +// frappe.ui.form.on("Reminder", { // refresh(frm) { // }, diff --git a/frappe/automation/doctype/docreminder/docreminder.json b/frappe/automation/doctype/reminder/reminder.json similarity index 83% rename from frappe/automation/doctype/docreminder/docreminder.json rename to frappe/automation/doctype/reminder/reminder.json index 38b0f681ca..a288f205a2 100644 --- a/frappe/automation/doctype/docreminder/docreminder.json +++ b/frappe/automation/doctype/reminder/reminder.json @@ -8,16 +8,18 @@ "engine": "InnoDB", "field_order": [ "user", - "reminder_doctype", - "reminder_docname", "remind_at", "description", + "reminder_doctype", + "reminder_docname", "notified" ], "fields": [ { + "default": "__user", "fieldname": "user", "fieldtype": "Link", + "hidden": 1, "label": "User", "options": "User", "reqd": 1, @@ -28,16 +30,19 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Document Type", - "options": "DocType" + "options": "DocType", + "read_only": 1 }, { "fieldname": "reminder_docname", "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Document Name", - "options": "reminder_doctype" + "options": "reminder_doctype", + "read_only": 1 }, { + "default": "now", "fieldname": "remind_at", "fieldtype": "Datetime", "in_list_view": 1, @@ -48,23 +53,23 @@ { "fieldname": "description", "fieldtype": "Small Text", - "label": "description", + "label": "Description", "reqd": 1 }, { "default": "0", "fieldname": "notified", "fieldtype": "Check", + "hidden": 1, "label": "notified" } ], - "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-02-24 12:28:00.642813", + "modified": "2023-02-24 13:47:50.419648", "modified_by": "Administrator", "module": "Automation", - "name": "DocReminder", + "name": "Reminder", "naming_rule": "Random", "owner": "Administrator", "permissions": [ @@ -77,7 +82,7 @@ "write": 1 } ], - "read_only": 1, + "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/frappe/automation/doctype/docreminder/docreminder.py b/frappe/automation/doctype/reminder/reminder.py similarity index 91% rename from frappe/automation/doctype/docreminder/docreminder.py rename to frappe/automation/doctype/reminder/reminder.py index 61832fa2a9..795cdfda69 100644 --- a/frappe/automation/doctype/docreminder/docreminder.py +++ b/frappe/automation/doctype/reminder/reminder.py @@ -8,13 +8,13 @@ from frappe.utils import cint from frappe.utils.data import add_to_date, get_datetime, now_datetime -class DocReminder(Document): +class Reminder(Document): @staticmethod def clear_old_logs(days=30): from frappe.query_builder import Interval from frappe.query_builder.functions import Now - table = frappe.qb.DocType("DocReminder") + table = frappe.qb.DocType("Reminder") frappe.db.delete(table, filters=(table.remind_at < (Now() - Interval(days=days)))) def validate(self): @@ -47,7 +47,7 @@ def create_new_reminder( reminder_doctype: str | None = None, reminder_docname: str | None = None, ): - reminder = frappe.new_doc("DocReminder") + reminder = frappe.new_doc("Reminder") reminder.description = description reminder.remind_at = remind_at @@ -65,7 +65,7 @@ def send_reminders(): lower_threshold = add_to_date(now_datetime(), hours=-8, as_string=True, as_datetime=True) pending_reminders = frappe.get_all( - "DocReminder", + "Reminder", filters=[ ("remind_at", "<=", upper_threshold), ("remind_at", ">=", lower_threshold), # dont send too old reminders if failed to send @@ -75,4 +75,4 @@ def send_reminders(): ) for reminder in pending_reminders: - frappe.get_doc("DocReminder", reminder).send_reminder() + frappe.get_doc("Reminder", reminder).send_reminder() diff --git a/frappe/automation/doctype/docreminder/test_docreminder.py b/frappe/automation/doctype/reminder/test_reminder.py similarity index 83% rename from frappe/automation/doctype/docreminder/test_docreminder.py rename to frappe/automation/doctype/reminder/test_reminder.py index 54cf47fc2e..84cc258701 100644 --- a/frappe/automation/doctype/docreminder/test_docreminder.py +++ b/frappe/automation/doctype/reminder/test_reminder.py @@ -2,13 +2,13 @@ # See license.txt import frappe -from frappe.automation.doctype.docreminder.docreminder import create_new_reminder, send_reminders +from frappe.automation.doctype.reminder.reminder import create_new_reminder, send_reminders from frappe.desk.doctype.notification_log.notification_log import get_notification_logs from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, now_datetime -class TestDocReminder(FrappeTestCase): +class TestReminder(FrappeTestCase): def test_reminder(self): description = "TEST_REMINDER" diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index eb617d2ee2..4a3b457bcc 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -20,7 +20,7 @@ DEFAULT_LOGTYPES_RETENTION = { "Submission Queue": 30, "Prepared Report": 30, "Webhook Request Log": 30, - "DocReminder": 30, + "Reminder": 30, } diff --git a/frappe/hooks.py b/frappe/hooks.py index aaed06124f..b0d8e53f4a 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -194,7 +194,7 @@ scheduler_events = { "frappe.email.doctype.email_account.email_account.notify_unreplied", "frappe.utils.global_search.sync_global_search", "frappe.monitor.flush", - "frappe.automation.doctype.docreminder.docreminder.send_reminders", + "frappe.automation.doctype.reminder.reminder.send_reminders", ], "hourly": [ "frappe.model.utils.link_count.update_link_count", diff --git a/frappe/public/js/frappe/form/reminders.js b/frappe/public/js/frappe/form/reminders.js index f417a709a0..f761e26594 100644 --- a/frappe/public/js/frappe/form/reminders.js +++ b/frappe/public/js/frappe/form/reminders.js @@ -85,7 +85,7 @@ export class ReminderManager { create_reminder() { frappe - .xcall("frappe.automation.doctype.docreminder.docreminder.create_new_reminder", { + .xcall("frappe.automation.doctype.reminder.reminder.create_new_reminder", { remind_at: this.dialog.get_value("remind_at"), description: this.dialog.get_value("description"), reminder_doctype: this.frm?.doc.doctype, From 34beb65009daac9e5a81a63d6c258a3654e31f2c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 24 Feb 2023 16:30:29 +0530 Subject: [PATCH 20/62] fix: link webhook logs with webhook document (#20155) --- frappe/hooks.py | 1 + .../integrations/doctype/webhook/webhook.py | 10 ++++----- .../webhook_request_log.json | 21 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index b0d8e53f4a..effc84a873 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -393,6 +393,7 @@ ignore_links_on_delete = [ "Document Share Key", "Integration Request", "Unhandled Email", + "Webhook Request Log", ] # Request Hooks diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index e3740f9d6b..9ab25f56b2 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -128,23 +128,23 @@ def enqueue_webhook(doc, webhook) -> None: ) r.raise_for_status() frappe.logger().debug({"webhook_success": r.text}) - log_request(doc.doctype, doc.name, webhook.request_url, headers, data, r) + log_request(webhook.name, doc.name, webhook.request_url, headers, data, r) break except requests.exceptions.ReadTimeout as e: frappe.logger().debug({"webhook_error": e, "try": i + 1}) - log_request(doc.doctype, doc.name, webhook.request_url, headers, data) + log_request(webhook.name, doc.name, webhook.request_url, headers, data) except Exception as e: frappe.logger().debug({"webhook_error": e, "try": i + 1}) - log_request(doc.doctype, doc.name, webhook.request_url, headers, data, r) + log_request(webhook.name, doc.name, webhook.request_url, headers, data, r) sleep(3 * i + 1) if i != 2: continue def log_request( - document_type: str, + webhook: str, docname: str, url: str, headers: dict, @@ -154,7 +154,7 @@ def log_request( request_log = frappe.get_doc( { "doctype": "Webhook Request Log", - "reference_document_type": document_type, + "webhook": webhook, "reference_document": docname, "user": frappe.session.user if frappe.session.user else None, "url": url, 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 8f38878f1f..ed5201df1f 100644 --- a/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json +++ b/frappe/integrations/doctype/webhook_request_log/webhook_request_log.json @@ -7,7 +7,7 @@ "engine": "InnoDB", "field_order": [ "user", - "reference_document_type", + "webhook", "reference_document", "headers", "data", @@ -55,32 +55,31 @@ "options": "User", "read_only": 1 }, - { - "fieldname": "reference_document_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Reference Document Type", - "options": "DocType" - }, { "fieldname": "reference_document", - "fieldtype": "Dynamic Link", + "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, "label": "Reference Document", - "options": "reference_document_type" + "read_only": 1 }, { "fieldname": "error", "fieldtype": "Text", "label": "Error", "read_only": 1 + }, + { + "fieldname": "webhook", + "fieldtype": "Link", + "label": "Webhook", + "options": "Webhook" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-02-20 13:45:16.662079", + "modified": "2023-02-24 14:59:24.743552", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook Request Log", From 305628bb210bacf412ccf7978aba27e55d552b15 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 24 Feb 2023 17:00:20 +0530 Subject: [PATCH 21/62] fix: Remove fallback empty string for value - Might cause issue if the value is 0 (a number) --- frappe/public/js/frappe/views/reports/print_grid.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html index a1e629fb0c..7af27145a6 100644 --- a/frappe/public/js/frappe/views/reports/print_grid.html +++ b/frappe/public/js/frappe/views/reports/print_grid.html @@ -36,8 +36,8 @@ {% for col in columns %} {% if col.name && col._id !== "_check" %} - {% var value = col.fieldname ? row[col.fieldname] : row[col.id] || "" %} - {% var longest_word = value.split(' ').reduce((longest, word) => word.length > longest.length ? word : longest, ''); %} + {% var value = col.fieldname ? row[col.fieldname] : row[col.id] %} + {% var longest_word = cstr(value).split(' ').reduce((longest, word) => word.length > longest.length ? word : longest, ''); %} 45 %} class="overflow-wrap-anywhere" {% endif %}> {% format_data = row.is_total_row && ["Currency", "Float"].includes(col.fieldtype) ? data[0] : row %} From 4c3cfd787388481367ca4b6d92a75baeda24533c Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 27 Feb 2023 12:23:41 +0530 Subject: [PATCH 22/62] fix: download upload button is hidden if grid row length is < 50 (#20164) --- frappe/public/js/frappe/form/grid.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 8771229884..7d224f9881 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -477,11 +477,16 @@ export default class Grid { this.wrapper.find(".grid-add-multiple-rows").removeClass("hidden"); } } - } else if (this.grid_rows.length < this.grid_pagination.page_length) { + } else if ( + this.grid_rows.length < this.grid_pagination.page_length && + !this.df.allow_bulk_edit + ) { this.wrapper.find(".grid-footer").toggle(false); } - this.wrapper.find(".grid-add-row, .grid-add-multiple-rows").toggle(this.is_editable()); + this.wrapper + .find(".grid-add-row, .grid-add-multiple-rows, .grid-upload") + .toggle(this.is_editable()); } truncate_rows() { From 6a37c19d0ed21ccbf8c326ad7a622eb4d1ce258c Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Mon, 27 Feb 2023 12:52:13 +0530 Subject: [PATCH 23/62] fix: dont try to connect to server again when appending email to sent folder --- frappe/email/doctype/email_account/email_account.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index f754869938..aa0935e028 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -657,8 +657,6 @@ class EmailAccount(Document): if not email_server: return - email_server.connect() - if email_server.imap: try: message = safe_encode(message) From 9321a2e86a193243724a9f28e740985e1741a12d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 27 Feb 2023 14:03:34 +0530 Subject: [PATCH 24/62] fix(UX): Better message for read only mode - The default message keeps confusing user because it doesn't contain the word "site update" which is usually the reason for putting site in read only mode. - Also fixes annoying popup while opening notifications in read only mode. [skip ci] --- frappe/database/database.py | 2 +- .../doctype/notification_settings/notification_settings.py | 3 +++ frappe/public/js/frappe/ui/toolbar/navbar.html | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index a9d1ddc3bb..5958bdb927 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -233,7 +233,7 @@ class Database: elif self.is_read_only_mode_error(e): frappe.throw( _( - "Site is running in read only mode, this action can not be performed right now. Please try again later." + "Site is running in read only mode for maintenance or site update, this action can not be performed right now. Please try again later." ), title=_("In Read Only Mode"), exc=frappe.InReadOnlyMode, diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 801d512fe7..cd85c7d06d 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -92,4 +92,7 @@ def get_permission_query_conditions(user): @frappe.whitelist() def set_seen_value(value, user): + if frappe.flags.read_only: + return + frappe.db.set_value("Notification Settings", user, "seen", value, update_modified=False) diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index ee070d4378..451a4e7099 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -7,7 +7,7 @@