diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index a3b0271c13..39ddc993ca 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -11,7 +11,7 @@ from frappe.desk.notifications import (delete_notification_count_for, common_default_keys = ["__default", "__global"] global_cache_keys = ("app_hooks", "installed_apps", - "app_modules", "module_app", "notification_config", 'system_settings', + "app_modules", "module_app", "system_settings", 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules', 'assignment_rule', 'server_script_map') @@ -114,4 +114,4 @@ def get_doctype_map(doctype, name, filters, order_by=None): def clear_doctype_map(doctype, name): cache_key = frappe.scrub(doctype) + '_map' - frappe.cache().hdel(cache_key, name) \ No newline at end of file + frappe.cache().hdel(cache_key, name) diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 21d924bb7f..fcd32c52c9 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -8,7 +8,8 @@ from frappe import _ import json from frappe.model.document import Document from frappe.core.doctype.user.user import extract_mentions -from frappe.utils import get_fullname, get_link_to_form +from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification +from frappe.utils import get_fullname from frappe.website.render import clear_cache from frappe.database.schema import add_column from frappe.exceptions import ImplicitCommitError @@ -54,31 +55,22 @@ class Comment(Document): title = self.reference_name if title_field == "name" else \ frappe.db.get_value(self.reference_doctype, self.reference_name, title_field) - if title != self.reference_name: - parent_doc_label = "{0}: {1} (#{2})".format(_(self.reference_doctype), - title, self.reference_name) - else: - parent_doc_label = "{0}: {1}".format(_(self.reference_doctype), - self.reference_name) - - subject = _("{0} mentioned you in a comment in {1}").format(sender_fullname, parent_doc_label) - recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") for name in mentions] - link = get_link_to_form(self.reference_doctype, self.reference_name, label=parent_doc_label) - frappe.sendmail( - recipients = recipients, - sender = frappe.session.user, - subject = subject, - template = "mentioned_in_comment", - args = { - "body_content": _("{0} mentioned you in a comment in {1}").format(sender_fullname, link), - "comment": self, - "link": link - }, - header = [_('New Mention'), 'orange'] - ) + notification_message = _('''{0} mentioned you in a comment in {1} {2}''')\ + .format(frappe.bold(sender_fullname), frappe.bold(self.reference_doctype), frappe.bold(title)) + + notification_doc = { + 'type': 'Mention', + 'document_type': self.reference_doctype, + 'document_name': self.reference_name, + 'subject': notification_message, + 'from_user': frappe.session.user, + 'email_content': self.content + } + + enqueue_create_notification(recipients, notification_doc) def on_doctype_update(): diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 26e2049273..771a15a2e7 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -14,13 +14,6 @@ def get_notification_config(): "Error Snapshot": {"seen": 0, "parent_error_snapshot": None}, "Workflow Action": {"status": 'Open'} }, - "for_other": { - "Likes": "frappe.core.notifications.get_unseen_likes", - "Email": "frappe.core.notifications.get_unread_emails", - }, - "for_module": { - "Social": "frappe.social.doctype.post.post.get_unseen_post_count" - } } def get_things_todo(as_list=False): diff --git a/frappe/desk/doctype/notification_log/__init__.py b/frappe/desk/doctype/notification_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/notification_log/notification_log.js b/frappe/desk/doctype/notification_log/notification_log.js new file mode 100644 index 0000000000..654b2b2b06 --- /dev/null +++ b/frappe/desk/doctype/notification_log/notification_log.js @@ -0,0 +1,12 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Notification Log', { + refresh: function(frm) { + let dt = frm.doc.document_type; + let dn = frm.doc.document_name; + frm.fields_dict.document_name.$input_wrapper + .find('.control-value') + .wrapInner(``); + } +}); diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json new file mode 100644 index 0000000000..11037a83c0 --- /dev/null +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -0,0 +1,106 @@ +{ + "creation": "2019-08-26 13:37:34.165254", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "for_user", + "type", + "email_content", + "column_break_4", + "document_type", + "seen", + "document_name", + "from_user" + ], + "fields": [ + { + "fieldname": "subject", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Subject", + "read_only": 1 + }, + { + "fieldname": "for_user", + "fieldtype": "Link", + "label": "For User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Type", + "options": "Mention\nEnergy Point\nAssignment\nShare", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "email_content", + "fieldtype": "Text", + "label": "Email Content", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "document_type", + "fieldtype": "Link", + "label": "Document Type", + "options": "DocType", + "read_only": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Seen" + }, + { + "fieldname": "document_name", + "fieldtype": "Data", + "label": "Document Name", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "from_user", + "fieldtype": "Link", + "label": "From User", + "options": "User", + "read_only": 1, + "search_index": 1 + } + ], + "in_create": 1, + "modified": "2019-10-09 15:03:56.682093", + "modified_by": "Administrator", + "module": "Desk", + "name": "Notification Log", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "subject", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py new file mode 100644 index 0000000000..ca699bbcbd --- /dev/null +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled, + is_email_notifications_enabled, is_email_notifications_enabled_for_type) + +class NotificationLog(Document): + def after_insert(self): + frappe.publish_realtime('notification', after_commit=True, user=self.for_user) + if is_email_notifications_enabled(self.for_user): + send_notification_email(self) + + +def get_permission_query_conditions(for_user): + if not for_user: + for_user = frappe.session.user + + if for_user == 'Administrator': + return + + return '''(`tabNotification Log`.for_user = '{user}')'''.format(user=for_user) + +def enqueue_create_notification(users, doc): + doc = frappe._dict(doc) + + if isinstance(users, frappe.string_types): + users = [user.strip() for user in users.split(',') if user.strip()] + + frappe.enqueue( + 'frappe.desk.doctype.notification_log.notification_log.make_notification_logs', + doc=doc, + users=users, + now=frappe.flags.in_test + ) + +def make_notification_logs(doc, users): + from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled + for user in users: + if frappe.db.exists('User', user): + if is_notifications_enabled(user): + if doc.type == 'Energy Point' and not is_energy_point_enabled(): + return + else: + _doc = frappe.new_doc('Notification Log') + _doc.update(doc) + _doc.for_user = user + _doc.subject = _doc.subject.replace('
', '').replace('
', '') + _doc.insert(ignore_permissions=True) + +def send_notification_email(doc): + is_type_enabled = is_email_notifications_enabled_for_type(doc.for_user, doc.type) + if not is_type_enabled: + return + + from frappe.utils import get_url_to_form, strip_html + + doc_link = get_url_to_form(doc.document_type, doc.document_name) + header = get_email_header(doc) + email_subject = strip_html(doc.subject) + + frappe.sendmail( + recipients = doc.for_user, + subject = email_subject, + template = "new_notification", + args = { + 'body_content': doc.subject, + 'description': doc.email_content, + 'document_type': doc.document_type, + 'document_name': doc.document_name, + 'doc_link': doc_link + }, + header = [header, 'orange'], + now=frappe.flags.in_test + ) + +def get_email_header(doc): + return { + 'Default': _('New Notification'), + 'Mention': _('New Mention'), + 'Assignment': _('New Assignment'), + 'Share': _('New Document Shared'), + 'Energy Point': _('Energy Point Update'), + }[doc.type or 'Default'] + + +@frappe.whitelist() +def mark_as_seen(docnames): + docnames = frappe.parse_json(docnames) + if docnames: + filters = {'name': ['in', docnames]} + frappe.db.set_value('Notification Log', filters, 'seen', 1, update_modified=False) diff --git a/frappe/desk/doctype/notification_log/test_notification_log.py b/frappe/desk/doctype/notification_log/test_notification_log.py new file mode 100644 index 0000000000..9431336aad --- /dev/null +++ b/frappe/desk/doctype/notification_log/test_notification_log.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +from frappe.desk.form.assign_to import add as assign_task +import unittest + +class TestNotificationLog(unittest.TestCase): + def test_assignment(self): + todo = get_todo() + user = get_user() + + assign_task({ + "assign_to": user, + "doctype": 'ToDo', + "name": todo.name, + "description": todo.description + }) + log_type = frappe.db.get_value('Notification Log', { + 'document_type': 'ToDo', + 'document_name': todo.name + }, 'type') + self.assertEqual(log_type, 'Assignment') + + def test_share(self): + todo = get_todo() + user = get_user() + + frappe.share.add('ToDo', todo.name, user) + log_type = frappe.db.get_value('Notification Log', { + 'document_type': 'ToDo', + 'document_name': todo.name + }, 'type') + self.assertEqual(log_type, 'Share') + + email = get_last_email_queue() + content = 'Subject: {} shared a document ToDo'.format(frappe.utils.get_fullname(frappe.session.user)) + self.assertTrue(content in email.message) + + +def get_last_email_queue(): + res = frappe.db.get_all('Email Queue', + fields=['message'], + order_by='creation desc', + limit=1 + ) + return res[0] + +def get_todo(): + if not frappe.get_all('ToDo'): + return frappe.get_doc({ 'doctype': 'ToDo', 'description': 'Test for Notification' }).insert() + + res = frappe.get_all('ToDo', limit=1) + return frappe.get_cached_doc('ToDo', res[0].name) + +def get_user(): + users = frappe.db.get_all('User', + filters={'name': ('not in', ['Administrator', 'Guest'])}, + fields='name', limit=1) + return users[0].name diff --git a/frappe/desk/doctype/notification_settings/__init__.py b/frappe/desk/doctype/notification_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json new file mode 100644 index 0000000000..4998edb42e --- /dev/null +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -0,0 +1,103 @@ +{ + "autoname": "Prompt", + "creation": "2019-09-11 22:15:44.851526", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "subscribed_documents", + "column_break_3", + "enable_email_notifications", + "enable_email_mention", + "enable_email_assignment", + "enable_email_energy_point", + "enable_email_share", + "user" + ], + "fields": [ + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "subscribed_documents", + "fieldtype": "Table MultiSelect", + "label": "Subscribed Documents", + "options": "Notification Subscribed Document" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "default": "1", + "fieldname": "enable_email_notifications", + "fieldtype": "Check", + "label": "Enable Email Notifications" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_mention", + "fieldtype": "Check", + "label": "Mentions" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_assignment", + "fieldtype": "Check", + "label": "Assignments" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_energy_point", + "fieldtype": "Check", + "label": "Energy Points" + }, + { + "default": "1", + "depends_on": "enable_email_notifications", + "fieldname": "enable_email_share", + "fieldtype": "Check", + "label": "Document Share" + }, + { + "default": "__user", + "fieldname": "user", + "fieldtype": "Link", + "hidden": 1, + "in_list_view": 1, + "label": "User", + "options": "User", + "read_only": 1 + } + ], + "in_create": 1, + "modified": "2019-10-09 15:58:16.746610", + "modified_by": "Administrator", + "module": "Desk", + "name": "Notification Settings", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py new file mode 100644 index 0000000000..b14c2fd219 --- /dev/null +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class NotificationSettings(Document): + def on_update(self): + from frappe.desk.notifications import clear_notification_config + clear_notification_config(frappe.session.user) + + +def is_notifications_enabled(user): + enabled = frappe.db.get_value('Notification Settings', user, 'enabled') + if enabled is None: + return True + return enabled + +def is_email_notifications_enabled(user): + enabled = frappe.db.get_value('Notification Settings', user, 'enable_email_notifications') + if enabled is None: + return True + return enabled + +def is_email_notifications_enabled_for_type(user, notification_type): + fieldname = 'enable_email_' + frappe.scrub(notification_type) + enabled = frappe.db.get_value('Notification Settings', user, fieldname) + if enabled is None: + return True + return enabled + +@frappe.whitelist() +def create_notification_settings(): + _doc = frappe.new_doc('Notification Settings') + _doc.name = frappe.session.user + _doc.insert(ignore_permissions=True) + frappe.db.commit() + + +@frappe.whitelist() +def get_subscribed_documents(): + try: + doc = frappe.get_doc('Notification Settings', frappe.session.user) + subscribed_documents = [item.document for item in doc.subscribed_documents] + except frappe.DoesNotExistError: + subscribed_documents = [] + + return subscribed_documents + + +def get_permission_query_conditions(user): + if not user: user = frappe.session.user + + return '''(`tabNotification Settings`.user = '{user}')'''.format(user=user) diff --git a/frappe/desk/doctype/notification_subscribed_document/__init__.py b/frappe/desk/doctype/notification_subscribed_document/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.json b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.json new file mode 100644 index 0000000000..b3f4046163 --- /dev/null +++ b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.json @@ -0,0 +1,30 @@ +{ + "creation": "2019-10-09 15:04:39.504787", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document" + ], + "fields": [ + { + "fieldname": "document", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document", + "options": "DocType", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-10-09 16:02:00.049237", + "modified_by": "Administrator", + "module": "Desk", + "name": "Notification Subscribed Document", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py new file mode 100644 index 0000000000..f005efae76 --- /dev/null +++ b/frappe/desk/doctype/notification_subscribed_document/notification_subscribed_document.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class NotificationSubscribedDocument(Document): + pass diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 9e0598b128..508720a488 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -190,4 +190,4 @@ "title_field": "description", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index 959eee7e60..5841f9474a 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -7,7 +7,8 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.desk.form.document_follow import follow_document -from frappe.utils import cint +from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification +import frappe.utils import frappe.share class DuplicateToDoError(frappe.ValidationError): pass @@ -80,7 +81,7 @@ def add(args=None): # notify notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\ - description=args.get("description"), notify=args.get('notify')) + description=args.get("description")) return get(args) @@ -147,7 +148,7 @@ def clear(doctype, name): return True def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', - description=None, notify=0): + description=None): """ Notify assignee that there is a change in assignment """ @@ -158,56 +159,28 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', return # Search for email address in description -- i.e. assignee - from frappe.utils import get_link_to_form - assignment = get_link_to_form(doc_type, doc_name, label="%s: %s" % (doc_type, doc_name)) - owner_name = frappe.get_cached_value('User', owner, 'full_name') user_name = frappe.get_cached_value('User', frappe.session.user, 'full_name') + title_field = frappe.get_meta(doc_type).get_title_field() + title = doc_name if title_field == "name" else \ + frappe.db.get_value(doc_type, doc_name, title_field) + description_html = "
{0}
".format(description) if description else None + if action=='CLOSE': - if owner == frappe.session.get('user'): - arg = { - 'contact': assigned_by, - 'txt': _("The task {0}, that you assigned to {1}, has been closed.").format(assignment, - owner_name) - } - else: - arg = { - 'contact': assigned_by, - 'txt': _("The task {0}, that you assigned to {1}, has been closed by {2}.").format(assignment, - owner_name, user_name) - } + subject = _('Your assignment on {0} {1} has been removed').format(frappe.bold(doc_type), frappe.bold(title)) else: - description_html = "

{0}

".format(description) - arg = { - 'contact': owner, - 'txt': _("A new task, {0}, has been assigned to you by {1}. {2}").format(assignment, - user_name, description_html), - 'notify': notify - } + user_name = frappe.bold(user_name) + document_type = frappe.bold(doc_type) + title = frappe.bold(title) + subject = _('{0} assigned a new task {1} {2} to you').format(user_name, document_type, title) - if arg and cint(arg.get("notify")): - _notify(arg) + notification_doc = { + 'type': 'Assignment', + 'document_type': doc_type, + 'subject': subject, + 'document_name': doc_name, + 'from_user': frappe.session.user, + 'email_content': description_html + } -def _notify(args): - from frappe.utils import get_fullname, get_url + enqueue_create_notification(owner, notification_doc) - args = frappe._dict(args) - contact = args.contact - txt = args.txt - - try: - if not isinstance(contact, list): - contact = [frappe.db.get_value("User", contact, "email") or contact] - - frappe.sendmail(\ - recipients=contact, - sender= frappe.db.get_value("User", frappe.session.user, "email"), - subject=_("New message from {0}").format(get_fullname(frappe.session.user)), - template="new_message", - args={ - "from": get_fullname(frappe.session.user), - "message": txt, - "link": get_url() - }, - header=[_('New Message'), 'orange']) - except frappe.OutgoingEmailError: - pass diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 8b88af60b0..3a00330a55 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -4,8 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import time_diff_in_seconds, now, now_datetime, DATETIME_FORMAT -from dateutil.relativedelta import relativedelta +from frappe.desk.doctype.notification_settings.notification_settings import get_subscribed_documents from six import string_types import json @@ -16,10 +15,7 @@ def get_notifications(): not frappe.db.get_single_value('System Settings', 'setup_complete')): return { "open_count_doctype": {}, - "open_count_module": {}, - "open_count_other": {}, "targets": {}, - "new_messages": [] } config = get_notification_config() @@ -37,58 +33,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), "targets": get_notifications_for_targets(config, notification_percent), - "new_messages": get_new_messages() } -def get_new_messages(): - last_update = frappe.cache().hget("notifications_last_update", frappe.session.user) - now_timestamp = now() - frappe.cache().hset("notifications_last_update", frappe.session.user, now_timestamp) - - if not last_update: - return [] - - if last_update and time_diff_in_seconds(now_timestamp, last_update) > 1800: - # no update for 30 mins, consider only the last 30 mins - last_update = (now_datetime() - relativedelta(seconds=1800)).strftime(DATETIME_FORMAT) - - return frappe.db.sql("""select sender_full_name, content - from `tabCommunication` - where communication_type in ('Chat', 'Notification') - and reference_doctype='user' - and reference_name = %s - and creation > %s - order by creation desc""", (frappe.session.user, last_update), as_dict=1) - -def get_notifications_for_modules(config, notification_count): - """Notifications for modules""" - 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[m] = notification_count[m] - else: - open_count[m] = frappe.get_attr(notification_map[m])() - - frappe.cache().hset("notification_count:" + m, frappe.session.user, open_count[m]) - except frappe.PermissionError: - frappe.clear_messages() - pass - # frappe.msgprint("Permission Error in notifications for {0}".format(m)) - - return open_count - def get_notifications_for_doctypes(config, notification_count): """Notifications for DocTypes""" can_read = frappe.get_user().get_can_read() @@ -170,12 +117,11 @@ def get_notifications_for_targets(config, notification_percent): def clear_notifications(user=None): if frappe.flags.in_install: return - + cache = frappe.cache() config = get_notification_config() for_doctype = list(config.get('for_doctype')) if config.get('for_doctype') else [] for_module = list(config.get('for_module')) if config.get('for_module') else [] groups = for_doctype + for_module - cache = frappe.cache() for name in groups: if user: @@ -185,6 +131,9 @@ def clear_notifications(user=None): frappe.publish_realtime('clear_notifications') +def clear_notification_config(user): + frappe.cache().hdel('notification_config', user) + def delete_notification_count_for(doctype): frappe.cache().delete_key("notification_count:" + doctype) frappe.publish_realtime('clear_notifications') @@ -200,9 +149,10 @@ def clear_doctype_notifications(doc, method=None, *args, **kwargs): delete_notification_count_for(doctype) return -def get_notification_info_for_boot(): - out = get_notifications() +@frappe.whitelist() +def get_notification_info(): config = get_notification_config() + out = get_notifications() can_read = frappe.get_user().get_can_read() conditions = {} module_doctypes = {} @@ -224,6 +174,7 @@ def get_notification_info_for_boot(): def get_notification_config(): def _get(): + subscribed_documents = get_subscribed_documents() config = frappe._dict() hooks = frappe.get_hooks() if hooks: @@ -231,15 +182,26 @@ def get_notification_config(): nc = frappe.get_attr(notification_config)() for key in ("for_doctype", "for_module", "for_other", "targets"): config.setdefault(key, {}) - config[key].update(nc.get(key, {})) + if key == "for_doctype": + if len(subscribed_documents) > 0: + key_config = nc.get(key, {}) + subscribed_docs_config = frappe._dict() + for document in subscribed_documents: + if key_config.get(document): + subscribed_docs_config[document] = key_config.get(document) + config[key].update(subscribed_docs_config) + else: + config[key].update(nc.get(key, {})) + else: + config[key].update(nc.get(key, {})) return config - return frappe.cache().get_value("notification_config", _get) + return frappe.cache().hget("notification_config", frappe.session.user, _get) def get_filters_for(doctype): '''get open filters for doctype''' config = get_notification_config() - return config.get('for_doctype').get(doctype, {}) + return config.get("for_doctype").get(doctype, {}) @frappe.whitelist() @frappe.read_only() @@ -253,7 +215,7 @@ def get_open_count(doctype, name, items=[]): if frappe.flags.in_migrate or frappe.flags.in_install: return { - 'count': [] + "count": [] } frappe.has_permission(doc=frappe.get_doc(doctype, name), throw=True) @@ -264,39 +226,39 @@ def get_open_count(doctype, name, items=[]): # compile all items in a list if not items: for group in links.transactions: - items.extend(group.get('items')) + items.extend(group.get("items")) if not isinstance(items, list): items = json.loads(items) out = [] for d in items: - if d in links.get('internal_links', {}): + if d in links.get("internal_links", {}): # internal link continue filters = get_filters_for(d) - fieldname = links.get('non_standard_fieldnames', {}).get(d, links.fieldname) - data = {'name': d} + fieldname = links.get("non_standard_fieldnames", {}).get(d, links.fieldname) + data = {"name": d} if filters: # get the fieldname for the current document # we only need open documents related to the current document filters[fieldname] = name - total = len(frappe.get_all(d, fields='name', + total = len(frappe.get_all(d, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True)) - data['open_count'] = total + data["open_count"] = total - total = len(frappe.get_all(d, fields='name', + total = len(frappe.get_all(d, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True)) - data['count'] = total + data["count"] = total out.append(data) out = { - 'count': out, + "count": out, } module = frappe.get_meta_module(doctype) - if hasattr(module, 'get_timeline_data'): - out['timeline_data'] = module.get_timeline_data(doctype, name) + if hasattr(module, "get_timeline_data"): + out["timeline_data"] = module.get_timeline_data(doctype, name) return out diff --git a/frappe/hooks.py b/frappe/hooks.py index aeba3c7445..a35fd78ebb 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -88,6 +88,8 @@ permission_query_conditions = { "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", "ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions", "User": "frappe.core.doctype.user.user.get_permission_query_conditions", + "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", + "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", "Note": "frappe.desk.doctype.note.note.get_permission_query_conditions", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions", "Contact": "frappe.contacts.address_and_contact.get_permission_query_conditions_for_contact", diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 33d7f8e0af..af67350ab6 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -20,7 +20,8 @@ from frappe.desk.doctype.tag.tag import delete_tags_for_document from frappe.exceptions import FileNotFoundError -doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File", "Version", "Document Follow", "Comment" , "View Log", "Tag Link") +doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File", + "Version", "Document Follow", "Comment" , "View Log", "Tag Link", "Notification Log") def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True): @@ -305,6 +306,7 @@ def delete_dynamic_links(doctype, name): delete_references('Comment', doctype, name) delete_references('View Log', doctype, name) delete_references('Document Follow', doctype, name, 'ref_doctype', 'ref_docname') + delete_references('Notification Log', doctype, name, 'document_type', 'document_name') # unlink communications clear_timeline_references(doctype, name) diff --git a/frappe/patches.txt b/frappe/patches.txt index 36aa390d65..8c3a176811 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -252,3 +252,5 @@ frappe.patches.v12_0.move_email_and_phone_to_child_table frappe.patches.v12_0.delete_duplicate_indexes frappe.patches.v12_0.set_default_incoming_email_port frappe.patches.v12_0.update_global_search +frappe.patches.v12_0.setup_tags +execute:frappe.reload_doc('desk', 'doctype', 'notification_settings') diff --git a/frappe/public/build.json b/frappe/public/build.json index c59df8034c..6c67c17374 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -70,6 +70,7 @@ "public/less/indicator.less", "public/less/avatar.less", "public/less/navbar.less", + "public/less/notifications.less", "public/less/sidebar.less", "public/less/page.less", "public/less/tree.less", @@ -185,6 +186,7 @@ "public/js/frappe/ui/toolbar/awesome_bar.js", "public/js/frappe/ui/toolbar/energy_points_notifications.js", + "public/js/frappe/ui/notifications/notifications.js", "public/js/frappe/ui/toolbar/search.js", "public/js/frappe/ui/toolbar/tag_utils.js", "public/js/frappe/ui/toolbar/search.html", diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 88a3ba9803..5a972e0dbe 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -74,8 +74,6 @@ frappe.Application = Class.extend({ // trigger app startup $(document).trigger('startup'); - this.start_notification_updates(); - $(document).trigger('app_ready'); if (frappe.boot.messages) { @@ -268,54 +266,6 @@ frappe.Application = Class.extend({ } }, - start_notification_updates: function() { - var me = this; - - // refresh_notifications will be called only once during a 1 second window - this.refresh_notifications = frappe.utils.debounce(this.refresh_notifications.bind(this), 1000); - - // kickoff - this.refresh_notifications(); - - frappe.realtime.on('clear_notifications', () => { - me.refresh_notifications(); - }); - - // first time loaded in boot - $(document).trigger("notification-update"); - - // refresh notifications if user is back after sometime - $(document).on("session_alive", function() { - me.refresh_notifications(); - }); - }, - - refresh_notifications: function() { - var me = this; - if(frappe.session_alive && frappe.boot && frappe.boot.home_page !== 'setup-wizard') { - if (this._refresh_notifications) { - this._refresh_notifications.abort(); - } - this._refresh_notifications = frappe.call({ - type: 'GET', - method: "frappe.desk.notifications.get_notifications", - callback: function(r) { - if(r.message) { - $.extend(frappe.boot.notification_info, r.message); - $(document).trigger("notification-update"); - - if(frappe.get_route()[0] != "messages") { - if(r.message.new_messages.length) { - frappe.utils.set_title_prefix("(" + r.message.new_messages.length + ")"); - } - } - } - }, - freeze: false - }); - } - }, - set_globals: function() { frappe.session.user = frappe.boot.user.name; frappe.session.user_email = frappe.boot.user.email; diff --git a/frappe/public/js/frappe/form/sidebar/assign_to.js b/frappe/public/js/frappe/form/sidebar/assign_to.js index 2f8c6469b3..61d1789518 100644 --- a/frappe/public/js/frappe/form/sidebar/assign_to.js +++ b/frappe/public/js/frappe/form/sidebar/assign_to.js @@ -140,7 +140,6 @@ frappe.ui.form.AssignToDialog = Class.extend({ { fieldtype: 'Section Break' }, { fieldtype: 'Column Break' }, { fieldtype: 'Date', fieldname: 'date', label: __("Complete By") }, - { fieldtype: 'Check', fieldname: 'notify', label: __("Notify by Email"), default: 1}, { fieldtype: 'Column Break' }, { fieldtype: 'Select', fieldname: 'priority', label: __("Priority"), options: [ @@ -171,12 +170,10 @@ frappe.ui.form.AssignToDialog = Class.extend({ var me = this; if($(myself).prop("checked")) { me.dialog.set_value("assign_to", frappe.session.user); - me.dialog.set_value("notify", 0); me.dialog.get_field("notify").$wrapper.toggle(false); me.dialog.get_field("assign_to").$wrapper.toggle(false); } else { me.dialog.set_value("assign_to", ""); - me.dialog.get_field("notify").$wrapper.toggle(true); me.dialog.get_field("assign_to").$wrapper.toggle(true); } }, diff --git a/frappe/public/js/frappe/form/sidebar/share.js b/frappe/public/js/frappe/form/sidebar/share.js index 0a7cc2b831..3cb5257e78 100644 --- a/frappe/public/js/frappe/form/sidebar/share.js +++ b/frappe/public/js/frappe/form/sidebar/share.js @@ -141,7 +141,6 @@ frappe.ui.form.Share = Class.extend({ read: $(d.body).find(".add-share-read").prop("checked") ? 1 : 0, write: $(d.body).find(".add-share-write").prop("checked") ? 1 : 0, share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0, - notify: $(d.body).find(".add-share-notify").prop("checked") ? 1 : 0 }, btn: this, callback: function(r) { diff --git a/frappe/public/js/frappe/form/templates/set_sharing.html b/frappe/public/js/frappe/form/templates/set_sharing.html index 617cb07848..1fb744d7d4 100644 --- a/frappe/public/js/frappe/form/templates/set_sharing.html +++ b/frappe/public/js/frappe/form/templates/set_sharing.html @@ -51,17 +51,5 @@

-
-
-
-
- -
-
-
{% endif %} \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js new file mode 100644 index 0000000000..a41e931402 --- /dev/null +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -0,0 +1,450 @@ +frappe.ui.Notifications = class Notifications { + constructor() { + frappe.model + .with_doc('Notification Settings', frappe.session.user) + .then(doc => { + this.notifications_settings = doc; + this.make(); + }); + } + + make() { + this.$dropdown = $('.navbar').find('.dropdown-notifications'); + this.$dropdown_list = this.$dropdown.find('.notifications-list'); + this.$notification_indicator = this.$dropdown.find( + '.notifications-indicator' + ); + this.user = frappe.session.user; + this.max_length = 20; + + this.render_dropdown_headers(); + this.$notifications = this.$dropdown_list.find( + '.category-list[data-category="Notifications"]' + ); + this.$open_docs = this.$dropdown_list.find( + '.category-list[data-category="Open Documents"]' + ); + this.$upcoming_events = this.$dropdown_list.find( + '.category-list[data-category="Upcoming Events"]' + ); + + frappe.utils.bind_actions_with_object(this.$dropdown_list, this); + this.setup_notifications(); + this.bind_events(); + } + + setup_notifications() { + this.get_notifications_list(this.max_length).then(list => { + this.dropdown_items = list; + this.render_notifications_dropdown(); + + if (this.$notifications.find('.unseen').length) { + this.$notification_indicator.show(); + } + }); + } + + render_upcoming_events(e, $target) { + let hide = $target.next().hasClass('in'); + if (!hide) { + let today = frappe.datetime.now_date(); + + frappe + .xcall('frappe.desk.doctype.event.event.get_events', { + start: today, + end: today + }) + .then(event_list => { + this.render_events_html(event_list); + }); + } + } + + render_events_html(event_list) { + let html = ''; + if (event_list.length) { + let get_event_html = event => { + let time = frappe.datetime.get_time(event.starts_on); + return ` + ${time} + ${event.subject} + `; + }; + html = event_list.map(get_event_html).join(''); + } else { + html = `
  • + ${__('No Upcoming Events')} +
  • `; + } + + this.$upcoming_events.html(html); + } + + get_open_document_config(e) { + this.open_docs_config = { + ToDo: { label: __('To Do') }, + Event: { label: __('Calendar'), route: 'List/Event/Calendar' } + }; + + let hide = $(e.currentTarget) + .next() + .hasClass('in'); + if (!hide) { + frappe + .xcall('frappe.desk.notifications.get_notification_info') + .then(r => { + this.open_document_list = r; + this.render_open_document_count(); + }); + } + } + + render_open_document_count() { + this.$open_docs.html(''); + let defaults = ['ToDo']; + this.get_counts(this.open_document_list['open_count_doctype'], 1, defaults); + let targets = { doctypes: {} }, + map = this.open_document_list['targets']; + + Object.keys(map).map(doctype => { + Object.keys(map[doctype]).map(doc => { + targets[doc] = map[doctype][doc]; + targets.doctypes[doc] = doctype; + }); + }); + + this.get_counts(targets, 1, null, ['doctypes'], true); + this.get_counts( + this.open_document_list['open_count_doctype'], + 0, + null, + defaults + ); + } + + get_counts(map, divide, keys, excluded = [], target = false) { + let empty_map = 1; + keys = keys + ? keys + : Object.keys(map).sort().filter(e => !excluded.includes(e)); + keys.map(key => { + let doc_dt = map.doctypes ? map.doctypes[key] : undefined; + if (map[key] > 0 || target) { + this.add_open_document_html(key, map[key], doc_dt, target); + empty_map = 0; + } + }); + + if (divide && !empty_map) { + this.$open_docs.append($('
  • ')); + } + } + + add_open_document_html(name, value, doc_dt, target = false) { + let label = this.open_docs_config[name] + ? this.open_docs_config[name].label + : name; + let title = target ? `title="${__('Your Target')}"` : ''; + let $list_item = !target + ? $(`
  • + ${label} + ${value} +
  • `) + : $(`
  • + ${label} +
    +
    +
    +
    +
    +
  • `); + + this.$open_docs.append($list_item); + if (!target) this.total += value; + } + + route_to_list_with_filters(doctype) { + let filters = this.open_document_list['conditions'][doctype]; + if (filters && $.isPlainObject(filters)) { + if (!frappe.route_options) { + frappe.route_options = {}; + } + $.extend(frappe.route_options, filters); + } + frappe.set_route('List', doctype); + } + + route_to_document_type(e) { + this.$dropdown.removeClass('open'); + this.$dropdown.trigger('hide.bs.dropdown'); + let doctype = $(e.currentTarget).attr('data-doctype'); + let docname = $(e.currentTarget).attr('data-docname'); + if (!docname) { + let config = this.open_docs_config[doctype] || {}; + if (config.route) { + frappe.set_route(config.route); + } else if (config.click) { + config.click(); + } else { + this.route_to_list_with_filters(doctype); + } + } else { + frappe.set_route('Form', doctype, docname); + } + } + + update_dropdown() { + this.get_notifications_list(1).then(r => { + let new_item = r[0]; + this.dropdown_items.unshift(new_item); + if (this.dropdown_items.length > this.max_length) { + this.$dropdown_list + .find('.recent-notification') + .last() + .remove(); + this.dropdown_items.pop(); + } + + this.insert_into_dropdown(); + }); + } + + insert_into_dropdown() { + let new_item = this.dropdown_items[0]; + let new_item_html = this.get_dropdown_item_html(new_item); + $(new_item_html).prependTo(this.$dropdown_list.find(this.$notifications)); + this.change_activity_status(); + } + + change_activity_status() { + if (this.$dropdown_list.find('.activity-status')) { + this.$dropdown_list.find('.activity-status').replaceWith( + ` + ${__('View Full Log')} + ` + ); + } + } + + mark_as_seen() { + let unseen_docnames = this.dropdown_items + .filter(item => item.seen === 0) + .map(d => d.name); + if (!unseen_docnames.length) return; + frappe.call( + 'frappe.desk.doctype.notification_log.notification_log.mark_as_seen', + { docnames: unseen_docnames } + ); + } + + get_notifications_list(limit) { + return frappe.db.get_list('Notification Log', { + fields: ['*'], + limit: limit, + order_by: 'creation desc' + }); + } + + render_notifications_dropdown() { + let body_html = ''; + let view_full_log_html = ''; + let dropdown_html; + + if (this.notifications_settings && !this.notifications_settings.enabled) { + dropdown_html = `
  • + + ${__('Notifications Disabled')} +
  • `; + } else { + if (this.dropdown_items.length) { + this.dropdown_items.forEach(field => { + let item_html = this.get_dropdown_item_html(field); + if (item_html) body_html += item_html; + }); + view_full_log_html = ` + ${__('View Full Log')} + `; + } else { + body_html += `
  • + + ${__('No activity')} +
  • `; + } + dropdown_html = body_html + view_full_log_html; + } + + this.$notifications.html(dropdown_html); + } + + get_dropdown_item_html(field) { + let doc_link = frappe.utils.get_form_link( + field.document_type, + field.document_name + ); + let seen_class = field.seen ? '' : 'unseen'; + let message = field.subject; + let message_html = `
    ${message}
    `; + let user = field.from_user; + let user_avatar = frappe.avatar(user, 'avatar-small user-avatar'); + let timestamp = frappe.datetime.comment_when(field.creation, true); + let item_html = ` + ${user_avatar} + ${message_html} +
    + ${timestamp} +
    +
    `; + + return item_html; + } + + render_dropdown_headers() { + this.categories = [ + { + label: __('Notifications'), + value: 'Notifications' + }, + { + label: __('Upcoming Events'), + value: 'Upcoming Events', + action: 'render_upcoming_events' + }, + { + label: __('Open Documents'), + value: 'Open Documents', + action: 'get_open_document_config' + } + ]; + + let get_headers_html = category => { + let category_id = frappe.dom.get_unique_id(); + let settings_html = + category.value === 'Notifications' + ? ` + ${__('Settings')} + ` + : ''; + let html = `
  • +
  • + ${category.label} + + ${settings_html} +
  • +
    +
    + ${__('Loading...')} +
    +
    + `; + + return html; + }; + + let html = this.categories + .map(get_headers_html) + .join('
  • '); + this.$dropdown_list.append(html); + this.$dropdown_list + .find('.category-list[data-category="Notifications"]') + .collapse('show'); + this.toggle_collapse_indicator( + this.$dropdown_list.find('.category-list[data-category="Notifications"]') + ); + } + + make_and_route_to_settings(e) { + e.stopImmediatePropagation(); + this.$dropdown.removeClass('open'); + this.$dropdown.trigger('hide.bs.dropdown'); + let method = + 'frappe.desk.doctype.notification_settings.notification_settings.create_notification_settings'; + + return Promise.resolve() + .then(() => { + if (!this.notifications_settings) return frappe.call(method); + }) + .then(() => { + frappe.set_route(`#Form/Notification Settings/${frappe.session.user}`); + }); + } + + bind_events() { + this.setup_notification_listener(); + this.setup_dropdown_events(); + + this.$dropdown_list.on('click', '.recent-item', () => { + this.$dropdown.removeClass('open'); + }); + + $('.category-list').on('hide.bs.collapse', e => { + this.toggle_collapse_indicator($(e.currentTarget)); + }); + + $('.category-list').on('show.bs.collapse', e => { + this.toggle_collapse_indicator($(e.currentTarget)); + }); + } + + setup_notification_listener() { + frappe.realtime.on('notification', () => { + this.$dropdown.find('.notifications-indicator').show(); + this.update_dropdown(); + }); + } + + setup_dropdown_events() { + this.$dropdown_list + .find( + '[data-category="Notifications"], [data-category="Upcoming Events"], [data-category="Open Documents"]' + ) + .collapse({ + toggle: false + }); + this.$dropdown.on('hide.bs.dropdown', e => { + this.$notification_indicator.hide(); + let hide = $(e.currentTarget).data('closable'); + if (hide) { + this.$dropdown_list + .find('[data-category="Notifications"]') + .collapse('show'); + this.$dropdown_list + .find( + '[data-category="Upcoming Events"], [data-category="Open Documents"]' + ) + .collapse('hide'); + } + this.$dropdown_list.find('.unseen').removeClass('unseen'); + $(e.currentTarget).data('closable', true); + return hide; + }); + + this.$dropdown.on('show.bs.dropdown', () => { + this.mark_as_seen(); + }); + + this.$dropdown.on('click', e => { + if ($(e.target).closest('.dropdown-toggle').length) { + $(e.currentTarget).data('closable', true); + } else { + $(e.currentTarget).data('closable', false); + } + }); + } + + toggle_collapse_indicator($el) { + $el + .prev() + .find('.collapse-indicator') + .toggleClass('octicon-chevron-down'); + $el + .prev() + .find('.collapse-indicator') + .toggleClass('octicon-chevron-up'); + } +}; diff --git a/frappe/public/js/frappe/ui/toolbar/energy_points_notifications.js b/frappe/public/js/frappe/ui/toolbar/energy_points_notifications.js deleted file mode 100644 index 5d166f41fd..0000000000 --- a/frappe/public/js/frappe/ui/toolbar/energy_points_notifications.js +++ /dev/null @@ -1,186 +0,0 @@ - -frappe.ui.EnergyPointsNotifications = class { - - constructor() { - this.$dropdown = $('.navbar').find('.dropdown-energy-points'); - this.$dropdown_list = this.$dropdown.find('.recent-points-list'); - this.$notification_indicator = this.$dropdown.find('.energy-points-notification'); - this.max_length = 20; - this.setup_energy_points_notifications(); - } - - setup_energy_points_notifications() { - this.get_energy_points_list(this.max_length).then(user_points_list => { - this.dropdown_items = user_points_list; - this.render_energy_points_dropdown(); - this.setup_view_full_log(); - if (this.$dropdown_list.find('.unseen').length) { - this.$notification_indicator.show(); - } - }); - - this.bind_events(); - } - - bind_events() { - frappe.realtime.on('energy_points_notification', () => { - this.$dropdown.find('.energy-points-notification').show(); - this.update_dropdown(); - }); - - this.$dropdown.on('hide.bs.dropdown', () => { - this.$notification_indicator.hide(); - this.$dropdown_list.find('.unseen').removeClass('unseen'); - }); - - this.$dropdown.on('show.bs.dropdown', () => { - this.check_seen(); - }); - } - - update_dropdown() { - this.get_energy_points_list(1).then(r => { - let new_item = r[0]; - this.dropdown_items.unshift(new_item); - if (this.dropdown_items.length > this.max_length) { - this.$dropdown_list.find('.recent-points-item').last().remove(); - this.dropdown_items.pop(); - } - this.insert_into_dropdown(); - }); - } - - insert_into_dropdown() { - let new_item = this.dropdown_items[0]; - let new_item_html = this.get_dropdown_item_html(new_item); - let new_item_date_range = this.get_date_range_title(new_item.creation); - let current_date_range = this.get_date_range_title(this.dropdown_items[1].creation); - if (current_date_range !== new_item_date_range) { - let $date_range = $(`
  • ${new_item_date_range}
  • `); - $date_range.insertAfter(this.$dropdown_list.find('.points-updates-header')); - $(new_item_html).insertAfter($date_range); - } else { - $(new_item_html).insertAfter(this.$dropdown_list.find('.points-date-range').eq(0)); - } - } - - check_seen() { - let unseen_logs = this.dropdown_items.filter(item => item.seen === 0); - frappe.call('frappe.social.doctype.energy_point_log.energy_point_log.set_notification_as_seen', {point_logs: unseen_logs}); - } - - get_date_range_title(date) { - let current_date = frappe.datetime.now_date(); - let prev_week = frappe.datetime.add_days(current_date, -7); - let prev_month = frappe.datetime.add_months(frappe.datetime.now_date(), -1); - if (date >= current_date) { - return __('Today'); - } else if (date > prev_week) { - return __('Last 7 days'); - } else if (date > prev_month) { - return __('Last 30 days'); - } else { - return __('Older'); - } - } - - get_energy_points_list(limit) { - return frappe.db.get_list('Energy Point Log', { - filters: { - user: frappe.session.user, - type: ['not in', ['Review']], - }, - fields: - ['name', 'user', 'points', 'reference_doctype', 'reference_name', 'reason', 'type', 'seen', 'rule', 'owner', 'creation'], - limit: limit, - order_by: 'creation desc' - }).then((energy_points_list) => { - return energy_points_list; - }); - } - - render_energy_points_dropdown() { - let header_html = - `
  • - ${__('Energy Points')} - -
  • `; - let body_html = ''; - let view_full_log_html = ''; - - if (this.dropdown_items.length) { - let date_range = this.get_date_range_title(this.dropdown_items[0].creation); - body_html += `
  • ${date_range}
  • `; - this.dropdown_items.forEach(field => { - let current_field_date_range = this.get_date_range_title(field.creation); - if (date_range !== current_field_date_range) { - body_html += `
  • ${current_field_date_range}
  • `; - date_range = current_field_date_range; - } - let item_html = this.get_dropdown_item_html(field); - if (item_html) body_html += item_html; - }); - view_full_log_html = `
  • ${__('View Full Log')}
  • `; - } else { - body_html += `
  • ${__('No activity')}
  • `; - } - let dropdown_html = header_html + body_html + view_full_log_html; - this.$dropdown_list.html(dropdown_html); - } - - get_dropdown_item_html(field) { - let doc_link = frappe.utils.get_form_link(field.reference_doctype, field.reference_name); - let link_html_string = field.seen ? ``: ``; - let points_html = `
    ${frappe.energy_points.get_points(field.points)}
    `; - let message_html = this.get_message_html(field); - - let item_html = `
  • - ${link_html_string} - ${points_html} -
    - ${message_html} -
    -
    -
  • `; - return item_html; - } - - get_message_html(field) { - let owner_name = frappe.user.full_name(field.owner).trim(); - owner_name = frappe.ellipsis(owner_name, 50); - let message_html = ''; - let reference_doc = ` - - ${field.reference_name} - - `; - let reason_string = ` - - `; - if (field.type === 'Auto' ) { - message_html = __('For {0} {1}', - [field.rule, reference_doc]); - } else { - if (field.type === 'Appreciation') { - message_html = __('{0} appreciated your work on {1} {2}', - [owner_name, reference_doc, reason_string]); - } else if (field.type === 'Criticism') { - message_html = __('{0} criticized your work on {1} {2}', - [owner_name, reference_doc, reason_string]); - } else if (field.type === 'Revert') { - message_html = __('{0} reverted your points on {1} {2}', - [owner_name, reference_doc, reason_string]); - } - } - return message_html; - } - - setup_view_full_log() { - this.$dropdown_list.find('.full-log-btn').on('click', () => { - frappe.set_route('List', 'Energy Point Log', {user: frappe.session.user}); - }); - } - -}; diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index 51c40a7d46..119e43e0e8 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -68,36 +68,19 @@
  • - - - diff --git a/frappe/public/js/frappe/ui/toolbar/notifications.js b/frappe/public/js/frappe/ui/toolbar/notifications.js deleted file mode 100644 index 432c232dd8..0000000000 --- a/frappe/public/js/frappe/ui/toolbar/notifications.js +++ /dev/null @@ -1,103 +0,0 @@ -frappe.provide("frappe.ui.notifications"); - -frappe.ui.notifications = { - config: { - "ToDo": { label: __("To Do") }, - "Event": { label: __("Calendar"), route: "List/Event/Calendar" }, - "Email": { label: __("Email"), route: "List/Communication/Inbox" } - }, - - update_notifications: function() { - this.total = 0; - this.dropdown = $("#dropdown-notification").empty(); - this.boot_info = frappe.boot.notification_info; - let defaults = ["Comment", "ToDo", "Event"]; - - this.get_counts(this.boot_info.open_count_doctype, 1, defaults); - this.get_counts(this.boot_info.open_count_other, 1); - - // Target counts are stored for docs per doctype - let targets = { doctypes : {} }, map = this.boot_info.targets; - Object.keys(map).map(doctype => { - Object.keys(map[doctype]).map(doc => { - targets[doc] = map[doctype][doc]; - targets.doctypes[doc] = doctype; - }); - }); - this.get_counts(targets, 1, null, ["doctypes"], true); - this.get_counts(this.boot_info.open_count_doctype, - 0, null, defaults); - - this.bind_list(); - - // switch colour on the navbar and disable if no notifications - $(".navbar-new-comments") - .html(this.total > 99 ? '99+' : this.total) - .toggleClass("navbar-new-comments-true", this.total ? true : false) - .parent().toggleClass("disabled", this.total ? false : true); - }, - - get_counts: function(map, divide, keys, excluded = [], target = false) { - let empty_map = 1; - keys = keys ? keys - : Object.keys(map).sort().filter(e => !excluded.includes(e)); - keys.map(key => { - let doc_dt = (map.doctypes) ? map.doctypes[key] : undefined; - if(map[key] > 0 || target) { - this.add_notification(key, map[key], doc_dt, target); - empty_map = 0; - } - }); - if(divide && !empty_map) { - this.dropdown.append($('
  • ')); - } - }, - - add_notification: function(name, value, doc_dt, target = false) { - let label = this.config[name] ? this.config[name].label : name; - let title = target ? `title="Your Target"` : ''; - let $list_item = !target - ? $(`
  • ${__(label)} - ${value} -
  • `) - : $(`
  • ${__(label)} -
    -
    -
    -
  • `); - this.dropdown.append($list_item); - if(!target) this.total += value; - }, - - bind_list: function() { - var me = this; - $("#dropdown-notification a").on("click", function() { - var doctype = $(this).attr("data-doctype"); - var doc = $(this).attr("data-doc"); - if(!doc) { - var config = me.config[doctype] || {}; - if (config.route) { - frappe.set_route(config.route); - } else if (config.click) { - config.click(); - } else { - frappe.ui.notifications.show_open_count_list(doctype); - } - } else { - frappe.set_route("Form", doctype, doc); - } - }); - }, - - show_open_count_list: function(doctype) { - let filters = this.boot_info.conditions[doctype]; - if(filters && $.isPlainObject(filters)) { - if (!frappe.route_options) { - frappe.route_options = {}; - } - $.extend(frappe.route_options, filters); - } - frappe.set_route("List", doctype); - }, -}; diff --git a/frappe/public/js/frappe/ui/toolbar/toolbar.js b/frappe/public/js/frappe/ui/toolbar/toolbar.js index b98ef94887..a7a14f5476 100644 --- a/frappe/public/js/frappe/ui/toolbar/toolbar.js +++ b/frappe/public/js/frappe/ui/toolbar/toolbar.js @@ -15,8 +15,7 @@ frappe.ui.toolbar.Toolbar = Class.extend({ awesome_bar.setup("#navbar-search"); awesome_bar.setup("#modal-search"); - this.setup_energy_point_notifications(); - + this.setup_notifications(); this.make(); }, @@ -30,10 +29,6 @@ frappe.ui.toolbar.Toolbar = Class.extend({ }, bind_events: function() { - $(document).on("notification-update", function() { - frappe.ui.notifications.update_notifications(); - }); - // clear all custom menus on page change $(document).on("page-change", function() { $("header .navbar .custom-menu").remove(); @@ -161,13 +156,8 @@ frappe.ui.toolbar.Toolbar = Class.extend({ } }, - setup_energy_point_notifications: function() { - if (frappe.boot.energy_points_enabled) { - $('.dropdown-energy-points').show(); - this.energy_points_notifications = new frappe.ui.EnergyPointsNotifications(); - } else { - $('.dropdown-energy-points').hide(); - } + setup_notifications: function() { + this.notifications = new frappe.ui.Notifications(); } }); diff --git a/frappe/public/js/frappe/views/components/ModuleDetail.vue b/frappe/public/js/frappe/views/components/ModuleDetail.vue index c0c7dc96e8..59d1034c1e 100644 --- a/frappe/public/js/frappe/views/components/ModuleDetail.vue +++ b/frappe/public/js/frappe/views/components/ModuleDetail.vue @@ -10,7 +10,6 @@ :key="section.label + item.label" :data-youtube-id="item.type==='help' ? item.youtube_id : false" v-bind="item" - :open_count="item.type==='doctype' ? frappe.boot.notification_info.open_count_doctype[item.doctype] : false" > diff --git a/frappe/public/js/frappe/views/interaction.js b/frappe/public/js/frappe/views/interaction.js index 458f4c8dc1..6f694801a6 100644 --- a/frappe/public/js/frappe/views/interaction.js +++ b/frappe/public/js/frappe/views/interaction.js @@ -262,7 +262,6 @@ frappe.views.InteractionComposer = class InteractionComposer { doctype: doc.doctype, name: doc.name, assign_to: assignee, - notify: 1 }, callback:function(r) { if(!r.exc) { diff --git a/frappe/public/less/navbar.less b/frappe/public/less/navbar.less index 684aefabcd..f6145e77bb 100644 --- a/frappe/public/less/navbar.less +++ b/frappe/public/less/navbar.less @@ -276,56 +276,3 @@ .navbar-default .navbar-brand { color: @text-muted; } - -.dropdown-energy-points .energy-points-icon { - height: 40px; - font-size: 14px; - text-align: center; -} - -.recent-points-list { - width: 300px; - max-height: 480px; - overflow-y: auto; -} - -.energy-points-notification { - font-size: 7px; - position: absolute; - top: 4px; - right: 8px; - color: @indicator-orange; - display: none; -} - -.points-update { - float: left; - text-align: right; - margin-right: 15px; - width: 7%; -} - -.points-reason { - display: flow-root; -} - -.recent-points-list .points-updates-header .points-leaderboard { - float: right; - padding: 0; -} - -.points-updates-header { - background: @navbar-bg; - padding: 10px; - position: sticky; - top: 0; -} - -.points-date-range { - padding: 10px 0 2px 10px; - font-weight: 500; -} - -.unseen { - background: @light-yellow; -} \ No newline at end of file diff --git a/frappe/public/less/notifications.less b/frappe/public/less/notifications.less new file mode 100644 index 0000000000..adc15ec3ae --- /dev/null +++ b/frappe/public/less/notifications.less @@ -0,0 +1,126 @@ +@import "variables.less"; +@import "mixins.less"; + + +.dropdown-notifications .header { + margin: 7px 15px 10px 15px; + cursor: pointer; +} + +.notification-settings { + margin-top: 2px; +} + +.collapse-indicator { + padding: 0px 5px; + color: #d1d8dd; +} + +.category-list[data-category="Open Documents"] li a { + text-decoration: none; + display: block; + padding: 14px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: normal; +} + +.category-list[data-category="Open Documents"] li a:hover { + background-color: #f0f4f7; +} + +.open-doc-count { + margin-left: 150px; +} + +.notifications-indicator { + font-size: 7px; + position: absolute; + top: 4px; + right: 8px; + color: @indicator-orange; + display: none; +} + +.dropdown-notifications .recent-item { + padding: 10px 14px; + white-space: normal; + text-decoration: none; + font-weight: normal; + display: flow-root; +} + +a.recent-item:hover { + background-color: #f0f4f7; +} + +.dropdown-energy-points .energy-points-icon { + height: 40px; + font-size: 14px; + text-align: center; +} + +.notifications-list { + width: 450px; + max-height: 480px; + overflow-y: auto; +} + +.energy-points-notification { + font-size: 7px; + position: absolute; + top: 4px; + right: 8px; + color: @indicator-orange; + display: none; +} + +.date-range { + padding: 10px 0 2px 10px; + font-weight: 500; +} + +.unseen { + background: @light-yellow; +} + +.notification-timestamp { + margin-top: 5px; + font-size: 11px; +} + +.user-avatar { + float: left; + margin-right: 10px; + margin-bottom: 30px; +} + +.event-time { + margin-right: 10px; +} + +.notifications-loading { + margin-bottom: 15px; +} + +@media (max-width: 767px) { + + .dropdown-notifications { + .notifications-list { + max-height: 100vh; + min-width: 100vw; + } + + .category-list[data-category="Open Documents"] li a { + margin: 0 15px; + } + + .recent-item .user-avatar { + margin-right: 10px; + margin-left: 5px; + } + } + +} \ No newline at end of file diff --git a/frappe/sessions.py b/frappe/sessions.py index 8b5900fe51..ffad4b5e7b 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -112,9 +112,8 @@ def clear_expired_sessions(): delete_session(sid, reason="Session Expired") def get(): + """get session boot info""" - from frappe.desk.notifications import \ - get_notification_info_for_boot, get_notifications from frappe.boot import get_bootinfo, get_unseen_notes bootinfo = None @@ -123,14 +122,12 @@ def get(): bootinfo = frappe.cache().hget("bootinfo", frappe.session.user) if bootinfo: bootinfo['from_cache'] = 1 - bootinfo["notification_info"].update(get_notifications()) bootinfo["user"]["recent"] = json.dumps(\ frappe.cache().hget("user_recent", frappe.session.user)) if not bootinfo: # if not create it bootinfo = get_bootinfo() - bootinfo["notification_info"] = get_notification_info_for_boot() frappe.cache().hset("bootinfo", frappe.session.user, bootinfo) try: frappe.cache().ping() diff --git a/frappe/share.py b/frappe/share.py index a03e02d551..c8471bfe2c 100644 --- a/frappe/share.py +++ b/frappe/share.py @@ -5,10 +5,11 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.desk.form.document_follow import follow_document +from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification from frappe.utils import cint @frappe.whitelist() -def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=None, notify=0): +def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=None): """Share the given document with a user.""" if not user: user = frappe.session.user @@ -40,7 +41,7 @@ def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=No }) doc.save(ignore_permissions=True) - notify_assignment(user, doctype, name, description=None, notify=notify) + notify_assignment(user, doctype, name, everyone) follow_document(doctype, name, user) @@ -145,16 +146,26 @@ def check_share_permission(doctype, name): if not frappe.has_permission(doctype, ptype="share", doc=name): frappe.throw(_("No permission to {0} {1} {2}".format("share", doctype, name)), frappe.PermissionError) -def notify_assignment(shared_by, doc_type, doc_name, description=None, notify=0): +def notify_assignment(shared_by, doctype, doc_name, everyone): - if not (shared_by and doc_type and doc_name): return + if not (shared_by and doctype and doc_name) or everyone: return - from frappe.utils import get_link_to_form - document = get_link_to_form(doc_type, doc_name, label="%s: %s" % (doc_type, doc_name)) + from frappe.utils import get_fullname - arg = { - 'contact': shared_by, - 'txt': _("A new document {0} has been shared by with you {1}.").format(document, - shared_by), - 'notify': notify - } \ No newline at end of file + title_field = frappe.get_meta(doctype).get_title_field() + title = doc_name if title_field == "name" else \ + frappe.db.get_value(doctype, doc_name, title_field) + + reference_user = get_fullname(frappe.session.user) + notification_message = _('{0} shared a document {1} {2} with you').format( + frappe.bold(reference_user), frappe.bold(doctype), frappe.bold(title)) + + notification_doc = { + 'type': 'Share', + 'document_type': doctype, + 'subject': notification_message, + 'document_name': doc_name, + 'from_user': frappe.session.user + } + + enqueue_create_notification(shared_by, notification_doc) diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index a612d6bdfa..c347c81b1e 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -7,6 +7,7 @@ import frappe from frappe import _ import json from frappe.model.document import Document +from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification from frappe.utils import cint, get_fullname, getdate, get_link_to_form class EnergyPointLog(Document): @@ -25,13 +26,58 @@ class EnergyPointLog(Document): alert_dict = get_alert_dict(self) if alert_dict: frappe.publish_realtime('energy_point_alert', message=alert_dict, user=self.user) - send_review_mail(self, alert_dict) frappe.cache().hdel('energy_points', self.user) frappe.publish_realtime('update_points', after_commit=True) if self.type != 'Review': - frappe.publish_realtime('energy_points_notification', after_commit=True, user=self.user) + reference_user = self.user if self.type == 'Auto' else self.owner + notification_doc = { + 'type': 'Energy Point', + 'document_type': self.reference_doctype, + 'document_name': self.reference_name, + 'subject': get_notification_message(self), + 'from_user': reference_user, + 'email_content': '
    {}
    '.format(self.reason) + } + + enqueue_create_notification(self.user, notification_doc) + +def get_notification_message(doc): + owner_name = get_fullname(doc.owner) + points = doc.points + title_field = frappe.get_meta(doc.reference_doctype).get_title_field() + title = doc.reference_name if title_field == "name" else \ + frappe.db.get_value(doc.reference_doctype, doc.reference_name, title_field) + + if doc.type == 'Auto': + owner_name = frappe.bold('You') + if points == 1: + message = _('{0} gained {1} point for {2} {3}') + else: + message = _('{0} gained {1} points for {2} {3}') + message = message.format(owner_name, frappe.bold(points), doc.rule, frappe.bold(title)) + elif doc.type == 'Appreciation': + if points == 1: + message = _('{0} appreciated your work on {1} with {2} point') + else: + message = _('{0} appreciated your work on {1} with {2} points') + message = message.format(frappe.bold(owner_name), frappe.bold(title), frappe.bold(points)) + elif doc.type == 'Criticism': + if points == 1: + message = _('{0} criticized your work on {1} with {2} point') + else: + message = _('{0} criticized your work on {1} with {2} points') + + message = message.format(frappe.bold(owner_name), frappe.bold(title), frappe.bold(points)) + elif doc.type == 'Revert': + if points == 1: + message = _('{0} reverted your point on {1}') + else: + message = _('{0} reverted your points on {1}') + message = message.format(frappe.bold(owner_name), frappe.bold(title)) + + return message def get_alert_dict(doc): alert_dict = frappe._dict() @@ -83,13 +129,6 @@ def get_alert_dict(doc): return alert_dict -def send_review_mail(doc, message_dict): - if doc.type in ['Appreciation', 'Criticism']: - frappe.sendmail(recipients=doc.user, - subject=_("You gained some energy points") if doc.points > 0 else _("You lost some energy points"), - message=message_dict.message + '

    {}

    '.format(doc.reason), - header=[_('Energy point update'), message_dict.indicator]) - def create_energy_points_log(ref_doctype, ref_name, doc): doc = frappe._dict(doc) log_exists = frappe.db.exists('Energy Point Log', { @@ -173,13 +212,6 @@ def get_user_energy_and_review_points(user=None, from_date=None, as_dict=True): dict_to_return[d.pop('user')] = d return dict_to_return - -@frappe.whitelist() -def set_notification_as_seen(point_logs): - point_logs = frappe.parse_json(point_logs) - for log in point_logs: - frappe.db.set_value('Energy Point Log', log['name'], 'seen', 1, update_modified=False) - @frappe.whitelist() def review(doc, points, to_user, reason, review_type='Appreciation'): current_review_points = get_energy_points(frappe.session.user).review_points diff --git a/frappe/templates/emails/mentioned_in_comment.html b/frappe/templates/emails/mentioned_in_comment.html deleted file mode 100644 index 92bf15723a..0000000000 --- a/frappe/templates/emails/mentioned_in_comment.html +++ /dev/null @@ -1,7 +0,0 @@ -

    - {{ body_content }} -

    -
    - {{ comment.content | markdown }} -
    diff --git a/frappe/templates/emails/new_notification.html b/frappe/templates/emails/new_notification.html new file mode 100644 index 0000000000..fb1fc98901 --- /dev/null +++ b/frappe/templates/emails/new_notification.html @@ -0,0 +1,13 @@ +

    +

    {{ body_content }}

    +

    +{% if description %} +
    + {{ description | markdown }} +
    +{% endif %} +
    + {{ _("Login and view in Browser") }} +
    +