diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index dda39115bf..ca34af11ab 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -10,6 +10,13 @@ frappe.ui.form.on('Server Script', { frm.dashboard.hide(); } + if (!frm.is_new()) { + frm.add_custom_button(__('Compare Versions'), () => { + new frappe.ui.DiffView("Server Script", "script", frm.doc.name); + }); + } + + frm.call('get_autocompletion_items') .then(r => r.message) .then(items => { diff --git a/frappe/custom/doctype/client_script/client_script.js b/frappe/custom/doctype/client_script/client_script.js index 27d11af4d1..ad9c9e4e42 100644 --- a/frappe/custom/doctype/client_script/client_script.js +++ b/frappe/custom/doctype/client_script/client_script.js @@ -43,6 +43,12 @@ frappe.ui.form.on('Client Script', { d.show(); }); }); + + if (!frm.is_new()) { + frm.add_custom_button(__('Compare Versions'), () => { + new frappe.ui.DiffView("Client Script", "script", frm.doc.name); + }); + } } frm.set_query('dt', { diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index d4c185e56f..86f0656bc6 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -11,6 +11,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils.user import get_enabled_system_users from frappe.desk.reportview import get_filters_cond +from frappe.desk.doctype.notification_settings.notification_settings import is_email_notifications_enabled_for_type weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] communication_mapping = {"": "Event", "Event": "Event", "Meeting": "Meeting", "Call": "Phone", "Sent/Received Email": "Email", "Other": "Other"} @@ -141,7 +142,12 @@ def has_permission(doc, user): def send_event_digest(): today = nowdate() - for user in get_enabled_system_users(): + + # select only those users that have event reminder email notifications enabled + users = [user for user in get_enabled_system_users() if + is_email_notifications_enabled_for_type(user.name, 'Event Reminders')] + + for user in users: events = get_events(today, today, user.name, for_reminder=True) if events: frappe.set_user_lang(user.name, user.language) diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index fc535fa405..1a6efd5a0d 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -14,6 +14,7 @@ "enable_email_assignment", "enable_email_energy_point", "enable_email_share", + "enable_email_event_reminders", "user", "seen", "system_notifications_section", @@ -97,12 +98,19 @@ "fieldname": "energy_points_system_notifications", "fieldtype": "Check", "label": "Energy Points" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_event_reminders", + "fieldtype": "Check", + "label": "Event Reminders" } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-16 12:18:46.955501", + "modified": "2021-11-24 14:45:31.931154", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", @@ -125,4 +133,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/desk/doctype/notification_settings/test_notification_settings.py b/frappe/desk/doctype/notification_settings/test_notification_settings.py new file mode 100644 index 0000000000..e3dac0af5f --- /dev/null +++ b/frappe/desk/doctype/notification_settings/test_notification_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +# import frappe +import unittest + +class TestNotificationSettings(unittest.TestCase): + pass diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py old mode 100755 new mode 100644 index a118240488..7c0e2dfe87 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -159,11 +159,10 @@ class Newsletter(WebsiteGenerator): def send_newsletter(self, emails: List[str]): """Trigger email generation for `emails` and add it in Email Queue. """ - # TODO: get rid of this maybe? - message = self.get_message() attachments = self.get_newsletter_attachments() sender = self.send_from or frappe.utils.get_formatted_email(self.owner) - args = {"message": message, "name": self.name} + args = self.as_dict() + args["message"] = self.get_message() is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes) frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test @@ -172,7 +171,6 @@ class Newsletter(WebsiteGenerator): subject=self.subject, sender=sender, recipients=emails, - message=message, attachments=attachments, template="newsletter", add_unsubscribe_link=self.send_unsubscribe_link, diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index 50947bd9bc..677b012efe 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -62,6 +62,7 @@ import "./frappe/utils/help_links.js"; import "./frappe/utils/address_and_contact.js"; import "./frappe/utils/preview_email.js"; import "./frappe/utils/file_manager.js"; +import "./frappe/utils/diffview"; import "./frappe/upload.js"; import "./frappe/ui/tree.js"; diff --git a/frappe/public/js/frappe/utils/diffview.js b/frappe/public/js/frappe/utils/diffview.js new file mode 100644 index 0000000000..ebface7f05 --- /dev/null +++ b/frappe/public/js/frappe/utils/diffview.js @@ -0,0 +1,100 @@ +frappe.provide("frappe.ui"); + +frappe.ui.DiffView = class DiffView { + constructor(doctype, fieldname, docname) { + this.dialog = null; + this.handler = null; + this.doctype = doctype; + this.fieldname = fieldname; + this.docname = docname; + + this.dialog = this.make_dialog(); + this.set_empty_state(); + this.dialog.show(); + } + + make_dialog() { + const get_query = () => ({ + query: "frappe.utils.diff.version_query", + filters: { docname: this.docname, ref_doctype: this.doctype }, + }); + const onchange = () => this.compute_diff(); + let dialog = new frappe.ui.Dialog({ + title: __("Compare Versions"), + fields: [ + { + label: __("From version"), + fieldtype: "Link", + fieldname: "from_version", + options: "Version", + reqd: 1, + get_query, + onchange, + }, + { + fieldtype: "Column Break", + fieldname: "cb", + }, + { + label: __("To version"), + fieldtype: "Link", + fieldname: "to_version", + options: "Version", + reqd: 1, + get_query, + onchange, + }, + { + fieldtype: "Section Break", + fieldname: "sb", + }, + { + label: __("Diff"), + fieldtype: "HTML", + fieldname: "diff", + }, + ], + size: "large", + }); + return dialog; + } + + compute_diff() { + const from_version = this.dialog.get_value("from_version"); + const to_version = this.dialog.get_value("to_version"); + const fieldname = this.fieldname; + + if (from_version && to_version) { + frappe + .xcall("frappe.utils.diff.get_version_diff", { + from_version, + to_version, + fieldname, + }) + .then((data) => { + this.dialog.set_value("diff", this.prettify_diff(data)); + }); + } else { + this.set_empty_state(); + } + } + + prettify_diff(diff) { + let html = ``; + + diff.forEach((line) => { + let line_class = ""; + if (line.startsWith("+")) { + line_class = "insert"; + } else if (line.startsWith("-")) { + line_class = "delete"; + } + html += `
${line}
`; + }); + return `
${html}
`; + } + + set_empty_state() { + this.dialog.set_value("diff", __("Select two versions to view the diff.")); + } +}; diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index c26b63a9f6..c70c64be0e 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -388,6 +388,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { this.$charts_wrapper.addClass('hidden'); this.save_view_user_settings( { chart_args: null }); + this.chart_args = null; } } diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss index d157a43bc3..7c2ae3c8b1 100644 --- a/frappe/public/scss/desk/global.scss +++ b/frappe/public/scss/desk/global.scss @@ -574,6 +574,31 @@ details > summary:focus { } } +.diffview { + font-family: monospace; + white-space: pre; + word-wrap: break-word; + color: var(--text-color); +} + + +.diffview .insert { + background-color: var(--green-100); +} + +.diffview .delete { + background-color: var(--red-100); +} + +[data-theme="dark"] { + .diffview .insert { + background-color: var(--green-800); + } + .diffview .delete { + background-color: var(--red-800); + } +} + // REDESIGN TODO: Handling of broken images? // img.no-image:before { // .img-background(); @@ -603,4 +628,4 @@ details > summary:focus { .chart-container { direction: ltr; } -*/ \ No newline at end of file +*/ diff --git a/frappe/templates/emails/newsletter.html b/frappe/templates/emails/newsletter.html index a3afb906cf..051840ef69 100644 --- a/frappe/templates/emails/newsletter.html +++ b/frappe/templates/emails/newsletter.html @@ -3,8 +3,11 @@ {{ message }} + +{% if published and send_webview_link %}
Open in web
-
\ No newline at end of file + +{% endif %} \ No newline at end of file diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index b35d973963..599a638ce2 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -7,6 +7,7 @@ from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url from frappe.utils import validate_url, validate_email_address from frappe.utils import ceil, floor from frappe.utils.data import cast, validate_python_code +from frappe.utils.diff import get_version_diff, version_query, _get_value_from_version from PIL import Image from frappe.utils.image import strip_exif_data, optimize_image @@ -269,3 +270,39 @@ class TestPythonExpressions(unittest.TestCase): ] for expr in invalid_expressions: self.assertRaises(frappe.ValidationError, validate_python_code, expr) + + +class TestDiffUtils(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.doc = frappe.get_doc(doctype="Client Script", dt="Client Script") + cls.doc.save(ignore_version=False) + cls.doc.script = "2;" + cls.doc.save(ignore_version=False) + cls.doc.script = "42;" + cls.doc.save(ignore_version=False) + + cls.versions = version_query(doctype="Version", txt="", searchfield="name", start=0, + page_len=20, filters={"ref_doctype": cls.doc.doctype, "docname": cls.doc.name}) + + @classmethod + def tearDownClass(cls): + cls.doc.delete() + + def test_version_query(self): + self.assertGreaterEqual(len(self.versions), 2) + + def test_get_field_value_from_version(self): + latest_version = self.versions[0][0] + self.assertEqual("42;", _get_value_from_version(latest_version, fieldname="script")[0]) + old_version = self.versions[1][0] + self.assertEqual("2;", _get_value_from_version(old_version, fieldname="script")[0]) + + def test_get_version_diff(self): + old_version = self.versions[1][0] + latest_version = self.versions[0][0] + + diff = get_version_diff(old_version, latest_version) + self.assertIn('-2;', diff) + self.assertIn('+42;', diff) diff --git a/frappe/utils/diff.py b/frappe/utils/diff.py new file mode 100644 index 0000000000..ac0e1b7439 --- /dev/null +++ b/frappe/utils/diff.py @@ -0,0 +1,61 @@ +import json +from difflib import unified_diff +from typing import List + +import frappe +from frappe.utils import pretty_date + + +@frappe.whitelist() +def get_version_diff( + from_version: str, to_version: str, fieldname: str = "script" +) -> List[str]: + + before, before_timestamp = _get_value_from_version(from_version, fieldname) + after, after_timestamp = _get_value_from_version(to_version, fieldname) + + if not (before and after): + return ["Values not available for diff"] + + before = before.split("\n") + after = after.split("\n") + + diff = unified_diff( + before, + after, + fromfile=from_version, + tofile=to_version, + fromfiledate=before_timestamp, + tofiledate=after_timestamp, + ) + return list(diff) + + +def _get_value_from_version(version_name: str, fieldname: str): + version = frappe.get_list( + "Version", fields=["data", "modified"], filters={"name": version_name} + ) + if version: + data = json.loads(version[0].data) + changed_fields = data.get("changed", []) + + # data structure of field: [fieldname, before_save, after_save] + for field in changed_fields: + if field[0] == fieldname: + return field[2], str(version[0].modified) + + return None, None + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def version_query(doctype, txt, searchfield, start, page_len, filters): + results = frappe.get_list( + "Version", + fields=["name", "modified"], + filters=filters, + limit_start=start, + limit_page_length=page_len, + order_by="modified desc", + ) + return [(d.name, pretty_date(d.modified), d.modified) for d in results]