@@ -3,6 +3,8 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe.utils import time_diff_in_seconds, now, now_datetime, DATETIME_FORMAT | |||||
from dateutil.relativedelta import relativedelta | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_notifications(): | def get_notifications(): | ||||
@@ -10,15 +12,54 @@ def get_notifications(): | |||||
return | return | ||||
config = get_notification_config() | config = get_notification_config() | ||||
can_read = frappe.user.get_can_read() | |||||
open_count_doctype = {} | |||||
open_count_module = {} | |||||
cache = frappe.cache() | |||||
notification_count = cache.get_all("notification_count:" \ | |||||
notification_count = frappe.cache().get_all("notification_count:" \ | |||||
+ frappe.session.user + ":").iteritems() | + frappe.session.user + ":").iteritems() | ||||
notification_count = dict([(d.rsplit(":", 1)[1], v) for d, v in notification_count]) | notification_count = dict([(d.rsplit(":", 1)[1], v) for d, v in notification_count]) | ||||
return { | |||||
"open_count_doctype": get_notifications_for_doctypes(config, notification_count), | |||||
"open_count_module": get_notifications_for_modules(config, notification_count), | |||||
"new_messages": get_new_messages() | |||||
} | |||||
def get_new_messages(): | |||||
cache_key = "notifications_last_update:" + frappe.session.user | |||||
last_update = frappe.cache().get_value(cache_key) | |||||
now_timestamp = now() | |||||
frappe.cache().set_value(cache_key, 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 comment_by_fullname, comment | |||||
from tabComment | |||||
where comment_doctype='Message' | |||||
and comment_docname = %s | |||||
and ifnull(creation, "2000-01-01") > %s | |||||
order by creation desc""", (frappe.session.user, last_update), as_dict=1) | |||||
def get_notifications_for_modules(config, notification_count): | |||||
open_count_module = {} | |||||
for m in config.for_module: | |||||
if m in notification_count: | |||||
open_count_module[m] = notification_count[m] | |||||
else: | |||||
open_count_module[m] = frappe.get_attr(config.for_module[m])() | |||||
frappe.cache().set_value("notification_count:" + frappe.session.user + ":" + m, | |||||
open_count_module[m]) | |||||
return open_count_module | |||||
def get_notifications_for_doctypes(config, notification_count): | |||||
can_read = frappe.user.get_can_read() | |||||
open_count_doctype = {} | |||||
for d in config.for_doctype: | for d in config.for_doctype: | ||||
if d in can_read: | if d in can_read: | ||||
condition = config.for_doctype[d] | condition = config.for_doctype[d] | ||||
@@ -41,22 +82,10 @@ def get_notifications(): | |||||
else: | else: | ||||
open_count_doctype[d] = result | open_count_doctype[d] = result | ||||
cache.set_value("notification_count:" + frappe.session.user + ":" + d, | |||||
frappe.cache().set_value("notification_count:" + frappe.session.user + ":" + d, | |||||
result) | result) | ||||
for m in config.for_module: | |||||
if m in notification_count: | |||||
open_count_module[m] = notification_count[m] | |||||
else: | |||||
open_count_module[m] = frappe.get_attr(config.for_module[m])() | |||||
cache.set_value("notification_count:" + frappe.session.user + ":" + m, | |||||
open_count_module[m]) | |||||
return { | |||||
"open_count_doctype": open_count_doctype, | |||||
"open_count_module": open_count_module | |||||
} | |||||
return open_count_doctype | |||||
def clear_notifications(user="*"): | def clear_notifications(user="*"): | ||||
frappe.cache().delete_keys("notification_count:" + (user or frappe.session.user) + ":") | frappe.cache().delete_keys("notification_count:" + (user or frappe.session.user) + ":") | ||||
@@ -128,7 +128,7 @@ frappe.desk.pages.Messages = Class.extend({ | |||||
contact: contact | contact: contact | ||||
}, | }, | ||||
hide_refresh: true, | hide_refresh: true, | ||||
no_loading: true, | |||||
freeze: false, | |||||
render_row: function(wrapper, data) { | render_row: function(wrapper, data) { | ||||
var row = $(frappe.render_template("messages_row", { | var row = $(frappe.render_template("messages_row", { | ||||
data: data | data: data | ||||
@@ -154,8 +154,15 @@ frappe.desk.pages.Messages = Class.extend({ | |||||
// check for updates every 5 seconds if page is active | // check for updates every 5 seconds if page is active | ||||
this.set_next_refresh(); | this.set_next_refresh(); | ||||
if(!frappe.session_alive) | |||||
if(!frappe.session_alive) { | |||||
// not in session | |||||
return; | return; | ||||
} | |||||
if(frappe.get_route()[0]!="messages") { | |||||
// not on messages page | |||||
return; | |||||
} | |||||
if (this.list) { | if (this.list) { | ||||
this.list.run(); | this.list.run(); | ||||
@@ -6,7 +6,7 @@ import frappe | |||||
from frappe.desk.notifications import delete_notification_count_for | from frappe.desk.notifications import delete_notification_count_for | ||||
from frappe.core.doctype.user.user import STANDARD_USERS | from frappe.core.doctype.user.user import STANDARD_USERS | ||||
from frappe.utils.user import get_enabled_system_users | from frappe.utils.user import get_enabled_system_users | ||||
from frappe.utils import cint | |||||
from frappe.utils import cint, get_fullname | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_list(arg=None): | def get_list(arg=None): | ||||
@@ -78,6 +78,7 @@ def post(txt, contact, parenttype=None, notify=False, subject=None): | |||||
d.comment = txt | d.comment = txt | ||||
d.comment_docname = contact | d.comment_docname = contact | ||||
d.comment_doctype = 'Message' | d.comment_doctype = 'Message' | ||||
d.comment_by_fullname = get_fullname(frappe.session.user) | |||||
d.insert(ignore_permissions=True) | d.insert(ignore_permissions=True) | ||||
delete_notification_count_for("Messages") | delete_notification_count_for("Messages") | ||||
@@ -18,7 +18,7 @@ | |||||
style="margin-right: 15px; margin-top: 7px;"> | style="margin-right: 15px; margin-top: 7px;"> | ||||
<label> | <label> | ||||
<input type="checkbox" class="is-email" | <input type="checkbox" class="is-email" | ||||
style="margin-top: 1px" checked> | |||||
style="margin-top: 1px"> | |||||
{%= __("Email") %} | {%= __("Email") %} | ||||
</label> | </label> | ||||
</div> | </div> | ||||
@@ -44,6 +44,7 @@ | |||||
"public/js/lib/jquery/jquery-ui.min.js", | "public/js/lib/jquery/jquery-ui.min.js", | ||||
"public/js/lib/Sortable.min.js", | "public/js/lib/Sortable.min.js", | ||||
"public/js/lib/tag-it.min.js", | "public/js/lib/tag-it.min.js", | ||||
"public/js/lib/notify.js", | |||||
"public/js/lib/bootstrap.min.js", | "public/js/lib/bootstrap.min.js", | ||||
"public/js/lib/nprogress.js", | "public/js/lib/nprogress.js", | ||||
"public/js/lib/moment/moment.min.js", | "public/js/lib/moment/moment.min.js", | ||||
@@ -107,6 +107,14 @@ frappe.Application = Class.extend({ | |||||
// update in module views | // update in module views | ||||
me.update_notification_count_in_modules(); | me.update_notification_count_in_modules(); | ||||
$.each(r.message.new_messages, function(i, m) { | |||||
if (Notify.needsPermission) { | |||||
Notify.requestPermission(function() { me.browser_notify(m); }); | |||||
} else { | |||||
me.browser_notify(m); | |||||
} | |||||
}); | |||||
} | } | ||||
}, | }, | ||||
freeze: false | freeze: false | ||||
@@ -114,6 +122,16 @@ frappe.Application = Class.extend({ | |||||
} | } | ||||
}, | }, | ||||
browser_notify: function(m) { | |||||
var notify = new Notify(__("Message from {0}", [m.comment_by_fullname]), { | |||||
body: m.comment, | |||||
notifyClick: function() { | |||||
frappe.set_route("messages"); | |||||
} | |||||
}); | |||||
notify.show(); | |||||
}, | |||||
update_notification_count_in_modules: function() { | update_notification_count_in_modules: function() { | ||||
$.each(frappe.boot.notification_info.open_count_doctype, function(doctype, count) { | $.each(frappe.boot.notification_info.open_count_doctype, function(doctype, count) { | ||||
if(count) { | if(count) { | ||||
@@ -173,7 +173,7 @@ frappe.ui.Listing = Class.extend({ | |||||
return frappe.call({ | return frappe.call({ | ||||
method: this.opts.method || 'frappe.desk.query_builder.runquery', | method: this.opts.method || 'frappe.desk.query_builder.runquery', | ||||
type: "GET", | type: "GET", | ||||
freeze: true, | |||||
freeze: this.opts.freeze || true, | |||||
args: this.get_call_args(), | args: this.get_call_args(), | ||||
callback: function(r) { | callback: function(r) { | ||||
if(!me.opts.no_loading) | if(!me.opts.no_loading) | ||||
@@ -92,7 +92,14 @@ frappe.views.CommunicationComposer = Class.extend({ | |||||
setup_subject_and_recipients: function() { | setup_subject_and_recipients: function() { | ||||
this.subject = this.subject || ""; | this.subject = this.subject || ""; | ||||
this.recipients = this.frm && this.frm.comments.get_recipient(); | |||||
if(this.last_email) { | |||||
this.recipients = this.last_email.comment_by; | |||||
} | |||||
if(!this.recipients) { | |||||
this.recipients = this.frm && this.frm.comments.get_recipient(); | |||||
} | |||||
if(!this.subject && this.frm) { | if(!this.subject && this.frm) { | ||||
// get subject from last communication | // get subject from last communication | ||||
@@ -0,0 +1,194 @@ | |||||
/* | |||||
* Author: Alex Gibson | |||||
* https://github.com/alexgibson/notify.js | |||||
* License: MIT license | |||||
*/ | |||||
(function(global, factory) { | |||||
if (typeof define === 'function' && define.amd) { | |||||
// AMD environment | |||||
define(function() { | |||||
return factory(global, global.document); | |||||
}); | |||||
} else if (typeof module !== 'undefined' && module.exports) { | |||||
// CommonJS environment | |||||
module.exports = factory(global, global.document); | |||||
} else { | |||||
// Browser environment | |||||
global.Notify = factory(global, global.document); | |||||
} | |||||
} (typeof window !== 'undefined' ? window : this, function (w, d) { | |||||
'use strict'; | |||||
function isFunction (item) { | |||||
return typeof item === 'function'; | |||||
} | |||||
function Notify(title, options) { | |||||
if (typeof title !== 'string') { | |||||
throw new Error('Notify(): first arg (title) must be a string.'); | |||||
} | |||||
this.title = title; | |||||
this.options = { | |||||
icon: '', | |||||
body: '', | |||||
tag: '', | |||||
notifyShow: null, | |||||
notifyClose: null, | |||||
notifyClick: null, | |||||
notifyError: null, | |||||
timeout: null | |||||
}; | |||||
this.permission = null; | |||||
if (!Notify.isSupported) { | |||||
return; | |||||
} | |||||
//User defined options for notification content | |||||
if (typeof options === 'object') { | |||||
for (var i in options) { | |||||
if (options.hasOwnProperty(i)) { | |||||
this.options[i] = options[i]; | |||||
} | |||||
} | |||||
//callback when notification is displayed | |||||
if (isFunction(this.options.notifyShow)) { | |||||
this.onShowCallback = this.options.notifyShow; | |||||
} | |||||
//callback when notification is closed | |||||
if (isFunction(this.options.notifyClose)) { | |||||
this.onCloseCallback = this.options.notifyClose; | |||||
} | |||||
//callback when notification is clicked | |||||
if (isFunction(this.options.notifyClick)) { | |||||
this.onClickCallback = this.options.notifyClick; | |||||
} | |||||
//callback when notification throws error | |||||
if (isFunction(this.options.notifyError)) { | |||||
this.onErrorCallback = this.options.notifyError; | |||||
} | |||||
} | |||||
} | |||||
// true if the browser supports HTML5 Notification | |||||
Notify.isSupported = 'Notification' in w; | |||||
// true if the permission is not granted | |||||
Notify.needsPermission = !(Notify.isSupported && Notification.permission === 'granted'); | |||||
// returns current permission level ('granted', 'default', 'denied' or null) | |||||
Notify.permissionLevel = (Notify.isSupported ? Notification.permission : null); | |||||
// asks the user for permission to display notifications. Then calls the callback functions is supplied. | |||||
Notify.requestPermission = function (onPermissionGrantedCallback, onPermissionDeniedCallback) { | |||||
if (!Notify.isSupported) { | |||||
return; | |||||
} | |||||
w.Notification.requestPermission(function (perm) { | |||||
switch (perm) { | |||||
case 'granted': | |||||
Notify.needsPermission = false; | |||||
if (isFunction(onPermissionGrantedCallback)) { | |||||
onPermissionGrantedCallback(); | |||||
} | |||||
break; | |||||
case 'denied': | |||||
if (isFunction(onPermissionDeniedCallback)) { | |||||
onPermissionDeniedCallback(); | |||||
} | |||||
break; | |||||
} | |||||
}); | |||||
}; | |||||
Notify.prototype.show = function () { | |||||
if (!Notify.isSupported) { | |||||
return; | |||||
} | |||||
this.myNotify = new Notification(this.title, { | |||||
'body': this.options.body, | |||||
'tag' : this.options.tag, | |||||
'icon' : this.options.icon | |||||
}); | |||||
if (this.options.timeout && !isNaN(this.options.timeout)) { | |||||
setTimeout(this.close.bind(this), this.options.timeout * 1000); | |||||
} | |||||
this.myNotify.addEventListener('show', this, false); | |||||
this.myNotify.addEventListener('error', this, false); | |||||
this.myNotify.addEventListener('close', this, false); | |||||
this.myNotify.addEventListener('click', this, false); | |||||
}; | |||||
Notify.prototype.onShowNotification = function (e) { | |||||
if (this.onShowCallback) { | |||||
this.onShowCallback(e); | |||||
} | |||||
}; | |||||
Notify.prototype.onCloseNotification = function (e) { | |||||
if (this.onCloseCallback) { | |||||
this.onCloseCallback(e); | |||||
} | |||||
this.destroy(); | |||||
}; | |||||
Notify.prototype.onClickNotification = function (e) { | |||||
if (this.onClickCallback) { | |||||
this.onClickCallback(e); | |||||
} | |||||
}; | |||||
Notify.prototype.onErrorNotification = function (e) { | |||||
if (this.onErrorCallback) { | |||||
this.onErrorCallback(e); | |||||
} | |||||
this.destroy(); | |||||
}; | |||||
Notify.prototype.destroy = function () { | |||||
this.myNotify.removeEventListener('show', this, false); | |||||
this.myNotify.removeEventListener('error', this, false); | |||||
this.myNotify.removeEventListener('close', this, false); | |||||
this.myNotify.removeEventListener('click', this, false); | |||||
}; | |||||
Notify.prototype.close = function () { | |||||
this.myNotify.close(); | |||||
}; | |||||
Notify.prototype.handleEvent = function (e) { | |||||
switch (e.type) { | |||||
case 'show': | |||||
this.onShowNotification(e); | |||||
break; | |||||
case 'close': | |||||
this.onCloseNotification(e); | |||||
break; | |||||
case 'click': | |||||
this.onClickNotification(e); | |||||
break; | |||||
case 'error': | |||||
this.onErrorNotification(e); | |||||
break; | |||||
} | |||||
}; | |||||
return Notify; | |||||
})); |