Browse Source

Merge branch 'anandpdoshi-feature/mentions-in-comments' into develop

version-14
Rushabh Mehta 9 years ago
parent
commit
600309cb1c
10 changed files with 351 additions and 8 deletions
  1. +1
    -1
      frappe/boot.py
  2. +32
    -1
      frappe/core/doctype/comment/comment.py
  3. +6
    -5
      frappe/core/doctype/doctype/doctype.py
  4. +81
    -1
      frappe/core/doctype/user/user.json
  5. +53
    -0
      frappe/core/doctype/user/user.py
  6. +1
    -0
      frappe/patches.txt
  7. +15
    -0
      frappe/patches/v6_15/set_username.py
  8. +1
    -0
      frappe/public/js/frappe/form/footer/timeline.html
  9. +154
    -0
      frappe/public/js/frappe/form/footer/timeline.js
  10. +7
    -0
      frappe/templates/emails/mentioned_in_comment.html

+ 1
- 1
frappe/boot.py View File

@@ -119,7 +119,7 @@ def get_fullnames():
ret = frappe.db.sql("""select name, ret = frappe.db.sql("""select name,
concat(ifnull(first_name, ''), concat(ifnull(first_name, ''),
if(ifnull(last_name, '')!='', ' ', ''), ifnull(last_name, '')) as fullname, 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) from tabUser where enabled=1 and user_type!="Website User" """, as_dict=1)


d = {} d = {}


+ 32
- 1
frappe/core/doctype/comment/comment.py View File

@@ -7,7 +7,8 @@ from frappe import _
from frappe.website.render import clear_cache from frappe.website.render import clear_cache
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.db_schema import add_column 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 exclude_from_linked_with = True


@@ -38,6 +39,7 @@ class Comment(Document):
"""Send realtime updates""" """Send realtime updates"""
if not self.comment_doctype: if not self.comment_doctype:
return return

if self.comment_doctype == 'Message': if self.comment_doctype == 'Message':
if self.comment_docname == frappe.session.user: if self.comment_docname == frappe.session.user:
message = self.as_dict() message = self.as_dict()
@@ -50,6 +52,8 @@ class Comment(Document):
frappe.publish_realtime('new_comment', self.as_dict(), doctype= self.comment_doctype, frappe.publish_realtime('new_comment', self.as_dict(), doctype= self.comment_doctype,
docname = self.comment_docname) docname = self.comment_docname)


self.notify_mentions()

def validate(self): def validate(self):
"""Raise exception for more than 50 comments.""" """Raise exception for more than 50 comments."""
if frappe.db.sql("""select count(*) from tabComment where comment_doctype=%s 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) 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(): def on_doctype_update():
"""Add index to `tabComment` `(comment_doctype, comment_name)`""" """Add index to `tabComment` `(comment_doctype, comment_name)`"""
if not frappe.db.sql("""show index from `tabComment` if not frappe.db.sql("""show index from `tabComment`


+ 6
- 5
frappe/core/doctype/doctype/doctype.py View File

@@ -296,12 +296,13 @@ def validate_fields(meta):
if d.fieldtype not in ("Data", "Link", "Read Only"): if d.fieldtype not in ("Data", "Link", "Read Only"):
frappe.throw(_("Fieldtype {0} for {1} cannot be unique").format(d.fieldtype, d.label)) 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"): 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)) frappe.throw(_("Fieldtype {0} for {1} cannot be indexed").format(d.fieldtype, d.label))


+ 81
- 1
frappe/core/doctype/user/user.json View File

@@ -24,6 +24,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -49,6 +50,7 @@
"oldfieldtype": "Check", "oldfieldtype": "Check",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -72,6 +74,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -97,6 +100,7 @@
"options": "Email", "options": "Email",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1, "reqd": 1,
@@ -121,6 +125,7 @@
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1, "reqd": 1,
@@ -145,6 +150,7 @@
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -169,6 +175,7 @@
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -176,6 +183,30 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 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, "allow_on_submit": 0,
"bold": 0, "bold": 0,
@@ -194,6 +225,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -216,6 +248,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -238,6 +271,7 @@
"oldfieldtype": "Column Break", "oldfieldtype": "Column Break",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "50%", "print_width": "50%",
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -264,6 +298,7 @@
"options": "Loading...", "options": "Loading...",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -287,6 +322,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -310,6 +346,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -333,6 +370,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -358,6 +396,7 @@
"options": "\nMale\nFemale\nOther", "options": "\nMale\nFemale\nOther",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -382,6 +421,7 @@
"oldfieldtype": "Date", "oldfieldtype": "Date",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -404,6 +444,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -425,6 +466,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -447,6 +489,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -470,6 +513,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -493,6 +537,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -515,6 +560,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -539,6 +585,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -561,6 +608,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -585,6 +633,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -609,6 +658,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -631,6 +681,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -655,6 +706,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -678,6 +730,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -702,6 +755,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -726,6 +780,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 1, "permlevel": 1,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -748,6 +803,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -771,6 +827,7 @@
"options": "UserRole", "options": "UserRole",
"permlevel": 1, "permlevel": 1,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -796,6 +853,7 @@
"permlevel": 1, "permlevel": 1,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -819,6 +877,7 @@
"permlevel": 1, "permlevel": 1,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -843,6 +902,7 @@
"permlevel": 1, "permlevel": 1,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -867,6 +927,7 @@
"oldfieldtype": "Column Break", "oldfieldtype": "Column Break",
"permlevel": 1, "permlevel": 1,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "50%", "print_width": "50%",
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
@@ -893,6 +954,7 @@
"options": "DefaultValue", "options": "DefaultValue",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -917,6 +979,7 @@
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -944,6 +1007,7 @@
"options": "System User\nWebsite User", "options": "System User\nWebsite User",
"permlevel": 1, "permlevel": 1,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1, "reqd": 1,
@@ -967,6 +1031,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 1, "permlevel": 1,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -990,6 +1055,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 1, "permlevel": 1,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1013,6 +1079,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 1, "permlevel": 1,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1035,6 +1102,7 @@
"oldfieldtype": "Column Break", "oldfieldtype": "Column Break",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "50%", "print_width": "50%",
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -1061,6 +1129,7 @@
"oldfieldtype": "Read Only", "oldfieldtype": "Read Only",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1085,6 +1154,7 @@
"oldfieldtype": "Read Only", "oldfieldtype": "Read Only",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1108,6 +1178,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1132,6 +1203,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1155,6 +1227,7 @@
"no_copy": 0, "no_copy": 0,
"permlevel": 1, "permlevel": 1,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1177,6 +1250,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1199,6 +1273,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1221,6 +1296,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1243,6 +1319,7 @@
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1265,6 +1342,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1287,6 +1365,7 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
@@ -1305,7 +1384,8 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 5, "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", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "User", "name": "User",


+ 53
- 0
frappe/core/doctype/user/user.py View File

@@ -10,6 +10,7 @@ from frappe.desk.notifications import clear_notifications
from frappe.utils.user import get_system_managers from frappe.utils.user import get_system_managers
import frappe.permissions import frappe.permissions
import frappe.share import frappe.share
import re


STANDARD_USERS = ("Guest", "Administrator") STANDARD_USERS = ("Guest", "Administrator")


@@ -38,6 +39,8 @@ class User(Document):
self.update_gravatar() self.update_gravatar()
self.ensure_unique_roles() self.ensure_unique_roles()
self.remove_all_roles_for_guest() self.remove_all_roles_for_guest()
self.validate_username()

if self.language == "Loading...": if self.language == "Loading...":
self.language = None self.language = None


@@ -296,6 +299,52 @@ class User(Document):
else: else:
exists.append(d.role) 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() @frappe.whitelist()
def get_languages(): def get_languages():
from frappe.translate import get_lang_dict 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"), frappe.sendmail(recipients=get_system_managers(), subject=_("Administrator Logged In"),
message=message, bulk=True) 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)

