feat: Enable mentions and notify users from any text fieldversion-14
@@ -3,24 +3,17 @@ | |||
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.model.utils import is_virtual_doctype | |||
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.model.utils import is_virtual_doctype | |||
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): | |||
@@ -64,40 +57,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"]) | |||
@@ -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 | |||
@@ -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 "" | |||
@@ -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 | |||
@@ -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 |
@@ -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'], | |||
@@ -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') : ''}`; | |||
} | |||
}; | |||
} | |||