From d2b3c9a04b4236c915541cc708a79500ec0c97c0 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Thu, 6 Dec 2018 15:15:57 +0000 Subject: [PATCH] [IMP] mail: upload a file to mark activity as done This commit introduces the "upload file" activity type category. For an activity of that time, uploading a file will mark the activity as done (and maybe create the forced next activities). This activity type is seen as a shortcut to upload a file with a reminder. Task-1915004 closes odoo/odoo#29861 --- addons/calendar/models/mail_activity.py | 4 +- addons/calendar/views/mail_activity_views.xml | 11 -- addons/mail/data/mail_activity_data.xml | 7 + addons/mail/models/mail_activity.py | 15 +- addons/mail/static/src/js/activity.js | 130 ++++++++++++++++-- addons/mail/static/src/xml/activity.xml | 17 ++- .../static/src/xml/web_kanban_activity.xml | 36 +++-- addons/mail/static/tests/chatter_tests.js | 84 +++++++++++ addons/mail/views/mail_activity_views.xml | 4 +- addons/note/__manifest__.py | 1 - addons/note/views/mail_activity_views.xml | 14 -- addons/test_mail/tests/test_performance.py | 4 +- 12 files changed, 266 insertions(+), 61 deletions(-) delete mode 100644 addons/note/views/mail_activity_views.xml diff --git a/addons/calendar/models/mail_activity.py b/addons/calendar/models/mail_activity.py index 2e85513afca..2bd33f2c6c9 100644 --- a/addons/calendar/models/mail_activity.py +++ b/addons/calendar/models/mail_activity.py @@ -29,9 +29,9 @@ class MailActivity(models.Model): } return action - def _action_done(self, feedback=False): + def _action_done(self, feedback=False, attachment_ids=False): events = self.mapped('calendar_event_id') - messages, activities = super(MailActivity, self)._action_done(feedback=feedback) + messages, activities = super(MailActivity, self)._action_done(feedback=feedback, attachment_ids=attachment_ids) if feedback: for event in events: description = event.description diff --git a/addons/calendar/views/mail_activity_views.xml b/addons/calendar/views/mail_activity_views.xml index 9075e09d233..cacdbe408f2 100644 --- a/addons/calendar/views/mail_activity_views.xml +++ b/addons/calendar/views/mail_activity_views.xml @@ -1,17 +1,6 @@ - - mail.activity.type.form.inherit.calendar - mail.activity.type - - - - 0 - - - - mail.activity.form.inherit.calendar mail.activity diff --git a/addons/mail/data/mail_activity_data.xml b/addons/mail/data/mail_activity_data.xml index a56ed790052..f05d7695ec1 100644 --- a/addons/mail/data/mail_activity_data.xml +++ b/addons/mail/data/mail_activity_data.xml @@ -23,6 +23,13 @@ 5 12 + + Upload Document + fa-upload + 5 + 25 + upload_file + Exception fa-warning diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index d693830eda6..9d9a5d06b2f 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -69,9 +69,9 @@ class MailActivityType(models.Model): domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", string='Preceding Activities') category = fields.Selection([ - ('default', 'Other')], default='default', - string='Category', - help='Categories may trigger specific behavior like opening calendar view') + ('default', 'Other'), ('upload_file', 'Upload Document') + ], default='default', string='Category', + help='Categories may trigger specific behavior like opening calendar view or automatically mark as done when a document is uploaded') mail_template_ids = fields.Many2many('mail.template', string='Mails templates') #Fields for display purpose only @@ -387,8 +387,8 @@ class MailActivity(models.Model): return messages.ids and messages.ids[0] or False @api.multi - def action_feedback(self, feedback=False): - messages, next_activities = self._action_done(feedback=feedback) + def action_feedback(self, feedback=False, attachment_ids=None): + messages, next_activities = self._action_done(feedback=feedback, attachment_ids=attachment_ids) return messages.ids and messages.ids[0] or False def action_done_schedule_next(self): @@ -419,10 +419,11 @@ class MailActivity(models.Model): 'target': 'new', } - def _action_done(self, feedback=False): + def _action_done(self, feedback=False, attachment_ids=None): """ Private implementation of marking activity as done: posting a message, deleting activity (since done), and eventually create the automatical next activity (depending on config). :param feedback: optional feedback from user when marking activity as done + :param attachment_ids: list of ir.attachment ids to attach to the posted mail.message :returns (messages, activities) where - messages is a recordset of posted mail.message - activities is a recordset of mail.activity of forced automically created activities @@ -458,6 +459,7 @@ class MailActivity(models.Model): }, subtype_id=self.env['ir.model.data'].xmlid_to_res_id('mail.mt_activities'), mail_activity_type_id=activity.activity_type_id.id, + attachment_ids=[(4, attachment_id) for attachment_id in attachment_ids] if attachment_ids else [], ) messages |= record.message_ids[0] @@ -470,7 +472,6 @@ class MailActivity(models.Model): def action_close_dialog(self): return {'type': 'ir.actions.act_window_close'} - @api.multi def activity_format(self): activities = self.read() mail_template_ids = set([template_id for activity in activities for template_id in activity["mail_template_ids"]]) diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index db9366f2795..9543a7ebff2 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -8,6 +8,7 @@ var BasicModel = require('web.BasicModel'); var core = require('web.core'); var field_registry = require('web.field_registry'); var session = require('web.session'); +var framework = require('web.framework'); var time = require('web.time'); var QWeb = core.qweb; @@ -102,10 +103,27 @@ var setDelayLabel = function (activities){ return activities; }; +/** + * Set the file upload identifier for 'upload_file' type activities + * + * @param {Array} activities list of activity Object + * @return {Array} : list of modified activity Object + */ +var setFileUploadID = function (activities) { + _.each(activities, function (activity) { + if (activity.activity_category === 'upload_file') { + activity.fileuploadID = _.uniqueId('o_fileupload'); + } + }); + return activities; +}; + var BasicActivity = AbstractField.extend({ events: { 'click .o_edit_activity': '_onEditActivity', + 'change input.o_input_file': '_onFileChanged', 'click .o_mark_as_done': '_onMarkActivityDone', + 'click .o_mark_as_done_upload_file': '_onMarkActivityDoneUploadFile', 'click .o_activity_template_preview': '_onPreviewMailTemplate', 'click .o_schedule_activity': '_onScheduleActivity', 'click .o_activity_template_send': '_onSendMailTemplate', @@ -139,13 +157,17 @@ var BasicActivity = AbstractField.extend({ * @private * @param {Object} params * @param {integer} params.activityID + * @param {integer[]} params.attachmentIds * @param {string} params.feedback + * + * @return {$.Promise} */ _markActivityDone: function (params) { var activityID = params.activityID; - var feedback = params.feedback; + var feedback = params.feedback || false; + var attachmentIds = params.attachmentIds || []; - this._sendActivityFeedback(activityID, feedback) + return this._sendActivityFeedback(activityID, feedback, attachmentIds) .then(this._reload.bind(this, { activity: true, thread: true })); }, /** @@ -209,14 +231,18 @@ var BasicActivity = AbstractField.extend({ * @private * @param {integer} activityID * @param {string} feedback + * @param {integer[]} attachmentIds * @return {$.Promise} */ - _sendActivityFeedback: function (activityID, feedback) { + _sendActivityFeedback: function (activityID, feedback, attachmentIds) { return this._rpc({ model: 'mail.activity', method: 'action_feedback', args: [[activityID]], - kwargs: {feedback: feedback}, + kwargs: { + feedback: feedback, + attachment_ids: attachmentIds || [], + }, context: this.record.getContext(), }); }, @@ -225,6 +251,28 @@ var BasicActivity = AbstractField.extend({ // Handlers //------------------------------------------------------------ + /** + * @private + * @param {Object[]} activities + */ + _bindOnUploadAction: function (activities) { + var self = this; + _.each(activities, function (activity) { + if (activity.fileuploadID) { + $(window).on(activity.fileuploadID, function() { + framework.unblockUI(); + // find the button clicked and display the feedback popup on it + var files = Array.prototype.slice.call(arguments, 1); + self._markActivityDone({ + activityID: activity.id, + attachmentIds: _.pluck(files, 'id') + }).then(function () { + self.trigger_up('reload'); + }); + }); + } + }); + }, /** Binds a focusout handler on a bootstrap popover * Useful to do some operations on the popover's HTML, * like keeping the user's input for the feedback @@ -257,7 +305,18 @@ var BasicActivity = AbstractField.extend({ var activityID = $(ev.currentTarget).data('activity-id'); return this._openActivityForm(activityID, this._reload.bind(this, { activity: true, thread: true })); }, - /** + /** + * @private + * @param {FormEvent} ev + */ + _onFileChanged: function (ev) { + ev.stopPropagation(); + ev.preventDefault(); + var $form = $(ev.currentTarget).closest('form'); + $form.submit(); + framework.blockUI(); + }, + /** * Called when marking an activity as done * * It lets the current user write a feedback in a popup menu. @@ -282,7 +341,10 @@ var BasicActivity = AbstractField.extend({ var $panel = self.$('#o_activity_form_' + activityID); if (!$panel.data('bs.collapse')) { - var $form = $(QWeb.render('mail.activity_feedback_form', { previous_activity_type_id: previousActivityTypeID, force_next: forceNextActivity})); + var $form = $(QWeb.render('mail.activity_feedback_form', { + previous_activity_type_id: previousActivityTypeID, + force_next: forceNextActivity + })); $panel.append($form); self._onMarkActivityDoneActions($markDoneBtn, $form, activityID); @@ -319,7 +381,10 @@ var BasicActivity = AbstractField.extend({ trigger:'click', placement: 'right', // FIXME: this should work, maybe a bug in the popper lib content : function () { - var $popover = $(QWeb.render('mail.activity_feedback_form', { previous_activity_type_id: previousActivityTypeID, force_next: forceNextActivity})); + var $popover = $(QWeb.render('mail.activity_feedback_form', { + previous_activity_type_id: previousActivityTypeID, + force_next: forceNextActivity + })); self._onMarkActivityDoneActions($markDoneBtn, $popover, activityID); return $popover; }, @@ -365,6 +430,17 @@ var BasicActivity = AbstractField.extend({ } }); }, + /** + * @private + * @param {MouseEvent} ev + */ + _onMarkActivityDoneUploadFile: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var fileuploadID = $(ev.currentTarget).data('fileupload-id'); + var $input = this.$("[target='" + fileuploadID + "'] > input.o_input_file"); + $input.click(); + }, /** * @private * @param {MouseEvent} ev @@ -439,6 +515,19 @@ var BasicActivity = AbstractField.extend({ }) .then(this._reload.bind(this, {activity: true})); }, + /** + * Unbind event triggered when a file is uploaded. + * + * @private + * @param {Array} activities: list of activity to unbind + */ + _unbindOnUploadAction: function (activities) { + _.each(activities, function (activity) { + if (activity.fileuploadID) { + $(window).off(activity.fileuploadID); + } + }); + }, }); // ----------------------------------------------------------------------------- @@ -457,11 +546,17 @@ var Activity = BasicActivity.extend({ this._super.apply(this, arguments); this._activities = this.record.specialData[this.name]; }, + /** + * @override + */ + destroy: function () { + this._unbindOnUploadAction(); + return this._super.apply(this, arguments); + }, //------------------------------------------------------------ // Private //------------------------------------------------------------ - /** * @private * @param {Object} fieldsToReload @@ -483,7 +578,7 @@ var Activity = BasicActivity.extend({ activity.note = ''; } }); - var activities = setDelayLabel(this._activities); + var activities = setFileUploadID(setDelayLabel(this._activities)); if (activities.length) { var nbActivities = _.countBy(activities, 'state'); this.$el.html(QWeb.render('mail.activity_items', { @@ -494,8 +589,12 @@ var Activity = BasicActivity.extend({ nbOverdueActivities: nbActivities.overdue, dateFormat: time.getLangDateFormat(), datetimeFormat: time.getLangDatetimeFormat(), + session: session, + widget: this, })); + this._bindOnUploadAction(this._activities); } else { + this._unbindOnUploadAction(this._activities); this.$el.empty(); } }, @@ -555,7 +654,13 @@ var KanbanActivity = BasicActivity.extend({ this.selection = selection; this._setState(record); }, - + /** + * @override + */ + destroy: function () { + this._unbindOnUploadAction(); + return this._super.apply(this, arguments); + }, //------------------------------------------------------------ // Private //------------------------------------------------------------ @@ -590,11 +695,14 @@ var KanbanActivity = BasicActivity.extend({ var self = this; this.$('.o_activity').html(QWeb.render('mail.KanbanActivityLoading')); return _readActivities(this, this.value.res_ids).then(function (activities) { + activities = setFileUploadID(activities); self.$('.o_activity').html(QWeb.render('mail.KanbanActivityDropdown', { selection: self.selection, records: _.groupBy(setDelayLabel(activities), 'state'), - uid: session.uid, + session: session, + widget: self, })); + self._bindOnUploadAction(activities); }); }, /** diff --git a/addons/mail/static/src/xml/activity.xml b/addons/mail/static/src/xml/activity.xml index d38afb5c799..dafad00b008 100644 --- a/addons/mail/static/src/xml/activity.xml +++ b/addons/mail/static/src/xml/activity.xml @@ -83,11 +83,24 @@ -
- +
+ Mark Done + + + Upload Document + + + + + /web/binary/upload_attachment + + + + + Edit diff --git a/addons/mail/static/src/xml/web_kanban_activity.xml b/addons/mail/static/src/xml/web_kanban_activity.xml index e7cf7be1daf..698091e9875 100644 --- a/addons/mail/static/src/xml/web_kanban_activity.xml +++ b/addons/mail/static/src/xml/web_kanban_activity.xml @@ -52,8 +52,8 @@ -
  • -
    +
  • +
  • diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index ca6c6877b7c..63f4f3bf53d 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -82,16 +82,27 @@ QUnit.module('Chatter', { type: 'selection', selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']], }, + activity_category: { + string: 'Category', + type: 'selection', + selection: [['default', 'Other'], ['upload_file', 'Upload File']], + }, note : { string: "Note", type: "char" }, }, }, 'mail.activity.type': { fields: { name: { string: "Name", type: "char" }, + category: { + string: 'Category', + type: 'selection', + selection: [['default', 'Other'], ['upload_file', 'Upload File']], + }, }, records: [ { id: 1, name: "Type 1" }, { id: 2, name: "Type 2" }, + { id: 3, name: "Type 3", category: 'upload_file' }, ], }, 'mail.message': { @@ -291,6 +302,79 @@ QUnit.test('Activity Done keep feedback on blur', function (assert) { }); }); +QUnit.test('Activity Done by uploading a file', function (assert) { + assert.expect(4); + + // simulate (shortcut) the upload and trigger the event when the + // attachment is created serverside. + testUtils.mock.patch(Activity, { + _onMarkActivityDoneUploadFile: function (ev) { + var $markDoneBtn = $(ev.currentTarget); + var fileuploadID = $markDoneBtn.data('fileupload-id'); + + $(window).trigger(fileuploadID, [{ + id:3, + name:"name2", + type:'binary', + mimetype:"application/x-msdos-program", + datas_fname:"file2.txt", + res_id: 5, + res_model: 'partner' + }]); + }, + }); + + // create the activity + this.data['mail.activity'].records = [{ + activity_type_id: 3, + id: 1, + can_write: true, + user_id: 2, + state: 'today', + note: "But I'm talkin' about Shaft", + activity_category: 'upload_file' + }]; + this.data.partner.records[0].activity_ids = [1]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + services: this.services, + res_id: 2, + arch:'
    ' + + '
    ' + + '' + + '
    ' + + '
    ', + mockRPC: function (route, args) { + if (args.method === 'action_feedback') { + var current_ids = this.data.partner.records[0].activity_ids; + var done_ids = args.args[0]; + this.data.partner.records[0].activity_ids = _.difference(current_ids, done_ids); + this.data.partner.records[0].activity_state = false; + return $.when(); + } + return this._super.apply(this, arguments); + }, + }); + + var $activity = form.$('.o_mail_activity[name=activity_ids]'); + assert.containsOnce($activity, '.o_thread_message', 'There should be one activity'); + assert.containsOnce($activity, '.o_hidden_input_file', 'There should be one hidden file upload form'); + assert.strictEqual($activity.find('.o_thread_message .o_thread_message_note').text().trim(), + 'But I\'m talkin\' about Shaft', 'The activity should have the right note'); + + // click on "upload" button + var $uploadBtn = $activity.find('.o_mark_as_done_upload_file'); + testUtils.dom.click($uploadBtn); + + assert.containsNone($activity, '.o_thread_message', 'The only activity should be marked as done'); + + testUtils.mock.unpatch(Activity); + form.destroy(); +}); + QUnit.test('attachmentBox basic rendering', function (assert) { assert.expect(19); this.data.partner.records.push({ diff --git a/addons/mail/views/mail_activity_views.xml b/addons/mail/views/mail_activity_views.xml index 0c1983beb71..8bca8e907b3 100644 --- a/addons/mail/views/mail_activity_views.xml +++ b/addons/mail/views/mail_activity_views.xml @@ -17,7 +17,7 @@
    - + @@ -37,7 +37,7 @@ + attrs="{'invisible': ['|', ('force_next', '=', True), ('category', '=', 'upload_file')]}" context="{'default_res_model_id': res_model_id}"/> - - - - mail.activity.type.form.inherit.note - mail.activity.type - - - - 0 - - - - \ No newline at end of file diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 4d90a1945e9..3b4a2df8281 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -182,7 +182,7 @@ class TestAdvMailPerformance(TransactionCase): #voip module read activity_type during create leading to one less query in enterprise on action_feedback category = activity.activity_type_id.category - with self.assertQueryCount(__system__=26, emp=50): # com runbot: 25 - 46 // test_mail only: 26 - 48 + with self.assertQueryCount(__system__=26, emp=51): # com runbot: 25 - 46 // test_mail only: 24 - 45 activity.action_feedback(feedback='Zizisse Done !') @users('__system__', 'emp') @@ -199,7 +199,7 @@ class TestAdvMailPerformance(TransactionCase): record.write({'name': 'Dupe write'}) - with self.assertQueryCount(__system__=28, emp=52): # com runbot: 27 - 86 // test_mail only: 28 - 50 + with self.assertQueryCount(__system__=28, emp=53): # com runbot: 27 - 86 // test_mail only: 26 - 47 record.action_close('Dupe feedback') self.assertEqual(record.activity_ids, self.env['mail.activity'])