diff --git a/frappe/core/doctype/event/test_event.py b/frappe/core/doctype/event/test_event.py index 7f393c4f41..ab9260b0c5 100644 --- a/frappe/core/doctype/event/test_event.py +++ b/frappe/core/doctype/event/test_event.py @@ -6,6 +6,7 @@ import frappe import frappe.defaults import unittest +import json test_records = frappe.get_test_records('Event') @@ -53,3 +54,43 @@ class TestEvent(unittest.TestCase): # the name should be same! self.assertEquals(ev.name, name) + def test_assign(self): + from frappe.widgets.form.assign_to import add + + ev = frappe.get_doc(test_records[0]).insert() + + add({ + "assign_to": "test@example.com", + "doctype": "Event", + "name": ev.name, + "description": "Test Assignment" + }) + + ev = frappe.get_doc("Event", ev.name) + + self.assertEquals(ev._assign, json.dumps(["test@example.com"])) + + # add another one + add({ + "assign_to": "test1@example.com", + "doctype": "Event", + "name": ev.name, + "description": "Test Assignment" + }) + + ev = frappe.get_doc("Event", ev.name) + + self.assertEquals(ev._assign, json.dumps(["test@example.com", "test1@example.com"])) + + # close an assignment + todo = frappe.get_doc("ToDo", {"reference_type": ev.doctype, "reference_name": ev.name, + "owner": "test1@example.com"}) + todo.status = "Closed" + todo.save() + + ev = frappe.get_doc("Event", ev.name) + self.assertEquals(ev._assign, json.dumps(["test@example.com"])) + + # cleanup + ev.delete() + diff --git a/frappe/core/doctype/todo/todo.py b/frappe/core/doctype/todo/todo.py index 1db7e1dbe6..5d67314780 100644 --- a/frappe/core/doctype/todo/todo.py +++ b/frappe/core/doctype/todo/todo.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import json from frappe.model.document import Document @@ -15,11 +16,17 @@ class ToDo(Document): if cur_status != self.status: self.add_comment(frappe._("Assignment Status Changed"), "Assignment Completed") + def on_update(self): + self.update_in_reference() + + def on_trash(self): + self.update_in_reference() + def add_comment(self, text, comment_type): if not self.reference_type and self.reference_name: return - comment = frappe.get_doc({ + frappe.get_doc({ "doctype":"Comment", "comment_by": frappe.session.user, "comment_type": comment_type, @@ -32,6 +39,36 @@ class ToDo(Document): description = self.description) }).insert(ignore_permissions=True) + def update_in_reference(self): + if not (self.reference_type and self.reference_name): + return + + try: + assignments = [d[0] for d in frappe.get_list("ToDo", + filters={ + "reference_type": self.reference_type, + "reference_name": self.reference_name, + "status": "Open" + }, + fields=["owner"], ignore_permissions=True, as_list=True)] + + assignments.reverse() + frappe.db.set_value(self.reference_type, self.reference_name, + "_assign", json.dumps(assignments)) + + except Exception, e: + if e.args[0] == 1146 and frappe.flags.in_install: + # no table + return + + elif e.args[0]==1054: + from frappe.model.db_schema import add_column + add_column(self.reference_type, "_assign", "Text") + self.update_in_reference() + + else: + raise + # NOTE: todo is viewable if either owner or assigned_to or System Manager in roles def get_permission_query_conditions(user): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 4bb8961f7a..8f35b41c19 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -134,7 +134,7 @@ class DatabaseQuery(object): columns = frappe.db.get_table_columns(self.doctype) to_remove = [] for fld in self.fields: - for f in ("_user_tags", "_comments"): + for f in ("_user_tags", "_comments", "_assign"): if f in fld and not f in columns: to_remove.append(fld) diff --git a/frappe/model/document.py b/frappe/model/document.py index 6c1b01b56b..bd58ad9b3e 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -413,6 +413,9 @@ class Document(BaseDocument): self.docstatus = 2 self.save() + def delete(self): + frappe.delete_doc(self.doctype, self.name) + def run_before_save_methods(self): if getattr(self, "ignore_validate", False): return diff --git a/frappe/patches.txt b/frappe/patches.txt index 1e413dc5a4..b074a843d3 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -51,3 +51,4 @@ frappe.patches.v4_1.file_manager_fix frappe.patches.v4_2.print_with_letterhead execute:frappe.delete_doc("DocType", "Control Panel", force=1) frappe.patches.v4_2.refactor_website_routing +frappe.patches.v4_2.set_assign_in_doc diff --git a/frappe/patches/v4_2/set_assign_in_doc.py b/frappe/patches/v4_2/set_assign_in_doc.py new file mode 100644 index 0000000000..1797eb68ff --- /dev/null +++ b/frappe/patches/v4_2/set_assign_in_doc.py @@ -0,0 +1,6 @@ +import frappe + +def execute(): + for name in frappe.db.sql_list("""select name from `tabToDo` + where ifnull(reference_type, '')!='' and ifnull(reference_name, '')!=''"""): + frappe.get_doc("ToDo", name).on_update() diff --git a/frappe/public/build.json b/frappe/public/build.json index af84cdffb0..6c4b1acf11 100644 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -71,6 +71,7 @@ "public/js/lib/microtemplate.js", "public/html/print_template.html", + "public/html/list_info_template.html", "public/js/legacy/globals.js", "public/js/legacy/datatype.js", diff --git a/frappe/public/css/avatar.css b/frappe/public/css/avatar.css index b48c5f9807..7949fa1c6c 100644 --- a/frappe/public/css/avatar.css +++ b/frappe/public/css/avatar.css @@ -25,3 +25,12 @@ width: 72px; height: 72px; } + +.avatar-xs { + margin-right: 3px; + margin-top: -2px; + width: 15px; + height: 15px; + border: none; + border-radius: 3px; +} diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 14b1adf873..c209b7b7d2 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -89,7 +89,7 @@ div#freeze { } .list-row { - padding: 5px 15px; + padding: 5px 15px 10px; margin: 0px -15px; border-bottom: 1px solid #c7c7c7; } @@ -177,7 +177,7 @@ div#freeze { margin-top: 12px; } -.doclist-row .filterable { +.filterable { cursor: pointer; } @@ -188,7 +188,7 @@ div#freeze { .list-timestamp { position: absolute; right: 15px; - bottom: 2px; + bottom: 5px; font-size: 70%; color: #888; } diff --git a/frappe/public/html/list_info_template.html b/frappe/public/html/list_info_template.html new file mode 100644 index 0000000000..61ffdcc143 --- /dev/null +++ b/frappe/public/html/list_info_template.html @@ -0,0 +1,17 @@ +{% if (tags.length) { %} + + {%= tags.join(", ") %} +{% } %} +{% if (comments.length) { %} + + {%= comments.length %} + +{% } %} +{% if (assign.length) { %} + {% for (var i=0, l=assign.length; i + {%= frappe.avatar(assign[i], "avatar-xs") %} + {% }%} +{% } %} +{%= comment_when(data.modified) %} diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index a2ce2ead19..9b896e29c6 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -101,6 +101,15 @@ frappe.form.formatters = { }); return html; }, + Assign: function(value) { + var html = ""; + $.each(JSON.parse(value || "[]"), function(i, v) { + if(v) html+= ''+v+''; + }); + return html; + }, SmallText: function(value) { return frappe.form.formatters.Text(value); }, diff --git a/frappe/public/js/frappe/misc/user.js b/frappe/public/js/frappe/misc/user.js index 940293711b..a92daa28ae 100644 --- a/frappe/public/js/frappe/misc/user.js +++ b/frappe/public/js/frappe/misc/user.js @@ -12,16 +12,15 @@ frappe.user_info = function(uid) { return frappe.boot.user_info[uid]; } -frappe.avatar = function(user, large, title) { +frappe.avatar = function(user, css_class, title) { var image = frappe.utils.get_file_link(frappe.user_info(user).image); - var to_size = large ? 72 : 30; if(!title) title = frappe.user_info(user).fullname; - return repl('\ + return repl('\ ', { image: image, title: title, - small_or_large: large ? "avatar-large" : "avatar-small" + css_class: css_class || "avatar-small" }); } @@ -61,9 +60,6 @@ $.extend(frappe.user, { image: function(uid) { return frappe.user_info(uid).image; }, - avatar: function(uid, large) { - return frappe.avatar(uid, large); - }, has_role: function(rl) { if(typeof rl=='string') rl = [rl]; diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 77c3ac4384..eef536d262 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -10,7 +10,9 @@ $.extend(frappe.model, { layout_fields: ['Section Break', 'Column Break', 'Fold'], std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by', - '_user_tags', '_comments', 'docstatus', 'parent', 'parenttype', 'parentfield', 'idx'], + '_user_tags', '_comments', '_assign', 'docstatus', + 'parent', 'parenttype', 'parentfield', 'idx'], + std_fields: [ {fieldname:'name', fieldtype:'Link', label:__('ID')}, {fieldname:'owner', fieldtype:'Data', label:__('Created By')}, @@ -20,6 +22,7 @@ $.extend(frappe.model, { {fieldname:'modified_by', fieldtype:'Data', label:__('Last Updated By')}, {fieldname:'_user_tags', fieldtype:'Data', label:__('Tags')}, {fieldname:'_comments', fieldtype:'Text', label:__('Comments')}, + {fieldname:'_assign', fieldtype:'Text', label:__('Assigned To')}, {fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, ], diff --git a/frappe/public/js/frappe/ui/listing.js b/frappe/public/js/frappe/ui/listing.js index 69799cf923..ffaaeb09c4 100644 --- a/frappe/public/js/frappe/ui/listing.js +++ b/frappe/public/js/frappe/ui/listing.js @@ -361,7 +361,7 @@ frappe.ui.Listing = Class.extend({ if(fieldname=='_user_tags') { // and for tags this.filter_list.add_filter(doctype, fieldname, - 'like', '%' + label); + 'like', '%' + label + '%'); } else { // or for rest using "in" filter.set_values(doctype, fieldname, 'in', v + ', ' + label); @@ -370,9 +370,9 @@ frappe.ui.Listing = Class.extend({ } else { // no filter for this item, // setup one - if(['_user_tags', '_comments'].indexOf(fieldname)!==-1) { + if(['_user_tags', '_comments', '_assign'].indexOf(fieldname)!==-1) { this.filter_list.add_filter(doctype, fieldname, - 'like', '%' + label); + 'like', '%' + label + '%'); } else { this.filter_list.add_filter(doctype, fieldname, '=', label); } diff --git a/frappe/public/js/frappe/ui/tags.js b/frappe/public/js/frappe/ui/tags.js index bc8ba636e8..38b1a5cae7 100644 --- a/frappe/public/js/frappe/ui/tags.js +++ b/frappe/public/js/frappe/ui/tags.js @@ -5,7 +5,7 @@ frappe.ui.TagEditor = Class.extend({ init: function(opts) { /* docs: Arguments - + - parent - user_tags - doctype @@ -20,21 +20,38 @@ frappe.ui.TagEditor = Class.extend({ placeholderText: __('Add Tag'), onTagAdded: function(ev, tag) { if(me.initialized && !me.refreshing) { + var tag = tag.find('.tagit-label').text(); return frappe.call({ method: 'frappe.widgets.tags.add_tag', - args: me.get_args(tag.find('.tagit-label').text()) - }); + args: me.get_args(tag), + callback: function(r) { + var user_tags = me.user_tags.split(","); + user_tags.push(tag) + me.user_tags = user_tags.join(","); + me.on_change && me.on_change(me.user_tags); + } + }); } }, onTagRemoved: function(ev, tag) { if(!me.refreshing) { + var tag = tag.find('.tagit-label').text(); return frappe.call({ method: 'frappe.widgets.tags.remove_tag', - args: me.get_args(tag.find('.tagit-label').text()) + args: me.get_args(tag), + callback: function(r) { + var user_tags = me.user_tags.split(","); + user_tags.splice(user_tags.indexOf(tag), 1); + me.user_tags = user_tags.join(","); + me.on_change && me.on_change(me.user_tags); + } }); } } - }); + }); + if (!this.user_tags) { + this.user_tags = ""; + } this.refresh(this.user_tags); this.initialized = true; }, @@ -51,15 +68,15 @@ frappe.ui.TagEditor = Class.extend({ me.refreshing = true; me.$tags.tagit("removeAll"); - if(!user_tags && this.frm) + if(!user_tags && this.frm) user_tags = frappe.model.get_value(this.frm.doctype, this.frm.docname, "_user_tags"); - + if(user_tags) { $.each(user_tags.split(','), function(i, v) { me.$tags.tagit("createTag", v); }); } me.refreshing = false; - + } -}) \ No newline at end of file +}) diff --git a/frappe/public/js/frappe/views/listview.js b/frappe/public/js/frappe/views/listview.js index 7c198edaef..0b57e7009d 100644 --- a/frappe/public/js/frappe/views/listview.js +++ b/frappe/public/js/frappe/views/listview.js @@ -44,7 +44,7 @@ frappe.views.ListView = Class.extend({ } $.each(['name', 'owner', 'docstatus', '_user_tags', '_comments', 'modified', - 'modified_by'], function(i, fieldname) { add_field(fieldname); }) + 'modified_by', '_assign'], function(i, fieldname) { add_field(fieldname); }) // add title field if(this.meta.title_field) { @@ -221,6 +221,7 @@ frappe.views.ListView = Class.extend({ var comments = data._comments ? JSON.parse(data._comments) : [], tags = $.map((data._user_tags || "").split(","), function(v) { return v ? v : null; }), + assign = data._assign ? JSON.parse(data._assign) : [], me = this; if(me.title_field && data[me.title_field]!==data.name) { @@ -230,24 +231,18 @@ frappe.views.ListView = Class.extend({ .html('#' + data.name + ""); } + $(row).find(".list-timestamp").remove(); + var timestamp_and_comment = $('
') .appendTo(row) - .html("" - + (tags.length ? ( - '' + tags.join(", ") + '' - ): "") - + (comments.length ? - (' ' - + comments.length + " " + ( - comments.length===1 ? __("comment") : __("comments")) + '') - : "") - + comment_when(data.modified)); - + .html(frappe.render(frappe.templates.list_info_template, { + "tags": tags, + "comments": comments, + "assign": assign, + "data": data, + "doctype": this.doctype + })); }, render_tags: function(row, data) { @@ -272,7 +267,11 @@ frappe.views.ListView = Class.extend({ doctype: this.doctype, docname: data.name }, - user_tags: data._user_tags + user_tags: data._user_tags, + on_change: function(user_tags) { + data._user_tags = user_tags; + me.render_timestamp_and_comments(row, data); + } }); tag_editor.$w.on("click", ".tagit-label", function() { me.doclistview.set_filter("_user_tags", diff --git a/frappe/public/js/frappe/views/reportview.js b/frappe/public/js/frappe/views/reportview.js index d88b15278d..e261f9db22 100644 --- a/frappe/public/js/frappe/views/reportview.js +++ b/frappe/public/js/frappe/views/reportview.js @@ -220,8 +220,12 @@ frappe.views.ReportView = frappe.ui.Listing.extend({ width: (docfield ? cint(docfield.width) : 120) || 120, formatter: function(row, cell, value, columnDef, dataContext) { var docfield = columnDef.docfield; - if(docfield.fieldname==="_user_tags") docfield.fieldtype = "Tag"; - if(docfield.fieldname==="_comments") docfield.fieldtype = "Comment"; + docfield.fieldtype = { + "_user_tags": "Tag", + "_comments": "Comment", + "_assign": "Assign" + }[docfield.fieldname] || docfield.fieldtype; + if(docfield.fieldtype==="Link" && docfield.fieldname!=="name") { docfield.link_onclick = repl('frappe.container.page.reportview.set_filter("%(fieldname)s", "%(value)s").page.reportview.run()', diff --git a/frappe/widgets/form/assign_to.py b/frappe/widgets/form/assign_to.py index 30fcf43526..e4429b247a 100644 --- a/frappe/widgets/form/assign_to.py +++ b/frappe/widgets/form/assign_to.py @@ -22,7 +22,15 @@ def get(args=None): @frappe.whitelist() def add(args=None): - """add in someone's to do list""" + """add in someone's to do list + args = { + "assign_to": , + "doctype": , + "name": , + "description": + } + + """ if not args: args = frappe.local.form_dict