diff --git a/frappe/change_log/current/like.md b/frappe/change_log/current/like.md new file mode 100644 index 0000000000..f6606603f0 --- /dev/null +++ b/frappe/change_log/current/like.md @@ -0,0 +1,4 @@ +- Ability to **Like** a document, comment or communication + - See notifications about likes that you received + - View it on Activity feed + - *Stars* have been converted to Likes diff --git a/frappe/core/doctype/comment/comment.json b/frappe/core/doctype/comment/comment.json index fd8d0e45f0..2405dcdd90 100644 --- a/frappe/core/doctype/comment/comment.json +++ b/frappe/core/doctype/comment/comment.json @@ -1,338 +1,338 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "hash", - "creation": "2012-08-08 10:40:11", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", + "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, + "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", - "permlevel": 0, - "print_hide": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + "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-11-16 06:29:43.314568", - "modified_by": "Administrator", - "module": "Core", - "name": "Comment", - "owner": "Administrator", + ], + "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, + "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, + ], + "read_only": 0, + "read_only_onload": 0, "title_field": "comment" -} \ No newline at end of file +} diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 70d71b8c0e..bcff554498 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -23,8 +23,8 @@ class Comment(Document): if self.comment_type in ("Created", "Submitted", "Cancelled", "Label"): comment_type = "Label" - elif self.comment_type == "Comment": - comment_type = "Comment" + elif self.comment_type in ("Comment", "Like"): + comment_type = self.comment_type else: comment_type = "Info" @@ -32,7 +32,9 @@ class Comment(Document): "subject": self.comment, "doctype": self.comment_doctype, "name": self.comment_docname, - "feed_type": comment_type + "feed_type": comment_type, + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name } def after_insert(self): @@ -182,3 +184,6 @@ def on_doctype_update(): 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") diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index a8053e672c..2e1a12fcd5 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -10,6 +10,7 @@ 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 @@ -246,7 +247,6 @@ class Communication(Document): # if it is a fetched email, add follows to CC cc.append(self.get_owner_email()) cc += self.get_assignees() - cc += self.get_starrers() if cc: # exclude email accounts, unfollows, recipients and unsubscribes @@ -306,10 +306,6 @@ class Communication(Document): return filtered - def get_starrers(self): - """Return list of users who have starred this document.""" - return [( get_formatted_email(user) or user ) for user in self.get_parent_doc().get_starred_by()] - def get_owner_email(self): owner = self.get_parent_doc().owner return get_formatted_email(owner) or owner @@ -337,6 +333,9 @@ def on_doctype_update(): """Add index in `tabCommunication` for `(reference_doctype, reference_name)`""" frappe.db.add_index("Communication", ["reference_doctype", "reference_name"]) + 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, diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 266884915e..626081f846 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -14,6 +14,9 @@ def get_notification_config(): "Comment": "frappe.core.notifications.get_unread_messages", "Error Snapshot": {"seen": 0, "parent_error_snapshot": None}, }, + "for_other": { + "Likes": "frappe.core.notifications.get_unseen_likes" + } } def get_things_todo(): @@ -41,3 +44,12 @@ def get_unread_messages(): AND comment_docname = %s AND docstatus=0 """, (frappe.session.user,))[0][0] + +def get_unseen_likes(): + """Returns count of unseen likes""" + return frappe.db.sql("""select count(*) from `tabFeed` + 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] diff --git a/frappe/desk/doctype/feed/feed.json b/frappe/desk/doctype/feed/feed.json index 8950f89490..5fda9adcaf 100644 --- a/frappe/desk/doctype/feed/feed.json +++ b/frappe/desk/doctype/feed/feed.json @@ -1,204 +1,309 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "creation": "2012-07-03 13:29:42", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", + "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", - "permlevel": 0, - "print_hide": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "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, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 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, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 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, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 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, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 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, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 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-11-16 06:29:47.123186", - "modified_by": "Administrator", - "module": "Desk", - "name": "Feed", - "owner": "Administrator", + ], + "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, + "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": 0, - "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, + "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": 0, "read_only_onload": 0 -} +} \ No newline at end of file diff --git a/frappe/desk/doctype/feed/feed.py b/frappe/desk/doctype/feed/feed.py index c9a8ee02fc..f901a978b6 100644 --- a/frappe/desk/doctype/feed/feed.py +++ b/frappe/desk/doctype/feed/feed.py @@ -12,7 +12,13 @@ from frappe import _ exclude_from_linked_with = True class Feed(Document): - pass + 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` @@ -24,9 +30,12 @@ def on_doctype_update(): def get_permission_query_conditions(user): if not user: user = frappe.session.user - if not frappe.permissions.apply_user_permissions("Feed", "read", 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() @@ -34,19 +43,17 @@ def get_permission_query_conditions(user): list(set(can_read) - set(user_permissions.keys()))] if not can_read_doctypes: - return "" + conditions += ["tabFeed.doc_type in ({})".format(", ".join(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 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))) + if can_read_docs: + conditions.append("concat_ws('|', tabFeed.doc_type, tabFeed.doc_name) in ({})".format( + ", ".join(can_read_docs))) return "(" + " or ".join(conditions) + ")" @@ -83,7 +90,10 @@ def update_feed(doc, method=None): "doc_type": doctype, "doc_name": name, "subject": feed.subject, - "full_name": get_fullname(doc.owner) + "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): diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 12860fa6a4..a15cae4e77 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -95,11 +95,11 @@ def get_docinfo(doc=None, doctype=None, name=None): def get_user_permissions(meta): out = {} all_user_permissions = frappe.defaults.get_user_permissions() - + for m in meta: for df in m.get_fields_to_check_permissions(all_user_permissions): out[df.options] = list(set(all_user_permissions[df.options])) - + return out def get_attachments(dt, dn): @@ -108,7 +108,7 @@ def get_attachments(dt, dn): 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 + 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""", @@ -116,7 +116,7 @@ def get_comments(dt, dn, limit=100): communications = frappe.db.sql("""select name, content as comment, sender as comment_by, creation, - communication_medium as comment_type, subject, delivery_status, + communication_medium as comment_type, subject, delivery_status, _liked_by, "Communication" as doctype from tabCommunication where reference_doctype=%s and reference_name=%s diff --git a/frappe/desk/like.py b/frappe/desk/like.py new file mode 100644 index 0000000000..78162b1604 --- /dev/null +++ b/frappe/desk/like.py @@ -0,0 +1,98 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals + +"""Allow adding of likes to documents""" + +import frappe, json +from frappe.model.db_schema import add_column +from frappe import _ +from frappe.utils import get_link_to_form + +@frappe.whitelist() +def toggle_like(doctype, name, add=False): + """Adds / removes the current user in the `__liked_by` property of the given document. + If column does not exist, will add it in the database. + + The `_liked_by` property is always set from this function and is ignored if set via + Document API + + :param doctype: DocType of the document to like + :param name: Name of the document to like + :param add: `Yes` if like is to be added. If not `Yes` the like will be removed.""" + + _toggle_like(doctype, name, add) + +def _toggle_like(doctype, name, add=False, user=None): + """Same as toggle_like but hides param `user` from API""" + + if not user: + user = frappe.session.user + + try: + liked_by = frappe.db.get_value(doctype, name, "_liked_by") + if liked_by: + liked_by = json.loads(liked_by) + else: + liked_by = [] + + if add=="Yes": + if user not in liked_by: + liked_by.append(user) + add_comment(doctype, name) + + else: + if user in liked_by: + liked_by.remove(user) + remove_like(doctype, name) + + frappe.db.set_value(doctype, name, "_liked_by", json.dumps(liked_by), update_modified=False) + + except Exception, e: + if e.args[0]==1054: + add_column(doctype, "_liked_by", "Text") + _toggle_like(doctype, name, add, user) + else: + raise + +def remove_like(doctype, name): + """Remove previous Like""" + # remove Comment + frappe.delete_doc("Comment", [c.name for c in frappe.get_all("Comment", + filters={ + "comment_doctype": doctype, + "comment_docname": name, + "comment_by": frappe.session.user, + "comment_type": "Like" + } + )]) + + # remove Feed + frappe.delete_doc("Feed", [c.name for c in frappe.get_all("Feed", + filters={ + "doc_type": doctype, + "doc_name": name, + "owner": frappe.session.user, + "feed_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("" + doc.comment + "", link), + reference_doctype=doc.comment_doctype, reference_name=doc.comment_docname) + + elif 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("" + doc.subject + "", link), + reference_doctype=doc.reference_doctype, reference_name=doc.reference_name) + + else: + doc.add_comment("Like", _("Liked")) diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 29a7cbcbaa..b818b79bd4 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -24,7 +24,9 @@ def get_notifications(): return { "open_count_doctype": get_notifications_for_doctypes(config, notification_count), "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(): @@ -48,19 +50,27 @@ def get_new_messages(): def get_notifications_for_modules(config, notification_count): """Notifications for modules""" - open_count_module = {} - for m in config.for_module: + return get_notifications_for("for_module", config, notification_count) + +def get_notifications_for_other(config, notification_count): + """Notifications for other items""" + return get_notifications_for("for_other", config, notification_count) + +def get_notifications_for(notification_type, config, notification_count): + open_count = {} + notification_map = config.get(notification_type) or {} + for m in notification_map: try: if m in notification_count: - open_count_module[m] = notification_count[m] + open_count[m] = notification_count[m] else: - open_count_module[m] = frappe.get_attr(config.for_module[m])() + open_count[m] = frappe.get_attr(notification_map[m])() - frappe.cache().hset("notification_count:" + m, frappe.session.user, open_count_module[m]) + frappe.cache().hset("notification_count:" + m, frappe.session.user, open_count[m]) except frappe.PermissionError: frappe.msgprint("Permission Error in notifications for {0}".format(m)) - return open_count_module + return open_count def get_notifications_for_doctypes(config, notification_count): """Notifications for DocTypes""" @@ -143,7 +153,7 @@ def get_notification_config(): config = frappe._dict() for notification_config in frappe.get_hooks().notification_config: nc = frappe.get_attr(notification_config)() - for key in ("for_doctype", "for_module", "for_module_doctypes"): + for key in ("for_doctype", "for_module", "for_module_doctypes", "for_other"): config.setdefault(key, {}) config[key].update(nc.get(key, {})) return config diff --git a/frappe/desk/page/activity/activity.css b/frappe/desk/page/activity/activity.css index 327533d3ce..b28d74ddc0 100644 --- a/frappe/desk/page/activity/activity.css +++ b/frappe/desk/page/activity/activity.css @@ -62,3 +62,12 @@ width: 97% !important; margin: auto; } + +#page-activity .list-filters { + display: none !important; +} + +#page-activity .octicon-heart { + color: #ff5858; + margin: 0px 5px; +} diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js index 9a386e17af..89183c2acf 100644 --- a/frappe/desk/page/activity/activity.js +++ b/frappe/desk/page/activity/activity.js @@ -18,36 +18,73 @@ frappe.pages['activity'].on_page_load = function(wrapper) { this.page.set_title(__("Activity")); - this.page.list = new frappe.ui.Listing({ - hide_refresh: true, - page: this.page, - method: 'frappe.desk.page.activity.activity.get_feed', - parent: $("
").appendTo(this.page.main), - render_row: function(row, data) { - new frappe.activity.Feed(row, data); - } - }); + frappe.model.with_doctype("Feed", function() { + me.page.list = new frappe.ui.Listing({ + hide_refresh: true, + page: me.page, + method: 'frappe.desk.page.activity.activity.get_feed', + parent: $("").appendTo(me.page.main), + render_row: function(row, data) { + new frappe.activity.Feed(row, data); + }, + show_filters: true, + doctype: "Feed", + get_args: function() { + if (frappe.route_options && frappe.route_options.show_likes) { + delete frappe.route_options.show_likes; + return { + show_likes: true + } + } else { + return {} + } + } + }); - this.page.list.run(); + me.page.list.run(); + + me.page.set_primary_action(__("Refresh"), function() { + me.page.list.filter_list.clear_filters(); + me.page.list.run(); + }, "octicon octicon-sync"); + }); frappe.activity.render_plot(this.page); this.page.main.on("click", ".activity-message", function() { - var doctype = $(this).attr("data-doctype"), + var reference_doctype = $(this).attr("data-reference-doctype"), + reference_name = $(this).attr("data-reference-name"), + doctype = $(this).attr("data-doctype"), docname = $(this).attr("data-docname"); + if (doctype && docname) { - frappe.set_route(["Form", doctype, docname]); + frappe.set_route(["Form", reference_doctype || doctype, reference_name || docname]); + + if (reference_doctype && reference_name) { + frappe.route_options = { + scroll_to: { "doctype": doctype, "name": docname } + } + } } }); - this.page.set_primary_action(__("Refresh"), function() { me.page.list.run(); }, "octicon octicon-sync"); - // Build Report Button if(frappe.boot.user.can_get_report.indexOf("Feed")!=-1) { - this.page.set_secondary_action(__('Build Report'), function() { + this.page.add_menu_item(__('Build Report'), function() { frappe.set_route('Report', "Feed"); - }, 'icon-th'); + }, 'icon-th') } + + this.page.add_menu_item(__('Show Likes'), function() { + frappe.route_options = { + show_likes: true + }; + me.page.list.run(); + }, 'octicon octicon-heart'); +}; + +frappe.pages['activity'].on_page_show = function() { + frappe.breadcrumbs.add("Desk"); } frappe.activity.last_feed_date = false; @@ -69,7 +106,7 @@ frappe.activity.Feed = Class.extend({ .find("a").addClass("grey"); }, scrub_data: function(data) { - data.by = frappe.user_info(data.owner).fullname; + data.by = frappe.user.full_name(data.owner); data.imgsrc = frappe.utils.get_file_link(frappe.user_info(data.owner).image); data.icon = "icon-flag"; diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py index 8298ddb503..ba02679559 100644 --- a/frappe/desk/page/activity/activity.py +++ b/frappe/desk/page/activity/activity.py @@ -3,14 +3,38 @@ from __future__ import unicode_literals import frappe +from frappe.utils import cint +from frappe.desk.doctype.feed.feed import get_permission_query_conditions @frappe.whitelist() -def get_feed(limit_start, limit_page_length): +def get_feed(limit_start, limit_page_length, show_likes=False): """get feed""" - return frappe.get_list("Feed", fields=["name", "feed_type", "doc_type", - "subject", "owner", "modified", "doc_name", "creation"], - limit_start = limit_start, limit_page_length = limit_page_length, - order_by="creation desc") + # directly use the permission query condition function of feed + match_conditions = get_permission_query_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` + where + ((feed_type='Like' and (owner=%(user)s or doc_owner=%(user)s)) or feed_type!='Like') + {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 ""), + { + "user": frappe.session.user, + "limit_start": cint(limit_start), + "limit_page_length": cint(limit_page_length) + }, as_dict=True) + + 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.local.flags.commit = True + + return result @frappe.whitelist() def get_months_activity(): diff --git a/frappe/desk/page/activity/activity_row.html b/frappe/desk/page/activity/activity_row.html index ccbd37dd07..2b72a95b02 100644 --- a/frappe/desk/page/activity/activity_row.html +++ b/frappe/desk/page/activity/activity_row.html @@ -1,10 +1,15 @@