From 53c22b04938c0c128c76802b6994f7fe7cfb95fe Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 22 Jul 2022 15:26:30 +0530 Subject: [PATCH 1/2] feat: Enable mentions and notify users from any text field --- frappe/core/doctype/comment/comment.py | 45 +------------- frappe/core/doctype/user/test_user.py | 2 +- frappe/core/doctype/user/user.py | 20 ------ frappe/desk/desktop.py | 1 - frappe/desk/notifications.py | 62 +++++++++++++++++++ .../public/js/frappe/form/controls/comment.js | 28 +-------- .../js/frappe/form/controls/text_editor.js | 28 ++++++++- 7 files changed, 92 insertions(+), 94 deletions(-) diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index dab9cfbfe4..796af748ca 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -3,23 +3,16 @@ import json import frappe -from frappe import _ -from frappe.core.doctype.user.user import extract_mentions from frappe.database.schema import add_column -from frappe.desk.doctype.notification_log.notification_log import ( - enqueue_create_notification, - get_title, - get_title_html, -) +from frappe.desk.notifications import notify_mentions from frappe.exceptions import ImplicitCommitError from frappe.model.document import Document -from frappe.utils import get_fullname from frappe.website.utils import clear_cache class Comment(Document): def after_insert(self): - self.notify_mentions() + notify_mentions(self.reference_doctype, self.reference_name, self.content) self.notify_change("add") def validate(self): @@ -63,40 +56,6 @@ class Comment(Document): update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) - def notify_mentions(self): - if self.reference_doctype and self.reference_name and self.content: - mentions = extract_mentions(self.content) - - if not mentions: - return - - sender_fullname = get_fullname(frappe.session.user) - title = get_title(self.reference_doctype, self.reference_name) - - recipients = [ - frappe.db.get_value( - "User", - {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, - "email", - ) - for name in mentions - ] - - notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format( - frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), get_title_html(title) - ) - - notification_doc = { - "type": "Mention", - "document_type": self.reference_doctype, - "document_name": self.reference_name, - "subject": notification_message, - "from_user": frappe.session.user, - "email_content": self.content, - } - - enqueue_create_notification(recipients, notification_doc) - def on_doctype_update(): frappe.db.add_index("Comment", ["reference_doctype", "reference_name"]) diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index d5e0a108b5..7582954175 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -8,13 +8,13 @@ from unittest.mock import patch import frappe import frappe.exceptions from frappe.core.doctype.user.user import ( - extract_mentions, reset_password, sign_up, test_password_strength, update_password, verify_password, ) +from frappe.desk.notifications import extract_mentions from frappe.frappeclient import FrappeClient from frappe.model.delete_doc import delete_doc from frappe.utils import get_url diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 6d0de186a5..232e915435 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -2,8 +2,6 @@ # License: MIT. See LICENSE from datetime import timedelta -from bs4 import BeautifulSoup - import frappe import frappe.defaults import frappe.permissions @@ -1044,24 +1042,6 @@ def notify_admin_access_to_system_manager(login_manager=None): ) -def extract_mentions(txt): - """Find all instances of @mentions in the html.""" - soup = BeautifulSoup(txt, "html.parser") - emails = [] - for mention in soup.find_all(class_="mention"): - if mention.get("data-is-group") == "true": - try: - user_group = frappe.get_cached_doc("User Group", mention["data-id"]) - emails += [d.user for d in user_group.user_group_members] - except frappe.DoesNotExistError: - pass - continue - email = mention["data-id"] - emails.append(email) - - return emails - - def handle_password_test_fail(result): suggestions = result["feedback"]["suggestions"][0] if result["feedback"]["suggestions"] else "" warning = result["feedback"]["warning"] if "warning" in result["feedback"] else "" diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index e2be2656a9..a90950f411 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -40,7 +40,6 @@ class Workspace: self.allowed_modules = self.get_cached("user_allowed_modules", self.get_allowed_modules) self.doc = frappe.get_cached_doc("Workspace", self.page_name) - if ( self.doc and self.doc.module diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 2a987f5539..77d40f44d2 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -3,10 +3,19 @@ import json +from bs4 import BeautifulSoup + import frappe +from frappe import _ +from frappe.desk.doctype.notification_log.notification_log import ( + enqueue_create_notification, + get_title, + get_title_html, +) from frappe.desk.doctype.notification_settings.notification_settings import ( get_subscribed_documents, ) +from frappe.utils import get_fullname @frappe.whitelist() @@ -298,3 +307,56 @@ def get_open_count(doctype, name, items=None): out["timeline_data"] = module.get_timeline_data(doctype, name) return out + + +def notify_mentions(ref_doctype, ref_name, content): + if ref_doctype and ref_name and content: + mentions = extract_mentions(content) + + if not mentions: + return + + sender_fullname = get_fullname(frappe.session.user) + title = get_title(ref_doctype, ref_name) + + recipients = [ + frappe.db.get_value( + "User", + {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, + "email", + ) + for name in mentions + ] + + notification_message = _("""{0} mentioned you in a comment in {1} {2}""").format( + frappe.bold(sender_fullname), frappe.bold(ref_doctype), get_title_html(title) + ) + + notification_doc = { + "type": "Mention", + "document_type": ref_doctype, + "document_name": ref_name, + "subject": notification_message, + "from_user": frappe.session.user, + "email_content": content, + } + + enqueue_create_notification(recipients, notification_doc) + + +def extract_mentions(txt): + """Find all instances of @mentions in the html.""" + soup = BeautifulSoup(txt, "html.parser") + emails = [] + for mention in soup.find_all(class_="mention"): + if mention.get("data-is-group") == "true": + try: + user_group = frappe.get_cached_doc("User Group", mention["data-id"]) + emails += [d.user for d in user_group.user_group_members] + except frappe.DoesNotExistError: + pass + continue + email = mention["data-id"] + emails.append(email) + + return emails diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index 4550d7045f..d4927dc0cd 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -71,36 +71,10 @@ frappe.ui.form.ControlComment = class ControlComment extends frappe.ui.form.Cont const options = super.get_quill_options(); return Object.assign(options, { theme: 'bubble', - bounds: this.quill_container[0], - modules: Object.assign(options.modules, { - mention: this.get_mention_options() - }) + bounds: this.quill_container[0] }); } - get_mention_options() { - if (!this.enable_mentions) { - return null; - } - let me = this; - return { - allowedChars: /^[A-Za-z0-9_]*$/, - mentionDenotationChars: ["@"], - isolateCharacter: true, - source: frappe.utils.debounce(async function(search_term, renderList) { - let method = me.mention_search_method || 'frappe.desk.search.get_names_for_mentions'; - let values = await frappe.xcall(method, { - search_term - }); - renderList(values, search_term); - }, 300), - renderItem(item) { - let value = item.value; - return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`; - } - }; - } - get_toolbar_options() { return [ ['bold', 'italic', 'underline', 'strike'], diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index e4e1fff18a..50b625f248 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -174,9 +174,33 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for toolbar: this.get_toolbar_options(), table: true, imageResize: {}, - magicUrl: true + magicUrl: true, + mention: this.get_mention_options() }, - theme: 'snow' + theme: 'snow', + }; + } + + get_mention_options() { + if (!this.enable_mentions && !this.df.enable_mentions) { + return null; + } + let me = this; + return { + allowedChars: /^[A-Za-z0-9_]*$/, + mentionDenotationChars: ["@"], + isolateCharacter: true, + source: frappe.utils.debounce(async function(search_term, renderList) { + let method = me.mention_search_method || 'frappe.desk.search.get_names_for_mentions'; + let values = await frappe.xcall(method, { + search_term + }); + renderList(values, search_term); + }, 300), + renderItem(item) { + let value = item.value; + return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`; + } }; } From c38f79edd60d04fb21f26b341d70cf13e41d8aae Mon Sep 17 00:00:00 2001 From: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Date: Sat, 23 Jul 2022 19:23:05 +0530 Subject: [PATCH 2/2] style: Fix import order --- frappe/core/doctype/comment/comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 36fcbe9bdd..02a5d2d58e 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -5,9 +5,9 @@ import json import frappe from frappe.database.schema import add_column from frappe.desk.notifications import notify_mentions -from frappe.model.utils import is_virtual_doctype from frappe.exceptions import ImplicitCommitError from frappe.model.document import Document +from frappe.model.utils import is_virtual_doctype from frappe.website.utils import clear_cache