[enhancement] Merge Comment and Feed into Communication 💥

This commit is contained in:
Anand Doshi 2016-01-18 19:58:16 +05:30
parent 2c95a9599e
commit 465318878e
76 changed files with 2512 additions and 2491 deletions

View file

@ -25,7 +25,7 @@ def get_data():
"label": _("Messages"),
"name": "messages",
"description": _("Chat messages and other notifications."),
"data_doctype": "Comment"
"data_doctype": "Communication"
},
{
"type": "doctype",

View file

@ -1 +0,0 @@
Comments added on Forms, Blogs and other places

View file

@ -1,4 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

View file

@ -1,338 +0,0 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "hash",
"creation": "2012-08-08 10:40:11",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment",
"oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Type",
"length": 0,
"no_copy": 0,
"options": "Email\nChat\nPhone\nSMS\nCreated\nSubmitted\nCancelled\nAssigned\nAssignment Completed\nComment\nWorkflow\nLabel\nAttachment\nAttachment Removed\nLike",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_by",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment By",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_by",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_by_fullname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment By Fullname",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_by_fullname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_time",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment Time",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_time",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_doctype",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Doctype",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_doctype",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_docname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Docname",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_docname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "post_topic",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Post Topic",
"length": 0,
"no_copy": 0,
"oldfieldname": "post_topic",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "unsubscribed",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Unsubscribed",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Reference DocType and Reference Name are used to render a comment as a link (href) to a Doc.",
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Name",
"length": 0,
"no_copy": 0,
"options": "reference_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-comments",
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-12-26 06:29:43.314568",
"modified_by": "Administrator",
"module": "Core",
"name": "Comment",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"read_only": 0,
"read_only_onload": 0,
"title_field": "comment"
}

View file