+ 1
- 0
frappe/patches.txt View File

@@ -109,3 +109,4 @@ frappe.patches.v6_9.rename_burmese_language
frappe.patches.v6_11.rename_field_in_email_account frappe.patches.v6_11.rename_field_in_email_account
execute:frappe.create_folder(os.path.join(frappe.local.site_path, 'private', 'files')) 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.remove_property_setter_for_previous_field
frappe.patches.v6_15.set_username

+ 15
- 0
frappe/patches/v6_15/set_username.py View File

@@ -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)

+ 1
- 0
frappe/public/js/frappe/form/footer/timeline.html View File

@@ -4,6 +4,7 @@
<div> <div>
<textarea style="height: 80px" style="margin-top: 10px;" <textarea style="height: 80px" style="margin-top: 10px;"
class="form-control"></textarea> class="form-control"></textarea>
<input type="data" class="hidden mention-input">
</div> </div>
<div class="media"> <div class="media">
<span class="pull-left avatar avatar-medium"> <span class="pull-left avatar avatar-medium">


+ 154
- 0
frappe/public/js/frappe/form/footer/timeline.js View File

@@ -40,7 +40,10 @@ frappe.ui.form.Comments = Class.extend({
this.list.on("click", ".toggle-blockquote", function() { this.list.on("click", ".toggle-blockquote", function() {
$(this).parent().siblings("blockquote").toggleClass("hidden"); $(this).parent().siblings("blockquote").toggleClass("hidden");
}); });

this.setup_mentions();
}, },

refresh: function(scroll_to_end) { refresh: function(scroll_to_end) {
var me = this; var me = this;


@@ -66,6 +69,7 @@ frappe.ui.form.Comments = Class.extend({
this.frm.sidebar.refresh_comments(); this.frm.sidebar.refresh_comments();


}, },

render_comment: function(c) { render_comment: function(c) {
var me = this; var me = this;
this.prepare_comment(c); this.prepare_comment(c);
@@ -310,5 +314,155 @@ frappe.ui.form.Comments = Class.extend({
}); });


return last_email; 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;
}
}
});
} }
}); });

+ 7
- 0
frappe/templates/emails/mentioned_in_comment.html View File

@@ -0,0 +1,7 @@
<p>
{{ _("{0} mentioned you in a comment in {1}").format(sender_fullname, link) }}
</p>
<blockquote
style="border-left: 3px solid #d1d8dd; padding: 7px 15px; margin-left: 0px;">
{{ comment.comment | markdown }}
</blockquote>

Loading…
Cancel
Save