From 3241a0969f40fa68ae40fd1dad3dd6892d765dde Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Mon, 28 Dec 2015 19:56:58 +0530 Subject: [PATCH] [feature] Ability to like a document, comment or communication, see notifications about it and view it on activity feed --- frappe/change_log/current/like.md | 4 + frappe/core/doctype/comment/comment.json | 612 +++++++++--------- frappe/core/doctype/comment/comment.py | 11 +- .../doctype/communication/communication.py | 9 +- frappe/core/notifications.py | 12 + frappe/desk/doctype/feed/feed.json | 469 ++++++++------ frappe/desk/doctype/feed/feed.py | 38 +- frappe/desk/form/load.py | 8 +- frappe/desk/like.py | 98 +++ frappe/desk/notifications.py | 24 +- frappe/desk/page/activity/activity.css | 9 + frappe/desk/page/activity/activity.js | 71 +- frappe/desk/page/activity/activity.py | 34 +- frappe/desk/page/activity/activity_row.html | 16 +- frappe/desk/page/messages/messages.js | 2 + frappe/desk/star.py | 53 -- .../current/api/desk/frappe.desk.star.html | 64 -- frappe/email/email_body.py | 9 +- frappe/model/__init__.py | 2 +- frappe/model/base_document.py | 2 +- frappe/model/db_query.py | 29 +- frappe/model/document.py | 14 +- frappe/patches.txt | 3 + frappe/patches/v5_0/bookmarks_to_stars.py | 4 +- frappe/patches/v6_16/__init__.py | 0 frappe/patches/v6_16/feed_doc_owner.py | 30 + frappe/patches/v6_16/star_to_like.py | 18 + frappe/public/build.json | 5 +- frappe/public/css/avatar.css | 9 + frappe/public/css/desk.css | 8 + frappe/public/css/form.css | 165 ++++- frappe/public/css/indicator.css | 8 + frappe/public/css/list.css | 22 +- frappe/public/css/mobile.css | 14 - frappe/public/css/sidebar.css | 5 + frappe/public/css/website.css | 18 + .../js/frappe/form/footer/timeline.html | 42 +- .../public/js/frappe/form/footer/timeline.js | 61 +- .../js/frappe/form/footer/timeline_item.html | 45 +- .../public/js/frappe/form/form_sidebar.html | 6 + frappe/public/js/frappe/form/formatters.js | 2 +- frappe/public/js/frappe/form/sidebar.js | 31 + frappe/public/js/frappe/form/toolbar.js | 17 - frappe/public/js/frappe/list/doclistview.js | 42 +- .../js/frappe/list/list_item_main_head.html | 4 +- .../public/js/frappe/list/list_item_row.html | 5 +- .../js/frappe/list/list_item_subject.html | 17 +- frappe/public/js/frappe/list/listview.js | 7 +- frappe/public/js/frappe/model/indicator.js | 2 +- frappe/public/js/frappe/model/model.js | 4 +- frappe/public/js/frappe/query_string.js | 42 ++ frappe/public/js/frappe/router.js | 10 +- frappe/public/js/frappe/ui/like.js | 128 ++++ frappe/public/js/frappe/ui/liked_by.html | 8 + frappe/public/js/frappe/ui/listing.js | 2 +- frappe/public/js/frappe/ui/star.js | 58 -- .../js/frappe/ui/toolbar/notifications.js | 33 +- frappe/public/js/frappe/views/breadcrumbs.js | 2 +- frappe/public/js/frappe/views/calendar.js | 4 +- .../public/js/frappe/views/communication.js | 8 +- .../js/frappe/views/reports/reportview.js | 2 +- frappe/public/js/legacy/form.js | 20 + frappe/public/less/avatar.less | 10 + frappe/public/less/desk.less | 11 + frappe/public/less/form.less | 199 +++++- frappe/public/less/indicator.less | 10 + frappe/public/less/list.less | 25 +- frappe/public/less/mobile.less | 20 - frappe/public/less/sidebar.less | 6 + frappe/public/less/variables.less | 4 + frappe/utils/data.py | 12 + frappe/website/js/website.js | 40 -- 72 files changed, 1839 insertions(+), 999 deletions(-) create mode 100644 frappe/change_log/current/like.md create mode 100644 frappe/desk/like.py delete mode 100644 frappe/desk/star.py delete mode 100644 frappe/docs/current/api/desk/frappe.desk.star.html create mode 100644 frappe/patches/v6_16/__init__.py create mode 100644 frappe/patches/v6_16/feed_doc_owner.py create mode 100644 frappe/patches/v6_16/star_to_like.py create mode 100644 frappe/public/js/frappe/query_string.js create mode 100644 frappe/public/js/frappe/ui/like.js create mode 100644 frappe/public/js/frappe/ui/liked_by.html delete mode 100644 frappe/public/js/frappe/ui/star.js 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 @@
{%= date_sep || "" %}
-
- +
+
{% if (feed_type==="Login") { %} @@ -15,6 +20,13 @@ {%= __("Commented on {0}: {1}", [link, "" + subject + ""]) %} {% } else if (doc_type && !feed_type) { %} {%= __("Updated {0}: {1}", [link, "" + subject + ""]) %} + {% } else if (feed_type==="Like" && doc_type) { %} + {%= by %} + {% if (in_list(["Comment", "Communication"], doc_type)) { %} + {%= subject %} + {% } else { %} + {%= link %} + {% } %} {% } else if (doc_type) { %} {%= __("{0}: {1}", [link, "" + subject + ""]) %} {% } else { %} diff --git a/frappe/desk/page/messages/messages.js b/frappe/desk/page/messages/messages.js index 897f193b31..e3d2312b9f 100644 --- a/frappe/desk/page/messages/messages.js +++ b/frappe/desk/page/messages/messages.js @@ -22,6 +22,8 @@ frappe.pages.messages.on_page_load = function(parent) { frappe.pages.messages.on_page_show = function() { // clear title prefix frappe.utils.set_title_prefix(""); + + frappe.breadcrumbs.add("Desk"); } frappe.desk.pages.Messages = Class.extend({ diff --git a/frappe/desk/star.py b/frappe/desk/star.py deleted file mode 100644 index e5c4fdf131..0000000000 --- a/frappe/desk/star.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals - -"""Allow adding of stars to documents""" - -import frappe, json -from frappe.model.db_schema import add_column - -@frappe.whitelist() -def toggle_star(doctype, name, add=False): - """Adds / removes the current user in the `__starred_by` property of the given document. - If column does not exist, will add it in the database. - - The `_starred_by` property is always set from this function and is ignored if set via - Document API - - :param doctype: DocType of the document to star - :param name: Name of the document to star - :param add: `Yes` if star is to be added. If not `Yes` the star will be removed.""" - - _toggle_star(doctype, name, add) - -def _toggle_star(doctype, name, add=False, user=None): - """Same as toggle_star but hides param `user` from API""" - - if not user: - user = frappe.session.user - - try: - starred_by = frappe.db.get_value(doctype, name, "_starred_by") - if starred_by: - starred_by = json.loads(starred_by) - else: - starred_by = [] - - if add=="Yes": - if user not in starred_by: - starred_by.append(user) - else: - if user in starred_by: - starred_by.remove(user) - - frappe.db.sql("""update `tab{0}` set `_starred_by`=%s where name=%s""".format(doctype), - (json.dumps(starred_by), name)) - - except Exception, e: - if e.args[0]==1054: - add_column(doctype, "_starred_by", "Text") - _toggle_star(doctype, name, add, user) - else: - raise diff --git a/frappe/docs/current/api/desk/frappe.desk.star.html b/frappe/docs/current/api/desk/frappe.desk.star.html deleted file mode 100644 index 94ce433135..0000000000 --- a/frappe/docs/current/api/desk/frappe.desk.star.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - -

- - - frappe.desk.star._toggle_star - (doctype, name, add=False, user=None) -

-

Same as toggle_star but hides param user from API

-
-
- - - - - - -

Public API -
/api/method/frappe.desk.star.toggle_star -

-

- - - frappe.desk.star.toggle_star - (doctype, name, add=False) -

-

Adds / removes the current user in the __starred_by property of the given document. -If column does not exist, will add it in the database.

- -

The _starred_by property is always set from this function and is ignored if set via -Document API

- -

Parameters:

- -
    -
  • doctype - DocType of the document to star
  • -
  • name - Name of the document to star
  • -
  • add - Yes if star is to be added. If not Yes the star will be removed.
  • -
-
-
- - - - - - \ No newline at end of file diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 66522d1d7f..56d2c9f55c 100644 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.utils.pdf import get_pdf from frappe.email.smtp import get_outgoing_email_account -from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails +from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails, to_markdown import email.utils from markdown2 import markdown @@ -89,12 +89,7 @@ class EMail: def set_html_as_text(self, html): """return html2text""" - import HTMLParser - from html2text import html2text - try: - self.set_text(html2text(html)) - except HTMLParser.HTMLParseError: - pass + self.set_text(to_markdown(html)) def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'): """Append the message with MIME content to the root node (as attachment)""" diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 0560043894..64b40502e9 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -14,7 +14,7 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by', integer_docfield_properties = ("reqd", "search_index", "in_list_view", "permlevel", "hidden", "read_only", "ignore_user_permissions", "allow_on_submit", "report_hide", "in_filter", "no_copy", "print_hide", "unique") -optional_fields = ("_user_tags", "_comments", "_assign", "_starred_by") +optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by") def rename(doctype, old, new, debug=False): import frappe.model.rename_doc diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index ab33d4b8c3..c660acb606 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -239,7 +239,7 @@ class BaseDocument(object): if k in default_fields: del doc[k] - for key in ("_user_tags", "__islocal", "__onload", "_starred_by", "__run_link_triggers"): + for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"): if self.get(key): doc[key] = self.get(key) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index e69758396a..3507daa9e3 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -25,7 +25,7 @@ class DatabaseQuery(object): def execute(self, query=None, fields=None, filters=None, or_filters=None, docstatus=None, group_by=None, order_by=None, limit_start=False, limit_page_length=None, as_list=False, with_childnames=False, debug=False, - ignore_permissions=False, user=None): + ignore_permissions=False, user=None, with_comment_count=False): if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user): raise frappe.PermissionError, self.doctype @@ -45,9 +45,14 @@ class DatabaseQuery(object): self.user = user or frappe.session.user if query: - return self.run_custom_query(query) + result = self.run_custom_query(query) else: - return self.build_and_run() + result = self.build_and_run() + + if with_comment_count and not as_list and self.doctype: + self.add_comment_count(result) + + return result def build_and_run(self): args = self.prepare_args() @@ -444,3 +449,21 @@ class DatabaseQuery(object): return 'limit %s, %s' % (self.limit_start, self.limit_page_length) else: return '' + + def add_comment_count(self, result): + for r in result: + if not r.name: + continue + + 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"}, + fieldname="count(name)")) + + communication_count = cint(frappe.db.get_value("Communication", + filters={"reference_doctype": self.doctype, "reference_name": r.name}, + fieldname="count(name)")) + + r._comment_count = comment_count + communication_count diff --git a/frappe/model/document.py b/frappe/model/document.py index 3c308f1b8a..9ea6569d11 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -764,7 +764,7 @@ 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): + def add_comment(self, comment_type, text=None, comment_by=None, reference_doctype=None, reference_name=None): """Add a comment to this document. :param comment_type: e.g. `Comment`. See Comment for more info.""" @@ -774,7 +774,9 @@ class Document(BaseDocument): "comment_type": comment_type, "comment_doctype": self.doctype, "comment_docname": self.name, - "comment": text or _(comment_type) + "comment": text or _(comment_type), + "reference_doctype": reference_doctype, + "reference_name": reference_name }).insert(ignore_permissions=True) return comment @@ -782,10 +784,10 @@ class Document(BaseDocument): """Returns signature (hash) for private URL.""" return hashlib.sha224(get_datetime_str(self.creation)).hexdigest() - def get_starred_by(self): - starred_by = getattr(self, "_starred_by", None) - if starred_by: - return json.loads(starred_by) + def get_liked_by(self): + liked_by = getattr(self, "_liked_by", None) + if liked_by: + return json.loads(liked_by) else: return [] diff --git a/frappe/patches.txt b/frappe/patches.txt index 5d0cb6f1f9..60780d2ff1 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -75,6 +75,7 @@ execute:frappe.db.sql("""update tabComment set comment = substr(comment, 6, loca frappe.patches.v5_0.fix_feed frappe.patches.v5_0.update_shared execute:frappe.reload_doc("core", "doctype", "docshare") #2015-07-21 +frappe.patches.v6_16.star_to_like frappe.patches.v5_0.bookmarks_to_stars frappe.patches.v5_0.style_settings_to_website_theme frappe.patches.v5_0.rename_ref_type_fieldnames @@ -112,3 +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_16.feed_doc_owner + diff --git a/frappe/patches/v5_0/bookmarks_to_stars.py b/frappe/patches/v5_0/bookmarks_to_stars.py index 4f25cf00f7..603697a1b7 100644 --- a/frappe/patches/v5_0/bookmarks_to_stars.py +++ b/frappe/patches/v5_0/bookmarks_to_stars.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import json import frappe import frappe.defaults -from frappe.desk.star import _toggle_star +from frappe.desk.like import _toggle_like def execute(): for user in frappe.get_all("User"): @@ -29,4 +29,4 @@ def execute(): or int(frappe.db.get_value("DocType", doctype, "issingle") or 0) or not frappe.db.table_exists(doctype)): continue - _toggle_star(doctype, docname, add="Yes", user=username) + _toggle_like(doctype, docname, add="Yes", user=username) diff --git a/frappe/patches/v6_16/__init__.py b/frappe/patches/v6_16/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v6_16/feed_doc_owner.py b/frappe/patches/v6_16/feed_doc_owner.py new file mode 100644 index 0000000000..163482a1dc --- /dev/null +++ b/frappe/patches/v6_16/feed_doc_owner.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doctype("Feed") + + frappe.db.sql("update `tabFeed` set seen=1") + + for doctype, name in frappe.db.sql("""select distinct doc_type, doc_name from `tabFeed` + where + (doc_type is not null and doc_type != '') + and (doc_name is not null and doc_name != '') + and doc_type != 'Feed' + for update"""): + + owner = frappe.db.get_value(doctype, name, "owner") + + if not owner: + continue + + frappe.db.sql("""update `tabFeed` + set doc_owner=%(owner)s + where + doc_type=%(doctype)s + and doc_name=%(name)s + and (doc_owner is null or doc_owner = '')""".format(doctype=doctype), { + "doctype": doctype, + "name": name, + "owner": owner + }) diff --git a/frappe/patches/v6_16/star_to_like.py b/frappe/patches/v6_16/star_to_like.py new file mode 100644 index 0000000000..5e568e4931 --- /dev/null +++ b/frappe/patches/v6_16/star_to_like.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals +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() + + for table in frappe.db.get_tables(): + columns = [r[0] for r in frappe.db.sql("DESC `{0}`".format(table))] + 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") diff --git a/frappe/public/build.json b/frappe/public/build.json index 9f6241b56b..17cc177f51 100644 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -16,6 +16,7 @@ "public/js/lib/highlight.pack.js", "public/js/frappe/class.js", "public/js/lib/microtemplate.js", + "public/js/frappe/query_string.js", "website/js/website.js", "public/js/lib/socket.io.min.js" ], @@ -121,6 +122,7 @@ "public/js/frappe/change_log.html", "public/js/frappe/desk.js", + "public/js/frappe/query_string.js", "public/html/error_object.html", "public/html/error_snapshot.html" @@ -180,7 +182,8 @@ "public/js/frappe/ui/filters/filters.js", "public/js/frappe/ui/filters/edit_filter.html", "public/js/frappe/ui/tags.js", - "public/js/frappe/ui/star.js", + "public/js/frappe/ui/like.js", + "public/js/frappe/ui/liked_by.html", "public/html/print_template.html", "public/js/frappe/list/doclistview.js", "public/js/frappe/list/list_sidebar.js", diff --git a/frappe/public/css/avatar.css b/frappe/public/css/avatar.css index 7bc44cacd9..952a706bd0 100644 --- a/frappe/public/css/avatar.css +++ b/frappe/public/css/avatar.css @@ -43,3 +43,12 @@ height: 0; padding-bottom: 100%; } +.avatar-frame { + width: 100%; + height: 0; + padding: 50% 0px; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + border-radius: 4px; +} diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index e19fbbb788..2d469fbbe1 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -546,3 +546,11 @@ ul.linked-with-list li { border-bottom: 1px solid #d1d8dd; border-radius: 0px; } +.liked-by-popover { + min-width: 200px; + margin-top: -10px; + margin-bottom: -10px; +} +.liked-by-popover li { + margin: 15px 0px; +} diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 53012a4dce..32970563f8 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -61,8 +61,7 @@ .empty-section { display: none !important; } -.shaded-section, -.timeline-item:nth-child(even) { +.shaded-section { background-color: #fafbfc; } .modal .form-layout { @@ -87,37 +86,165 @@ background-color: #e74c3c; } .timeline { - border: 1px solid #d1d8dd; margin: 30px 0px; } +.timeline .timeline-head .comment-input { + height: auto; +} .timeline-item { margin-top: 0px; - padding: 15px 30px 7px; - border-bottom: 1px solid #d1d8dd; -} -.timeline-item .icon-fixed-width { - text-align: center; } .timeline-item blockquote { font-size: inherit; } -.timeline-item:last-child { - border-bottom: 0px; +.timeline-items { + position: relative; } -.timeline-item .reply { - margin-top: 5px; +.timeline { + position: relative; +} +.timeline::before { + content: " "; + border-left: 1px solid #d1d8dd; + position: absolute; + top: 0px; + bottom: -124px; + left: 43px; + z-index: -1; +} +@media (max-width: 991px) { + .timeline::before { + bottom: -64px; + } +} +.timeline-item.user-content { + margin: 30px 0px 30px 30px; +} +.timeline-item.user-content .media-body { + border: 1px solid #d1d8dd; + border-radius: 2px; + margin-left: -7px; + position: relative; + overflow: visible; +} +.timeline-item.user-content .comment-header { + background-color: #fafbfc; + padding: 7px 15px; + margin: 0px; + color: #8D99A6; + border-bottom: 1px solid #EBEFF2; +} +.timeline-item.user-content .comment-header .octicon-heart { + color: #ff5858; + cursor: pointer; +} +.timeline-item.user-content .reply { + padding: 10px 15px; +} +.timeline-item.user-content .reply > div > p:first-child { + margin-top: 0px; +} +.timeline-item.user-content .reply > div > p:last-child { + margin-bottom: 0px; +} +.timeline-item.user-content .close-btn-container { + padding: 2px 15px; +} +.timeline-item.user-content .comment-likes { + margin-left: 5px; +} +.timeline-item.user-content .media-body:after, +.timeline-item.user-content .media-body:before { + right: 100%; + top: 15px; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} +.timeline-item.user-content .media-body:after { + border-color: rgba(136, 183, 213, 0); + border-right-color: #fafbfc; + border-width: 6px; + margin-top: -6px; +} +.timeline-item.user-content .media-body:before { + border-color: rgba(194, 225, 245, 0); + border-right-color: #d1d8dd; + border-width: 7px; + margin-top: -7px; +} +.timeline-item.notification-content { + padding-left: 30px; + margin: 30px 0px; + position: relative; + color: #8D99A6; +} +.timeline-item.notification-content * { + color: #8D99A6; +} +.timeline-item.notification-content .icon-fixed-width { + margin-left: 36px; +} +.timeline-item.notification-content .octicon-heart { + color: #ff5858 !important; +} +.timeline-item.notification-content::before { + content: " "; + width: 7px; + height: 7px; + background-color: #d1d8dd; + position: absolute; + left: 40px; + border-radius: 50%; + top: 5px; } .timeline-item .reply-link { - padding: 0px 7px; -} -.timeline-item h6, -.timeline-head h6 { - margin-top: 6px; + padding-left: 7px; } .timeline-head { + background-color: white; + border: 1px solid #d1d8dd; + border-radius: 2px; +} +.timeline-head .comment-input-header { background-color: #fafbfc; - padding: 15px 30px; - border-bottom: 1px solid #d1d8dd; + padding: 7px 15px; + border-bottom: 1px solid #EBEFF2; +} +.timeline-head .comment-input-container { + padding: 15px; +} +.timeline-head .comment-input { + border-color: #EBEFF2; + max-width: 100%; +} +.timeline-head .comment-input:focus { + box-shadow: none; +} +@media (max-width: 767px) { + .timeline-head { + border-left: none; + border-right: none; + border-radius: 0px; + } +} +.timeline-new-email { + margin: 30px 0px; + padding: 0px 65px; + position: relative; +} +.timeline-new-email::before { + content: " "; + width: 7px; + height: 7px; + background-color: #d1d8dd; + position: absolute; + left: 40px; + border-radius: 50%; + top: 5px; } .form-footer h5 { margin: 15px 0px; diff --git a/frappe/public/css/indicator.css b/frappe/public/css/indicator.css index 80ce0872f4..711df4d87a 100644 --- a/frappe/public/css/indicator.css +++ b/frappe/public/css/indicator.css @@ -48,3 +48,11 @@ .indicator-right.darkgrey::after { background: #b8c2cc; } +.indicator.yellow::before, +.indicator-right.yellow::after { + background: #FEEF72; +} +.indicator.light-blue::before, +.indicator-right.light-blue::after { + background: #7CD6FD; +} diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 38cc4ccd3d..2ed7e89401 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -98,6 +98,13 @@ .doclist-row { font-size: 12px; } +.doclist-row .likes-count { + display: inline-block; + width: 15px; + margin-left: -5px; + color: #8D99A6; + font-size: 12px; +} .doclist-row .docstatus .octicon { font-size: 12px; } @@ -134,14 +141,14 @@ margin: 0px -15px; padding: 5px 15px; } -.listview-main-section .icon-star { +.listview-main-section .octicon-heart { cursor: pointer; } -.list-row-head .icon-star { - vertical-align: middle; +.list-row-head .octicon-heart { + margin-right: 13px; } -.star-action.icon-star { - color: #ffdb4c; +.like-action.octicon-heart { + color: #ff5858; } .list-id { font-weight: bold; @@ -163,3 +170,8 @@ display: none; } } +.list-comment-count { + display: inline-block; + width: 37px; + text-align: left; +} diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css index 9334d975f3..70886c5120 100644 --- a/frappe/public/css/mobile.css +++ b/frappe/public/css/mobile.css @@ -330,20 +330,6 @@ body { border-left-color: transparent !important; border-right-color: transparent !important; } - .timeline { - border-left: none !important; - border-right: none !important; - } - .timeline .timeline-head { - padding: 7px 15px; - } - .timeline .timeline-item { - padding: 15px; - border-bottom: 1px dashed #d1d8dd; - } - .timeline .timeline-item:last-child { - border-bottom: none; - } .list-row { padding: 13px 15px !important; } diff --git a/frappe/public/css/sidebar.css b/frappe/public/css/sidebar.css index 9431506c26..e198520e99 100644 --- a/frappe/public/css/sidebar.css +++ b/frappe/public/css/sidebar.css @@ -161,6 +161,10 @@ body[data-route^="Module"] .main-menu .form-sidebar { .form-sidebar .form-viewers .shared-with-everyone .octicon { color: #36414C !important; } +.form-sidebar .liked-by .octicon-heart { + font-size: 16px; + cursor: pointer; +} .form-sidebar .form-shared .share-doc-btn:hover, .form-sidebar .form-shared .share-doc-btn:focus, .form-sidebar .form-shared .share-doc-btn:active { @@ -174,6 +178,7 @@ body[data-route^="Module"] .main-menu .form-sidebar { .sidebar-left .form-sidebar .form-tags, .sidebar-left .form-sidebar .assignment-row, .sidebar-left .form-sidebar .form-shared, +.sidebar-left .form-sidebar .liked-by, .sidebar-left .form-sidebar .modified-by, .sidebar-left .form-sidebar .created-by, .sidebar-left .form-sidebar .tags-label, diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index c210b316a5..fe2b3635da 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -264,6 +264,16 @@ a.no-decoration:active { height: 0; padding-bottom: 100%; } +.avatar-frame { + width: 100%; + height: 0; + padding: 50% 0px; + background-size: cover; + background-repeat: no-repeat; + background-position: center top; + border-radius: 0.5em; + border: 1px solid #EBEFF2; +} .indicator, .indicator-right { background: none; @@ -314,6 +324,14 @@ a.no-decoration:active { .indicator-right.darkgrey::after { background: #b8c2cc; } +.indicator.yellow::before, +.indicator-right.yellow::after { + background: #FEEF72; +} +.indicator.light-blue::before, +.indicator-right.light-blue::after { + background: #7CD6FD; +} .navbar-brand { max-width: none; } diff --git a/frappe/public/js/frappe/form/footer/timeline.html b/frappe/public/js/frappe/form/footer/timeline.html index 833d99a3c0..2615cb5c54 100644 --- a/frappe/public/js/frappe/form/footer/timeline.html +++ b/frappe/public/js/frappe/form/footer/timeline.html @@ -1,37 +1,21 @@
-
{%= __("Add a comment") %}
-
- +
+ {%= __("Add a comment") %} + +
+
+
-
- - - -
-
-
-
{%= __("You") %}
-
-
- -
- -
-
-
-
-
-
+
+ +
diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 3482d445ac..7909cb38a0 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -14,33 +14,31 @@ frappe.ui.form.Comments = Class.extend({ this.list = this.wrapper.find(".timeline-items"); this.input = this.wrapper.find(".form-control"); - this.button = this.wrapper.find(".btn-go") + this.comment_button = this.wrapper.find(".btn-comment") .on("click", function() { - if(me.wrapper.find(".is-email").prop("checked")) { - new frappe.views.CommunicationComposer({ - doc: me.frm.doc, - txt: frappe.markdown(me.input.val()), - frm: me.frm, - recipients: me.get_recipient() - }) - } else { - me.add_comment(this); - } + me.add_comment(this); }); this.input.keydown("meta+return ctrl+return", function(e) { - me.button.trigger("click"); + me.comment_button.trigger("click"); }); - this.email_check = this.wrapper.find(".timeline-head input[type='checkbox']") - .on("change", function() { - me.button.html($(this).prop("checked") ? __("Compose") : __("Comment")); + this.email_button = this.wrapper.find(".btn-new-email") + .on("click", function() { + new frappe.views.CommunicationComposer({ + doc: me.frm.doc, + txt: frappe.markdown(me.input.val()), + frm: me.frm, + recipients: me.get_recipient() + }) }); this.list.on("click", ".toggle-blockquote", function() { $(this).parent().siblings("blockquote").toggleClass("hidden"); }); + this.setup_comment_like(); + this.setup_mentions(); }, @@ -116,7 +114,7 @@ frappe.ui.form.Comments = Class.extend({ prepare_comment: function(c) { if((c.comment_type || "Comment") === "Comment" && frappe.model.can_delete("Comment")) { - c["delete"] = '×'; + c["delete"] = ''; } else { c["delete"] = ""; } @@ -130,7 +128,7 @@ frappe.ui.form.Comments = Class.extend({ c.image = frappe.user_info(c.comment_by).image || frappe.get_gravatar(c.comment_by); c.comment_on = comment_when(c.creation); - c.fullname = frappe.user_info(c.comment_by).fullname; + c.fullname = frappe.user.full_name(c.comment_by); if(c.attachments && typeof c.attachments==="string") c.attachments = JSON.parse(c.attachments); @@ -167,6 +165,15 @@ frappe.ui.form.Comments = Class.extend({ if(c.comment_type==="Comment") { c.comment_html = c.comment_html.replace(/(^|\W)(@\w+)/g, "$1$2"); } + + if (in_list(["Comment", "Email"], c.comment_type)) { + c.user_content = true; + if (!$.isArray(c._liked_by)) { + c._liked_by = JSON.parse(c._liked_by || "[]"); + } + + c.liked_by_user = c._liked_by.indexOf(user)!==-1; + } } }, set_icon_and_color: function(c) { @@ -186,7 +193,8 @@ frappe.ui.form.Comments = Class.extend({ "Attachment": "octicon octicon-cloud-upload", "Attachment Removed": "octicon octicon-trashcan", "Shared": "octicon octicon-eye", - "Unshared": "octicon octicon-circle-slash" + "Unshared": "octicon octicon-circle-slash", + "Like": "octicon octicon-heart" }[c.comment_type] c.color = { @@ -348,10 +356,13 @@ frappe.ui.form.Comments = Class.extend({ this.mention_input = this.wrapper.find(".mention-input"); + var source = Object.keys(username_user_map); + source.sort(); + this.mention_input.autocomplete({ minLength: 0, autoFocus: true, - source: Object.keys(username_user_map), + source: source, select: function(event, ui) { var value = ui.item.value; var textarea_value = me.input.val(); @@ -467,9 +478,21 @@ frappe.ui.form.Comments = Class.extend({ me.mention_widget.trigger(me.enter); // prevent default + return false; + } + } else { + if (e.which==me.codes.TAB) { + me.comment_button.focus(); + return false; } } }); + }, + + setup_comment_like: function() { + this.wrapper.on("click", ".comment-likes .octicon-heart", frappe.ui.click_toggle_like); + + frappe.ui.setup_like_popover(this.wrapper, ".comment-likes"); } }); diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html index e8bcfa6cae..0d4b9e6cc4 100644 --- a/frappe/public/js/frappe/form/footer/timeline_item.html +++ b/frappe/public/js/frappe/form/footer/timeline_item.html @@ -1,16 +1,19 @@ -
+
+ {% if (data.user_content) { %} - +
-
-
-
+ {% } %} + +
+
+
{%= data.delete %}
{% if(data.doctype=="Communication" || data.comment_type=="Comment") { %} -
+
{%= data.fullname %} @@ -47,14 +50,23 @@ {%= __("Reply") %} {% } %} -
+ + + + +
{%= data.comment_html %}
{% } else if(in_list(["Assignment Completed", "Assigned", "Shared", "Unshared"], data.comment_type)) { %} -
+
{% if(data.reference_doctype && data.reference_name) { %} @@ -65,14 +77,19 @@ {% } %} – {%= data.comment_on %} -
+
{% } else { %} -
+
- {%= data.fullname %} {%= data.comment %} - - – {%= data.comment_on %} -
+ {% if (data.comment_type == "Like") { %} + {%= __("Liked by {0}", [data.fullname]) %} + {% } else { %} + {%= data.fullname %} + {%= data.comment %} + {% } %} + + – {%= data.comment_on %} +
{% } %} {% if(data.attachments && data.attachments.length) { %}
diff --git a/frappe/public/js/frappe/form/form_sidebar.html b/frappe/public/js/frappe/form/form_sidebar.html index 53acbf0384..856be2d9be 100644 --- a/frappe/public/js/frappe/form/form_sidebar.html +++ b/frappe/public/js/frappe/form/form_sidebar.html @@ -45,6 +45,12 @@
  • diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index d3fa5c1d56..588f279848 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -117,7 +117,7 @@ frappe.form.formatters = { return frappe.form.formatters.Data(value); }, - StarredBy: function(value) { + LikedBy: function(value) { var html = ""; $.each(JSON.parse(value || "[]"), function(i, v) { if(v) html+= '" + frappe.user.full_name(this.frm.doc.owner) + "", "
    " + comment_when(this.frm.doc.creation)])); + + this.refresh_like(); } }, @@ -113,4 +122,26 @@ frappe.ui.form.Sidebar = Class.extend({ this.user_actions.addClass("hide") this.user_actions.find(".user-action-row").remove(); }, + + make_like: function() { + this.like_wrapper = this.sidebar.find(".liked-by"); + this.like_icon = this.sidebar.find(".liked-by .octicon-heart"); + this.like_count = this.sidebar.find(".liked-by .like-count"); + frappe.ui.setup_like_popover(this.sidebar.find(".liked-by-parent"), ".liked-by"); + }, + + refresh_like: function() { + if (!this.like_icon) { + return; + } + + this.like_wrapper.attr("data-liked-by", this.frm.doc._liked_by); + + this.like_icon.toggleClass("text-extra-muted not-liked", + !frappe.ui.is_liked(this.frm.doc)) + .attr("data-doctype", this.frm.doctype) + .attr("data-name", this.frm.doc.name); + + this.like_count.text(JSON.parse(this.frm.doc._liked_by || "[]").length); + }, }); diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 17ac8d6994..c852dc222b 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -15,7 +15,6 @@ frappe.ui.form.Toolbar = Class.extend({ this.page.clear_user_actions(); this.show_title_as_dirty(); this.set_primary_action(); - this.refresh_star(); if(this.frm.meta.hide_toolbar) { this.page.hide_menu(); @@ -23,11 +22,9 @@ frappe.ui.form.Toolbar = Class.extend({ if(this.frm.doc.__islocal) { this.page.hide_menu(); this.print_icon && this.print_icon.addClass("hide"); - this.star_icon && this.star_icon.addClass("hide"); } else { this.page.show_menu(); this.print_icon && this.print_icon.removeClass("hide"); - this.star_icon && this.star_icon.removeClass("hide"); } } }, @@ -97,13 +94,6 @@ frappe.ui.form.Toolbar = Class.extend({ this.page.clear_indicator(); } }, - refresh_star: function() { - this.star_icon && - this.star_icon.toggleClass("text-extra-muted not-starred", - !frappe.ui.is_starred(this.frm.doc)) - .attr("data-doctype", this.frm.doctype) - .attr("data-name", this.frm.doc.name); - }, make_menu: function() { var me = this; var p = this.frm.perm[0]; @@ -117,13 +107,6 @@ frappe.ui.form.Toolbar = Class.extend({ me.frm.print_doc();}); } - // star - if(!this.frm.meta.issingle) { - this.star_icon = this.page.add_action_icon("icon-star", function() { - frappe.ui.toggle_star(me.star_icon, me.frm.doctype, me.frm.doc.name); - }).removeClass("text-muted").find(".icon-star").addClass("star-action"); - } - // email if(frappe.model.can_email(null, me.frm) && me.frm.doc.docstatus < 2) { this.page.add_menu_item(__("Email"), function() { diff --git a/frappe/public/js/frappe/list/doclistview.js b/frappe/public/js/frappe/list/doclistview.js index e53667234f..6ab74f7aab 100644 --- a/frappe/public/js/frappe/list/doclistview.js +++ b/frappe/public/js/frappe/list/doclistview.js @@ -116,7 +116,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ this.setup_filterable(); this.init_filters(); this.init_headers(); - this.init_star(); + this.init_like(); this.init_select_all(); }, @@ -168,9 +168,9 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ } }); this.$page.find(".result-list").on("click", ".list-row-left", function(e) { - // don't open in case of checkbox, star, filterable + // don't open in case of checkbox, like, filterable if ((e.target.className || "").indexOf("filterable")!==-1 - || (e.target.className || "").indexOf("icon-star")!==-1 + || (e.target.className || "").indexOf("octicon-heart")!==-1 || e.target.type==="checkbox") { return; } @@ -307,8 +307,8 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ } } - this.list_header.find(".list-starred-by-me") - .toggleClass("text-extra-muted not-starred", !this.is_star_filtered()); + this.list_header.find(".list-liked-by-me") + .toggleClass("text-extra-muted not-liked", !this.is_star_filtered()); this.last_updated_on = new Date(); this.dirty = false; @@ -355,6 +355,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ filters: this.filter_list.get_filters(), order_by: this.listview.order_by || undefined, group_by: this.listview.group_by || undefined, + with_comment_count: true } // apply default filters, if specified for a listing @@ -368,15 +369,15 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ this.filter_list.add_filter(this.doctype, "_assign", 'like', '%' + user + '%'); this.run(); }, - starred_by_me: function() { - this.filter_list.add_filter(this.doctype, "_starred_by", 'like', '%' + user + '%'); + liked_by_me: function() { + this.filter_list.add_filter(this.doctype, "_liked_by", 'like', '%' + user + '%'); this.run(); }, - remove_starred_by_me: function() { - this.filter_list.get_filter("_starred_by").remove(); + remove_liked_by_me: function() { + this.filter_list.get_filter("_liked_by").remove(); }, is_star_filtered: function() { - return this.filter_list.filter_exists(this.doctype, "_starred_by", 'like', '%' + user + '%'); + return this.filter_list.filter_exists(this.doctype, "_liked_by", 'like', '%' + user + '%'); }, init_menu: function() { var me = this; @@ -453,19 +454,20 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ }, true) }, - init_star: function() { + init_like: function() { var me = this; - this.$page.find(".result-list").on("click", ".star-action", function() { - frappe.ui.toggle_star($(this), me.doctype, $(this).attr("data-name")); - return false; - }); - this.list_header.find(".list-starred-by-me").on("click", function() { + this.$page.find(".result-list").on("click", ".like-action", frappe.ui.click_toggle_like); + this.list_header.find(".list-liked-by-me").on("click", function() { if (me.is_star_filtered()) { - me.remove_starred_by_me(); + me.remove_liked_by_me(); } else { - me.starred_by_me(); + me.liked_by_me(); } }); + + if (!frappe.dom.is_touchscreen()) { + frappe.ui.setup_like_popover(this.$page.find(".result-list"), ".liked-by"); + } }, init_select_all: function() { @@ -576,7 +578,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ return false; } else { // second filter set for this field - if(fieldname=='_user_tags' || fieldname=="_starred_by") { + if(fieldname=='_user_tags' || fieldname=="_liked_by") { // and for tags this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label); } else { @@ -587,7 +589,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ } else { // no filter for this item, // setup one - if(fieldname=='_user_tags' || fieldname=="_starred_by") { + if(fieldname=='_user_tags' || fieldname=="_liked_by") { this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label); } else { this.filter_list.add_filter(this.doctype, fieldname, '=', label); diff --git a/frappe/public/js/frappe/list/list_item_main_head.html b/frappe/public/js/frappe/list/list_item_main_head.html index aa3a4a2189..f51c095ab5 100644 --- a/frappe/public/js/frappe/list/list_item_main_head.html +++ b/frappe/public/js/frappe/list/list_item_main_head.html @@ -23,8 +23,8 @@ title="{%= __("Select All") %}"> {% } %} - + {% } %} diff --git a/frappe/public/js/frappe/list/list_item_row.html b/frappe/public/js/frappe/list/list_item_row.html index d03ac45f64..4dee4f1110 100644 --- a/frappe/public/js/frappe/list/list_item_row.html +++ b/frappe/public/js/frappe/list/list_item_row.html @@ -33,9 +33,10 @@ {% } else { %} {% } %} - + - {%= data._comments_list.length || 0 %} + {%= (data._comment_count > 99 ? "99+" : data._comment_count) || 0 %}
    diff --git a/frappe/public/js/frappe/list/list_item_subject.html b/frappe/public/js/frappe/list/list_item_subject.html index 5d5442dab6..1a00e96db7 100644 --- a/frappe/public/js/frappe/list/list_item_subject.html +++ b/frappe/public/js/frappe/list/list_item_subject.html @@ -2,13 +2,16 @@ {% } %} - - + + + + 1) { + var query_params = get_query_params(parts[1]); + frappe.route_options = $.extend(frappe.route_options || {}, query_params); + } + + return route; } frappe.get_prev_route = function() { diff --git a/frappe/public/js/frappe/ui/like.js b/frappe/public/js/frappe/ui/like.js new file mode 100644 index 0000000000..0ebd429a2d --- /dev/null +++ b/frappe/public/js/frappe/ui/like.js @@ -0,0 +1,128 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.ui.is_liked = function(doc) { + var liked = frappe.ui.get_liked_by(doc); + return liked.indexOf(user)===-1 ? false : true; +} + +frappe.ui.get_liked_by = function(doc) { + var liked = doc._liked_by; + if(liked) { + liked = JSON.parse(liked); + } + + return liked || []; +} + +frappe.ui.toggle_like = function($btn, doctype, name, callback) { + var add = $btn.hasClass("not-liked") ? "Yes" : "No"; + // disable click + $btn.css('pointer-events', 'none'); + + frappe.call({ + method: "frappe.desk.like.toggle_like", + quiet: true, + args: { + doctype: doctype, + name: name, + add: add, + }, + callback: function(r) { + // renable click + $btn.css('pointer-events', 'auto'); + + if(!r.exc) { + // update in all local-buttons + var action_buttons = $('.like-action[data-name="'+ name.replace(/"/g, '\"') + +'"][data-doctype="'+ doctype.replace(/"/g, '\"')+'"]'); + + if(add==="Yes") { + action_buttons.removeClass("not-liked text-extra-muted"); + } else { + action_buttons.addClass("not-liked text-extra-muted"); + } + + // update in locals (form) + var doc = locals[doctype] && locals[doctype][name]; + if(doc) { + var liked_by = JSON.parse(doc._liked_by || "[]"), + idx = liked_by.indexOf(user); + if(add==="Yes") { + if(idx===-1) + liked_by.push(user); + } else { + if(idx!==-1) { + liked_by = liked_by.slice(0,idx).concat(liked_by.slice(idx+1)) + } + } + doc._liked_by = JSON.stringify(liked_by); + } + + if(callback) { + callback(); + } + } + } + }); +}; + +frappe.ui.click_toggle_like = function() { + var $btn = $(this); + var $count = $btn.siblings(".likes-count"); + var not_liked = $btn.hasClass("not-liked"); + var doctype = $btn.attr("data-doctype"); + var name = $btn.attr("data-name"); + + frappe.ui.toggle_like($btn, doctype, name, function() { + if (not_liked) { + $count.text(cint($count.text()) + 1); + } else { + $count.text(cint($count.text()) - 1); + } + }); + + return false; +} + +frappe.ui.setup_like_popover = function($parent, selector) { + if (frappe.dom.is_touchscreen()) { + return; + } + + $parent.on("mouseover", selector, function() { + var $wrapper = $(this); + + $wrapper.popover({ + animation: true, + placement: "right", + content: function() { + var liked_by = JSON.parse($wrapper.attr('data-liked-by') || "[]"); + + // hack + if ($wrapper.find(".not-liked").length) { + if (liked_by.indexOf(user)!==-1) { + liked_by.splice(liked_by.indexOf(user), 1); + } + } else { + if (liked_by.indexOf(user)===-1) { + liked_by.push(user); + } + } + + if (!liked_by.length) { + return ""; + } + return frappe.render_template("liked_by", {"liked_by": liked_by}); + }, + html: true, + container: 'body' + }); + + $wrapper.popover('show'); + }); + + $parent.on("mouseout", selector, function() { + $(this).popover('destroy'); + }); +} diff --git a/frappe/public/js/frappe/ui/liked_by.html b/frappe/public/js/frappe/ui/liked_by.html new file mode 100644 index 0000000000..0b836c5561 --- /dev/null +++ b/frappe/public/js/frappe/ui/liked_by.html @@ -0,0 +1,8 @@ +
      + {% for (var i in liked_by) { var liked_by_user = liked_by[i]; %} +
    • + {%= frappe.avatar(liked_by_user) %} + {%= frappe.user.full_name(liked_by_user) %} +
    • + {% } %} +
    diff --git a/frappe/public/js/frappe/ui/listing.js b/frappe/public/js/frappe/ui/listing.js index 40a72c35c3..814c33230c 100644 --- a/frappe/public/js/frappe/ui/listing.js +++ b/frappe/public/js/frappe/ui/listing.js @@ -318,7 +318,7 @@ frappe.ui.Listing = Class.extend({ } else { // no filter for this item, // setup one - if(['_user_tags', '_comments', '_assign', '_starred_by'].indexOf(fieldname)!==-1) { + if(['_user_tags', '_comments', '_assign', '_liked_by'].indexOf(fieldname)!==-1) { this.filter_list.add_filter(doctype, fieldname, 'like', '%' + label + '%'); } else { diff --git a/frappe/public/js/frappe/ui/star.js b/frappe/public/js/frappe/ui/star.js deleted file mode 100644 index c07516bf70..0000000000 --- a/frappe/public/js/frappe/ui/star.js +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -frappe.ui.is_starred = function(doc) { - var starred = frappe.ui.get_starred_by(doc); - return starred.indexOf(user)===-1 ? false : true; -} - -frappe.ui.get_starred_by = function(doc) { - var starred = doc._starred_by; - if(starred) { - starred = JSON.parse(starred); - } - - return starred || []; -} - -frappe.ui.toggle_star = function($btn, doctype, name) { - var add = $btn.hasClass("not-starred") ? "Yes" : "No"; - frappe.call({ - method: "frappe.desk.star.toggle_star", - quiet: true, - args: { - doctype: doctype, - name: name, - add: add, - }, - callback: function(r) { - if(!r.exc) { - // update in all local-buttons - var action_buttons = $('.star-action[data-name="'+ name.replace(/"/g, '\"') - +'"][data-doctype="'+ doctype.replace(/"/g, '\"')+'"]'); - - if(add==="Yes") { - action_buttons.removeClass("not-starred").removeClass("text-extra-muted"); - } else { - action_buttons.addClass("not-starred").addClass("text-extra-muted"); - } - - // update in locals (form) - var doc = locals[doctype] && locals[doctype][name]; - if(doc) { - var starred_by = JSON.parse(doc._starred_by || "[]"), - idx = starred_by.indexOf(user); - if(add==="Yes") { - if(idx===-1) - starred_by.push(user); - } else { - if(idx!==-1) { - starred_by = starred_by.slice(0,idx).concat(starred_by.slice(idx+1)) - } - } - doc._starred_by = JSON.stringify(starred_by); - } - } - } - }); -}; diff --git a/frappe/public/js/frappe/ui/toolbar/notifications.js b/frappe/public/js/frappe/ui/toolbar/notifications.js index 0cbb897a9e..60c98a3691 100644 --- a/frappe/public/js/frappe/ui/toolbar/notifications.js +++ b/frappe/public/js/frappe/ui/toolbar/notifications.js @@ -4,6 +4,7 @@ frappe.ui.notifications.update_notifications = function() { frappe.ui.notifications.total = 0; var doctypes = keys(frappe.boot.notification_info.open_count_doctype).sort(); var modules = keys(frappe.boot.notification_info.open_count_module).sort(); + var other = keys(frappe.boot.notification_info.open_count_other).sort(); // clear toolbar / sidebar notifications frappe.ui.notifications.navbar_notification = $("#navbar-notification").empty(); @@ -14,6 +15,12 @@ frappe.ui.notifications.update_notifications = function() { frappe.ui.notifications.add_notification("ToDo"); frappe.ui.notifications.add_notification("Event"); + // add other + $.each(other, function(i, name) { + frappe.ui.notifications.add_notification(name, frappe.boot.notification_info.open_count_other); + }); + + // add a divider if(frappe.ui.notifications.total) { var divider = '
  • '; @@ -34,6 +41,8 @@ frappe.ui.notifications.update_notifications = function() { var config = frappe.ui.notifications.config[doctype] || {}; if (config.route) { frappe.set_route(config.route); + } else if (config.click) { + config.click(); } else { frappe.views.show_open_count_list(this); } @@ -46,8 +55,12 @@ frappe.ui.notifications.update_notifications = function() { } -frappe.ui.notifications.add_notification = function(doctype) { - var count = frappe.boot.notification_info.open_count_doctype[doctype]; +frappe.ui.notifications.add_notification = function(doctype, notifications_map) { + if(!notifications_map) { + notifications_map = frappe.boot.notification_info.open_count_doctype; + } + + var count = notifications_map[doctype]; if(count) { var config = frappe.ui.notifications.config[doctype] || {}; var label = config.label || doctype; @@ -72,7 +85,21 @@ frappe.ui.notifications.add_notification = function(doctype) { frappe.ui.notifications.config = { "ToDo": { label: __("To Do") }, "Comment": { label: __("Messages"), route: "messages"}, - "Event": { label: __("Calendar"), route: "Calendar/Event" } + "Event": { label: __("Calendar"), route: "Calendar/Event" }, + "Likes": { + label: __("Likes"), + click: function() { + frappe.route_options = { + show_likes: true + }; + + if (frappe.get_route()[0]=="activity") { + frappe.pages['activity'].on_page_show(); + } else { + frappe.set_route("activity"); + } + } + } }; frappe.views.show_open_count_list = function(element) { diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 2a1187dfe8..df648ff93b 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -40,7 +40,7 @@ frappe.breadcrumbs = { breadcrumbs.module = frappe.breadcrumbs.preferred[breadcrumbs.doctype]; } - if(breadcrumbs.module && breadcrumbs.module != "Desk") { + if(breadcrumbs.module) { if(in_list(["Core", "Email", "Custom", "Workflow", "Print"], breadcrumbs.module)) breadcrumbs.module = "Setup"; diff --git a/frappe/public/js/frappe/views/calendar.js b/frappe/public/js/frappe/views/calendar.js index e0c0155227..fdf8e7003b 100644 --- a/frappe/public/js/frappe/views/calendar.js +++ b/frappe/public/js/frappe/views/calendar.js @@ -42,9 +42,7 @@ frappe.views.Calendar = frappe.views.CalendarBase.extend({ var module = locals.DocType[this.doctype].module; this.page.set_title(__("Calendar") + " - " + __(this.doctype)); - if (module !== "Desk") { - frappe.breadcrumbs.add(module, this.doctype) - } + frappe.breadcrumbs.add(module, this.doctype) this.add_filters(); diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index c9692f6546..23df5cb3d3 100644 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -96,10 +96,10 @@ frappe.views.CommunicationComposer = Class.extend({ var cc = [ [this.frm.doc.owner, 1] ]; - var starred_by = frappe.ui.get_starred_by(this.frm.doc); - if (starred_by) { - for ( var i=0, l=starred_by.length; i div > p:first-child { + margin-top: 0px; + } + + & > div > p:last-child { + margin-bottom: 0px; + } + } + + .close-btn-container { + padding: 2px 15px; + } + + .comment-likes { + margin-left: 5px; + } + + .media-body:after, .media-body:before { + right: 100%; + top: 15px; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + .media-body:after { + border-color: rgba(136, 183, 213, 0); + border-right-color: #fafbfc; + border-width: 6px; + margin-top: -6px; + } + .media-body:before { + border-color: rgba(194, 225, 245, 0); + border-right-color: @border-color; + border-width: 7px; + margin-top: -7px; + } +} + +.timeline-item.notification-content { + padding-left: 30px; + margin: 30px 0px; + position: relative; + color: @text-muted; + + * { + color: @text-muted; + } + + .icon-fixed-width { + margin-left: 36px; + } + + .octicon-heart { + color: @heart-color !important; + } +} + +.timeline-indicator() { + content: " "; + width: 7px; + height: 7px; + background-color: @border-color; + // background-color: white; + // border: 1px solid @border-color; + position: absolute; + left: 40px; + border-radius: 50%; + top: 5px; +} + +.timeline-item.notification-content::before { + .timeline-indicator(); } .timeline-item .reply-link { - padding: 0px 7px; -} - -.timeline-item h6, .timeline-head h6 { - margin-top: 6px; + padding-left: 7px; } .timeline-head { - background-color: @light-bg; - padding: 15px 30px; - border-bottom: 1px solid @border-color; + background-color: white; + // padding: 15px 30px; + border: 1px solid @border-color; + border-radius: 2px; + + .comment-input-header { + background-color: @light-bg; + padding: 7px 15px; + border-bottom: 1px solid @light-border-color; + } + + .comment-input-container { + padding: 15px; + } + + .comment-input { + border-color: @light-border-color; + max-width: 100%; + + &:focus { + box-shadow: none; + } + } +} + +@media(max-width: @screen-xs) { + .timeline-head { + border-left: none; + border-right: none; + border-radius: 0px; + } +} + +.timeline-new-email { + margin: 30px 0px; + padding: 0px 65px; + position: relative; +} + +.timeline-new-email::before { + .timeline-indicator(); } .form-footer h5 { diff --git a/frappe/public/less/indicator.less b/frappe/public/less/indicator.less index d0cc2af0a3..900acf7408 100644 --- a/frappe/public/less/indicator.less +++ b/frappe/public/less/indicator.less @@ -54,3 +54,13 @@ .indicator-right.darkgrey::after { background: @indicator-darkgrey; } + +.indicator.yellow::before, +.indicator-right.yellow::after { + background: @indicator-yellow; +} + +.indicator.light-blue::before, +.indicator-right.light-blue::after { + background: @indicator-light-blue; +} diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less index c3454d10b1..880543a549 100644 --- a/frappe/public/less/list.less +++ b/frappe/public/less/list.less @@ -124,6 +124,14 @@ .doclist-row { font-size: 12px; + + .likes-count { + display: inline-block; + width: 15px; + margin-left: -5px; + color: @text-muted; + font-size: 12px; + } } .doclist-row .docstatus .octicon { @@ -172,16 +180,17 @@ padding: 5px 15px; } -.listview-main-section .icon-star { +.listview-main-section .octicon-heart { cursor: pointer; } -.list-row-head .icon-star { - vertical-align: middle; +.list-row-head .octicon-heart { + margin-right: 13px; } -.star-action.icon-star { - color: #ffdb4c; +.like-action.octicon-heart { + // color: #ffdb4c; + color: @heart-color; } .list-id { @@ -209,3 +218,9 @@ display: none; } } + +.list-comment-count { + display: inline-block; + width: 37px; + text-align: left; +} diff --git a/frappe/public/less/mobile.less b/frappe/public/less/mobile.less index a7409e3111..19e4c2bc90 100644 --- a/frappe/public/less/mobile.less +++ b/frappe/public/less/mobile.less @@ -261,26 +261,6 @@ border-right-color: transparent !important; } - // timeline - - .timeline { - border-left: none !important; - border-right: none !important; - - .timeline-head { - padding: 7px 15px; - } - - .timeline-item { - padding: 15px; - border-bottom: 1px dashed @border-color; - } - - .timeline-item:last-child { - border-bottom: none; - } - } - // listviews .list-row { padding: 13px 15px !important; diff --git a/frappe/public/less/sidebar.less b/frappe/public/less/sidebar.less index af703acab9..be68a6e6fb 100644 --- a/frappe/public/less/sidebar.less +++ b/frappe/public/less/sidebar.less @@ -146,6 +146,11 @@ body[data-route^="Module"] .main-menu { } } } + + .liked-by .octicon-heart { + font-size: 16px; + cursor: pointer; + } } .form-sidebar .form-shared .share-doc-btn& { @@ -164,6 +169,7 @@ body[data-route^="Module"] .main-menu { .form-tags, .assignment-row, .form-shared, + .liked-by, .modified-by, .created-by, .tags-label, diff --git a/frappe/public/less/variables.less b/frappe/public/less/variables.less index c64e7c3a87..64f9857a09 100644 --- a/frappe/public/less/variables.less +++ b/frappe/public/less/variables.less @@ -24,6 +24,10 @@ @indicator-orange: #ffa00a; @indicator-purple: #743ee2; @indicator-darkgrey: #b8c2cc; +@indicator-yellow: #FEEF72; +@indicator-light-blue:#7CD6FD; + +@heart-color: @indicator-red; @navbar-default-color: #6C7680; @navbar-inverse-color: #9D9D9D; diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 0008f027c4..c125ab42d3 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -10,6 +10,9 @@ import re, urllib, datetime, math import babel.dates from dateutil import parser from num2words import num2words +import HTMLParser +from html2text import html2text + DATE_FORMAT = "%Y-%m-%d" TIME_FORMAT = "%H:%M:%S.%f" @@ -620,3 +623,12 @@ def unique(seq): def strip(val, chars=None): # \ufeff is no-width-break, \u200b is no-width-space return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars) + +def to_markdown(html): + text = None + try: + text = html2text(html) + except HTMLParser.HTMLParseError: + pass + + return text diff --git a/frappe/website/js/website.js b/frappe/website/js/website.js index acbf1ef5ea..e8437940ff 100644 --- a/frappe/website/js/website.js +++ b/frappe/website/js/website.js @@ -506,46 +506,6 @@ function valid_email(id) { var validate_email = valid_email; -function get_url_arg(name) { - return get_query_params()[name] || ""; -} - -function get_query_params() { - var query_params = {}; - var query_string = location.search.substring(1); - var query_list = query_string.split("&"); - for (var i=0, l=query_list.length; i < l; i++ ){ - var pair = query_list[i].split("="); - var key = pair[0]; - if (!key) { - continue; - } - - var value = pair[1]; - if (typeof value === "string") { - value = decodeURIComponent(value); - } - - if (key in query_params) { - if (typeof query_params[key] === undefined) { - query_params[key] = []; - } else if (typeof query_params[key] === "string") { - query_params[key] = [query_params[key]]; - } - query_params[key].push(value); - } else { - query_params[key] = value; - } - } - return query_params; -} - -function make_query_string(obj) { - var query_params = []; - $.each(obj, function(k, v) { query_params.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); }); - return "?" + query_params.join("&"); -} - function repl(s, dict) { if(s==null)return ''; for(key in dict) {