From bae50496830d7906ea302789ec7a214915d32fed Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 12 Sep 2019 21:14:05 +0530 Subject: [PATCH] feat(Notifications): Add integrated notifications --- frappe/core/doctype/comment/comment.py | 39 ++- .../doctype/communication/communication.py | 30 ++ .../core/doctype/notification_log/__init__.py | 0 .../notification_log/notification_log.js | 10 + .../notification_log/notification_log.json | 97 ++++++ .../notification_log/notification_log.py | 45 +++ .../notification_log/test_notification_log.py | 12 + .../doctype/notification_settings/__init__.py | 0 .../notification_settings.js | 8 + .../notification_settings.json | 60 ++++ .../notification_settings.py | 23 ++ .../test_notification_settings.py | 10 + .../doctype/subscribed_documents/__init__.py | 0 .../subscribed_documents.json | 30 ++ .../subscribed_documents.py | 10 + frappe/desk/form/assign_to.py | 31 +- frappe/desk/notifications.py | 83 ++--- frappe/hooks.py | 1 + frappe/public/build.json | 2 + frappe/public/js/frappe/desk.js | 66 ++-- .../frappe/ui/notifications/notifications.js | 292 ++++++++++++++++++ .../public/js/frappe/ui/toolbar/navbar.html | 21 +- .../js/frappe/ui/toolbar/notifications.js | 186 +++++------ frappe/public/js/frappe/ui/toolbar/toolbar.js | 19 +- frappe/public/less/notifications.less | 99 ++++++ frappe/sessions.py | 8 +- .../energy_point_log/energy_point_log.py | 48 +++ 27 files changed, 1039 insertions(+), 191 deletions(-) create mode 100644 frappe/core/doctype/notification_log/__init__.py create mode 100644 frappe/core/doctype/notification_log/notification_log.js create mode 100644 frappe/core/doctype/notification_log/notification_log.json create mode 100644 frappe/core/doctype/notification_log/notification_log.py create mode 100644 frappe/core/doctype/notification_log/test_notification_log.py create mode 100644 frappe/core/doctype/notification_settings/__init__.py create mode 100644 frappe/core/doctype/notification_settings/notification_settings.js create mode 100644 frappe/core/doctype/notification_settings/notification_settings.json create mode 100644 frappe/core/doctype/notification_settings/notification_settings.py create mode 100644 frappe/core/doctype/notification_settings/test_notification_settings.py create mode 100644 frappe/core/doctype/subscribed_documents/__init__.py create mode 100644 frappe/core/doctype/subscribed_documents/subscribed_documents.json create mode 100644 frappe/core/doctype/subscribed_documents/subscribed_documents.py create mode 100644 frappe/public/js/frappe/ui/notifications/notifications.js create mode 100644 frappe/public/less/notifications.less diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 21d924bb7f..cb8f0404e5 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -8,6 +8,7 @@ from frappe import _ import json from frappe.model.document import Document from frappe.core.doctype.user.user import extract_mentions +from frappe.core.doctype.notification_log.notification_log import create_notification_log from frappe.utils import get_fullname, get_link_to_form from frappe.website.render import clear_cache from frappe.database.schema import add_column @@ -67,18 +68,32 @@ class Comment(Document): 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'] - ) + # from frappe.core.doctype.notification_settings.notification_settings import is_notifications_enabled + # if is_notifications_enabled: + notification_message = _('''{0} mentioned you in a comment in {1} {2}''')\ + .format(sender_fullname, self.reference_doctype, title) + + notification_doc = { + 'type': 'Mention', + 'reference_doctype': self.reference_doctype, + 'subject': notification_message, + 'reference_name': self.reference_name, + 'reference_user': frappe.session.user + } + create_notification_log(recipients, notification_doc) + + # 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'] + # ) def on_doctype_update(): diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 2be07cadd2..e8a7f8aac3 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -8,6 +8,7 @@ from frappe.model.document import Document from frappe.utils import validate_email_address, get_fullname, strip_html, cstr from frappe.core.doctype.communication.email import (validate_email, notify, _notify, update_parent_mins_to_first_response) +from frappe.core.doctype.notification_log.notification_log import create_notification_log from frappe.core.utils import get_parent_doc from frappe.utils.bot import BotReply from frappe.utils import parse_addr @@ -103,6 +104,10 @@ class Communication(Document): doctype=self.reference_doctype, docname=self.reference_name, after_commit=True) + # from frappe.core.doctype.notification_settings.notification_settings import is_notifications_enabled + # if is_notifications_enabled: + create_notification(self) + elif self.communication_type in ("Chat", "Notification", "Bot"): if self.reference_name == frappe.session.user: message = self.as_dict() @@ -413,3 +418,28 @@ def get_email_without_link(email): email_host = email.split("@")[1] return "{0}@{1}".format(email_id, email_host) + +def create_notification(self): + title_field = frappe.get_meta(self.reference_doctype).get_title_field() + title = self.reference_name if title_field == "name" else \ + frappe.db.get_value(self.reference_doctype, self.reference_name, title_field) + + if self.cc or self.bcc: + names = self.cc or '' + self.bcc or '' + notification_message = _('''{0} included you in an Email {1}''').format(self.sender_full_name, title) + args = { + 'type': 'Communication', + 'reference_doctype': self.reference_doctype, + 'subject': notification_message, + 'reference_name': self.reference_name + } + create_notification_log(names, args) + notification_message = _('''{0} replied to your Email {1}''').format(self.sender_full_name, title) + args = { + 'type': 'Communication', + 'reference_doctype': self.reference_doctype, + 'subject': notification_message, + 'reference_name': self.reference_name, + 'reference_user': frappe.session.user + } + create_notification_log(self.recipients, args) \ No newline at end of file diff --git a/frappe/core/doctype/notification_log/__init__.py b/frappe/core/doctype/notification_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/notification_log/notification_log.js b/frappe/core/doctype/notification_log/notification_log.js new file mode 100644 index 0000000000..8f6c2bb564 --- /dev/null +++ b/frappe/core/doctype/notification_log/notification_log.js @@ -0,0 +1,10 @@ +// 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.reference_doctype; + let dn = frm.doc.reference_name; + frm.fields_dict.reference_name.$input_wrapper.find('.control-value').wrapInner(``); + } +}); diff --git a/frappe/core/doctype/notification_log/notification_log.json b/frappe/core/doctype/notification_log/notification_log.json new file mode 100644 index 0000000000..b90736f783 --- /dev/null +++ b/frappe/core/doctype/notification_log/notification_log.json @@ -0,0 +1,97 @@ +{ + "creation": "2019-08-26 13:37:34.165254", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "type", + "subject", + "column_break_4", + "reference_doctype", + "seen", + "reference_name", + "reference_user" + ], + "fields": [ + { + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Type", + "options": "Mention\nEnergy Point\nCommunication\nAssignment", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Text", + "label": "Subject", + "read_only": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference 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": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "label": "Reference Name", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "reference_user", + "fieldtype": "Link", + "label": "Reference User", + "options": "User", + "read_only": 1, + "search_index": 1 + } + ], + "in_create": 1, + "modified": "2019-09-11 15:45:05.102753", + "modified_by": "Administrator", + "module": "Core", + "name": "Notification Log", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/notification_log/notification_log.py b/frappe/core/doctype/notification_log/notification_log.py new file mode 100644 index 0000000000..fba7e8e871 --- /dev/null +++ b/frappe/core/doctype/notification_log/notification_log.py @@ -0,0 +1,45 @@ +# -*- 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 NotificationLog(Document): + + def after_insert(self): + frappe.publish_realtime('notification', after_commit=True, user=self.user) + + +def get_permission_query_conditions(user): + if not user: user = frappe.session.user + + if user == "Administrator": + return "" + + return """(`tabNotification Log`.user = "{user}")""".format(user=user) + +def create_notification_log(names, doc): + doc = frappe._dict(doc) + if(not isinstance(names, list)): + if names: + names = filter(None, names.split(', ')) + for name in names: + if frappe.db.exists('User', name.strip()): + _doc = frappe.new_doc('Notification Log') + _doc.type = doc.type + _doc.user = name.strip() + _doc.reference_doctype = doc.reference_doctype + _doc.reference_name = doc.reference_name + _doc.reference_user = doc.reference_user + _doc.subject = doc.subject.replace('
','').replace('
','') + _doc.insert(ignore_permissions=True) + _doc.save() + +@frappe.whitelist() +def set_notification_as_seen(notification_log): + notification_log = frappe.parse_json(notification_log) + for log in notification_log: + frappe.db.set_value('Notification Log', log['name'], 'seen', 1, update_modified=False) + diff --git a/frappe/core/doctype/notification_log/test_notification_log.py b/frappe/core/doctype/notification_log/test_notification_log.py new file mode 100644 index 0000000000..2cebe2e2e7 --- /dev/null +++ b/frappe/core/doctype/notification_log/test_notification_log.py @@ -0,0 +1,12 @@ + + +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestNotificationLog(unittest.TestCase): + pass diff --git a/frappe/core/doctype/notification_settings/__init__.py b/frappe/core/doctype/notification_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/notification_settings/notification_settings.js b/frappe/core/doctype/notification_settings/notification_settings.js new file mode 100644 index 0000000000..741553a2fd --- /dev/null +++ b/frappe/core/doctype/notification_settings/notification_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Notification Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/notification_settings/notification_settings.json b/frappe/core/doctype/notification_settings/notification_settings.json new file mode 100644 index 0000000000..d2dab91bde --- /dev/null +++ b/frappe/core/doctype/notification_settings/notification_settings.json @@ -0,0 +1,60 @@ +{ + "creation": "2019-09-11 22:15:44.851526", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable", + "enable_energy_point_notifications", + "enable_email_notifications", + "subscribed_documents" + ], + "fields": [ + { + "default": "1", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "default": "1", + "fieldname": "enable_energy_point_notifications", + "fieldtype": "Check", + "label": "Enable Energy Point Notifications" + }, + { + "default": "1", + "fieldname": "enable_email_notifications", + "fieldtype": "Check", + "label": "Enable Email Notifications" + }, + { + "fieldname": "subscribed_documents", + "fieldtype": "Table MultiSelect", + "label": "Subscribed Documents", + "options": "Subscribed Documents" + } + ], + "in_create": 1, + "modified": "2019-09-12 17:32:38.026945", + "modified_by": "Administrator", + "module": "Core", + "name": "Notification Settings", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/notification_settings/notification_settings.py b/frappe/core/doctype/notification_settings/notification_settings.py new file mode 100644 index 0000000000..44240988b0 --- /dev/null +++ b/frappe/core/doctype/notification_settings/notification_settings.py @@ -0,0 +1,23 @@ +# -*- 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): + pass + # def validate(self): + # create_notification_settings() + +def is_notifications_enabled(): + return frappe.get_cached_value('Notification Settings', None, 'enable') + +def is_energy_point_notifications_enabled(): + return frappe.get_cached_value('Notification Settings', None, 'enable_energy_point_notifications') + +@frappe.whitelist() +def create_notification_settings(): + _doc = frappe.new_doc('Notification Settings') + _doc.insert(ignore_permissions=True) \ No newline at end of file diff --git a/frappe/core/doctype/notification_settings/test_notification_settings.py b/frappe/core/doctype/notification_settings/test_notification_settings.py new file mode 100644 index 0000000000..386280306f --- /dev/null +++ b/frappe/core/doctype/notification_settings/test_notification_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestNotificationSettings(unittest.TestCase): + pass diff --git a/frappe/core/doctype/subscribed_documents/__init__.py b/frappe/core/doctype/subscribed_documents/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/subscribed_documents/subscribed_documents.json b/frappe/core/doctype/subscribed_documents/subscribed_documents.json new file mode 100644 index 0000000000..12f2757c47 --- /dev/null +++ b/frappe/core/doctype/subscribed_documents/subscribed_documents.json @@ -0,0 +1,30 @@ +{ + "creation": "2019-09-11 20:14:22.433772", + "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-09-11 20:14:22.433772", + "modified_by": "Administrator", + "module": "Core", + "name": "Subscribed Documents", + "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/core/doctype/subscribed_documents/subscribed_documents.py b/frappe/core/doctype/subscribed_documents/subscribed_documents.py new file mode 100644 index 0000000000..534e7ec763 --- /dev/null +++ b/frappe/core/doctype/subscribed_documents/subscribed_documents.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 SubscribedDocuments(Document): + pass diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index 959eee7e60..960895c06f 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -7,6 +7,8 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.desk.form.document_follow import follow_document +from frappe.core.doctype.notification_log.notification_log import create_notification_log +import frappe.utils from frappe.utils import cint import frappe.share @@ -78,9 +80,11 @@ def add(args=None): # make this document followed by assigned user follow_document(args['doctype'], args['name'], args['assign_to']) - # notify + # from frappe.core.doctype.notification_settings.notification_settings import is_notifications_enabled + # if is_notifications_enabled: + # 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"), notify=args.get('notify')) return get(args) @@ -162,7 +166,11 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', 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) if action=='CLOSE': + print('CLOSED!!!') if owner == frappe.session.get('user'): arg = { 'contact': assigned_by, @@ -175,6 +183,15 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', '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(doc_type, title) + notification_doc = { + 'type': 'Assignment', + 'reference_doctype': doc_type, + 'subject': subject, + 'reference_name': doc_name, + 'reference_user': frappe.session.user + } + create_notification_log(owner, notification_doc) else: description_html = "

{0}

".format(description) arg = { @@ -183,6 +200,16 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', user_name, description_html), 'notify': notify } + user = arg['contact'] + subject = '''{0} assigned a new task {1} {2} to you'''.format(user_name, doc_type, title) + notification_doc = { + 'type': 'Assignment', + 'reference_doctype': doc_type, + 'subject': subject, + 'reference_name': doc_name, + 'reference_user': frappe.session.user + } + create_notification_log(user, notification_doc) if arg and cint(arg.get("notify")): _notify(arg) diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 8b88af60b0..29ac7a1193 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -37,57 +37,57 @@ 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), + # "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() + # "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) +# 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 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) +# 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) +# 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_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_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])() +# 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)) +# 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 +# return open_count def get_notifications_for_doctypes(config, notification_count): """Notifications for DocTypes""" @@ -200,7 +200,8 @@ def clear_doctype_notifications(doc, method=None, *args, **kwargs): delete_notification_count_for(doctype) return -def get_notification_info_for_boot(): +@frappe.whitelist() +def get_notification_info(): out = get_notifications() config = get_notification_config() can_read = frappe.get_user().get_can_read() diff --git a/frappe/hooks.py b/frappe/hooks.py index fd7c940fa4..3ad52abf98 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -88,6 +88,7 @@ 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.core.doctype.notification_log.notification_log.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/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..ca343ad8a5 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -272,49 +272,49 @@ frappe.Application = Class.extend({ 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); + // this.refresh_notifications = frappe.utils.debounce(this.refresh_notifications.bind(this), 1000); // kickoff - this.refresh_notifications(); + // this.refresh_notifications(); - frappe.realtime.on('clear_notifications', () => { - me.refresh_notifications(); - }); + // frappe.realtime.on('clear_notifications', () => { + // me.refresh_notifications(); + // }); // first time loaded in boot - $(document).trigger("notification-update"); + // $(document).trigger("notification-update"); // refresh notifications if user is back after sometime - $(document).on("session_alive", function() { - me.refresh_notifications(); - }); + // $(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"); + // 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 - }); - } - }, + // 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; 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..5b0b8a5190 --- /dev/null +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -0,0 +1,292 @@ + +frappe.ui.Notifications = class Notifications { + + constructor() { + 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.categories = ['Notifications', 'Todays Events', 'Open Documents']; + + this.render_dropdown_headers(); + this.$notifications = this.$dropdown_list.find('#notifications'); + this.$open_docs = this.$dropdown_list.find('#open-documents'); + this.$todays_events = this.$dropdown_list.find('#todays-events'); + + this.setup_notifications(); + this.setup_todays_events(); + this.setup_open_document_count(); + this.bind_events(); + } + + setup_notifications() { + this.get_notifications_list(this.max_length).then(list => { + this.dropdown_items = list; + this.render_notifications_dropdown(); + this.setup_view_full_log(); + if (this.$notifications.find('.unseen').length) { + this.$notification_indicator.show(); + } + }); + } + + setup_todays_events() { + this.$dropdown_list.on('click', '.todays-events-header', (e) => { + 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 Events Today')} +
  • `; + } + this.$todays_events.html(html); + } + + setup_open_document_count() { + this.open_docs_config = { + "ToDo": { label: __("To Do") }, + "Event": { label: __("Calendar"), route: "List/Event/Calendar" }, + "Email": { label: __("Email"), route: "List/Communication/Inbox" } + }; + this.$dropdown_list.on('click', '.open-documents-header', (e) => { + frappe.xcall('frappe.desk.notifications.get_notification_info').then((r) => { + this.open_document_list = r; + this.render_open_document_count(); + }); + }); + } + + render_open_document_count() { + let defaults = ["Comment", "ToDo", "Event"]; + 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_notification(key, map[key], doc_dt, target); + empty_map = 0; + } + }); + + if(divide && !empty_map) { + this.$open_docs.append($('
  • ')); + } + } + + add_notification(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; + } + + 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('#notifications')); + this.$dropdown_list.find('#notifications').collapse('show') + } + + + check_seen() { + let unseen_logs = this.dropdown_items.filter(item => item.seen === 0); + frappe.call( + 'frappe.core.doctype.notification_log.notification_log.set_notification_as_seen', + {notification_log: unseen_logs}); + } + + get_notifications_list(limit) { + return frappe.db.get_list('Notification Log', { + fields: + ['*'], + limit: limit, + order_by: 'creation desc' + }).then((notifications_list) => { + return notifications_list; + }); + } + + render_notifications_dropdown() { + let body_html = ''; + let view_full_log_html = ''; + + 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')}
  • `; + } + let dropdown_html = body_html + view_full_log_html; + this.$notifications.append(dropdown_html); + } + + get_dropdown_item_html(field) { + let doc_link = frappe.utils.get_form_link(field.reference_doctype, field.reference_name); + let seen_class = field.seen? '': 'unseen'; + let message = field.subject; + let message_html = `
    ${message}
    `; + let user = field.reference_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; + } + + + setup_view_full_log() { + this.$dropdown_list.find('.full-log-btn').on('click', () => { + frappe.set_route('List', 'Notification Log'); + }); + } + + + render_dropdown_headers() { + let get_headers_html = (category) => { + let category_id = frappe.scrub(category, '-'); + let category_header_class = frappe.scrub(category, '-') + '-header'; + return `
  • +
  • + ${category} + +
  • +
    + `; + } + + let html = this.categories.map(get_headers_html).join('
  • '); + this.$dropdown_list.append(html); + } + + bind_events() { + frappe.realtime.on('notification', () => { + this.$dropdown.find('.notifications-indicator').show(); + this.update_dropdown(); + }); + + let me = this; + this.$dropdown.on('hide.bs.dropdown', function(e) { + me.$notification_indicator.hide(); + let hide = $(this).data('closable'); + me.$dropdown_list.find('.unseen').removeClass('unseen'); + $(this).data('closable', true); + return hide; + }); + + this.$dropdown.on('show.bs.dropdown', () => { + this.check_seen(); + }); + + this.$dropdown.on('click', function(e) { + if ($(e.target).closest('.dropdown-toggle').length) { + $(this).data('closable', true); + } else { + $(this).data('closable', false); + } + }); + + this.$dropdown_list.on('click', '.recent-item', (e) => { + this.$dropdown.removeClass('open'); + }); + + this.$dropdown.find(".header").on('click', function(e) { + let hide = me.$dropdown.find('.header').next().hasClass("in"); + $(this).find('.collapse-indicator').toggleClass("octicon-chevron-down", hide); + $(this).find('.collapse-indicator').toggleClass("octicon-chevron-up", !hide); + }); + + this.$open_docs.on('click', 'li a', function() { + var doctype = $(this).attr('data-doctype'); + var doc = $(this).attr('data-doc'); + if(!doc) { + var config = me.open_docs_config[doctype] || {}; + console.log('config', config) + if (config.route) { + frappe.set_route(config.route); + } else if (config.click) { + config.click(); + } else { + me.show_open_count_list(doctype); + } + } else { + frappe.set_route("Form", doctype, doc); + } + }); + } + + +} diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index 1508fb1cb7..afc88fc4f0 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -86,7 +86,24 @@ - + +