@@ -1000,7 +1000,10 @@ def publish_realtime(*args, **kwargs): | |||||
:param event: Event name, like `task_progress` etc. | :param event: Event name, like `task_progress` etc. | ||||
:param message: JSON message object. For async must contain `task_id` | :param message: JSON message object. For async must contain `task_id` | ||||
:param room: Room in which to publish update (default entire site)""" | |||||
:param room: Room in which to publish update (default entire site) | |||||
:param user: Transmit to user | |||||
:param doctype: Transmit to doctype, docname | |||||
:param docname: Transmit to doctype, docname""" | |||||
import frappe.async | import frappe.async | ||||
frappe.async.publish_realtime(*args, **kwargs) | |||||
return frappe.async.publish_realtime(*args, **kwargs) |
@@ -120,7 +120,7 @@ def is_file_old(file_path): | |||||
return ((time.time() - os.stat(file_path).st_mtime) > TASK_LOG_MAX_AGE) | return ((time.time() - os.stat(file_path).st_mtime) > TASK_LOG_MAX_AGE) | ||||
def publish_realtime(event, message, room=None, user=None, doctype=None, docname=None): | |||||
def publish_realtime(event, message=None, room=None, user=None, doctype=None, docname=None): | |||||
"""Publish real-time updates | """Publish real-time updates | ||||
:param event: Event name, like `task_progress` etc. | :param event: Event name, like `task_progress` etc. | ||||
@@ -128,18 +128,23 @@ def publish_realtime(event, message, room=None, user=None, doctype=None, docname | |||||
:param room: Room in which to publish update (default entire site) | :param room: Room in which to publish update (default entire site) | ||||
:param user: Transmit to user | :param user: Transmit to user | ||||
:param doctype: Transmit to doctype, docname | :param doctype: Transmit to doctype, docname | ||||
:param doctype: Transmit to doctype, docname""" | |||||
:param docname: Transmit to doctype, docname""" | |||||
if message is None: | |||||
message = {} | |||||
if not room: | if not room: | ||||
if user: | if user: | ||||
get_user_room(user) | |||||
if doctype and docname: | |||||
get_doc_room(doctype, docname) | |||||
room = get_user_room(user) | |||||
elif doctype and docname: | |||||
room = get_doc_room(doctype, docname) | |||||
message["doctype"] = doctype | |||||
message["name"] = docname | |||||
else: | else: | ||||
room = get_site_room() | room = get_site_room() | ||||
emit_via_redis(event, message, room) | emit_via_redis(event, message, room) | ||||
def emit_via_redis(event, message, room=None): | |||||
def emit_via_redis(event, message, room): | |||||
"""Publish real-time updates via redis | """Publish real-time updates via redis | ||||
:param event: Event name, like `task_progress` etc. | :param event: Event name, like `task_progress` etc. | ||||
@@ -42,7 +42,9 @@ frappe.desk.pages.Messages = Class.extend({ | |||||
setup_realtime: function() { | setup_realtime: function() { | ||||
frappe.realtime.on('new_message', function(comment) { | frappe.realtime.on('new_message', function(comment) { | ||||
frappe.utils.notify(__("Message from {0}", [comment.comment_by_fullname]), comment.comment); | |||||
if(comment.modified_by !== user) { | |||||
frappe.utils.notify(__("Message from {0}", [comment.comment_by_fullname]), comment.comment); | |||||
} | |||||
if (frappe.get_route()[0] === 'messages') { | if (frappe.get_route()[0] === 'messages') { | ||||
var current_contact = $(cur_page.page).find('[data-contact]').data('contact'); | var current_contact = $(cur_page.page).find('[data-contact]').data('contact'); | ||||
var on_broadcast_page = current_contact === user; | var on_broadcast_page = current_contact === user; | ||||
@@ -8,11 +8,15 @@ | |||||
class="form-control messages-textarea"></textarea> | class="form-control messages-textarea"></textarea> | ||||
</div> | </div> | ||||
<div style="padding-top: 15px;"> | <div style="padding-top: 15px;"> | ||||
<button class="pull-right btn btn-default btn-sm btn-post" data-contact="{%= contact %}"> | |||||
<button class="pull-right btn btn-primary btn-sm btn-post" data-contact="{%= contact %}"> | |||||
{%= __("Post") %} | {%= __("Post") %} | ||||
</button> | </button> | ||||
{% if (contact === user) { %} | {% if (contact === user) { %} | ||||
<span class="pull-right indicator orange">{%= __("Public") %}</span> | |||||
<span class="pull-right" | |||||
style="margin-top: 4px; margin-right: 10px;"> | |||||
<i class="octicon octicon-rss"></i> | |||||
<span class="text-muted small">{%= __("Public") %}</span> | |||||
</span> | |||||
{% } %} | {% } %} | ||||
<div class="pull-right checkbox text-muted small" | <div class="pull-right checkbox text-muted small" | ||||
style="margin-right: 15px; margin-top: 7px;"> | style="margin-right: 15px; margin-top: 7px;"> | ||||
@@ -1,9 +1,12 @@ | |||||
<div class="row message-row small"> | <div class="row message-row small"> | ||||
{% if (data.owner==data.comment_docname && data.parenttype!="Assignment") { %} | |||||
<span class="pull-left indicator orange" title="{%= __("Public") %}"></span> | |||||
{% } %} | |||||
<div class="col-sm-9"> | <div class="col-sm-9"> | ||||
<div class="media"> | <div class="media"> | ||||
{% if (data.owner==data.comment_docname | |||||
&& data.parenttype!="Assignment") { %} | |||||
<span class="pull-left" title="{{ __("Public") }}"><i class="octicon octicon-rss text-muted" style="margin-top: 3px;"></i></span> | |||||
{% } else { %} | |||||
<span class="pull-left" title="{{ __("Public") }}" style="width: 20px; height: 16px; display: inline-block;"></span> | |||||
{% } %} | |||||
<div class="pull-left hidden-xs"> | <div class="pull-left hidden-xs"> | ||||
<span class="avatar avatar-small" title="{%= frappe.user.full_name(data.owner) %} "> | <span class="avatar avatar-small" title="{%= frappe.user.full_name(data.owner) %} "> | ||||
<img class="media-object {{ data.is_system_message ? "grayscale" : "" }}" | <img class="media-object {{ data.is_system_message ? "grayscale" : "" }}" | ||||
@@ -574,6 +574,9 @@ class Document(BaseDocument): | |||||
frappe.cache().hdel("last_modified", self.doctype) | frappe.cache().hdel("last_modified", self.doctype) | ||||
frappe.publish_realtime("doc_update", {"modified_by": frappe.session.user}, | |||||
doctype=self.doctype, docname=self.name) | |||||
self.latest = None | self.latest = None | ||||
def check_no_back_links_exist(self): | def check_no_back_links_exist(self): | ||||
@@ -163,7 +163,7 @@ select.form-control { | |||||
} | } | ||||
.form-headline .alert { | .form-headline .alert { | ||||
font-size: 12px; | font-size: 12px; | ||||
border-color: #d1d8dd; | |||||
background-color: #fffce7; | |||||
margin-bottom: 0px; | margin-bottom: 0px; | ||||
} | } | ||||
.delivery-status-indicator { | .delivery-status-indicator { | ||||
@@ -22,6 +22,7 @@ frappe.Application = Class.extend({ | |||||
this.startup(); | this.startup(); | ||||
}, | }, | ||||
startup: function() { | startup: function() { | ||||
frappe.model.init(); | |||||
this.load_bootinfo(); | this.load_bootinfo(); | ||||
this.make_nav_bar(); | this.make_nav_bar(); | ||||
this.set_favicon(); | this.set_favicon(); | ||||
@@ -23,6 +23,7 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
this.wrapper.toggle(true); | this.wrapper.toggle(true); | ||||
}, | }, | ||||
set_headline_alert: function(text, alert_class, icon) { | set_headline_alert: function(text, alert_class, icon) { | ||||
if(!alert_class) alert_class = "alert-warning"; | |||||
this.set_headline(repl('<div class="alert %(alert_class)s">%(icon)s%(text)s</div>', { | this.set_headline(repl('<div class="alert %(alert_class)s">%(icon)s%(text)s</div>', { | ||||
"alert_class": alert_class || "", | "alert_class": alert_class || "", | ||||
"icon": icon ? '<i class="'+icon+'" /> ' : "", | "icon": icon ? '<i class="'+icon+'" /> ' : "", | ||||
@@ -38,12 +38,16 @@ frappe.views.ListFactory = frappe.views.Factory.extend({ | |||||
}); | }); | ||||
$(document).on("save", function(event, doc) { | $(document).on("save", function(event, doc) { | ||||
var list_page = "List/" + doc.doctype; | |||||
frappe.views.set_list_as_dirty(doc.doctype); | |||||
}); | |||||
frappe.views.set_list_as_dirty = function(doctype) { | |||||
var list_page = "List/" + doctype; | |||||
if(frappe.pages[list_page]) { | if(frappe.pages[list_page]) { | ||||
if(frappe.pages[list_page].doclistview) | if(frappe.pages[list_page].doclistview) | ||||
frappe.pages[list_page].doclistview.dirty = true; | frappe.pages[list_page].doclistview.dirty = true; | ||||
} | } | ||||
}) | |||||
} | |||||
frappe.views.DocListView = frappe.ui.Listing.extend({ | frappe.views.DocListView = frappe.ui.Listing.extend({ | ||||
init: function(opts) { | init: function(opts) { | ||||
@@ -34,6 +34,32 @@ $.extend(frappe.model, { | |||||
new_names: {}, | new_names: {}, | ||||
events: {}, | events: {}, | ||||
init: function() { | |||||
// setup refresh if the document is updated somewhere else | |||||
frappe.realtime.on("doc_update", function(data) { | |||||
// set list dirty | |||||
frappe.views.set_list_as_dirty(data.doctype); | |||||
var doc = locals[data.doctype] && locals[data.doctype][data.name]; | |||||
if(doc) { | |||||
// current document is dirty, show message if its not me | |||||
if(cur_frm.doc.doctype===doc.doctype && cur_frm.doc.name===doc.name) { | |||||
if(data.modified_by!==user) { | |||||
doc.__needs_refresh = true; | |||||
cur_frm.show_if_needs_refresh(); | |||||
} | |||||
} else { | |||||
if(!doc.__unsaved) { | |||||
// no local changes, remove from locals | |||||
frappe.model.remove_from_locals(doc.doctype, doc.name); | |||||
} else { | |||||
// show message when user navigates back | |||||
doc.__needs_refresh = true; | |||||
} | |||||
} | |||||
} | |||||
}); | |||||
}, | |||||
is_value_type: function(fieldtype) { | is_value_type: function(fieldtype) { | ||||
// not in no-value type | // not in no-value type | ||||
return frappe.model.no_value_type.indexOf(fieldtype)===-1; | return frappe.model.no_value_type.indexOf(fieldtype)===-1; | ||||
@@ -1,5 +1,6 @@ | |||||
frappe.socket = { | frappe.socket = { | ||||
open_tasks: {}, | open_tasks: {}, | ||||
open_docs: [], | |||||
init: function() { | init: function() { | ||||
if (frappe.boot.disable_async) { | if (frappe.boot.disable_async) { | ||||
return; | return; | ||||
@@ -16,9 +17,9 @@ frappe.socket = { | |||||
frappe.socket.doc_subscribe(frm.doctype, frm.docname); | frappe.socket.doc_subscribe(frm.doctype, frm.docname); | ||||
}); | }); | ||||
$(document).on('form-unload', function(e, frm) { | |||||
frappe.socket.doc_unsubscribe(frm.doctype, frm.docname); | |||||
}); | |||||
// $(document).on('form-unload', function(e, frm) { | |||||
// frappe.socket.doc_unsubscribe(frm.doctype, frm.docname); | |||||
// }); | |||||
}, | }, | ||||
subscribe: function(task_id, opts) { | subscribe: function(task_id, opts) { | ||||
frappe.socket.socket.emit('task_subscribe', task_id); | frappe.socket.socket.emit('task_subscribe', task_id); | ||||
@@ -28,11 +29,17 @@ frappe.socket = { | |||||
}, | }, | ||||
doc_subscribe: function(doctype, docname) { | doc_subscribe: function(doctype, docname) { | ||||
frappe.socket.socket.emit('doc_subscribe', doctype, docname); | frappe.socket.socket.emit('doc_subscribe', doctype, docname); | ||||
frappe.socket.open_doc = {doctype: doctype, docname: docname}; | |||||
frappe.socket.open_docs.push({doctype: doctype, docname: docname}); | |||||
}, | }, | ||||
doc_unsubscribe: function(doctype, docname) { | doc_unsubscribe: function(doctype, docname) { | ||||
frappe.socket.socket.emit('doc_unsubscribe', doctype, docname); | frappe.socket.socket.emit('doc_unsubscribe', doctype, docname); | ||||
frappe.socket.open_doc = null; | |||||
frappe.socket.open_docs = $.filter(frappe.socket.open_docs, function(d) { | |||||
if(d.doctype===doctype && d.name===docname) { | |||||
return null; | |||||
} else { | |||||
return d; | |||||
} | |||||
}) | |||||
}, | }, | ||||
setup_listeners: function() { | setup_listeners: function() { | ||||
frappe.socket.socket.on('task_status_change', function(data) { | frappe.socket.socket.on('task_status_change', function(data) { | ||||
@@ -47,7 +54,6 @@ frappe.socket = { | |||||
frappe.socket.socket.on('task_progress', function(data) { | frappe.socket.socket.on('task_progress', function(data) { | ||||
frappe.socket.process_response(data, "progress"); | frappe.socket.process_response(data, "progress"); | ||||
}); | }); | ||||
}, | }, | ||||
setup_reconnect: function() { | setup_reconnect: function() { | ||||
// subscribe again to open_tasks | // subscribe again to open_tasks | ||||
@@ -55,11 +61,15 @@ frappe.socket = { | |||||
$.each(frappe.socket.open_tasks, function(task_id, opts) { | $.each(frappe.socket.open_tasks, function(task_id, opts) { | ||||
frappe.socket.subscribe(task_id, opts); | frappe.socket.subscribe(task_id, opts); | ||||
}); | }); | ||||
// re-connect open docs | |||||
$.each(frappe.socket.open_docs, function(d) { | |||||
if(locals[d.doctype] && locals[d.doctype][d.name]) { | |||||
frappe.socket.doc_subscribe(d.doctype, d.name); | |||||
} | |||||
}) | |||||
}); | }); | ||||
if(frappe.socket.open_doc) { | |||||
frappe.socket.doc_subscribe(frappe.socket.open_doc.doctype, frappe.socket.open_doc.docname); | |||||
} | |||||
}, | }, | ||||
process_response: function(data, method) { | process_response: function(data, method) { | ||||
if(!data) { | if(!data) { | ||||
@@ -87,7 +97,7 @@ frappe.socket = { | |||||
$(frappe.socket.init); | $(frappe.socket.init); | ||||
frappe.require("frappe.realtime"); | |||||
frappe.provide("frappe.realtime"); | |||||
frappe.realtime.on = function(event, callback) { | frappe.realtime.on = function(event, callback) { | ||||
frappe.socket.socket.on(event, callback); | frappe.socket.socket.on(event, callback); | ||||
} | } |
@@ -406,6 +406,16 @@ _f.Frm.prototype.refresh = function(docname) { | |||||
if(this.print_preview.wrapper.is(":visible")) { | if(this.print_preview.wrapper.is(":visible")) { | ||||
this.print_preview.preview(); | this.print_preview.preview(); | ||||
} | } | ||||
this.show_if_needs_refresh(); | |||||
} | |||||
} | |||||
_f.Frm.prototype.show_if_needs_refresh = function() { | |||||
if(this.doc.__needs_refresh) { | |||||
this.dashboard.set_headline_alert(__("This form has been modified after you have loaded it") | |||||
+ '<a class="btn btn-xs btn-primary pull-right" onclick="cur_frm.reload_doc()">' | |||||
+ __("Refresh") + '</a>', "alert-warning"); | |||||
} | } | ||||
} | } | ||||
@@ -212,8 +212,7 @@ select.form-control { | |||||
.form-headline .alert { | .form-headline .alert { | ||||
font-size: @text-medium; | font-size: @text-medium; | ||||
border-color: @border-color; | |||||
// background-color: @light-bg; | |||||
background-color: @light-yellow; | |||||
margin-bottom: 0px; | margin-bottom: 0px; | ||||
} | } | ||||
@@ -43,6 +43,7 @@ io.on('connection', function(socket){ | |||||
var room = get_user_room(socket, res.body.message.user); | var room = get_user_room(socket, res.body.message.user); | ||||
// console.log('joining', room); | // console.log('joining', room); | ||||
socket.join(room); | socket.join(room); | ||||
socket.join(get_site_room(socket)); | |||||
} | } | ||||
}) | }) | ||||
socket.on('task_subscribe', function(task_id) { | socket.on('task_subscribe', function(task_id) { | ||||
@@ -64,7 +65,7 @@ io.on('connection', function(socket){ | |||||
docname: docname | docname: docname | ||||
}) | }) | ||||
.end(function(err, res) { | .end(function(err, res) { | ||||
console.log(err) | |||||
if(err) console.log(err); | |||||
if(res.status == 200) { | if(res.status == 200) { | ||||
var room = get_doc_room(socket, doctype, docname); | var room = get_doc_room(socket, doctype, docname); | ||||
// console.log('joining', room) | // console.log('joining', room) | ||||
@@ -89,7 +90,7 @@ function send_existing_lines(task_id, socket) { | |||||
}) | }) | ||||
} | } | ||||
subscriber.on("message", function(channel, message) { | subscriber.on("message", function(channel, message) { | ||||
message = JSON.parse(message); | message = JSON.parse(message); | ||||
io.to(message.room).emit(message.event, message.message); | io.to(message.room).emit(message.event, message.message); | ||||
@@ -97,7 +98,7 @@ subscriber.on("message", function(channel, message) { | |||||
}); | }); | ||||
subscriber.subscribe("events"); | subscriber.subscribe("events"); | ||||
http.listen(3000, function(){ | http.listen(3000, function(){ | ||||
console.log('listening on *:3000'); | console.log('listening on *:3000'); | ||||
}); | }); | ||||