diff --git a/frappe/boot.py b/frappe/boot.py index 483cfd3127..3536109d8c 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -119,7 +119,7 @@ def get_fullnames(): ret = frappe.db.sql("""select name, concat(ifnull(first_name, ''), if(ifnull(last_name, '')!='', ' ', ''), ifnull(last_name, '')) as fullname, - user_image as image, gender, email + user_image as image, gender, email, username from tabUser where enabled=1 and user_type!="Website User" """, as_dict=1) d = {} diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 43e68cab33..70d71b8c0e 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -7,7 +7,8 @@ from frappe import _ from frappe.website.render import clear_cache from frappe.model.document import Document from frappe.model.db_schema import add_column -from frappe.utils import get_fullname +from frappe.utils import get_fullname, get_link_to_form +from frappe.core.doctype.user.user import extract_mentions exclude_from_linked_with = True @@ -38,6 +39,7 @@ class Comment(Document): """Send realtime updates""" if not self.comment_doctype: return + if self.comment_doctype == 'Message': if self.comment_docname == frappe.session.user: message = self.as_dict() @@ -50,6 +52,8 @@ class Comment(Document): frappe.publish_realtime('new_comment', self.as_dict(), doctype= self.comment_doctype, docname = self.comment_docname) + self.notify_mentions() + def validate(self): """Raise exception for more than 50 comments.""" if frappe.db.sql("""select count(*) from tabComment where comment_doctype=%s @@ -144,6 +148,33 @@ class Comment(Document): self.update_comments_in_parent(_comments) + def notify_mentions(self): + if self.comment_doctype and self.comment_docname and self.comment and self.comment_type=="Comment": + mentions = extract_mentions(self.comment) + + if not mentions: + return + + sender_fullname = get_fullname(frappe.session.user) + parent_doc_label = "{0} {1}".format(_(self.comment_doctype), self.comment_docname) + subject = _("{0} mentioned you in a comment in {1}").format(sender_fullname, parent_doc_label) + message = frappe.get_template("templates/emails/mentioned_in_comment.html").render({ + "sender_fullname": sender_fullname, + "comment": self, + "link": get_link_to_form(self.comment_doctype, self.comment_docname, label=parent_doc_label) + }) + + recipients = [frappe.db.get_value("User", {"enabled": 1, "username": username, "user_type": "System User"}) + for username in mentions] + + frappe.sendmail( + recipients=recipients, + sender=frappe.session.user, + subject=subject, + message=message, + bulk=True + ) + def on_doctype_update(): """Add index to `tabComment` `(comment_doctype, comment_name)`""" if not frappe.db.sql("""show index from `tabComment` diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 3742945d30..7a878bd948 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -296,12 +296,13 @@ def validate_fields(meta): if d.fieldtype not in ("Data", "Link", "Read Only"): frappe.throw(_("Fieldtype {0} for {1} cannot be unique").format(d.fieldtype, d.label)) - has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*) - from `tab{doctype}` group by `{fieldname}` having count(*) > 1 limit 1""".format( - doctype=d.parent, fieldname=d.fieldname)) + if not d.get("__islocal"): + has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*) + from `tab{doctype}` group by `{fieldname}` having count(*) > 1 limit 1""".format( + doctype=d.parent, fieldname=d.fieldname)) - if has_non_unique_values and has_non_unique_values[0][0]: - frappe.throw(_("Field '{0}' cannot be set as Unique as it has non-unique values").format(d.label)) + if has_non_unique_values and has_non_unique_values[0][0]: + frappe.throw(_("Field '{0}' cannot be set as Unique as it has non-unique values").format(d.label)) if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"): frappe.throw(_("Fieldtype {0} for {1} cannot be indexed").format(d.fieldtype, d.label)) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 34e2d6ded7..9650b623fa 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -24,6 +24,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -49,6 +50,7 @@ "oldfieldtype": "Check", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -72,6 +74,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -97,6 +100,7 @@ "options": "Email", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 1, @@ -121,6 +125,7 @@ "oldfieldtype": "Data", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 1, @@ -145,6 +150,7 @@ "oldfieldtype": "Data", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -169,6 +175,7 @@ "oldfieldtype": "Data", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -176,6 +183,30 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "username", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Username", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 1 + }, { "allow_on_submit": 0, "bold": 0, @@ -194,6 +225,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -216,6 +248,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -238,6 +271,7 @@ "oldfieldtype": "Column Break", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50%", "read_only": 0, "report_hide": 0, @@ -264,6 +298,7 @@ "options": "Loading...", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -287,6 +322,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -310,6 +346,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -333,6 +370,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -358,6 +396,7 @@ "options": "\nMale\nFemale\nOther", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -382,6 +421,7 @@ "oldfieldtype": "Date", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -404,6 +444,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -425,6 +466,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -447,6 +489,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -470,6 +513,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -493,6 +537,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -515,6 +560,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -539,6 +585,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -561,6 +608,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 1, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -585,6 +633,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -609,6 +658,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -631,6 +681,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -655,6 +706,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -678,6 +730,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -702,6 +755,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -726,6 +780,7 @@ "no_copy": 0, "permlevel": 1, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -748,6 +803,7 @@ "no_copy": 0, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -771,6 +827,7 @@ "options": "UserRole", "permlevel": 1, "print_hide": 1, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -796,6 +853,7 @@ "permlevel": 1, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -819,6 +877,7 @@ "permlevel": 1, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -843,6 +902,7 @@ "permlevel": 1, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -867,6 +927,7 @@ "oldfieldtype": "Column Break", "permlevel": 1, "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50%", "read_only": 1, "report_hide": 0, @@ -893,6 +954,7 @@ "options": "DefaultValue", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -917,6 +979,7 @@ "oldfieldtype": "Section Break", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -944,6 +1007,7 @@ "options": "System User\nWebsite User", "permlevel": 1, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 1, @@ -967,6 +1031,7 @@ "no_copy": 0, "permlevel": 1, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -990,6 +1055,7 @@ "no_copy": 0, "permlevel": 1, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -1013,6 +1079,7 @@ "no_copy": 0, "permlevel": 1, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -1035,6 +1102,7 @@ "oldfieldtype": "Column Break", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "print_width": "50%", "read_only": 0, "report_hide": 0, @@ -1061,6 +1129,7 @@ "oldfieldtype": "Read Only", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1085,6 +1154,7 @@ "oldfieldtype": "Read Only", "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1108,6 +1178,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1132,6 +1203,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1155,6 +1227,7 @@ "no_copy": 0, "permlevel": 1, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -1177,6 +1250,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1199,6 +1273,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1221,6 +1296,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1243,6 +1319,7 @@ "permlevel": 0, "precision": "", "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, "reqd": 0, @@ -1265,6 +1342,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1287,6 +1365,7 @@ "no_copy": 1, "permlevel": 0, "print_hide": 0, + "print_hide_if_no_value": 0, "read_only": 1, "report_hide": 0, "reqd": 0, @@ -1305,7 +1384,8 @@ "issingle": 0, "istable": 0, "max_attachments": 5, - "modified": "2015-11-16 06:29:59.828065", + "menu_index": 0, + "modified": "2015-12-23 02:45:19.261689", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 85ffeffaa9..8aed3d75aa 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -10,6 +10,7 @@ from frappe.desk.notifications import clear_notifications from frappe.utils.user import get_system_managers import frappe.permissions import frappe.share +import re STANDARD_USERS = ("Guest", "Administrator") @@ -38,6 +39,8 @@ class User(Document): self.update_gravatar() self.ensure_unique_roles() self.remove_all_roles_for_guest() + self.validate_username() + if self.language == "Loading...": self.language = None @@ -296,6 +299,52 @@ class User(Document): else: exists.append(d.role) + def validate_username(self): + if not self.username and self.is_new(): + self.username = frappe.scrub(self.first_name) + + if not self.username: + return + + # strip space and @ + self.username = self.username.strip(" @") + + if self.username_exists(): + frappe.msgprint(_("Username {0} already exists")) + self.suggest_username() + raise frappe.DuplicateEntryError(self.username) + + if not self.is_new(): + old_username = self.get_db_value("username") + if old_username and self.username != old_username and "System Manager" not in frappe.get_roles(): + frappe.throw(_("Only a System Manager can change Username")) + + # should be made up of characters, numbers and underscore only + if not re.match(r"^[\w]+$", self.username): + frappe.throw(_("Username should not contain any special characters other than letters, numbers and underscore")) + + def suggest_username(self): + def _check_suggestion(suggestion): + if self.username != suggestion and not self.username_exists(suggestion): + return suggestion + + return None + + # @firstname + username = _check_suggestion(frappe.scrub(self.first_name)) + + if not username: + # @firstname_last_name + username = _check_suggestion(frappe.scrub("{0} {1}".format(self.first_name, self.last_name or ""))) + + if username: + frappe.msgprint(_("Suggested Username: {0}").format(username)) + + return username + + def username_exists(self, username=None): + return frappe.db.get_value("User", {"username": username or self.username, "name": ("!=", self.name)}) + @frappe.whitelist() def get_languages(): from frappe.translate import get_lang_dict @@ -490,3 +539,7 @@ def notifify_admin_access_to_system_manager(login_manager=None): frappe.sendmail(recipients=get_system_managers(), subject=_("Administrator Logged In"), message=message, bulk=True) +def extract_mentions(txt): + """Find all instances of @username in the string. + The mentions will be separated by non-word characters or may appear at the start of the string""" + return re.findall(r'(?:[^\w]|^)@([\w]*)', txt) diff --git a/frappe/patches.txt b/frappe/patches.txt index ae12371ea3..1867670131 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -109,3 +109,4 @@ frappe.patches.v6_9.rename_burmese_language frappe.patches.v6_11.rename_field_in_email_account execute:frappe.create_folder(os.path.join(frappe.local.site_path, 'private', 'files')) frappe.patches.v6_15.remove_property_setter_for_previous_field +frappe.patches.v6_15.set_username diff --git a/frappe/patches/v6_15/set_username.py b/frappe/patches/v6_15/set_username.py new file mode 100644 index 0000000000..bd787cffe1 --- /dev/null +++ b/frappe/patches/v6_15/set_username.py @@ -0,0 +1,15 @@ +import frappe + +def execute(): + frappe.reload_doctype("User") + + # give preference to System Users + users = frappe.db.sql_list("""select name from `tabUser` order by if(user_type='System User', 0, 1)""") + for name in users: + user = frappe.get_doc("User", name) + if user.username: + continue + + username = user.suggest_username() + if username: + user.db_set("username", username, update_modified=False) diff --git a/frappe/public/js/frappe/form/footer/timeline.html b/frappe/public/js/frappe/form/footer/timeline.html index 187b9bcb90..833d99a3c0 100644 --- a/frappe/public/js/frappe/form/footer/timeline.html +++ b/frappe/public/js/frappe/form/footer/timeline.html @@ -4,6 +4,7 @@
+
diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 1c78149f16..e0ab13f400 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -40,7 +40,10 @@ frappe.ui.form.Comments = Class.extend({ this.list.on("click", ".toggle-blockquote", function() { $(this).parent().siblings("blockquote").toggleClass("hidden"); }); + + this.setup_mentions(); }, + refresh: function(scroll_to_end) { var me = this; @@ -66,6 +69,7 @@ frappe.ui.form.Comments = Class.extend({ this.frm.sidebar.refresh_comments(); }, + render_comment: function(c) { var me = this; this.prepare_comment(c); @@ -310,5 +314,155 @@ frappe.ui.form.Comments = Class.extend({ }); return last_email; + }, + + setup_mentions: function() { + var me = this; + + this.cursor_from = this.cursor_to = 0 + this.codes = $.ui.keyCode; + this.up = $.Event("keydown", {"keyCode": this.codes.UP}); + this.down = $.Event("keydown", {"keyCode": this.codes.DOWN}); + this.enter = $.Event("keydown", {"keyCode": this.codes.ENTER}); + + this.setup_autocomplete_for_mentions(); + + this.setup_textarea_event(); + }, + + setup_autocomplete_for_mentions: function() { + var me = this; + + var username_user_map = {}; + for (var name in frappe.boot.user_info) { + var _user = frappe.boot.user_info[name]; + username_user_map[_user.username] = _user; + } + + this.mention_input = this.wrapper.find(".mention-input"); + + this.mention_input.autocomplete({ + minLength: 0, + autoFocus: true, + source: Object.keys(username_user_map), + select: function(event, ui) { + var value = ui.item.value; + var textarea_value = me.input.val(); + + var new_value = textarea_value.substring(0, me.cursor_from) + + value + + textarea_value.substring(me.cursor_to); + + me.input.val(new_value); + + var new_cursor_location = me.cursor_from + value.length; + + // move cursor to right position + if (me.input[0].setSelectionRange) { + me.input.focus(); + me.input[0].setSelectionRange(new_cursor_location, new_cursor_location); + + } else if (me.input[0].createTextRange) { + var range = input[0].createTextRange(); + range.collapse(true); + range.moveEnd('character', new_cursor_location); + range.moveStart('character', new_cursor_location); + range.select(); + + } else { + me.input.focus(); + } + } + }); + + this.mention_widget = this.mention_input.autocomplete("widget"); + + this.autocomplete_open = false; + this.mention_input + .on('autocompleteclose', function() { + me.autocomplete_open = false; + }) + .on('autocompleteopen', function() { + me.autocomplete_open = true; + }); + + // dirty hack to prevent backspace from navigating back to history + $(document).on("keydown", function(e) { + if (e.which===me.codes.BACKSPACE && me.autocomplete_open && document.activeElement==me.mention_widget.get(0)) { + // me.input.focus(); + + return false; + } + }); + }, + + setup_textarea_event: function() { + var me = this; + + // binding this in keyup to get the value after it is set in textarea + this.input.keyup(function(e) { + if (e.which===16) { + // don't trigger for shift + return; + + } else if ([me.codes.UP, me.codes.DOWN].indexOf(e.which)!==-1) { + // focus on autocomplete if up and down arrows + if (me.autocomplete_open) { + me.mention_widget.focus(); + me.mention_widget.trigger(e.which===me.codes.UP ? me.up : me.down); + } + return; + + } else if ([me.codes.ENTER, me.codes.ESCAPE, me.codes.TAB, me.codes.SPACE].indexOf(e.which)!==-1) { + me.mention_input.autocomplete("close"); + return; + + } else if (e.which !== 0 && !e.ctrlKey && !e.metaKey && !e.altKey) { + if(!String.fromCharCode(e.which)) { + // no point in parsing it if it is not a character key + return; + } + } + + var value = $(this).val() || ""; + var i = e.target.selectionStart; + var key = value[i-1]; + var substring = value.substring(0, i); + var mention = substring.match(/(?=[^\w]|^)@([\w]*)$/); + + if (mention && mention.length) { + var mention = mention[0].slice(1); + + // record location of cursor + me.cursor_from = i - mention.length; + me.cursor_to = i; + + // render autocomplete at the bottom of the textbox and search for mention + me.mention_input.autocomplete("option", "position", { + of: me.input, + my: "left top", + at: "left bottom" + }); + me.mention_input.autocomplete("search", mention); + + } else { + me.cursor_from = me.cursor_to = 0; + me.mention_input.autocomplete("close"); + } + }); + + // binding this in keydown to prevent default action + this.input.keydown(function(e) { + // enter, escape, tab + if (me.autocomplete_open) { + if ([me.codes.ENTER, me.codes.TAB].indexOf(e.which)!==-1) { + // set focused value + me.mention_widget.trigger(me.enter); + + // prevent default + return false; + } + } + }); } }); diff --git a/frappe/templates/emails/mentioned_in_comment.html b/frappe/templates/emails/mentioned_in_comment.html new file mode 100644 index 0000000000..2e84e6b847 --- /dev/null +++ b/frappe/templates/emails/mentioned_in_comment.html @@ -0,0 +1,7 @@ +

+ {{ _("{0} mentioned you in a comment in {1}").format(sender_fullname, link) }} +

+
+ {{ comment.comment | markdown }} +