Просмотр исходного кода

[notify] added browser notifications

version-14
Rushabh Mehta 10 лет назад
Родитель
Сommit
9aaf15cbe7
9 измененных файлов: 282 добавлений и 25 удалений
  1. +48
    -19
      frappe/desk/notifications.py
  2. +9
    -2
      frappe/desk/page/messages/messages.js
  3. +2
    -1
      frappe/desk/page/messages/messages.py
  4. +1
    -1
      frappe/desk/page/messages/messages_main.html
  5. +1
    -0
      frappe/public/build.json
  6. +18
    -0
      frappe/public/js/frappe/desk.js
  7. +1
    -1
      frappe/public/js/frappe/ui/listing.js
  8. +8
    -1
      frappe/public/js/frappe/views/communication.js
  9. +194
    -0
      frappe/public/js/lib/notify.js

+ 48
- 19
frappe/desk/notifications.py Просмотреть файл

@@ -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) + ":")


+ 9
- 2
frappe/desk/page/messages/messages.js Просмотреть файл

@@ -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();


+ 2
- 1
frappe/desk/page/messages/messages.py Просмотреть файл

@@ -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")


+ 1
- 1
frappe/desk/page/messages/messages_main.html Просмотреть файл

@@ -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>


+ 1
- 0
frappe/public/build.json Просмотреть файл

@@ -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",


+ 18
- 0
frappe/public/js/frappe/desk.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) {


+ 1
- 1
frappe/public/js/frappe/ui/listing.js Просмотреть файл

@@ -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)


+ 8
- 1
frappe/public/js/frappe/views/communication.js Просмотреть файл

@@ -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


+ 194
- 0
frappe/public/js/lib/notify.js Просмотреть файл

@@ -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;

}));

Загрузка…
Отмена
Сохранить