@@ -3,6 +3,8 @@ | |||
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 | |||
@frappe.whitelist() | |||
def get_notifications(): | |||
@@ -10,15 +12,54 @@ def get_notifications(): | |||
return | |||
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() | |||
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: | |||
if d in can_read: | |||
condition = config.for_doctype[d] | |||
@@ -41,22 +82,10 @@ def get_notifications(): | |||
else: | |||
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) | |||
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="*"): | |||
frappe.cache().delete_keys("notification_count:" + (user or frappe.session.user) + ":") | |||
@@ -128,7 +128,7 @@ frappe.desk.pages.Messages = Class.extend({ | |||
contact: contact | |||
}, | |||
hide_refresh: true, | |||
no_loading: true, | |||
freeze: false, | |||
render_row: function(wrapper, data) { | |||
var row = $(frappe.render_template("messages_row", { | |||
data: data | |||
@@ -154,8 +154,15 @@ frappe.desk.pages.Messages = Class.extend({ | |||
// check for updates every 5 seconds if page is active | |||
this.set_next_refresh(); | |||
if(!frappe.session_alive) | |||
if(!frappe.session_alive) { | |||
// not in session | |||
return; | |||
} | |||
if(frappe.get_route()[0]!="messages") { | |||
// not on messages page | |||
return; | |||
} | |||
if (this.list) { | |||
this.list.run(); | |||
@@ -6,7 +6,7 @@ import frappe | |||
from frappe.desk.notifications import delete_notification_count_for | |||
from frappe.core.doctype.user.user import STANDARD_USERS | |||
from frappe.utils.user import get_enabled_system_users | |||
from frappe.utils import cint | |||
from frappe.utils import cint, get_fullname | |||
@frappe.whitelist() | |||
def get_list(arg=None): | |||
@@ -78,6 +78,7 @@ def post(txt, contact, parenttype=None, notify=False, subject=None): | |||
d.comment = txt | |||
d.comment_docname = contact | |||
d.comment_doctype = 'Message' | |||
d.comment_by_fullname = get_fullname(frappe.session.user) | |||
d.insert(ignore_permissions=True) | |||
delete_notification_count_for("Messages") | |||
@@ -18,7 +18,7 @@ | |||
style="margin-right: 15px; margin-top: 7px;"> | |||
<label> | |||
<input type="checkbox" class="is-email" | |||
style="margin-top: 1px" checked> | |||
style="margin-top: 1px"> | |||
{%= __("Email") %} | |||
</label> | |||
</div> | |||
@@ -44,6 +44,7 @@ | |||
"public/js/lib/jquery/jquery-ui.min.js", | |||
"public/js/lib/Sortable.min.js", | |||
"public/js/lib/tag-it.min.js", | |||
"public/js/lib/notify.js", | |||
"public/js/lib/bootstrap.min.js", | |||
"public/js/lib/nprogress.js", | |||
"public/js/lib/moment/moment.min.js", | |||
@@ -107,6 +107,14 @@ frappe.Application = Class.extend({ | |||
// update in module views | |||
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 | |||
@@ -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() { | |||
$.each(frappe.boot.notification_info.open_count_doctype, function(doctype, count) { | |||
if(count) { | |||
@@ -173,7 +173,7 @@ frappe.ui.Listing = Class.extend({ | |||
return frappe.call({ | |||
method: this.opts.method || 'frappe.desk.query_builder.runquery', | |||
type: "GET", | |||
freeze: true, | |||
freeze: this.opts.freeze || true, | |||
args: this.get_call_args(), | |||
callback: function(r) { | |||
if(!me.opts.no_loading) | |||
@@ -92,7 +92,14 @@ frappe.views.CommunicationComposer = Class.extend({ | |||
setup_subject_and_recipients: function() { | |||
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) { | |||
// 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; | |||
})); |