@ -1,191 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, json
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, get_link_to_form
from frappe.core.doctype.user.user import extract_mentions
exclude_from_linked_with = True
class Comment(Document):
"""Comments are added to Documents via forms or views like blogs etc."""
no_feed_on_delete = True
def get_feed(self):
"""Returns feed HTML from Comment."""
if self.comment_doctype == "Message":
return
if self.comment_type in ("Created", "Submitted", "Cancelled", "Label"):
comment_type = "Label"
elif self.comment_type in ("Comment", "Like"):
comment_type = self.comment_type
else:
comment_type = "Info"
return {
"subject": self.comment,
"doctype": self.comment_doctype,
"name": self.comment_docname,
"feed_type": comment_type,
"reference_doctype": self.reference_doctype,
"reference_name": self.reference_name
}
def after_insert(self):
"""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()
message['broadcast'] = True
frappe.publish_realtime('new_message', message, after_commit=True)
else:
# comment_docname contains the user who is addressed in the messages' page comment
frappe.publish_realtime('new_message', self.as_dict(),
user=self.comment_docname, after_commit=True)
else:
frappe.publish_realtime('new_comment', self.as_dict(),
doctype= self.comment_doctype, docname = self.comment_docname,
after_commit=True)
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
and comment_docname=%s""", (self.doctype, self.name))[0][0] >= 50:
frappe.throw(_("Cannot add more than 50 comments"))
if not self.comment_by_fullname and self.comment_by:
self.comment_by_fullname = get_fullname(self.comment_by)
def on_update(self):
"""Updates `_comments` property in parent Document."""
self.update_comment_in_doc()
def update_comment_in_doc(self):
"""Updates `_comments` (JSON) property in parent Document.
Creates a column `_comments` if property does not exist.
`_comments` format
{
"comment": [String],
"by": [user],
"name": [Comment Document name]
}"""
if self.comment_doctype and self.comment_docname and self.comment and self.comment_type=="Comment":
_comments = self.get_comments_from_parent()
updated = False
for c in _comments:
if c.get("name")==self.name:
c["comment"] = self.comment
updated = True
if not updated:
_comments.append({
"comment": self.comment,
"by": self.comment_by or self.owner,
"name":self.name
})
self.update_comments_in_parent(_comments)
def get_comments_from_parent(self):
try:
_comments = frappe.db.get_value(self.comment_doctype,
self.comment_docname, "_comments") or "[]"
return json.loads(_comments)
except Exception, e:
if e.args[0]==1054:
if frappe.flags.in_test:
return
add_column(self.comment_doctype, "_comments", "Text")
return self.get_comments_from_parent()
elif e.args[0]==1146:
# no table
pass
else:
raise
def update_comments_in_parent(self, _comments):
"""Updates `_comments` property in parent Document with given dict.
:param _comments: Dict of comments."""
if not self.comment_doctype or frappe.db.get_value("DocType", self.comment_doctype, "issingle"):
return
# use sql, so that we do not mess with the timestamp
frappe.db.sql("""update `tab%s` set `_comments`=%s where name=%s""" % (self.comment_doctype,
"%s", "%s"), (json.dumps(_comments), self.comment_docname))
comment_doc = frappe.get_doc(self.comment_doctype, self.comment_docname)
if getattr(comment_doc, "get_route", None):
clear_cache(comment_doc.get_route())
def on_trash(self):
"""Removes from `_comments` in parent Document"""
if self.comment_doctype == "Message":
return
if (self.comment_type or "Comment") != "Comment":
frappe.only_for("System Manager")
_comments = self.get_comments_from_parent()
for c in _comments:
if c.get("name")==self.name:
_comments.remove(c)
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`
where Key_name="comment_doctype_docname_index" """):
frappe.db.commit()
frappe.db.sql("""alter table `tabComment`
add index comment_doctype_docname_index(comment_doctype, comment_docname)""")
if "_liked_by" not in frappe.db.get_table_columns("Comment"):
add_column("Comment", "_liked_by", "Text")

View file

@ -1,52 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, unittest, json
# commented due to commits -- only run when comments are modified
# class TestComment(unittest.TestCase):
# def setUp(self):
# self.cleanup()
# self.test_rec = frappe.get_doc({
# "doctype":"Event",
# "subject":"__Comment Test Event",
# "event_type": "Private",
# "starts_on": "2011-01-01 10:00:00",
# "ends_on": "2011-01-01 10:00:00",
# }).insert()
#
# def tearDown(self):
# self.cleanup()
#
# def cleanup(self):
# frappe.db.sql("""delete from tabEvent where subject='__Comment Test Event'""")
# frappe.db.sql("""delete from tabComment where comment='__Test Comment'""")
# frappe.db.commit()
# if "_comments" in frappe.db.get_table_columns("Event"):
# frappe.db.commit()
# frappe.db.sql("""alter table `tabEvent` drop column `_comments`""")
#
# def test_add_comment(self):
# self.comment = frappe.get_doc({
# "doctype":"Comment",
# "comment_doctype": self.test_rec.doctype,
# "comment_docname": self.test_rec.name,
# "comment": "__Test Comment"
# }).insert()
#
# test_rec = frappe.get_doc(self.test_rec.doctype, self.test_rec.name)
# _comments = json.loads(test_rec.get("_comments"))
# self.assertTrue(_comments[0].get("comment")=="__Test Comment")
#
# def test_remove_comment(self):
# self.test_add_comment()
# frappe.delete_doc("Comment", self.comment.name)
# test_rec = frappe.get_doc(self.test_rec.doctype, self.test_rec.name)
# _comments = json.loads(test_rec.get("_comments"))
# self.assertEqual(len(_comments), 0)
#
#
# if __name__=="__main__":
# unittest.main()

View file

@ -1,7 +0,0 @@
[
{
"doctype": "Comment",
"name": "_Test Comment 1",
"comment": "test comment"
}
]

View file

@ -0,0 +1,150 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import
import frappe
from frappe import _
import json
from frappe.core.doctype.user.user import extract_mentions
from frappe.utils import get_fullname, get_link_to_form
from frappe.model.db_schema import add_column
from frappe.website.render import clear_cache
def validate_comment(doc):
"""Raise exception for more than 50 comments."""
if not (doc.communication_type=='Comment' and doc.reference_doctype and doc.reference_name):
return
comment_count = frappe.db.sql("""select count(*) from `tabCommunication`
where
communication_type='Comment'
and reference_doctype=%(reference_doctype)s
and reference_name=%(reference_name)s""",
{"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name})[0][0]
if comment_count >= 50:
frappe.throw(_("Cannot add more than 50 comments"))
def on_trash(doc):
if doc.communication_type != "Comment":
return
if doc.reference_doctype == "Message":
return
if (doc.comment_type or "Comment") != "Comment":
frappe.only_for("System Manager")
_comments = get_comments_from_parent(doc)
for c in _comments:
if c.get("name")==doc.name:
_comments.remove(c)
update_comments_in_parent(doc, _comments)
def update_comment_in_doc(doc):
"""Updates `_comments` (JSON) property in parent Document.
Creates a column `_comments` if property does not exist.
`_comments` format
{
"comment": [String],
"by": [user],
"name": [Comment Document name]
}"""
if doc.communication_type != "Comment":
return
if doc.reference_doctype and doc.reference_name and doc.content and doc.comment_type=="Comment":
_comments = get_comments_from_parent(doc)
updated = False
for c in _comments:
if c.get("name")==doc.name:
c["comment"] = doc.content
updated = True
if not updated:
_comments.append({
"comment": doc.content,
"by": doc.sender or doc.owner,
"name": doc.name
})
update_comments_in_parent(doc, _comments)
def notify_mentions(doc):
if doc.communication_type != "Comment":
return
if doc.reference_doctype and doc.reference_name and doc.content and doc.comment_type=="Comment":
mentions = extract_mentions(doc.content)
if not mentions:
return
sender_fullname = get_fullname(frappe.session.user)
parent_doc_label = "{0} {1}".format(_(doc.reference_doctype), doc.reference_name)
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": doc,
"link": get_link_to_form(doc.reference_doctype, doc.reference_name, 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 get_comments_from_parent(doc):
try:
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]"
return json.loads(_comments)
except Exception, e:
if e.args[0]==1054:
if frappe.flags.in_test:
return
add_column(doc.reference_doctype, "_comments", "Text")
return get_comments_from_parent(doc)
elif e.args[0]==1146:
# no table
pass
else:
raise
def update_comments_in_parent(doc, _comments):
"""Updates `_comments` property in parent Document with given dict.
:param _comments: Dict of comments."""
if not doc.reference_doctype or frappe.db.get_value("DocType", doc.reference_doctype, "issingle"):
return
# use sql, so that we do not mess with the timestamp
frappe.db.sql("""update `tab%s` set `_comments`=%s where name=%s""" % (doc.reference_doctype,
"%s", "%s"), (json.dumps(_comments), doc.reference_name))
reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
if getattr(reference_doc, "get_route", None):
clear_cache(reference_doc.get_route())
def add_info_comment(**kwargs):
kwargs.update({
"doctype": "Communication",
"communication_type": "Comment",
"comment_type": "Info"
})
return frappe.get_doc(kwargs).insert(ignore_permissions=True)

File diff suppressed because it is too large Load diff

View file

@ -3,22 +3,103 @@
from __future__ import unicode_literals, absolute_import
import frappe
import json
from email.utils import formataddr, parseaddr
from frappe.utils import get_url, get_formatted_email, cint, validate_email_add, split_emails
from frappe.utils.file_manager import get_file
from frappe.email.bulk import check_bulk_limit
import frappe.email.smtp
from frappe import _
from frappe.model.db_schema import add_column
from frappe.model.document import Document
from frappe.utils import validate_email_add, get_fullname, strip_html
from frappe.model.db_schema import add_column
from frappe.core.doctype.communication.comment import validate_comment, notify_mentions, update_comment_in_doc
from frappe.core.doctype.communication.email import validate_email, notify, _notify, update_parent_status
exclude_from_linked_with = True
class Communication(Document):
no_feed_on_delete = True
"""Communication represents an external communication like Email."""
def validate(self):
if self.reference_doctype and self.reference_name:
if not self.reference_owner:
self.reference_owner = frappe.db.get_value(self.reference_doctype, self.reference_name, "owner")
# prevent communication against a child table
if frappe.get_meta(self.reference_doctype).istable:
frappe.throw(_("Cannot create a {0} against a child document: {1}")
.format(_(self.communication_type), _(self.reference_doctype)))
if not self.user:
self.user = frappe.session.user
if not self.subject:
self.subject = strip_html((self.content or "")[:141])
self.set_status()
self.set_sender_full_name()
validate_email(self)
validate_comment(self)
self.set_timeline_doc()
def after_insert(self):
if not (self.reference_doctype and self.reference_name):
return
if self.communication_type in ("Communication", "Comment"):
# send new comment to listening clients
frappe.publish_realtime('new_communication', self.as_dict(),
doctype= self.reference_doctype, docname = self.reference_name,
after_commit=True)
if self.communication_type == "Comment":
notify_mentions(self)
elif self.communication_type in ("Chat", "Notification"):
if self.reference_name == frappe.session.user:
message = self.as_dict()
message['broadcast'] = True
frappe.publish_realtime('new_message', message, after_commit=True)
else:
# reference_name contains the user who is addressed in the messages' page comment
frappe.publish_realtime('new_message', self.as_dict(),
user=self.reference_name, after_commit=True)
def on_update(self):
"""Update parent status as `Open` or `Replied`."""
update_parent_status(self)
update_comment_in_doc(self)
def on_trash(self):
if (not self.flags.ignore_permissions
and self.communication_type=="Comment" and self.comment_type != "Comment"):
# prevent deletion of auto-created comments if not ignore_permissions
frappe.throw(_("Sorry! You cannot delete auto-generated comments"))
if self.communication_type in ("Communication", "Comment"):
# send delete comment to listening clients
frappe.publish_realtime('delete_communication', self.as_dict(),
doctype= self.reference_doctype, docname = self.reference_name,
after_commit=True)
def set_status(self):
if not self.is_new():
return
if self.reference_doctype and self.reference_name:
self.status = "Linked"
elif self.communication_type=="Communication":
self.status = "Open"
else:
self.status = "Closed"
def set_sender_full_name(self):
if not self.sender_full_name and self.sender:
if self.sender == "Administrator":
self.sender_full_name = self.sender
self.sender = frappe.db.get_value("User", "Administrator", "email")
else:
validate_email_add(self.sender, throw=True)
self.sender_full_name = get_fullname(self.sender)
def get_parent_doc(self):
"""Returns document of `reference_doctype`, `reference_doctype`"""
if not hasattr(self, "parent_doc"):
@ -28,50 +109,25 @@ class Communication(Document):
self.parent_doc = None
return self.parent_doc
def validate(self):
if self.get("__islocal"):
if self.reference_doctype and self.reference_name:
self.status = "Linked"
else:
self.status = "Open"
# validate recipients
for email in split_emails(self.recipients):
validate_email_add(email, throw=True)
# validate CC
for email in split_emails(self.cc):
validate_email_add(email, throw=True)
def after_insert(self):
# send new comment to listening clients
comment = self.as_dict()
comment["comment"] = comment["content"]
comment["comment_by"] = comment["sender"]
comment["comment_type"] = comment["communication_medium"]
frappe.publish_realtime('new_comment', comment, doctype = self.reference_doctype,
docname = self.reference_name, after_commit=True)
def on_update(self):
"""Update parent status as `Open` or `Replied`."""
self.update_parent()
def update_parent(self):
"""Update status of parent document based on who is replying."""
parent = self.get_parent_doc()
if not parent:
def set_timeline_doc(self):
"""Set timeline_doctype and timeline_name"""
parent_doc = self.get_parent_doc()
if (self.timeline_doctype and self.timeline_name) or not parent_doc:
return
status_field = parent.meta.get_field("status")
timeline_field = parent_doc.meta.timeline_field
if not timeline_field:
return
if status_field and "Open" in (status_field.options or "").split("\n"):
to_status = "Open" if self.sent_or_received=="Received" else "Replied"
doctype = parent_doc.meta.get_link_doctype(timeline_field)
name = parent_doc.get(timeline_field)
if to_status in status_field.options.splitlines():
parent.db_set("status", to_status)
if doctype and name:
self.timeline_doctype = doctype
self.timeline_name = name
parent.notify_update()
else:
return
def send(self, print_html=None, print_format=None, attachments=None,
send_me_a_copy=False, recipients=None):
@ -95,239 +151,12 @@ class Communication(Document):
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
"""
recipients, cc = self.get_recipients_and_cc(recipients, cc,
fetched_from_email_account=fetched_from_email_account)
self.emails_not_sent_to = set(self.all_email_addresses) - set(self.sent_email_addresses)
if frappe.flags.in_test:
# for test cases, run synchronously
self._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc)
else:
check_bulk_limit(list(set(self.sent_email_addresses)))
from frappe.tasks import sendmail
sendmail.delay(frappe.local.site, self.name,
print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc, lang=frappe.local.lang, session=frappe.local.session)
notify(self, print_html, print_format, attachments, recipients, cc, fetched_from_email_account)
def _notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None):
self.prepare_to_notify(print_html, print_format, attachments)
frappe.sendmail(
recipients=(recipients or []) + (cc or []),
show_as_cc=(cc or []),
expose_recipients=True,
sender=self.sender,
reply_to=self.incoming_email_account,
subject=self.subject,
content=self.content,
reference_doctype=self.reference_doctype,
reference_name=self.reference_name,
attachments=self.attachments,
message_id=self.name,
unsubscribe_message=_("Leave this conversation"),
bulk=True
)
def get_recipients_and_cc(self, recipients, cc, fetched_from_email_account=False):
self.all_email_addresses = []
self.sent_email_addresses = []
self.previous_email_sender = None
if not recipients:
recipients = self.get_recipients(fetched_from_email_account=fetched_from_email_account)
if not cc:
cc = self.get_cc(recipients, fetched_from_email_account=fetched_from_email_account)
if fetched_from_email_account:
# email was already sent to the original recipient by the sender's email service
original_recipients, recipients = recipients, []
# send email to the sender of the previous email in the thread which this email is a reply to
if self.previous_email_sender:
recipients.append(self.previous_email_sender)
# cc that was received in the email
original_cc = split_emails(self.cc)
# don't cc to people who already received the mail from sender's email service
cc = list(set(cc) - set(original_cc) - set(original_recipients))
return recipients, cc
def prepare_to_notify(self, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
:param print_html: Send given value as HTML attachment.
:param print_format: Attach print format of parent document."""
if print_format:
self.content += self.get_attach_link(print_format)
self.set_incoming_outgoing_accounts()
if not self.sender or cint(self.outgoing_email_account.always_use_account_email_id_as_sender):
self.sender = formataddr([frappe.session.data.full_name or "Notification", self.outgoing_email_account.email_id])
self.attachments = []
if print_html or print_format:
self.attachments.append(frappe.attach_print(self.reference_doctype, self.reference_name,
print_format=print_format, html=print_html))
if attachments:
if isinstance(attachments, basestring):
attachments = json.loads(attachments)
for a in attachments:
if isinstance(a, basestring):
# is it a filename?
try:
file = get_file(a)
self.attachments.append({"fname": file[0], "fcontent": file[1]})
except IOError:
frappe.throw(_("Unable to find attachment {0}").format(a))
else:
self.attachments.append(a)
def set_incoming_outgoing_accounts(self):
self.incoming_email_account = self.outgoing_email_account = None
if self.reference_doctype:
self.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id")
self.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True)
if not self.incoming_email_account:
self.incoming_email_account = frappe.db.get_value("Email Account",
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
if not self.outgoing_email_account:
self.outgoing_email_account = frappe.db.get_value("Email Account",
{"default_outgoing": 1, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict()
def get_recipients(self, fetched_from_email_account=False):
"""Build a list of email addresses for To"""
# [EDGE CASE] self.recipients can be None when an email is sent as BCC
recipients = split_emails(self.recipients)
if fetched_from_email_account and self.in_reply_to:
# add sender of previous reply
self.previous_email_sender = frappe.db.get_value("Communication", self.in_reply_to, "sender")
recipients.append(self.previous_email_sender)
if recipients:
# exclude email accounts
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
recipients = self.filter_email_list(recipients, exclude)
return recipients
def get_cc(self, recipients=None, fetched_from_email_account=False):
"""Build a list of email addresses for CC"""
# get a copy of CC list
cc = split_emails(self.cc)
if self.reference_doctype and self.reference_name:
if fetched_from_email_account:
# if it is a fetched email, add follows to CC
cc.append(self.get_owner_email())
cc += self.get_assignees()
if cc:
# exclude email accounts, unfollows, recipients and unsubscribes
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
exclude += [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)]
exclude += [(parseaddr(email)[1] or "").lower() for email in recipients]
if fetched_from_email_account:
# exclude sender when pulling email
exclude += [parseaddr(self.sender)[1]]
if self.reference_doctype and self.reference_name:
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
{"reference_doctype": self.reference_doctype, "reference_name": self.reference_name}, as_list=True)]
cc = self.filter_email_list(cc, exclude, is_cc=True)
if getattr(self, "send_me_a_copy", False) and self.sender not in cc:
self.all_email_addresses.append((parseaddr(self.sender)[1] or "").lower())
cc.append(self.sender)
return cc
def filter_email_list(self, email_list, exclude, is_cc=False):
# temp variables
filtered = []
email_address_list = []
for email in list(set(email_list)):
email_address = (parseaddr(email)[1] or "").lower()
if not email_address:
continue
# this will be used to eventually find email addresses that aren't sent to
self.all_email_addresses.append(email_address)
if (email in exclude) or (email_address in exclude):
continue
if is_cc:
is_user_enabled = frappe.db.get_value("User", email_address, "enabled")
if is_user_enabled==0:
# don't send to disabled users
continue
# make sure of case-insensitive uniqueness of email address
if email_address not in email_address_list:
# append the full email i.e. "Human <human@example.com>"
filtered.append(email)
email_address_list.append(email_address)
self.sent_email_addresses.extend(email_address_list)
return filtered
def get_owner_email(self):
owner = self.get_parent_doc().owner
return get_formatted_email(owner) or owner
def get_assignees(self):
return [( get_formatted_email(d.owner) or d.owner ) for d in
frappe.db.get_all("ToDo", filters={
"reference_type": self.reference_doctype,
"reference_name": self.reference_name,
"status": "Open"
}, fields=["owner"])
]
def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
return frappe.get_template("templates/emails/print_link.html").render({
"url": get_url(),
"doctype": self.reference_doctype,
"name": self.reference_name,
"print_format": print_format,
"key": self.get_parent_doc().get_signature()
})
_notify(self, print_html, print_format, attachments, recipients, cc)
def on_doctype_update():
"""Add index in `tabCommunication` for `(reference_doctype, reference_name)`"""
@ -336,60 +165,8 @@ def on_doctype_update():
if "_liked_by" not in frappe.db.get_table_columns("Communication"):
add_column("Communication", "_liked_by", "Text")
@frappe.whitelist()
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False,
send_me_a_copy=False, cc=None):
"""Make a new communication.
:param doctype: Reference DocType.
:param name: Reference Document name.
:param content: Communication body.
:param subject: Communication subject.
:param sent_or_received: Sent or Received (default **Sent**).
:param sender: Communcation sender (default current user).
:param recipients: Communication recipients as list.
:param communication_medium: Medium of communication (default **Email**).
:param send_mail: Send via email (default **False**).
:param print_html: HTML Print format to be sent as attachment.
:param print_format: Print Format name of parent document to be sent as attachment.
:param attachments: List of attachments as list of files or JSON string.
:param send_me_a_copy: Send a copy to the sender (default **False**).
"""
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not ignore_doctype_permissions:
raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format(
doctype=doctype, name=name))
if not sender:
sender = get_formatted_email(frappe.session.user)
comm = frappe.get_doc({
"doctype":"Communication",
"subject": subject,
"content": content,
"sender": sender,
"recipients": recipients,
"cc": cc or None,
"communication_medium": communication_medium,
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
"reference_name": name
})
comm.insert(ignore_permissions=True)
# needed for communication.notify which uses celery delay
# if not committed, delayed task doesn't find the communication
frappe.db.commit()
if send_email:
comm.send_me_a_copy = send_me_a_copy
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy)
return {
"name": comm.name,
"emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None
}
def has_permission(doc, ptype, user):
if ptype=="read" and doc.reference_doctype and doc.reference_name:
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
return True

View file

@ -1,3 +1,5 @@
frappe.listview_settings['Communication'] = {
add_fields: ["sent_or_received", "recipients", "subject", "communication_medium"]
add_fields: ["sent_or_received", "recipients", "subject", "communication_medium", "communication_type"],
default_filters: [["Communication", "communication_type", "=", "Communication"]],
filters: [["status", "=", "Open"]]
};

View file

@ -0,0 +1,348 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import
import frappe
import json
from email.utils import formataddr, parseaddr
from frappe.utils import get_url, get_formatted_email, cint, validate_email_add, split_emails
from frappe.utils.file_manager import get_file
from frappe.email.bulk import check_bulk_limit
import frappe.email.smtp
from frappe import _
@frappe.whitelist()
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False,
send_me_a_copy=False, cc=None):
"""Make a new communication.
:param doctype: Reference DocType.
:param name: Reference Document name.
:param content: Communication body.
:param subject: Communication subject.
:param sent_or_received: Sent or Received (default **Sent**).
:param sender: Communcation sender (default current user).
:param recipients: Communication recipients as list.
:param communication_medium: Medium of communication (default **Email**).
:param send_mail: Send via email (default **False**).
:param print_html: HTML Print format to be sent as attachment.
:param print_format: Print Format name of parent document to be sent as attachment.
:param attachments: List of attachments as list of files or JSON string.
:param send_me_a_copy: Send a copy to the sender (default **False**).
"""
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not ignore_doctype_permissions:
raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format(
doctype=doctype, name=name))
if not sender:
sender = get_formatted_email(frappe.session.user)
comm = frappe.get_doc({
"doctype":"Communication",
"subject": subject,
"content": content,
"sender": sender,
"recipients": recipients,
"cc": cc or None,
"communication_medium": communication_medium,
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
"reference_name": name
})
comm.insert(ignore_permissions=True)
# needed for communication.notify which uses celery delay
# if not committed, delayed task doesn't find the communication
frappe.db.commit()
if send_email:
comm.send_me_a_copy = send_me_a_copy
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy)
return {
"name": comm.name,
"emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None
}
def validate_email(doc):
"""Validate Email Addresses of Recipients and CC"""
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email"):
return
# validate recipients
for email in split_emails(doc.recipients):
validate_email_add(email, throw=True)
# validate CC
for email in split_emails(doc.cc):
validate_email_add(email, throw=True)
def notify(doc, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, fetched_from_email_account=False):
"""Calls a delayed celery task 'sendmail' that enqueus email in Bulk Email queue
:param print_html: Send given value as HTML attachment
:param print_format: Attach print format of parent document
:param attachments: A list of filenames that should be attached when sending this email
:param recipients: Email recipients
:param cc: Send email as CC to
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
"""
recipients, cc = get_recipients_and_cc(doc, recipients, cc,
fetched_from_email_account=fetched_from_email_account)
doc.emails_not_sent_to = set(doc.all_email_addresses) - set(doc.sent_email_addresses)
if frappe.flags.in_test:
# for test cases, run synchronously
doc._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc)
else:
check_bulk_limit(list(set(doc.sent_email_addresses)))
from frappe.tasks import sendmail
sendmail.delay(frappe.local.site, doc.name,
print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc, lang=frappe.local.lang, session=frappe.local.session)
def _notify(doc, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None):
prepare_to_notify(doc, print_html, print_format, attachments)
frappe.sendmail(
recipients=(recipients or []) + (cc or []),
show_as_cc=(cc or []),
expose_recipients=True,
sender=doc.sender,
reply_to=doc.incoming_email_account,
subject=doc.subject,
content=doc.content,
reference_doctype=doc.reference_doctype,
reference_name=doc.reference_name,
attachments=doc.attachments,
message_id=doc.name,
unsubscribe_message=_("Leave this conversation"),
bulk=True
)
def update_parent_status(doc):
"""Update status of parent document based on who is replying."""
if doc.communication_type != "Communication":
return
parent = doc.get_parent_doc()
if not parent:
return
status_field = parent.meta.get_field("status")
if status_field and "Open" in (status_field.options or "").split("\n"):
to_status = "Open" if doc.sent_or_received=="Received" else "Replied"
if to_status in status_field.options.splitlines():
parent.db_set("status", to_status)
parent.notify_update()
def get_recipients_and_cc(doc, recipients, cc, fetched_from_email_account=False):
doc.all_email_addresses = []
doc.sent_email_addresses = []
doc.previous_email_sender = None
if not recipients:
recipients = get_recipients(doc, fetched_from_email_account=fetched_from_email_account)
if not cc:
cc = get_cc(doc, recipients, fetched_from_email_account=fetched_from_email_account)
if fetched_from_email_account:
# email was already sent to the original recipient by the sender's email service
original_recipients, recipients = recipients, []
# send email to the sender of the previous email in the thread which this email is a reply to
if doc.previous_email_sender:
recipients.append(doc.previous_email_sender)
# cc that was received in the email
original_cc = split_emails(doc.cc)
# don't cc to people who already received the mail from sender's email service
cc = list(set(cc) - set(original_cc) - set(original_recipients))
return recipients, cc
def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
:param print_html: Send given value as HTML attachment.
:param print_format: Attach print format of parent document."""
if print_format:
doc.content += get_attach_link(doc, print_format)
set_incoming_outgoing_accounts(doc)
if not doc.sender or cint(doc.outgoing_email_account.always_use_account_email_id_as_sender):
doc.sender = formataddr([frappe.session.data.full_name or "Notification", doc.outgoing_email_account.email_id])
doc.attachments = []
if print_html or print_format:
doc.attachments.append(frappe.attach_print(doc.reference_doctype, doc.reference_name,
print_format=print_format, html=print_html))
if attachments:
if isinstance(attachments, basestring):
attachments = json.loads(attachments)
for a in attachments:
if isinstance(a, basestring):
# is it a filename?
try:
file = get_file(a)
doc.attachments.append({"fname": file[0], "fcontent": file[1]})
except IOError:
frappe.throw(_("Unable to find attachment {0}").format(a))
else:
doc.attachments.append(a)
def set_incoming_outgoing_accounts(doc):
doc.incoming_email_account = doc.outgoing_email_account = None
if doc.reference_doctype:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, "enable_incoming": 1}, "email_id")
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True)
if not doc.incoming_email_account:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
if not doc.outgoing_email_account:
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"default_outgoing": 1, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict()
def get_recipients(doc, fetched_from_email_account=False):
"""Build a list of email addresses for To"""
# [EDGE CASE] doc.recipients can be None when an email is sent as BCC
recipients = split_emails(doc.recipients)
if fetched_from_email_account and doc.in_reply_to:
# add sender of previous reply
doc.previous_email_sender = frappe.db.get_value("Communication", doc.in_reply_to, "sender")
recipients.append(doc.previous_email_sender)
if recipients:
# exclude email accounts
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
recipients = filter_email_list(doc, recipients, exclude)
return recipients
def get_cc(doc, recipients=None, fetched_from_email_account=False):
"""Build a list of email addresses for CC"""
# get a copy of CC list
cc = split_emails(doc.cc)
if doc.reference_doctype and doc.reference_name:
if fetched_from_email_account:
# if it is a fetched email, add follows to CC
cc.append(get_owner_email(doc))
cc += get_assignees(doc)
if cc:
# exclude email accounts, unfollows, recipients and unsubscribes
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
exclude += [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)]
exclude += [(parseaddr(email)[1] or "").lower() for email in recipients]
if fetched_from_email_account:
# exclude sender when pulling email
exclude += [parseaddr(doc.sender)[1]]
if doc.reference_doctype and doc.reference_name:
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
{"reference_doctype": doc.reference_doctype, "reference_name": doc.reference_name}, as_list=True)]
cc = filter_email_list(doc, cc, exclude, is_cc=True)
if getattr(doc, "send_me_a_copy", False) and doc.sender not in cc:
doc.all_email_addresses.append((parseaddr(doc.sender)[1] or "").lower())
cc.append(doc.sender)
return cc
def filter_email_list(doc, email_list, exclude, is_cc=False):
# temp variables
filtered = []
email_address_list = []
for email in list(set(email_list)):
email_address = (parseaddr(email)[1] or "").lower()
if not email_address:
continue
# this will be used to eventually find email addresses that aren't sent to
doc.all_email_addresses.append(email_address)
if (email in exclude) or (email_address in exclude):
continue
if is_cc:
is_user_enabled = frappe.db.get_value("User", email_address, "enabled")
if is_user_enabled==0:
# don't send to disabled users
continue
# make sure of case-insensitive uniqueness of email address
if email_address not in email_address_list:
# append the full email i.e. "Human <human@example.com>"
filtered.append(email)
email_address_list.append(email_address)
doc.sent_email_addresses.extend(email_address_list)
return filtered
def get_owner_email(doc):
owner = doc.get_parent_doc().owner
return get_formatted_email(owner) or owner
def get_assignees(doc):
return [( get_formatted_email(d.owner) or d.owner ) for d in
frappe.db.get_all("ToDo", filters={
"reference_type": doc.reference_doctype,
"reference_name": doc.reference_name,
"status": "Open"
}, fields=["owner"])
]
def get_attach_link(doc, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
return frappe.get_template("templates/emails/print_link.html").render({
"url": get_url(),
"doctype": doc.reference_doctype,
"name": doc.reference_name,
"print_format": print_format,
"key": doc.get_parent_doc().get_signature()
})

View file

@ -0,0 +1,87 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
from __future__ import unicode_literals
import frappe
import frappe.defaults
import frappe.permissions
from frappe.model.document import Document
from frappe.utils import get_fullname
from frappe import _
from frappe.core.doctype.communication.comment import add_info_comment
def update_feed(doc, method=None):
"adds a new communication with comment_type='Updated'"
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
if doc._action!="save":
return
if doc.doctype == "Communication" or doc.meta.issingle:
return
if hasattr(doc, "get_feed"):
feed = doc.get_feed()
if feed:
if isinstance(feed, basestring):
feed = {"subject": feed}
feed = frappe._dict(feed)
doctype = feed.doctype or doc.doctype
name = feed.name or doc.name
# delete earlier feed
frappe.db.sql("""delete from `tabCommunication`
where
reference_doctype=%s and reference_name=%s
and communication_type='Comment'
and comment_type='Updated'""", (doctype, name))
frappe.get_doc({
"doctype": "Communication",
"communication_type": "Comment",
"comment_type": "Updated",
"reference_doctype": doctype,
"reference_name": name,
"subject": feed.subject,
"full_name": get_fullname(doc.owner),
"reference_owner": frappe.db.get_value(doctype, name, "owner"),
"link_doctype": feed.link_doctype,
"link_name": feed.link_name
}).insert(ignore_permissions=True)
def login_feed(login_manager):
add_info_comment(**{
"subject": _("{0} logged in").format(get_fullname(login_manager.user)),
"full_name": get_fullname(login_manager.user)
})
def get_feed_match_conditions(user=None, force=True):
if not user: user = frappe.session.user
conditions = ['`tabCommunication`.owner="{user}" or `tabCommunication`.reference_owner="{user}"'.format(user=frappe.db.escape(user))]
user_permissions = frappe.defaults.get_user_permissions(user)
can_read = frappe.get_user().get_can_read()
can_read_doctypes = ['"{}"'.format(doctype) for doctype in
list(set(can_read) - set(user_permissions.keys()))]
if can_read_doctypes:
conditions += ["""(tabCommunication.reference_doctype is null
or tabCommunication.reference_doctype = ''
or tabCommunication.reference_doctype in ({}))""".format(", ".join(can_read_doctypes))]
if user_permissions:
can_read_docs = []
for doctype, names in user_permissions.items():
for n in names:
can_read_docs.append('"{}|{}"'.format(doctype, n))
if can_read_docs:
conditions.append("concat_ws('|', tabCommunication.reference_doctype, tabCommunication.reference_name) in ({})".format(
", ".join(can_read_docs)))
return "(" + " or ".join(conditions) + ")"

View file

@ -383,6 +383,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "column_break_15",
"fieldtype": "Column Break",
"hidden": 0,
@ -405,6 +406,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:!doc.istable",
"description": "Show this field as title",
"fieldname": "title_field",
"fieldtype": "Data",
@ -429,6 +431,33 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:!doc.istable",
"description": "Comments and Communications will be associated with this linked document",
"fieldname": "timeline_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Timeline Field",
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:!doc.istable",
"fieldname": "search_fields",
"fieldtype": "Data",
"hidden": 0,
@ -455,6 +484,7 @@
"bold": 0,
"collapsible": 0,
"default": "modified",
"depends_on": "eval:!doc.istable",
"description": "",
"fieldname": "sort_field",
"fieldtype": "Data",
@ -480,6 +510,7 @@
"bold": 0,
"collapsible": 0,
"default": "DESC",
"depends_on": "eval:!doc.istable",
"fieldname": "sort_order",
"fieldtype": "Select",
"hidden": 0,
@ -926,7 +957,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-01-19 12:04:52.332070",
"modified": "2016-02-01 07:55:35.810722",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -388,6 +388,19 @@ def validate_fields(meta):
_validate_title_field_pattern(df.options)
_validate_title_field_pattern(df.default)
def check_timeline_field(meta):
if not meta.timeline_field:
return
fieldname_list = [d.fieldname for d in fields]
if meta.timeline_field not in fieldname_list:
frappe.throw(_("Timeline field must be a valid fieldname"), InvalidFieldNameError)
df = meta.get("fields", {"fieldname": meta.timeline_field})[0]
if df.fieldtype not in ("Link", "Dynamic Link"):
frappe.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError)
fields = meta.get("fields")
for d in fields:
if not d.permlevel: d.permlevel = 0
@ -407,6 +420,7 @@ def validate_fields(meta):
check_fold(fields)
check_search_fields(meta)
check_title_field(meta)
check_timeline_field(meta)
def validate_permissions_for_doctype(doctype, for_remove=False):
"""Validates if permissions are set correctly."""

View file

@ -7,6 +7,8 @@ import frappe
from frappe.model.document import Document
class ErrorSnapshot(Document):
no_feed_on_delete = True
def onload(self):
if not self.parent_error_snapshot:
self.db_set('seen', True, update_modified=False)

View file

@ -225,8 +225,10 @@ class User(Document):
and event_type='Private'""", (self.name,))
# delete messages
frappe.db.sql("""delete from `tabComment` where comment_doctype='Message'
and (comment_docname=%s or owner=%s)""", (self.name, self.name))
frappe.db.sql("""delete from `tabCommunication`
where communication_type in ('Chat', 'Notification')
and reference_doctype='User'
and (reference_name=%s or owner=%s)""", (self.name, self.name))
def before_rename(self, olddn, newdn, merge=False):
frappe.clear_cache(user=olddn)

View file

@ -8,14 +8,14 @@ def get_notification_config():
return {
"for_doctype": {
"Scheduler Log": {"seen": 0},
"Communication": {"status": "Open"},
"Communication": {"status": "Open", "communication_type": "Communication"},
"ToDo": "frappe.core.notifications.get_things_todo",
"Event": "frappe.core.notifications.get_todays_events",
"Comment": "frappe.core.notifications.get_unread_messages",
"Error Snapshot": {"seen": 0, "parent_error_snapshot": None},
},
"for_other": {
"Likes": "frappe.core.notifications.get_unseen_likes"
"Likes": "frappe.core.notifications.get_unseen_likes",
"Messages": "frappe.core.notifications.get_unread_messages",
}
}
@ -39,17 +39,19 @@ def get_unread_messages():
"returns unread (docstatus-0 messages for a user)"
return frappe.db.sql("""\
SELECT count(*)
FROM `tabComment`
WHERE comment_doctype IN ('My Company', 'Message')
AND comment_docname = %s
AND docstatus=0
FROM `tabCommunication`
WHERE communication_type in ('Chat', 'Notification')
AND reference_doctype = 'User'
AND reference_name = %s
AND seen=0
""", (frappe.session.user,))[0][0]
def get_unseen_likes():
"""Returns count of unseen likes"""
return frappe.db.sql("""select count(*) from `tabFeed`
return frappe.db.sql("""select count(*) from `tabCommunication`
where
feed_type='Like'
and owner is not null and owner!=%(user)s
and doc_owner=%(user)s
and seen=0""", {"user": frappe.session.user})[0][0]
communication_type='Comment'
and comment_type='Like'
and owner is not null and owner!=%(user)s
and reference_owner=%(user)s
and seen=0""", {"user": frappe.session.user})[0][0]

View file

@ -1 +0,0 @@
Activity Feed.

View file

@ -1 +0,0 @@
from __future__ import unicode_literals

View file

@ -1,309 +0,0 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"creation": "2012-07-03 13:29:42",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "feed_type",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Feed Type",
"length": 0,
"no_copy": 0,
"options": "\nComment\nLogin\nLabel\nInfo\nLike",
"permlevel": 0,
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Doc Type",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Doc Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "subject",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Subject",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "color",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Color",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Full Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_owner",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Doc Owner",
"length": 0,
"no_copy": 0,
"options": "",
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Seen",
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Use this to provide alternative link to a feed record",
"fieldname": "reference_doctype",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference DocType",
"length": 0,
"no_copy": 0,
"options": "",
"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": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reference_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Name",
"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": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-rss",
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-12-30 02:48:03.860188",
"modified_by": "Administrator",
"module": "Desk",
"name": "Feed",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
{
"amend": 0,
"apply_user_permissions": 1,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"read_only": 0,
"read_only_onload": 0
}

View file

@ -1,105 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: See license.txt
from __future__ import unicode_literals
import frappe
import frappe.defaults
import frappe.permissions
from frappe.model.document import Document
from frappe.utils import get_fullname
from frappe import _
exclude_from_linked_with = True
class Feed(Document):
no_feed_on_delete = True
def validate(self):
if not (self.reference_doctype and self.reference_name):
# reset both if even one is missing
self.reference_doctype = self.reference_name = None
def on_doctype_update():
if not frappe.db.sql("""show index from `tabFeed`
where Key_name="feed_doctype_docname_index" """):
frappe.db.commit()
frappe.db.sql("""alter table `tabFeed`
add index feed_doctype_docname_index(doc_type, doc_name)""")
def get_permission_query_conditions(user):
if not user: user = frappe.session.user
use_user_permissions = frappe.permissions.apply_user_permissions("Feed", "read", user)
if not use_user_permissions:
return ""
conditions = ['`tabFeed`.owner="{user}" or `tabFeed`.doc_owner="{user}"'.format(user=frappe.db.escape(user))]
user_permissions = frappe.defaults.get_user_permissions(user)
can_read = frappe.get_user().get_can_read()
can_read_doctypes = ['"{}"'.format(doctype) for doctype in
list(set(can_read) - set(user_permissions.keys()))]
if not can_read_doctypes:
conditions += ["tabFeed.doc_type in ({})".format(", ".join(can_read_doctypes))]
if user_permissions:
can_read_docs = []
for doctype, names in user_permissions.items():
for n in names:
can_read_docs.append('"{}|{}"'.format(doctype, n))
if can_read_docs:
conditions.append("concat_ws('|', tabFeed.doc_type, tabFeed.doc_name) in ({})".format(
", ".join(can_read_docs)))
return "(" + " or ".join(conditions) + ")"
def has_permission(doc, user):
return frappe.has_permission(doc.doc_type, "read", doc.doc_name, user=user)
def update_feed(doc, method=None):
"adds a new feed"
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
if doc.doctype == "Feed" or doc.meta.issingle:
return
if hasattr(doc, "get_feed"):
feed = doc.get_feed()
if feed:
if isinstance(feed, basestring):
feed = {"subject": feed}
feed = frappe._dict(feed)
doctype = feed.doctype or doc.doctype
name = feed.name or doc.name
# delete earlier feed
frappe.db.sql("""delete from tabFeed
where doc_type=%s and doc_name=%s
and ifnull(feed_type,'')=''""", (doctype, name))
frappe.get_doc({
"doctype": "Feed",
"feed_type": feed.feed_type or "",
"doc_type": doctype,
"doc_name": name,
"subject": feed.subject,
"full_name": get_fullname(doc.owner),
"doc_owner": frappe.db.get_value(doctype, name, "owner"),
"reference_doctype": feed.reference_doctype,
"reference_name": feed.reference_name
}).insert(ignore_permissions=True)
def login_feed(login_manager):
frappe.get_doc({
"doctype": "Feed",
"feed_type": "Login",
"subject": _("{0} logged in").format(get_fullname(login_manager.user)),
"full_name": get_fullname(login_manager.user)
}).insert(ignore_permissions=True)

View file

@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
# test_records = frappe.get_test_records('Feed')
class TestFeed(unittest.TestCase):
pass

View file

@ -14,41 +14,40 @@ exclude_from_linked_with = True
class ToDo(Document):
def validate(self):
self._assignment = None
if self.is_new():
self.add_assign_comment(frappe._("Assigned to {0}: {1}").format(get_fullname(self.owner), self.description), "Assigned")
self._assignment = {
"text": frappe._("Assigned to {0}: {1}").format(get_fullname(self.owner), self.description),
"comment_type": "Assigned"
}
else:
# NOTE the previous value is only available in validate method
if self.get_db_value("status") != self.status:
self.add_assign_comment(frappe._("Assignment closed by {0}".format(get_fullname(frappe.session.user))),
"Assignment Completed")
self._assignment = {
"text": frappe._("Assignment closed by {0}".format(get_fullname(frappe.session.user))),
"comment_type": "Assignment Completed"
}
def on_update(self):
if self._assignment:
self.add_assign_comment(**self._assignment)
self.update_in_reference()
def on_trash(self):
# unlink assignment comment
frappe.db.sql("""update `tabComment` set reference_doctype=null and reference_name=null
where reference_doctype='ToDo' and reference_name=%s""", self.name)
# unlink todo from linked comments
frappe.db.sql("""update `tabCommunication` set link_doctype=null, link_name=null
where link_doctype=%(doctype)s and link_name=%(name)s""", {"doctype": self.doctype, "name": self.name})
self.update_in_reference()
def add_assign_comment(self, text, comment_type):
if not self.reference_type and self.reference_name:
if not (self.reference_type and self.reference_name):
return
comment = frappe.get_doc({
"doctype":"Comment",
"comment_by": frappe.session.user,
"comment_type": comment_type,
"comment_doctype": self.reference_type,
"comment_docname": self.reference_name,
"comment": """{text}""".format(text=text),
"reference_doctype": self.doctype,
"reference_name": self.name
})
comment.flags.ignore_permissions = True
comment.flags.ignore_links = True
comment.insert()
frappe.get_doc(self.reference_type, self.reference_name).add_comment(comment_type, text,
link_doctype=self.doctype, link_name=self.name)
def update_in_reference(self):
if not (self.reference_type and self.reference_name):

View file

@ -9,6 +9,7 @@ import frappe.defaults
import frappe.desk.form.meta
from frappe.permissions import get_doc_permissions
from frappe import _
from frappe.core.doctype.communication.feed import get_feed_match_conditions
@frappe.whitelist()
def getdoc(doctype, name, user=None):
@ -85,7 +86,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
frappe.response["docinfo"] = {
"attachments": get_attachments(doc.doctype, doc.name),
"comments": get_comments(doc.doctype, doc.name),
"communications": _get_communications(doc.doctype, doc.name),
"assignments": get_assignments(doc.doctype, doc.name),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name,
@ -106,31 +107,46 @@ def get_attachments(dt, dn):
return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": dn, "attached_to_doctype": dt})
def get_comments(dt, dn, limit=100):
comments = frappe.db.sql("""select name, comment, comment_by, creation,
reference_doctype, reference_name, comment_type, "Comment" as doctype, _liked_by
from `tabComment`
where comment_doctype=%s and comment_docname=%s
order by creation desc limit %s""",
(dt, dn, limit), as_dict=1)
@frappe.whitelist()
def get_communications(doctype, name, start=0, limit=20):
doc = frappe.get_doc(doctype, name)
if not doc.has_permission("read"):
raise frappe.PermissionError
communications = frappe.db.sql("""select name,
content as comment, sender as comment_by, creation,
communication_medium as comment_type, subject, delivery_status, _liked_by,
return _get_communications(doctype, name, start, limit)
def _get_communications(doctype, name, start=0, limit=20):
match_conditions = get_feed_match_conditions()
communications = frappe.db.sql("""select name, communication_type,
communication_medium, comment_type,
content, sender, sender_full_name, creation, subject, delivery_status, _liked_by,
timeline_doctype, timeline_name,
reference_doctype, reference_name,
"Communication" as doctype
from tabCommunication
where reference_doctype=%s and reference_name=%s
order by creation desc limit %s""", (dt, dn, limit),
where
communication_type in ("Communication", "Comment")
and (
(reference_doctype=%(doctype)s and reference_name=%(name)s)
or (timeline_doctype=%(doctype)s and timeline_name=%(name)s)
)
and (comment_type is null or comment_type != 'Update')
{match_conditions}
order by creation desc limit %(start)s, %(limit)s"""
.format(match_conditions=("and " + match_conditions) if match_conditions else ""),
{ "doctype": doctype, "name": name, "start": frappe.utils.cint(start), "limit": limit },
as_dict=True)
for c in communications:
c.attachments = json.dumps(frappe.get_all("File",
fields=["file_url", "is_private"],
filters={"attached_to_doctype": "Communication",
"attached_to_name": c.name}
))
if c.communication_type=="Communication":
c.attachments = json.dumps(frappe.get_all("File",
fields=["file_url", "is_private"],
filters={"attached_to_doctype": "Communication",
"attached_to_name": c.name}
))
return comments + communications
return communications
def get_assignments(dt, dn):
cl = frappe.db.sql("""select owner, description from `tabToDo`

View file

@ -51,10 +51,10 @@ def add_comment(doc):
"""allow any logged user to post a comment"""
doc = frappe.get_doc(json.loads(doc))
if doc.doctype != "Comment":
if not (doc.doctype=="Communication" and doc.communication_type=='Comment'):
frappe.throw(_("This method can only be used to create a Comment"), frappe.PermissionError)
doc.insert(ignore_permissions = True)
doc.insert(ignore_permissions=True)
return doc.as_dict()

View file

@ -64,40 +64,26 @@ def _toggle_like(doctype, name, add, user=None):
def remove_like(doctype, name):
"""Remove previous Like"""
# remove Comment
frappe.delete_doc("Comment", [c.name for c in frappe.get_all("Comment",
frappe.delete_doc("Communication", [c.name for c in frappe.get_all("Communication",
filters={
"comment_doctype": doctype,
"comment_docname": name,
"comment_by": frappe.session.user,
"comment_type": "Like"
}
)], ignore_permissions=True)
# remove Feed
frappe.delete_doc("Feed", [c.name for c in frappe.get_all("Feed",
filters={
"doc_type": doctype,
"doc_name": name,
"communication_type": "Comment",
"reference_doctype": doctype,
"reference_name": name,
"owner": frappe.session.user,
"feed_type": "Like"
"comment_type": "Like"
}
)], ignore_permissions=True)
def add_comment(doctype, name):
doc = frappe.get_doc(doctype, name)
if doctype=="Comment":
link = get_link_to_form(doc.comment_doctype, doc.comment_docname,
"{0} {1}".format(_(doc.comment_doctype), doc.comment_docname))
doc.add_comment("Like", _("Comment: {0} in {1}").format("<b>" + doc.comment + "</b>", link),
reference_doctype=doc.comment_doctype, reference_name=doc.comment_docname)
elif doctype=="Communication":
if doctype=="Communication":
link = get_link_to_form(doc.reference_doctype, doc.reference_name,
"{0} {1}".format(_(doc.reference_doctype), doc.reference_name))
doc.add_comment("Like", _("Communication: {0} in {1}").format("<b>" + doc.subject + "</b>", link),
reference_doctype=doc.reference_doctype, reference_name=doc.reference_name)
doc.add_comment("Like", _("{0}: {1} in {2}").format(_(doc.communication_type),
"<b>" + doc.subject + "</b>", link),
link_doctype=doc.reference_doctype, link_name=doc.reference_name)
else:
doc.add_comment("Like", _("Liked"))

View file

@ -26,7 +26,6 @@ def get_notifications():
"open_count_module": get_notifications_for_modules(config, notification_count),
"open_count_other": get_notifications_for_other(config, notification_count),
"new_messages": get_new_messages()
# "likes": get_count_of_new_likes()
}
def get_new_messages():
@ -41,11 +40,12 @@ def get_new_messages():
# no update for 30 mins, consider only the last 30 mins
last_update = (now_datetime() - relativedelta(seconds=1800)).strftime(DATETIME_FORMAT)
return frappe.db.sql("""select comment_by_fullname, comment
from tabComment
where comment_doctype='Message'
and comment_docname = %s
and ifnull(creation, "2000-01-01") > %s
return frappe.db.sql("""select sender_full_name, content
from `tabCommunication`
where communication_type in ('Chat', 'Notification')
and reference_doctype='user'
and reference_name = %s
and creation > %s
order by creation desc""", (frappe.session.user, last_update), as_dict=1)
def get_notifications_for_modules(config, notification_count):

View file

@ -18,7 +18,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) {
this.page.set_title(__("Activity"));
frappe.model.with_doctype("Feed", function() {
frappe.model.with_doctype("Communication", function() {
me.page.list = new frappe.ui.Listing({
hide_refresh: true,
page: me.page,
@ -28,7 +28,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) {
new frappe.activity.Feed(row, data);
},
show_filters: true,
doctype: "Feed",
doctype: "Communication",
get_args: function() {
if (frappe.route_options && frappe.route_options.show_likes) {
delete frappe.route_options.show_likes;
@ -52,19 +52,19 @@ frappe.pages['activity'].on_page_load = function(wrapper) {
frappe.activity.render_plot(this.page);
this.page.main.on("click", ".activity-message", function() {
var reference_doctype = $(this).attr("data-reference-doctype"),
reference_name = $(this).attr("data-reference-name"),
var link_doctype = $(this).attr("data-link-doctype"),
link_name = $(this).attr("data-link-name"),
doctype = $(this).attr("data-doctype"),
docname = $(this).attr("data-docname");
if (doctype && docname) {
frappe.set_route(["Form", reference_doctype || doctype, reference_name || docname]);
if (reference_doctype && reference_name) {
if (link_doctype && link_name) {
frappe.route_options = {
scroll_to: { "doctype": doctype, "name": docname }
}
}
frappe.set_route(["Form", link_doctype || doctype, link_name || docname]);
}
});
@ -96,9 +96,23 @@ frappe.activity.Feed = Class.extend({
data.add_class = "label-default";
data.link = "";
if (data.doc_type && data.doc_name) {
data.link = frappe.format(data.doc_name, {fieldtype: "Link", options: data.doc_type},
{label: __(data.doc_type) + " " + __(data.doc_name)});
if (data.link_doctype && data.link_name) {
data.link = frappe.format(data.link_name, {fieldtype: "Link", options: data.link_doctype},
{label: __(data.link_doctype) + " " + __(data.link_name)});
} else if (data.feed_type==="Comment" && data.comment_type==="Comment") {
// hack for backward compatiblity
data.link_doctype = data.reference_doctype;
data.link_name = data.reference_name;
data.reference_doctype = "Communication";
data.reference_name = data.name;
data.link = frappe.format(data.link_name, {fieldtype: "Link", options: data.link_doctype},
{label: __(data.link_doctype) + " " + __(data.link_name)});
} else if (data.reference_doctype && data.reference_name) {
data.link = frappe.format(data.reference_name, {fieldtype: "Link", options: data.reference_doctype},
{label: __(data.reference_doctype) + " " + __(data.reference_name)});
}
$(row)
@ -122,10 +136,10 @@ frappe.activity.Feed = Class.extend({
"Comment": "label-danger",
"Assignment": "label-warning",
"Login": "label-default"
}[data.feed_type] || "label-info"
}[data.comment_type || data.communication_medium] || "label-info"
data.when = comment_when(data.creation);
data.feed_type = __(data.feed_type);
data.feed_type = data.comment_type || data.communication_medium;
},
add_date_separator: function(row, data) {
var date = dateutil.str_to_obj(data.creation);

View file

@ -4,25 +4,27 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
from frappe.desk.doctype.feed.feed import get_permission_query_conditions
from frappe.core.doctype.communication.feed import get_feed_match_conditions
@frappe.whitelist()
def get_feed(limit_start, limit_page_length, show_likes=False):
"""get feed"""
# directly use the permission query condition function of feed
match_conditions = get_permission_query_conditions(frappe.session.user)
match_conditions = get_feed_match_conditions(frappe.session.user)
result = frappe.db.sql("""select name, feed_type, doc_type, doc_name, subject,
owner, modified, creation, seen, reference_doctype, reference_name
from `tabFeed`
result = frappe.db.sql("""select name, owner, modified, creation, seen, comment_type,
reference_doctype, reference_name, link_doctype, link_name, subject,
communication_type, communication_medium, content
from `tabCommunication`
where
((feed_type='Like' and (owner=%(user)s or doc_owner=%(user)s)) or feed_type!='Like')
communication_type in ("Communication", "Comment")
and (comment_type is null or comment_type != "Like"
or (comment_type="Like" and (owner=%(user)s or reference_owner=%(user)s)))
{match_conditions}
{show_likes}
order by creation desc
limit %(limit_start)s, %(limit_page_length)s"""
.format(match_conditions="and {0}".format(match_conditions) if match_conditions else "",
show_likes="and feed_type='Like'" if show_likes else ""),
show_likes="and comment_type='Like'" if show_likes else ""),
{
"user": frappe.session.user,
"limit_start": cint(limit_start),
@ -31,7 +33,8 @@ def get_feed(limit_start, limit_page_length, show_likes=False):
if show_likes:
# mark likes as seen!
frappe.db.sql("update `tabFeed` set seen=1 where feed_type='Like' and doc_owner=%s", frappe.session.user)
frappe.db.sql("""update `tabCommunication` set seen=1
where comment_type='Like' and reference_owner=%s""", frappe.session.user)
frappe.local.flags.commit = True
return result
@ -39,6 +42,10 @@ def get_feed(limit_start, limit_page_length, show_likes=False):
@frappe.whitelist()
def get_months_activity():
return frappe.db.sql("""select date(creation), count(name)
from `tabFeed` where date(creation) > subdate(curdate(), interval 1 month)
from `tabCommunication`
where
communication_type in ("Communication", "Comment")
and date(creation) > subdate(curdate(), interval 1 month)
group by date(creation)
order by creation asc""", as_list=1)

View file

@ -2,10 +2,10 @@
<div class="col-xs-3 text-right activity-date"><span class="{%= date_class %}">
{%= date_sep || "" %}</span></div>
<div class="col-xs-9 activity-message"
data-doctype="{%= doc_type %}"
data-docname="{%= doc_name %}"
data-reference-doctype="{{ reference_doctype }}"
data-reference-name="{{ reference_name }}"
data-doctype="{%= reference_doctype %}"
data-docname="{%= reference_name %}"
data-link-doctype="{{ link_doctype }}"
data-link-name="{{ link_name }}"
title="{%= by %} / {%= dateutil.str_to_user(creation) %}">
<span class="avatar avatar-small">
<div class="avatar-frame" style="background-image: url({{ imgsrc }});"></div>
@ -16,19 +16,25 @@
{%= __("Logged in") %}
{% } else if (feed_type==="Label") { %}
{%= __("{0} {1}", ["<strong>" + subject + "</strong>", link]) %}
{% } else if (doc_type && feed_type==="Comment") { %}
{%= __("Commented on {0}: {1}", [link, "<strong>" + subject + "</strong>"]) %}
{% } else if (doc_type && !feed_type) { %}
{% } else if (reference_doctype && feed_type==="Comment") { %}
{%= __("Commented on {0}: {1}", [link, "<strong>" + content + "</strong>"]) %}
{% } else if (reference_doctype && communication_type==="Communication") { %}
{%= __("Communicated via {0} on {1}: {2}", [__(feed_type), link, "<strong>" + subject + "</strong>"]) %}
{% } else if (reference_doctype && !feed_type) { %}
{%= __("Updated {0}: {1}", [link, "<strong>" + subject + "</strong>"]) %}
{% } else if (feed_type==="Like" && doc_type) { %}
{% } else if (feed_type==="Like" && reference_doctype) { %}
{%= by %} <i class="octicon octicon-heart"></i>
{% if (in_list(["Comment", "Communication"], doc_type)) { %}
{%= subject %}
{% if (in_list(["Comment", "Communication"], reference_doctype)) { %}
{%= content %}
{% } else { %}
{%= link %}
{% } %}
{% } else if (doc_type) { %}
{%= __("{0}: {1}", [link, "<strong>" + subject + "</strong>"]) %}
{% } else if (in_list(["Created", "Submitted", "Cancelled", "Deleted"], feed_type)) { %}
{%= __("{0} {1}", ["<strong>" + __(feed_type) + "</strong>", feed_type==="Deleted" ? subject : link ]) %}
{% } else if (feed_type==="Updated") { %}
{%= __("Updated {0}: {1}", [link, "<strong>" + subject + "</strong>"]) %}
{% } else if (reference_doctype && reference_name) { %}
{%= __("{0}: {1}", [link, "<strong>" + content + "</strong>"]) %}
{% } else { %}
{%= subject %}
{% } %}

View file

@ -46,7 +46,7 @@ frappe.desk.pages.Messages = Class.extend({
var me = this;
frappe.realtime.on('new_message', function(comment) {
if(comment.modified_by !== user) {
frappe.utils.notify(__("Message from {0}", [comment.comment_by_fullname]), comment.comment);
frappe.utils.notify(__("Message from {0}", [comment.sender_full_name]), comment.content);
}
if (frappe.get_route()[0] === 'messages' && comment.owner !== user) {
var current_contact = $(cur_page.page).find('[data-contact]').data('contact');
@ -166,7 +166,7 @@ frappe.desk.pages.Messages = Class.extend({
hide_refresh: true,
freeze: false,
render_row: function(wrapper, data) {
if(data.parenttype==="Assignment" || data.comment_type==="Shared") {
if(data.communication_type==="Notification" || data.comment_type==="Shared") {
data.is_system_message = 1;
}
var row = $(frappe.render_template("messages_row", {

View file

@ -3,10 +3,11 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.notifications import delete_notification_count_for
from frappe.core.doctype.user.user import STANDARD_USERS
from frappe.utils.user import get_enabled_system_users
from frappe.utils import cint, get_fullname
from frappe.utils import cint
@frappe.whitelist()
def get_list(arg=None):
@ -17,34 +18,36 @@ def get_list(arg=None):
# set all messages as read
frappe.db.begin()
frappe.db.sql("""UPDATE `tabComment`
set docstatus = 1 where comment_doctype in ('My Company', 'Message')
and comment_docname = %s
""", frappe.session.user)
frappe.db.sql("""UPDATE `tabCommunication` set seen = 1
where
communication_type in ('Chat', 'Notification')
and reference_doctype = 'User'
and reference_name = %s""", frappe.session.user)
delete_notification_count_for("Messages")
frappe.db.commit()
frappe.local.flags.commit = True
if frappe.form_dict['contact'] == frappe.session['user']:
# return messages
return frappe.db.sql("""select * from `tabComment`
where (owner=%(contact)s
or comment_docname=%(user)s
or (owner=comment_docname and ifnull(parenttype, "")!="Assignment")
or owner=comment_docname)
and comment_doctype ='Message'
order by creation desc
limit %(limit_start)s, %(limit_page_length)s""", frappe.local.form_dict, as_dict=1)
return frappe.db.sql("""select * from `tabCommunication`
where
communication_type in ('Chat', 'Notification')
and reference_doctype ='User'
and (owner=%(contact)s
or reference_name=%(user)s
or owner=reference_name)
order by creation desc
limit %(limit_start)s, %(limit_page_length)s""", frappe.local.form_dict, as_dict=1)
else:
return frappe.db.sql("""select * from `tabComment`
where ((owner=%(contact)s and comment_docname=%(user)s)
or (owner=%(user)s and comment_docname=%(contact)s)
or (owner=%(contact)s and comment_docname=%(contact)s))
and comment_doctype ='Message'
order by creation desc
limit %(limit_start)s, %(limit_page_length)s""", frappe.local.form_dict, as_dict=1)
return frappe.db.sql("""select * from `tabCommunication`
where
communication_type in ('Chat', 'Notification')
and reference_doctype ='User'
and ((owner=%(contact)s and reference_name=%(user)s)
or (owner=%(contact)s and reference_name=%(contact)s))
order by creation desc
limit %(limit_start)s, %(limit_page_length)s""", frappe.local.form_dict, as_dict=1)
@frappe.whitelist()
def get_active_users():
@ -71,15 +74,15 @@ def get_active_users():
@frappe.whitelist()
def post(txt, contact, parenttype=None, notify=False, subject=None):
import frappe
"""post message"""
d = frappe.new_doc('Comment')
d.parenttype = parenttype
d.comment = txt
d.comment_docname = contact
d.comment_doctype = 'Message'
d.comment_by_fullname = get_fullname(frappe.session.user)
d = frappe.new_doc('Communication')
d.communication_type = 'Notification' if parenttype else 'Chat'
d.subject = subject
d.content = txt
d.reference_doctype = 'User'
d.reference_name = contact
d.sender = frappe.session.user
d.insert(ignore_permissions=True)
delete_notification_count_for("Messages")
@ -94,7 +97,7 @@ def post(txt, contact, parenttype=None, notify=False, subject=None):
@frappe.whitelist()
def delete(arg=None):
frappe.get_doc("Comment", frappe.form_dict['name']).delete()
frappe.get_doc("Communication", frappe.form_dict['name']).delete()
def _notify(contact, txt, subject=None):
from frappe.utils import get_fullname, get_url

View file

@ -1,8 +1,8 @@
<div class="row message-row small">
<div class="col-sm-9">
<div class="media">
{% if (data.owner==data.comment_docname
&& data.parenttype!="Assignment") { %}
{% if (data.owner==data.reference_name
&& data.communication_type!="Notification") { %}
<span class="pull-left" title="{{ __("Public") }}"><i class="octicon octicon-rss text-muted" style="margin-top: 3px;"></i></span>
{% } else { %}
<span class="pull-left" title="{{ __("Public") }}" style="width: 20px; height: 16px; display: inline-block;"></span>
@ -14,7 +14,7 @@
</span>
</div>
<div class="media-body {{ data.is_system_message ? "text-muted" : "" }}">
{%= data.comment %}
{%= data.content %}
</div>
</div>
</div>
@ -23,7 +23,7 @@
<span class="hidden-sm hidden-md hidden-lg">{%= frappe.user.full_name(data.owner) %}, </span>
{%= comment_when(data.modified) %}
</div>
{% if (data.owner==user /* && !data.comment_type && data.parenttype!="Assignment" */ ) { %}
{% if (data.owner==user /* && !data.comment_type && data.communication_type!="Notification" */ ) { %}
<div>
<a class="delete text-extra-muted" data-name="{%= data.name %}"
onclick="frappe.desk.pages.messages.delete(this)">Delete</a>

View file

@ -18,6 +18,7 @@ from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta
from frappe.desk.form import assign_to
from frappe.utils.user import get_system_managers
from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts
class SentEmailInInbox(Exception): pass
@ -333,7 +334,7 @@ class EmailAccount(Document):
def send_auto_reply(self, communication, email):
"""Send auto reply if set."""
if self.enable_auto_reply:
communication.set_incoming_outgoing_accounts()
set_incoming_outgoing_accounts(communication)
frappe.sendmail(recipients = [email.from_email],
sender = self.email_id,

View file

@ -6,7 +6,7 @@ import unittest, email
test_records = frappe.get_test_records('Email Account')
from frappe.core.doctype.communication.communication import make
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.utils.file_manager import delete_file_from_filesystem
from frappe.email.doctype.email_account.email_account import notify_unreplied

View file

@ -16,19 +16,21 @@ class TestEmailAlert(unittest.TestCase):
frappe.set_user("Administrator")
def test_new_and_save(self):
comment = frappe.new_doc("Comment")
comment.comment = "test"
comment.insert(ignore_permissions=True)
communication = frappe.new_doc("Communication")
communication.communication_type = 'Comment'
communication.subject = "test"
communication.content = "test"
communication.insert(ignore_permissions=True)
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Comment",
"reference_name": comment.name, "status":"Not Sent"}))
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Communication",
"reference_name": communication.name, "status":"Not Sent"}))
frappe.db.sql("""delete from `tabBulk Email`""")
comment.description = "test"
comment.save()
communication.content = "test 2"
communication.save()
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Comment",
"reference_name": comment.name, "status":"Not Sent"}))
self.assertTrue(frappe.db.get_value("Bulk Email", {"reference_doctype": "Communication",
"reference_name": communication.name, "status":"Not Sent"}))
def test_condition(self):
event = frappe.new_doc("Event")

View file

@ -2,10 +2,11 @@
{
"doctype": "Email Alert",
"subject":"_Test Email Alert 1",
"document_type": "Comment",
"document_type": "Communication",
"event": "New",
"attach_print": 0,
"message": "New comment {{ doc.comment }} created",
"message": "New comment {{ doc.content }} created",
"condition": "doc.communication_type=='Comment'",
"recipients": [
{ "email_by_document_field": "owner" }
]
@ -13,10 +14,11 @@
{
"doctype": "Email Alert",
"subject":"_Test Email Alert 2",
"document_type": "Comment",
"document_type": "Communication",
"event": "Save",
"attach_print": 0,
"message": "New comment {{ doc.comment }} saved",
"message": "New comment {{ doc.content }} saved",
"condition": "doc.communication_type=='Comment'",
"recipients": [
{ "email_by_document_field": "owner" }
]

View file

@ -173,9 +173,9 @@ class FrappeClient(object):
new_doc.insert()
if not meta.istable:
if doctype != "Comment":
self.migrate_doctype("Comment", {"comment_doctype": doctype, "comment_docname": doc["name"]},
update={"comment_docname": new_doc.name}, verbose=0)
if doctype != "Communication":
self.migrate_doctype("Communication", {"reference_doctype": doctype, "reference_name": doc["name"]},
update={"reference_name": new_doc.name}, verbose=0)
if doctype != "File":
self.migrate_doctype("File", {"attached_to_doctype": doctype,

View file

@ -60,7 +60,7 @@ calendars = ["Event"]
# login
on_session_creation = [
"frappe.desk.doctype.feed.feed.login_feed",
"frappe.core.doctype.communication.feed.login_feed",
"frappe.core.doctype.user.user.notifify_admin_access_to_system_manager"
]
@ -70,16 +70,15 @@ permission_query_conditions = {
"Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
"ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions",
"User": "frappe.core.doctype.user.user.get_permission_query_conditions",
"Feed": "frappe.desk.doctype.feed.feed.get_permission_query_conditions",
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions"
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions",
}
has_permission = {
"Event": "frappe.desk.doctype.event.event.has_permission",
"ToDo": "frappe.desk.doctype.todo.todo.has_permission",
"User": "frappe.core.doctype.user.user.has_permission",
"Feed": "frappe.desk.doctype.feed.feed.has_permission",
"Note": "frappe.desk.doctype.note.note.has_permission"
"Note": "frappe.desk.doctype.note.note.has_permission",
"Communication": "frappe.core.doctype.communication.communication.has_permission"
}
standard_queries = {
@ -93,12 +92,11 @@ doc_events = {
"on_update": [
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.email.doctype.email_alert.email_alert.trigger_email_alerts",
"frappe.desk.doctype.feed.feed.update_feed"
"frappe.core.doctype.communication.feed.update_feed"
],
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_submit": [
"frappe.email.doctype.email_alert.email_alert.trigger_email_alerts",
"frappe.desk.doctype.feed.feed.update_feed"
],
"on_cancel": [
"frappe.desk.notifications.clear_doctype_notifications",
@ -141,12 +139,12 @@ get_translated_dict = {
}
sounds = [
{"name": "email", "src": "/assets/frappe/sounds/email.mp3"},
{"name": "submit", "src": "/assets/frappe/sounds/submit.mp3"},
{"name": "cancel", "src": "/assets/frappe/sounds/cancel.mp3"},
{"name": "delete", "src": "/assets/frappe/sounds/delete.mp3"},
{"name": "click", "src": "/assets/frappe/sounds/click.mp3"},
{"name": "error", "src": "/assets/frappe/sounds/error.mp3"},
{"name": "email", "src": "/assets/frappe/sounds/email.mp3", "volume": 0.1},
{"name": "submit", "src": "/assets/frappe/sounds/submit.mp3", "volume": 0.1},
{"name": "cancel", "src": "/assets/frappe/sounds/cancel.mp3", "volume": 0.1},
{"name": "delete", "src": "/assets/frappe/sounds/delete.mp3", "volume": 0.05},
{"name": "click", "src": "/assets/frappe/sounds/click.mp3", "volume": 0.05},
{"name": "error", "src": "/assets/frappe/sounds/error.mp3", "volume": 0.1},
# {"name": "alert", "src": "/assets/frappe/sounds/alert.mp3"},
# {"name": "chime", "src": "/assets/frappe/sounds/chime.mp3"},
]

View file

@ -429,8 +429,12 @@ class DatabaseQuery(object):
) and not self.group_by)
if not group_function_without_group_by:
args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype,
meta.get("sort_field") or "modified", meta.get("sort_order") or "desc")
sort_field = sort_order = None
if meta.sort_field:
sort_field = meta.sort_field
sort_order = meta.sort_order
args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc")
# draft docs always on top
if meta.is_submittable:
@ -458,12 +462,21 @@ class DatabaseQuery(object):
if "_comments" in r:
comment_count = len(json.loads(r._comments or "[]"))
else:
comment_count = cint(frappe.db.get_value("Comment",
filters={"comment_doctype": self.doctype, "comment_docname": r.name, "comment_type": "Comment"},
comment_count = cint(frappe.db.get_value("Communication",
filters={
"communication_type": "Comment",
"reference_doctype": self.doctype,
"reference_name": r.name,
"comment_type": "Comment"
},
fieldname="count(name)"))
communication_count = cint(frappe.db.get_value("Communication",
filters={"reference_doctype": self.doctype, "reference_name": r.name},
filters={
"communication_type": "Communication",
"reference_doctype": self.doctype,
"reference_name": r.name
},
fieldname="count(name)"))
r._comment_count = comment_count + communication_count

View file

@ -195,15 +195,22 @@ def delete_linked_todos(doc):
def delete_linked_comments(doc):
"""Delete comments from the document"""
delete_doc("Comment", frappe.db.sql_list("""select name from `tabComment`
where comment_doctype=%s and comment_docname=%s""", (doc.doctype, doc.name)), ignore_on_trash=True,
delete_doc("Communication", frappe.db.sql_list("""select name from `tabCommunication`
where
communication_type = 'Comment'
and reference_doctype=%s
and reference_name=%s""", (doc.doctype, doc.name)),
ignore_on_trash=True,
ignore_permissions=True)
def delete_linked_communications(doc):
# make communications orphans
frappe.db.sql("""update `tabCommunication`
set reference_doctype=null, reference_name=null
where reference_doctype=%s and reference_name=%s""", (doc.doctype, doc.name))
where
communication_type = 'Communication'
and reference_doctype=%s
and reference_name=%s""", (doc.doctype, doc.name))
def insert_feed(doc):
from frappe.utils import get_fullname
@ -212,11 +219,11 @@ def insert_feed(doc):
return
frappe.get_doc({
"doctype": "Feed",
"feed_type": "Label",
"doc_type": doc.doctype,
"doc_name": doc.name,
"subject": _("Deleted"),
"doctype": "Communication",
"communication_type": "Comment",
"comment_type": "Deleted",
"reference_doctype": doc.doctype,
"subject": "{0} {1}".format(_(doc.doctype), doc.name),
"full_name": get_fullname(doc.owner)
}).insert(ignore_permissions=True)

View file

@ -770,19 +770,21 @@ class Document(BaseDocument):
"""Returns Desk URL for this document. `/desk#Form/{doctype}/{name}`"""
return "/desk#Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name)
def add_comment(self, comment_type, text=None, comment_by=None, reference_doctype=None, reference_name=None):
def add_comment(self, comment_type, text=None, comment_by=None, link_doctype=None, link_name=None):
"""Add a comment to this document.
:param comment_type: e.g. `Comment`. See Comment for more info."""
:param comment_type: e.g. `Comment`. See Communication for more info."""
comment = frappe.get_doc({
"doctype":"Comment",
"comment_by": comment_by or frappe.session.user,
"doctype":"Communication",
"communication_type": "Comment",
"sender": comment_by or frappe.session.user,
"comment_type": comment_type,
"comment_doctype": self.doctype,
"comment_docname": self.name,
"comment": text or _(comment_type),
"reference_doctype": reference_doctype,
"reference_name": reference_name
"reference_doctype": self.doctype,
"reference_name": self.name,
"content": text or _(comment_type),
"link_doctype": link_doctype,
"link_name": link_name
}).insert(ignore_permissions=True)
return comment

View file

@ -102,6 +102,18 @@ class Meta(Document):
def get_options(self, fieldname):
return self.get_field(fieldname).options
def get_link_doctype(self, fieldname):
df = self.get_field(fieldname)
if df.fieldtype == "Link":
return df.options
elif df.fieldtype == "Dynamic Link":
return self.get_options(df.options)
else:
return None
def get_search_fields(self):
search_fields = self.search_fields or "name"
search_fields = [d.strip() for d in search_fields.split(",")]
@ -167,16 +179,16 @@ class Meta(Document):
if self.get("_idx"):
newlist = []
pending = self.get("fields")
for fieldname in json.loads(self.get("_idx")):
d = self.get("fields", {"fieldname": fieldname}, limit=1)
if d:
newlist.append(d[0])
pending.remove(d[0])
if pending:
newlist += pending
# renum
idx = 1
for d in newlist:
@ -243,10 +255,10 @@ def get_field_currency(df, doc=None):
if not doc:
return None
if not getattr(frappe.local, "field_currency", None):
frappe.local.field_currency = frappe._dict()
if not frappe.local.field_currency.get((doc.doctype, doc.parent or doc.name), {}).get(df.fieldname):
if ":" in cstr(df.get("options")):
split_opts = df.get("options").split(":")
@ -256,11 +268,11 @@ def get_field_currency(df, doc=None):
currency = doc.get(df.get("options"))
if not currency and doc.parent:
currency = frappe.db.get_value(doc.parenttype, doc.parent, df.get("options"))
if currency:
frappe.local.field_currency.setdefault((doc.doctype, doc.parent or doc.name), frappe._dict())\
.setdefault(df.fieldname, currency)
return frappe.local.field_currency.get((doc.doctype, doc.parent or doc.name), {}).get(df.fieldname)
def get_field_precision(df, doc=None, currency=None):
@ -355,4 +367,3 @@ def clear_cache(doctype=None):
# clear all
for name in groups:
cache.delete_value(name)

View file

@ -41,7 +41,6 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
if doctype=='DocType':
rename_doctype(doctype, old, new, force)
update_comments(doctype, old, new, force)
update_attachments(doctype, old, new)
if merge:
@ -118,14 +117,6 @@ def rename_doctype(doctype, old, new, force=False):
# change parenttype for fieldtype Table
update_parenttype_values(old, new)
# rename comments
frappe.db.sql("""update tabComment set comment_doctype=%s where comment_doctype=%s""",
(new, old))
def update_comments(doctype, old, new, force=False):
frappe.db.sql("""update `tabComment` set comment_docname=%s
where comment_doctype=%s and comment_docname=%s""", (new, doctype, old))
def update_child_docs(old, new, meta):
# update "parent"
for df in meta.get_table_fields():

View file

@ -1,156 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, os
import frappe.modules
from frappe.utils import cstr
from frappe.modules import export_doc, get_module_path, scrub
def listfolders(path, only_name=0):
"""
Returns the list of folders (with paths) in the given path,
If only_name is set, it returns only the folder names
"""
out = []
for each in os.listdir(path):
each = cstr(each)
dirname = each.split(os.path.sep)[-1]
fullpath = os.path.join(path, dirname)
if os.path.isdir(fullpath) and not dirname.startswith('.'):
out.append(only_name and dirname or fullpath)
return out
def switch_module(dt, dn, to, frm=None, export=None):
"""
Change the module of the given doctype, if export is true, then also export txt and copy
code files from src
"""
frappe.db.sql("update `tab"+dt+"` set module=%s where name=%s", (to, dn))
if export:
export_doc(dt, dn)
# copy code files
if dt in ('DocType', 'Page', 'Report'):
from_path = os.path.join(get_module_path(frm), scrub(dt), scrub(dn), scrub(dn))
to_path = os.path.join(get_module_path(to), scrub(dt), scrub(dn), scrub(dn))
# make dire if exists
os.system('mkdir -p %s' % os.path.join(get_module_path(to), scrub(dt), scrub(dn)))
for ext in ('py','js','html','css'):
os.system('cp %s %s')
def commonify_doclist(doclist, with_comments=1):
"""
Makes a doclist more readable by extracting common properties.
This is used for printing Documents in files
"""
from frappe.utils import get_common_dict, get_diff_dict
def make_common(doclist):
c = {}
if with_comments:
c['##comment'] = 'These values are common in all dictionaries'
for k in common_keys:
c[k] = doclist[0][k]
return c
def strip_common_and_idx(d):
for k in common_keys:
if k in d: del d[k]
if 'idx' in d: del d['idx']
return d
def make_common_dicts(doclist):
common_dict = {} # one per doctype
# make common dicts for all records
for d in doclist:
if not d['doctype'] in common_dict:
d1 = d.copy()
if d1.has_key("name"):
del d1['name']
common_dict[d['doctype']] = d1
else:
common_dict[d['doctype']] = get_common_dict(common_dict[d['doctype']], d)
return common_dict
common_keys = ['owner','docstatus','creation','modified','modified_by']
common_dict = make_common_dicts(doclist)
# make docs
final = []
for d in doclist:
f = strip_common_and_idx(get_diff_dict(common_dict[d['doctype']], d))
f['doctype'] = d['doctype'] # keep doctype!
# strip name for child records (only an auto generated number!)
if f['doctype'] != doclist[0]['doctype'] and f.has_key("name"):
del f['name']
if with_comments:
f['##comment'] = d['doctype'] + ('name' in f and (', ' + f['name']) or '')
final.append(f)
# add commons
commons = []
for d in common_dict.values():
d['name']='__common__'
if with_comments:
d['##comment'] = 'These values are common for all ' + d['doctype']
commons.append(strip_common_and_idx(d))
common_values = make_common(doclist)
return [common_values]+commons+final
def uncommonify_doclist(dl):
"""
Expands an commonified doclist
"""
# first one has common values
common_values = dl[0]
common_dict = frappe._dict()
final = []
idx_dict = {}
for d in dl[1:]:
if 'name' in d and d['name']=='__common__':
# common for a doctype -
del d['name']
common_dict[d['doctype']] = d
else:
dt = d['doctype']
if not dt in idx_dict: idx_dict[dt] = 1;
d1 = frappe._dict(common_values.copy())
# update from common and global
d1.update(common_dict[dt])
d1.update(d)
# idx by sequence
d1['idx'] = idx_dict[dt]
# increment idx
idx_dict[dt] += 1
final.append(d1)
return final
def pprint_doclist(doclist, with_comments = 1):
from json import dumps
return dumps(commonify_doclist(doclist, False), indent=1, sort_keys=True)
def peval_doclist(txt):
from json import loads
try:
return uncommonify_doclist(loads(txt))
except Exception, e:
return uncommonify_doclist(eval(txt))

View file

@ -113,5 +113,5 @@ execute:frappe.create_folder(os.path.join(frappe.local.site_path, 'private', 'fi
frappe.patches.v6_15.remove_property_setter_for_previous_field #2015-12-29
frappe.patches.v6_15.set_username
execute:frappe.permissions.reset_perms("Error Snapshot")
frappe.patches.v6_19.comment_feed_communication
frappe.patches.v6_16.feed_doc_owner

View file

@ -1,20 +0,0 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("desk", "doctype", "feed")
for feed in frappe.db.sql("select name from `tabFeed` where doc_type='Comment'", as_dict=True):
feed = frappe.get_doc("Feed", feed)
comment = frappe.get_doc(feed.doc_type, feed.doc_name)
new_feed = comment.get_feed()
if not new_feed:
frappe.db.sql("delete from `tabFeed` where name=%s", feed.name)
continue
feed.subject = new_feed["subject"]
feed.doc_type = new_feed["doctype"]
feed.doc_name = new_feed["name"]
feed.feed_type = new_feed["feed_type"]
feed.save()

View file

@ -2,15 +2,14 @@ from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doctype("Feed")
frappe.reload_doctype("Communication")
frappe.db.sql("update `tabFeed` set seen=1")
for doctype, name in frappe.db.sql("""select distinct doc_type, doc_name from `tabFeed`
for doctype, name in frappe.db.sql("""select distinct reference_doctype, reference_name
from `tabCommunication`
where
(doc_type is not null and doc_type != '')
and (doc_name is not null and doc_name != '')
and doc_type != 'Feed'
(reference_doctype is not null and reference_doctype != '')
and (reference_name is not null and reference_name != '')
and (reference_owner is null or reference_owner = '')
for update"""):
owner = frappe.db.get_value(doctype, name, "owner")
@ -18,12 +17,12 @@ def execute():
if not owner:
continue
frappe.db.sql("""update `tabFeed`
set doc_owner=%(owner)s
frappe.db.sql("""update `tabCommunication`
set reference_owner=%(owner)s
where
doc_type=%(doctype)s
and doc_name=%(name)s
and (doc_owner is null or doc_owner = '')""".format(doctype=doctype), {
reference_doctype=%(doctype)s
and reference_name=%(name)s
and (reference_owner is null or reference_owner = '')""".format(doctype=doctype), {
"doctype": doctype,
"name": name,
"owner": owner

View file

@ -3,8 +3,6 @@ import frappe
from frappe.model.db_schema import add_column
def execute():
frappe.reload_doctype("Feed")
frappe.db.sql("""update `tabSingles` set field='_liked_by' where field='_starred_by'""")
frappe.db.commit()
@ -13,6 +11,5 @@ def execute():
if "_starred_by" in columns:
frappe.db.sql_ddl("""alter table `{0}` change `_starred_by` `_liked_by` Text """.format(table))
for doctype in ("Comment", "Communication"):
if not frappe.db.has_column(doctype, "_liked_by"):
add_column(doctype, "_liked_by", "Text")
if not frappe.db.has_column("Communication", "_liked_by"):
add_column("Communication", "_liked_by", "Text")

View file

View file

@ -0,0 +1,305 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.rename_doc import get_link_fields, dynamic_link_queries
from frappe.permissions import reset_perms
def execute():
frappe.reload_doctype("DocType")
frappe.reload_doctype("Communication")
reset_perms("Communication")
migrate_comments()
frappe.delete_doc("DocType", "Comment")
# frappe.db.sql_ddl("drop table `tabComment`")
migrate_feed()
frappe.delete_doc("DocType", "Feed")
# frappe.db.sql_ddl("drop table `tabFeed`")
update_timeline_doc_for("Blogger")
def migrate_comments():
# comments
frappe.db.sql("""insert ignore into `tabCommunication` (
subject,
content,
sender,
sender_full_name,
comment_type,
communication_date,
reference_doctype,
reference_name,
link_doctype,
link_name,
name,
user,
owner,
creation,
modified_by,
modified,
status,
sent_or_received,
communication_type,
seen
)
select
substring(comment, 1, 100) as subject,
comment as content,
comment_by as sender,
comment_by_fullname as sender_full_name,
comment_type,
ifnull(timestamp(comment_date, comment_time), creation) as communication_date,
comment_doctype as reference_doctype,
comment_docname as reference_name,
reference_doctype as link_doctype,
reference_name as link_name,
name,
owner as user,
owner,
creation,
modified_by,
modified,
'Linked' as status,
'Sent' as sent_or_received,
'Comment' as communication_type,
1 as seen
from `tabComment` where comment_doctype is not null and comment_doctype not in ('Message', 'My Company')""")
# chat and assignment notifications
frappe.db.sql("""insert ignore into `tabCommunication` (
subject,
content,
sender,
sender_full_name,
comment_type,
communication_date,
reference_doctype,
reference_name,
link_doctype,
link_name,
name,
user,
owner,
creation,
modified_by,
modified,
status,
sent_or_received,
communication_type,
seen
)
select
case
when parenttype='Assignment' then %(assignment)s
else substring(comment, 1, 100)
end
as subject,
comment as content,
comment_by as sender,
comment_by_fullname as sender_full_name,
comment_type,
ifnull(timestamp(comment_date, comment_time), creation) as communication_date,
'User' as reference_doctype,
comment_docname as reference_name,
reference_doctype as link_doctype,
reference_name as link_name,
name,
owner as user,
owner,
creation,
modified_by,
modified,
'Linked' as status,
'Sent' as sent_or_received,
case
when parenttype='Assignment' then 'Notification'
else 'Chat'
end
as communication_type,
1 as seen
from `tabComment` where comment_doctype in ('Message', 'My Company')""", {"assignment": _("Assignment")})
def migrate_feed():
# migrate delete feed
for doctype in frappe.db.sql("""select distinct doc_type from `tabFeed` where subject=%(deleted)s""", {"deleted": _("Deleted")}):
frappe.db.sql("""insert ignore into `tabCommunication` (
subject,
sender,
sender_full_name,
comment_type,
communication_date,
reference_doctype,
name,
user,
owner,
creation,
modified_by,
modified,
status,
sent_or_received,
communication_type,
seen
)
select
concat_ws(" ", %(_doctype)s, doc_name) as subject,
owner as sender,
full_name as sender_full_name,
'Deleted' as comment_type,
creation as communication_date,
doc_type as reference_doctype,
name,
owner as user,
owner,
creation,
modified_by,
modified,
'Linked' as status,
'Sent' as sent_or_received,
'Comment' as communication_type,
1 as seen
from `tabFeed` where subject=%(deleted)s and doc_type=%(doctype)s""", {
"deleted": _("Deleted"),
"doctype": doctype,
"_doctype": _(doctype)
})
# migrate feed type login or empty
frappe.db.sql("""insert ignore into `tabCommunication` (
subject,
sender,
sender_full_name,
comment_type,
communication_date,
reference_doctype,
reference_name,
reference_owner,
link_doctype,
link_name,
name,
user,
owner,
creation,
modified_by,
modified,
status,
sent_or_received,
communication_type,
seen
)
select
subject,
owner as sender,
full_name as sender_full_name,
case
when feed_type='Login' then 'Info'
else 'Updated'
end as comment_type,
creation as communication_date,
doc_type as reference_doctype,
doc_name as reference_name,
doc_owner as reference_owner,
reference_doctype as link_doctype,
reference_name as link_name,
name,
owner as user,
owner,
creation,
modified_by,
modified,
'Linked' as status,
'Sent' as sent_or_received,
'Comment' as communication_type,
1 as seen
from `tabFeed` where (feed_type in ('Login', '') or feed_type is null)""")
def update_timeline_doc_for(timeline_doctype):
"""NOTE: This method may be used by other apps for patching. It also has COMMIT after each update."""
# find linked doctypes
# link fields
update_for_linked_docs(timeline_doctype)
# dynamic link fields
update_for_dynamically_linked_docs(timeline_doctype)
def update_for_linked_docs(timeline_doctype):
for df in get_link_fields(timeline_doctype):
if df.issingle:
continue
reference_doctype = df.parent
if not is_valid_timeline_doctype(reference_doctype, timeline_doctype):
continue
for doc in frappe.get_all(reference_doctype, fields=["name", df.fieldname]):
timeline_name = doc.get(df.fieldname)
update_communication(timeline_doctype, timeline_name, reference_doctype, doc.name)
def update_for_dynamically_linked_docs(timeline_doctype):
dynamic_link_fields = []
for query in dynamic_link_queries:
for df in frappe.db.sql(query, as_dict=True):
dynamic_link_fields.append(df)
for df in dynamic_link_fields:
reference_doctype = df.parent
if not is_valid_timeline_doctype(reference_doctype, timeline_doctype):
continue
try:
docs = frappe.get_all(reference_doctype, fields=["name", df.fieldname],
filters={ df.options: timeline_doctype })
except frappe.SQLError, e:
if e.args and e.args[0]==1146:
# single
continue
else:
raise
for doc in docs:
timeline_name = doc.get(df.fieldname)
update_communication(timeline_doctype, timeline_name, reference_doctype, doc.name)
def update_communication(timeline_doctype, timeline_name, reference_doctype, reference_name):
if not timeline_name:
return
frappe.db.sql("""update `tabCommunication` set timeline_doctype=%(timeline_doctype)s, timeline_name=%(timeline_name)s
where (reference_doctype=%(reference_doctype)s and reference_name=%(reference_name)s)
and (timeline_doctype is null or timeline_doctype='')
and (timeline_name is null or timeline_name='')""", {
"timeline_doctype": timeline_doctype,
"timeline_name": timeline_name,
"reference_doctype": reference_doctype,
"reference_name": reference_name
})
frappe.db.commit()
def is_valid_timeline_doctype(reference_doctype, timeline_doctype):
# for reloading timeline_field
frappe.reload_doctype(reference_doctype)
# make sure the timeline field's doctype is same as timeline doctype
meta = frappe.get_meta(reference_doctype)
if not meta.timeline_field:
return False
doctype = meta.get_link_doctype(meta.timeline_field)
if doctype != timeline_doctype:
return False
return True

View file

@ -101,6 +101,7 @@
"public/js/frappe/model/perm.js",
"public/js/frappe/model/workflow.js",
"public/js/lib/md5.min.js",
"public/js/frappe/misc/user.js",
"public/js/frappe/misc/pretty_date.js",
"public/js/frappe/misc/utils.js",

View file

@ -97,6 +97,9 @@
.timeline-item blockquote {
font-size: inherit;
}
.timeline-item .btn-more {
margin-left: 65px;
}
.timeline-items {
position: relative;
}

View file

@ -139,8 +139,8 @@ frappe.ui.form.Attachments = Class.extend({
return;
}
me.remove_fileid(fileid);
me.frm.get_docinfo().comments.push(r.message);
me.frm.comments.refresh();
me.frm.get_docinfo().communications.push(r.message);
me.frm.timeline.refresh();
if (callback) callback();
}
});
@ -180,8 +180,8 @@ frappe.ui.form.Attachments = Class.extend({
this.add_to_attachments(attachment);
this.refresh();
if(comment) {
this.frm.get_docinfo().comments.push(comment);
this.frm.comments.refresh();
this.frm.get_docinfo().communications.push(comment);
this.frm.timeline.refresh();
}
}
},

View file

@ -22,7 +22,7 @@ frappe.ui.form.Footer = Class.extend({
},
make_comments: function() {
this.frm.comments = new frappe.ui.form.Comments({
this.frm.timeline = new frappe.ui.form.Timeline({
parent: this.wrapper.find(".form-comments"),
frm: this.frm
})
@ -32,7 +32,7 @@ frappe.ui.form.Footer = Class.extend({
this.parent.addClass("hide");
} else {
this.parent.removeClass("hide");
this.frm.comments.refresh();
this.frm.timeline.refresh();
}
},
});

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.Comments = Class.extend({
frappe.ui.form.Timeline = Class.extend({
init: function(opts) {
$.extend(this, opts);
this.make();
@ -40,6 +40,34 @@ frappe.ui.form.Comments = Class.extend({
this.setup_comment_like();
this.setup_mentions();
this.list.on("click", ".btn-more", function() {
var communications = me.get_communications();
frappe.call({
btn: this,
method: "frappe.desk.form.load.get_communications",
args: {
doctype: me.frm.doc.doctype,
name: me.frm.doc.name,
start: communications.length
},
callback: function(r) {
if (!r.exc) {
if (r.message) {
var new_communications = r.message;
var communications = me.get_communications().concat(new_communications);
frappe.model.set_docinfo(me.frm.doc.doctype, me.frm.doc.name, "communications", communications);
} else {
me.more = false;
}
me.refresh();
}
}
});
});
},
refresh: function(scroll_to_end) {
@ -54,23 +82,42 @@ frappe.ui.form.Comments = Class.extend({
this.wrapper.toggle(true);
this.list.empty();
var comments = [{"comment": __("Created"), "comment_type": "Created",
"comment_by": this.frm.doc.owner, "creation": this.frm.doc.creation}].concat(this.get_comments());
// var communications = [].concat(this.get_communications());
$.each(comments.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }),
var communications = this.get_communications();
$.each(communications.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }),
function(i, c) {
if(c.comment) me.render_comment(c);
if(c.content) {
c.frm = me.frm;
me.render_timeline_item(c);
}
});
// more btn
if (this.more===undefined && communications.length===20) {
this.more = true;
}
if (this.more) {
var $more = $('<div class="timeline-item">\
<button class="btn btn-default btn-xs btn-more">More</button>\
</div>').appendTo(me.list);
}
// created
me.render_timeline_item({"content": __("Created"), "comment_type": "Created", "communication_type": "Comment",
"sender": this.frm.doc.owner, "creation": this.frm.doc.creation, "frm": this.frm});
this.wrapper.find(".is-email").prop("checked", this.last_type==="Email").change();
this.frm.sidebar.refresh_comments();
},
render_comment: function(c) {
render_timeline_item: function(c) {
var me = this;
this.prepare_comment(c);
this.prepare_timeline_item(c);
var $timeline_item = $(frappe.render_template("timeline_item", {data:c}))
.appendTo(me.list)
@ -81,8 +128,8 @@ frappe.ui.form.Comments = Class.extend({
});
if(c.comment_type==="Email") {
this.last_type = c.comment_type;
if(c.communication_type=="Communication" && c.communication_medium==="Email") {
this.last_type = c.communication_medium;
this.add_reply_btn_event($timeline_item, c);
}
@ -95,7 +142,7 @@ frappe.ui.form.Comments = Class.extend({
var last_email = null;
// find the email tor reply to
me.get_comments().forEach(function(c) {
me.get_communications().forEach(function(c) {
if(c.name==name) {
last_email = c;
return false;
@ -112,61 +159,61 @@ frappe.ui.form.Comments = Class.extend({
});
},
prepare_comment: function(c) {
if((c.comment_type || "Comment") === "Comment" && frappe.model.can_delete("Comment")) {
prepare_timeline_item: function(c) {
if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment" && frappe.model.can_delete("Communication")) {
c["delete"] = '<a class="close" href="#"><i class="octicon octicon-trashcan"></i></a>';
} else {
c["delete"] = "";
}
if(!c.comment_by) c.comment_by = this.frm.doc.owner;
if(!c.sender) c.sender = this.frm.doc.owner;
if(c.comment_by.indexOf("<")!==-1) {
c.comment_by = c.comment_by.split("<")[1].split(">")[0];
if(c.sender.indexOf("<")!==-1) {
c.sender = c.sender.split("<")[1].split(">")[0];
}
c.image = frappe.user_info(c.comment_by).image
|| frappe.get_gravatar(c.comment_by);
c.image = frappe.user_info(c.sender).image || frappe.get_gravatar(c.sender);
c.comment_on = comment_when(c.creation);
c.fullname = frappe.user.full_name(c.comment_by);
c.fullname = c.sender_full_name || frappe.user.full_name(c.sender);
if(c.attachments && typeof c.attachments==="string")
c.attachments = JSON.parse(c.attachments);
if(!c.comment_type)
c.comment_type = "Comment"
if(c.communication_type=="Comment" && !c.comment_type) {
c.comment_type = "Comment";
}
this.set_icon_and_color(c);
// label view
if(c.comment_type==="Workflow" || c.comment_type==="Label") {
c.comment_html = repl('<span class="label label-%(style)s">%(text)s</span>', {
style: frappe.utils.guess_style(c.comment),
text: __(c.comment)
style: frappe.utils.guess_style(c.content),
text: __(c.content)
});
} else {
if(c.comment_type=="Email") {
c.comment = c.comment.split("<!-- original-reply -->")[0];
c.comment = frappe.utils.strip_original_content(c.comment);
c.comment = frappe.dom.remove_script_and_style(c.comment);
if(c.communication_type=="Communication" && c.communication_medium=="Email") {
c.content = c.content.split("<!-- original-reply -->")[0];
c.content = frappe.utils.strip_original_content(c.content);
c.content = frappe.dom.remove_script_and_style(c.content);
c.original_comment = c.comment;
c.comment = frappe.utils.toggle_blockquote(c.comment);
c.original_content = c.content;
c.content = frappe.utils.toggle_blockquote(c.content);
}
if(!frappe.utils.is_html(c.comment)) {
c.comment_html = frappe.markdown(__(c.comment));
if(!frappe.utils.is_html(c.content)) {
c.content_html = frappe.markdown(__(c.content));
} else {
c.comment_html = c.comment;
c.comment_html = frappe.utils.strip_whitespace(c.comment_html);
c.content_html = c.content;
c.content_html = frappe.utils.strip_whitespace(c.content_html);
}
// bold @mentions
if(c.comment_type==="Comment") {
c.comment_html = c.comment_html.replace(/(^|\W)(@\w+)/g, "$1<b>$2</b>");
c.content_html = c.content_html.replace(/(^|\W)(@\w+)/g, "$1<b>$2</b>");
}
if (in_list(["Comment", "Email"], c.comment_type)) {
if (this.is_communication_or_comment(c)) {
c.user_content = true;
if (!$.isArray(c._liked_by)) {
c._liked_by = JSON.parse(c._liked_by || "[]");
@ -176,6 +223,11 @@ frappe.ui.form.Comments = Class.extend({
}
}
},
is_communication_or_comment: function(c) {
return c.communication_type==="Communication" || (c.communication_type==="Comment" && c.comment_type==="Comment");
},
set_icon_and_color: function(c) {
c.icon = {
"Email": "octicon octicon-mail",
@ -195,7 +247,7 @@ frappe.ui.form.Comments = Class.extend({
"Shared": "octicon octicon-eye",
"Unshared": "octicon octicon-circle-slash",
"Like": "octicon octicon-heart"
}[c.comment_type]
}[c.comment_type || c.communication_medium]
c.color = {
"Email": "#3498db",
@ -212,18 +264,18 @@ frappe.ui.form.Comments = Class.extend({
"Label": "#2c3e50",
"Attachment": "#7f8c8d",
"Attachment Removed": "#eee"
}[c.comment_type];
}[c.comment_type || c.communication_medium];
c.icon_fg = {
"Attachment Removed": "#333",
}[c.comment_type]
}[c.comment_type || c.communication_medium]
if(!c.icon_fg)
c.icon_fg = "#fff";
},
get_comments: function() {
return this.frm.get_docinfo().comments;
get_communications: function() {
return this.frm.get_docinfo().communications;
},
add_comment: function(btn) {
var txt = this.input.val();
@ -238,12 +290,13 @@ frappe.ui.form.Comments = Class.extend({
method: "frappe.desk.form.utils.add_comment",
args: {
doc:{
doctype: "Comment",
doctype: "Communication",
communication_type: "Comment",
comment_type: comment_type || "Comment",
comment_doctype: this.frm.doctype,
comment_docname: this.frm.docname,
comment: comment,
comment_by: user
reference_doctype: this.frm.doctype,
reference_name: this.frm.docname,
content: comment,
sender: user
}
},
btn: btn,
@ -254,7 +307,7 @@ frappe.ui.form.Comments = Class.extend({
frappe.utils.play_sound("click");
var comment = r.message;
var comments = me.get_comments();
var comments = me.get_communications();
var comment_exists = false;
for (var i=0, l=comments.length; i<l; i++) {
if (comments[i].name==comment.name) {
@ -266,7 +319,7 @@ frappe.ui.form.Comments = Class.extend({
return;
}
me.frm.get_docinfo().comments = comments.concat([r.message]);
me.frm.get_docinfo().communications = comments.concat([r.message]);
me.refresh(true);
}
}
@ -279,15 +332,15 @@ frappe.ui.form.Comments = Class.extend({
return frappe.call({
method: "frappe.client.delete",
args: {
doctype: "Comment",
doctype: "Communication",
name: name
},
callback: function(r) {
if(!r.exc) {
frappe.utils.play_sound("delete");
me.frm.get_docinfo().comments =
$.map(me.frm.get_docinfo().comments,
me.frm.get_docinfo().communications =
$.map(me.frm.get_docinfo().communications,
function(v) {
if(v.name==name) return null;
else return v;
@ -309,13 +362,13 @@ frappe.ui.form.Comments = Class.extend({
get_last_email: function(from_recipient) {
var last_email = null,
comments = this.frm.get_docinfo().comments,
communications = this.frm.get_docinfo().communications,
email = this.get_recipient();
$.each(comments.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }), function(i, c) {
if(c.comment_type=="Email") {
$.each(communications.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }), function(i, c) {
if(c.communication_type=='Communication' && c.communication_medium=="Email") {
if(from_recipient) {
if(c.comment_by.indexOf(email)!==-1) {
if(c.sender.indexOf(email)!==-1) {
last_email = c;
return false;
}

View file

@ -12,13 +12,21 @@
{%= data.delete %}
</span>
</div>
{% if(data.doctype=="Communication" || data.comment_type=="Comment") { %}
{% if(data.communication_type==="Communication" || (data.communication_type==="Comment" && data.comment_type==="Comment")) { %}
<div class="comment-header small">
<i class="{%= data.icon %} icon-fixed-width"></i>
<span title="{%= data.comment_by %}">{%= data.fullname %}</span>
<span>
{% if (data.timeline_doctype===data.frm.doc.doctype && data.timeline_name===data.frm.doc.name) { %}
&ndash;
<a href="#Form/{%= data.reference_doctype %}/{%= data.reference_name %}" class="text-muted">
<strong>{{ __(data.reference_doctype) }}</strong> {{ data.reference_name }}
</a>
{% } %}
</span>
<span class="text-muted" style="font-weight: normal;">
&ndash; {%= data.comment_on %}</span>
{% if(data.doctype=="Communication") { %}
{% if(data.communication_type==="Communication") { %}
{% if (frappe.model.can_read(\'Communication\')) { %}
<a href="#Form/{%= data.doctype %}/{%= data.name %}"
class="text-muted">
@ -47,8 +55,10 @@
</a>
{% } %}
{% if (data.communication_medium === "Email") { %}
<a class="text-muted reply-link pull-right"
data-name="{%= data.name %}">{%= __("Reply") %}</a>
{% } %}
{% } %}
<span class="comment-likes" data-liked-by=\'{{ JSON.stringify(data._liked_by) }}\'>
<i class="octicon octicon-heart like-action
@ -62,17 +72,23 @@
</div>
<div class="reply">
<div>
{%= data.comment_html %}
{%= data.content_html %}
</div>
</div>
{% } else if(in_list(["Assignment Completed", "Assigned", "Shared", "Unshared"], data.comment_type)) { %}
<div class="small">
<i class="{%= data.icon %} icon-fixed-width"></i>
{% if(data.reference_doctype && data.reference_name) { %}
<a href="#Form/{%= data.reference_doctype %}/{%= data.reference_name %}">
{% if (data.timeline_doctype===data.frm.doc.doctype && data.timeline_name===data.frm.doc.name) { %}
<a href="#Form/{%= data.reference_doctype %}/{%= data.reference_name %}">
<strong>{{ __(data.reference_doctype) }}</strong> {{ data.reference_name }}
</a>
&ndash;
{% } %}
{%= data.comment %}
{% if(data.reference_doctype && data.reference_name) { %}
{% if(data.link_doctype && data.link_name) { %}
<a href="#Form/{%= data.link_doctype %}/{%= data.link_name %}">
{% } %}
{%= data.content %}
{% if(data.link_doctype && data.link_name) { %}
</a>
{% } %}
<span class="text-muted" style="font-weight: normal;">
@ -82,10 +98,23 @@
<div class="small">
<i class="{%= data.icon %} icon-fixed-width"></i>
{% if (data.comment_type == "Like") { %}
<span title="{%= data.comment_by %}">{%= __("Liked by {0}", [data.fullname]) %}</span>
<span title="{%= data.comment_by %}">
{% if (data.timeline_doctype===data.frm.doc.doctype && data.timeline_name===data.frm.doc.name) { %}
<a href="#Form/{%= data.reference_doctype %}/{%= data.reference_name %}">
<strong>{{ __(data.reference_doctype) }}</strong> {{ data.reference_name }}
</a> &ndash;
{% } %}
{%= __("Liked by {0}", [data.fullname]) %}
</span>
{% } else { %}
<span title="{%= data.comment_by %}">{%= data.fullname %}</span>
{%= data.comment %}
{%= data.content %}
{% if (data.timeline_doctype===data.frm.doc.doctype && data.timeline_name===data.frm.doc.name) { %}
&ndash;
<a href="#Form/{%= data.reference_doctype %}/{%= data.reference_name %}">
<strong>{{ __(data.reference_doctype) }}</strong> {{ data.reference_name }}
</a>
{% } %}
{% } %}
<span class="text-muted" style="font-weight: normal;">
&ndash; {%= data.comment_on %}</span>

View file

@ -68,8 +68,8 @@ frappe.ui.form.Sidebar = Class.extend({
},
refresh_comments: function() {
var comments = $.map(this.frm.comments.get_comments(), function(c) {
return (c.comment_type==="Email" || c.comment_type==="Comment") ? c : null;
var comments = $.map(this.frm.timeline.get_communications(), function(c) {
return (c.communication_type==="Communication" || (c.communication_type=="Comment" && c.comment_type==="Comment")) ? c : null;
});
this.comments.find(".n-comments").html(comments.length);
},

View file

@ -101,7 +101,7 @@ frappe.ui.form.States = Class.extend({
// success - add a comment
var success = function() {
me.frm.comments.insert_comment("Workflow", next_state);
me.frm.timeline.insert_comment("Workflow", next_state);
}
if(new_docstatus==1 && me.frm.doc.docstatus==0) {
me.frm.savesubmit(null, success, on_error);

View file

@ -359,7 +359,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
}
// apply default filters, if specified for a listing
$.each((this.listview.default_filters || []), function(i, f) {
$.each((this.listview.default_filters || this.listview.settings.default_filters || []), function(i, f) {
args.filters.push(f);
});

View file

@ -52,7 +52,6 @@ frappe.avatar = function(user, css_class, title) {
frappe.gravatars = {};
frappe.get_gravatar = function(email_id) {
frappe.require("/assets/frappe/js/lib/md5.min.js");
if(!frappe.gravatars[email_id]) {
frappe.gravatars[email_id] = "https://secure.gravatar.com/avatar/" + md5(email_id) + "?d=retro";
}

View file

@ -159,29 +159,53 @@ $.extend(frappe.model, {
}
},
new_comment: function(comment) {
var reference_doctype = comment.comment_doctype || comment.reference_doctype;
var reference_name = comment.comment_docname || comment.reference_name;
if (frappe.model.docinfo[reference_doctype] && frappe.model.docinfo[reference_doctype][reference_name]) {
var comments = frappe.model.docinfo[reference_doctype][reference_name].comments;
var comment_exists = false;
for (var i=0, l=comments.length; i<l; i++) {
if (comments[i].name==comment.name) {
comment_exists = true;
new_communication: function(communication) {
var docinfo = frappe.model.get_docinfo(communication.reference_doctype, communication.reference_name);
if (docinfo && docinfo.communications) {
var communications = docinfo.communications;
var communication_exists = false;
for (var i=0, l=communications.length; i<l; i++) {
if (communications[i].name==communication.name) {
communication_exists = true;
break;
}
}
if (!comment_exists) {
frappe.model.docinfo[reference_doctype][reference_name].comments = comments.concat([comment]);
if (!communication_exists) {
docinfo.communications = communications.concat([communication]);
}
}
if (cur_frm.doctype === reference_doctype && cur_frm.docname === reference_name) {
cur_frm.comments && cur_frm.comments.refresh();
if (cur_frm.doctype === communication.reference_doctype && cur_frm.docname === communication.reference_name) {
cur_frm.timeline && cur_frm.timeline.refresh();
}
},
delete_communication: function(communication) {
var docinfo = frappe.model.get_docinfo(communication.reference_doctype, communication.reference_name);
if (docinfo && docinfo.communications) {
var communications = docinfo.communications;
var index = -1;
for (var i=0, l=communications.length; i<l; i++) {
if (communications[i].name==communication.name) {
index = i;
break;
}
}
if (index !== -1) {
// remove it from communications list
docinfo.communications.splice(index, 1);
}
}
if (cur_frm.doctype === communication.reference_doctype && cur_frm.docname === communication.reference_name) {
cur_frm.timeline && cur_frm.timeline.refresh();
}
},
get_shared: function(doctype, name) {
return frappe.model.get_docinfo(doctype, name).shared;
},

View file

@ -235,7 +235,6 @@ frappe.ui.Filter = Class.extend({
}
var field_area = me.$w.find('.filter_field').empty().get(0);
var f = frappe.ui.form.make_control({
df: df,
parent: field_area,

View file

@ -84,7 +84,7 @@ frappe.ui.notifications.add_notification = function(doctype, notifications_map)
// default notification config
frappe.ui.notifications.config = {
"ToDo": { label: __("To Do") },
"Comment": { label: __("Messages"), route: "messages"},
"Messages": { label: __("Messages"), route: "messages"},
"Event": { label: __("Calendar"), route: "Calendar/Event" },
"Likes": {
label: __("Likes"),
@ -94,12 +94,12 @@ frappe.ui.notifications.config = {
};
if (frappe.get_route()[0]=="activity") {
frappe.pages['activity'].on_page_show();
frappe.pages['activity'].page.list.refresh();
} else {
frappe.set_route("activity");
}
}
}
},
};
frappe.views.show_open_count_list = function(element) {

View file

@ -110,10 +110,10 @@ frappe.views.CommunicationComposer = Class.extend({
}
}
var comments = this.frm.get_docinfo().comments;
if (comments) {
for ( var i=0, l=comments.length; i<l; i++ ) {
cc.push( [comments[i].comment_by, 0] );
var communications = this.frm.get_docinfo().communications;
if (communications) {
for ( var i=0, l=communications.length; i<l; i++ ) {
cc.push( [communications[i].sender, 0] );
}
}
@ -160,21 +160,21 @@ frappe.views.CommunicationComposer = Class.extend({
this.subject = this.subject || "";
if(!this.recipients && this.last_email) {
this.recipients = this.last_email.comment_by;
this.recipients = this.last_email.sender;
}
if(!this.recipients) {
this.recipients = this.frm && this.frm.comments.get_recipient();
this.recipients = this.frm && this.frm.timeline.get_recipient();
}
if(!this.subject && this.frm) {
// get subject from last communication
var last = this.frm.comments.get_last_email();
var last = this.frm.timeline.get_last_email();
if(last) {
this.subject = last.subject;
if(!this.recipients) {
this.recipients = last.comment_by;
this.recipients = last.sender;
}
// prepend "Re:"
@ -420,7 +420,7 @@ frappe.views.CommunicationComposer = Class.extend({
};
return frappe.call({
method:"frappe.core.doctype.communication.communication.make",
method:"frappe.core.doctype.communication.email.make",
args: {
recipients: form_values.recipients,
cc: form_values.cc,
@ -453,7 +453,7 @@ frappe.views.CommunicationComposer = Class.extend({
delete frappe.last_edited_communication[cur_frm.doctype][cur_frm.docname];
}
// clear input
cur_frm.comments.input.val("");
cur_frm.timeline.input.val("");
cur_frm.reload_doc();
}
} else {
@ -469,7 +469,7 @@ frappe.views.CommunicationComposer = Class.extend({
last_email = this.last_email;
if(!last_email) {
last_email = this.frm && this.frm.comments.get_last_email(true);
last_email = this.frm && this.frm.timeline.get_last_email(true);
}
if(!frappe.utils.is_html(signature)) {
@ -489,13 +489,13 @@ frappe.views.CommunicationComposer = Class.extend({
+ (signature ? ("<br>" + signature) : "");
if(last_email) {
var last_email_content = last_email.original_comment || last_email.comment;
var last_email_content = last_email.original_comment || last_email.content;
fields.content.set_input(reply
+ "<br><!-- original-reply --><br>"
+ '<blockquote>' +
'<p>' + __("On {0}, {1} wrote:",
[frappe.datetime.global_date_format(last_email.creation) , last_email.comment_by]) + '</p>' +
[frappe.datetime.global_date_format(last_email.creation) , last_email.sender]) + '</p>' +
last_email_content +
'<blockquote>');
} else {

View file

@ -25,8 +25,12 @@ frappe.views.FormFactory = frappe.views.Factory.extend({
frappe.ui.form.close_grid_form();
});
frappe.realtime.on("new_comment", function(data) {
frappe.model.new_comment(data);
frappe.realtime.on("new_communication", function(data) {
frappe.model.new_communication(data);
});
frappe.realtime.on("delete_communication", function(data) {
frappe.model.delete_communication(data);
});
frappe.realtime.on("doc_viewers", function(data) {

View file

@ -815,7 +815,7 @@ _f.Frm.prototype.reload_docinfo = function(callback) {
callback: function(r) {
// docinfo will be synced
if(callback) callback(r.docinfo);
me.comments.refresh();
me.timeline.refresh();
me.assign_to.refresh();
me.attachments.refresh();
}

View file

@ -127,6 +127,10 @@
blockquote {
font-size: inherit;
}
.btn-more {
margin-left: 65px;
}
}
.timeline-items {

View file

@ -3,5 +3,5 @@
</p>
<blockquote
style="border-left: 3px solid #d1d8dd; padding: 7px 15px; margin-left: 0px;">
{{ comment.comment | markdown }}
{{ comment.content | markdown }}
</blockquote>

View file

@ -1,16 +1,16 @@
<div class="blog-comment-row">
<div class="inline-block" style="vertical-align: top">
<div class="avatar avatar-medium" style="margin-top: 11px;">
<img itemprop="thumbnailUrl" src="{{ frappe.get_gravatar(comment.comment_by) }}" />
<img itemprop="thumbnailUrl" src="{{ frappe.get_gravatar(comment.sender) }}" />
</div>
</div>
<div class="inline-block" style="width: calc(100% - 100px)">
<h4 itemprop="name">{{ comment.comment_by_fullname }}
<h4 itemprop="name">{{ comment.sender_full_name }}
<br>
<small class="text-muted">
<span itemprop="commentTime">{{ comment.creation|global_date_format }}</span>
</small>
</h4>
<div itemprop="commentText">{{ comment.comment|markdown }}</div>
<div itemprop="commentText">{{ comment.content|markdown }}</div>
</div>
</div>

View file

@ -81,8 +81,8 @@
comment_by_fullname: $("[name='comment_by_fullname']").val(),
comment_by: $("[name='comment_by']").val(),
comment: $("[name='comment']").val(),
comment_doctype: "{{ comment_doctype or doctype }}",
comment_docname: "{{ comment_docname or name }}",
reference_doctype: "{{ reference_doctype or doctype }}",
reference_name: "{{ reference_name or name }}",
comment_type: "Comment",
page_name: "{{ pathname }}",
}

View file

@ -15,48 +15,46 @@ def add_comment(args=None):
'comment': '',
'comment_by': '',
'comment_by_fullname': '',
'comment_doctype': '',
'comment_docname': '',
'reference_doctype': '',
'reference_name': '',
'page_name': '',
}
"""
if not args:
args = frappe.local.form_dict
args['doctype'] = "Comment"
page_name = args.get("page_name")
if "page_name" in args:
del args["page_name"]
if "cmd" in args:
del args["cmd"]
comment = frappe.get_doc(args)
comment.comment_type = "Comment"
doc = frappe.get_doc(args["reference_doctype"], args["reference_name"])
comment = doc.add_comment("Comment", args["comment"], comment_by=args["comment_by"])
comment.flags.ignore_permissions = True
comment.insert()
comment.sender_full_name = args["comment_by_fullname"]
comment.save()
# since comments are embedded in the page, clear the web cache
clear_cache(page_name)
# notify commentors
commentors = [d[0] for d in frappe.db.sql("""select comment_by from tabComment where
comment_doctype=%s and comment_docname=%s and
unsubscribed=0""", (comment.comment_doctype, comment.comment_docname))]
commentors = [d[0] for d in frappe.db.sql("""select sender from `tabCommunication`
where
communication_type = 'Comment' and comment_type = 'Comment'
and reference_doctype=%s
and reference_name=%s""", (comment.reference_doctype, comment.reference_name))]
owner = frappe.db.get_value(comment.comment_doctype, comment.comment_docname, "owner")
owner = frappe.db.get_value(doc.doctype, doc.name, "owner")
recipients = list(set(commentors if owner=="Administrator" else (commentors + [owner])))
message = _("{0} by {1}").format(markdown2.markdown(args.get("comment")), comment.comment_by_fullname)
message = _("{0} by {1}").format(markdown2.markdown(args.get("comment")), comment.sender_full_name)
message += "<p><a href='{0}/{1}' style='font-size: 80%'>{2}</a></p>".format(frappe.utils.get_request_site_address(),
page_name, _("View it in your browser"))
from frappe.email.bulk import send
send(recipients=recipients,
subject = _("New comment on {0} {1}").format(comment.comment_doctype, comment.comment_docname),
subject = _("New comment on {0} {1}").format(doc.doctype, doc.name),
message = message,
reference_doctype=comment.comment_doctype, reference_name=comment.comment_docname)
reference_doctype=doc.doctype, reference_name=doc.name)
template = frappe.get_template("templates/includes/comments/comment.html")

View file

@ -141,8 +141,12 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len
t1.published_on as creation,
ifnull(t1.blog_intro, t1.content) as content,
t2.full_name, t2.avatar, t1.blogger,
(select count(name) from `tabComment` where
comment_doctype='Blog Post' and comment_docname=t1.name and comment_type="Comment") as comments
(select count(name) from `tabCommunication`
where
communication_type='Comment'
and comment_type='Comment'
and reference_doctype='Blog Post'
and reference_name=t1.name) as comments
from `tabBlog Post` t1, `tabBlogger` t2
where ifnull(t1.published,0)=1
and t1.blogger = t2.name

View file

@ -103,8 +103,8 @@ class WebForm(WebsiteGenerator):
context.doc = frappe.get_doc(self.doc_type, frappe.form_dict.name)
context.title = context.doc.get(context.doc.meta.get_title_field())
context.comment_doctype = context.doc.doctype
context.comment_docname = context.doc.name
context.reference_doctype = context.doc.doctype
context.reference_name = context.doc.name
if self.allow_comments and frappe.form_dict.name:
context.comment_list = get_comment_list(context.doc.doctype, context.doc.name)

View file

@ -26,10 +26,14 @@ def can_cache(no_cache=False):
def get_comment_list(doctype, name):
return frappe.db.sql("""select
comment, comment_by_fullname, creation, comment_by
from `tabComment` where comment_doctype=%s
and ifnull(comment_type, "Comment")="Comment"
and comment_docname=%s order by creation""", (doctype, name), as_dict=1) or []
content, sender_full_name, creation, sender
from `tabCommunication`
where
communication_type='Comment'
and reference_doctype=%s
and reference_name=%s
and (comment_type is null or comment_type='Comment')
order by creation""", (doctype, name), as_dict=1) or []
def get_home_page():
if frappe.local.flags.home_page: