diff options
| author | Martin Geubelle <mge@odoo.com> | 2017-04-21 13:18:24 +0200 |
|---|---|---|
| committer | Martin Geubelle <mge@odoo.com> | 2017-04-24 15:28:57 +0200 |
| commit | 9def2009ee2233283e72d0aeaea1ba63aec7f827 (patch) | |
| tree | a2698a2f93bd34261c3fcb118ea582f38c036420 | |
| parent | eafa14d3bc16e7212000d0c9c30a3ed922395574 (diff) | |
[FIX] web: reintroduce many2many_binary field widget
The field has been adapated to the new views.
This commit also introduces some tests.
| -rw-r--r-- | addons/web/static/src/js/fields/field_registry.js | 1 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/relational_fields.js | 190 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 41 | ||||
| -rw-r--r-- | addons/web/static/tests/fields/relational_fields_tests.js | 80 |
4 files changed, 291 insertions, 21 deletions
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js index db2f547c469..41889fcfa85 100644 --- a/addons/web/static/src/js/fields/field_registry.js +++ b/addons/web/static/src/js/fields/field_registry.js @@ -57,6 +57,7 @@ registry .add('list.many2one', relational_fields.ListFieldMany2One) .add('kanban.many2one', relational_fields.KanbanFieldMany2One) .add('many2many', relational_fields.FieldMany2Many) + .add('many2many_binary', relational_fields.FieldMany2ManyBinaryMultiFiles) .add('many2many_tags', relational_fields.FieldMany2ManyTags) .add('form.many2many_tags', relational_fields.FormFieldMany2ManyTags) .add('kanban.many2many_tags', relational_fields.KanbanFieldMany2ManyTags) diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 995d8792cf4..ca5c584c47b 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -943,6 +943,195 @@ var FieldMany2Many = FieldX2Many.extend({ }, }); +/** + * Widget to upload or delete one or more files at the same time. + */ +var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({ + template: "FieldBinaryFileUploader", + supportedFieldTypes: ['many2many'], + fieldsToFetch: { + name: {type: 'char'}, + datas_fname: {type: 'char'}, + mimetype: {type: 'char'}, + }, + events: { + 'click .o_attach': '_onAttach', + 'click .oe_delete': '_onDelete', + 'change .o_form_input_file': '_onFileChanged', + }, + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + + if (this.field.type !== 'many2many' || this.field.relation !== 'ir.attachment') { + var msg = _t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."); + throw _.str.sprintf(msg, this.field.string); + } + + this.uploadingFiles = []; + this.fileupload_id = _.uniqueId('oe_fileupload_temp'); + $(window).on(this.fileupload_id, this._onFileLoaded.bind(this)); + + this.metadata = {}; + this._generatedMetadata(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Compute the URL of an attachment. + * + * @private + * @param {Object} attachment + * @returns {string} URL of the attachment + */ + _getFileUrl: function (attachment) { + return '/web/content/' + attachment.id + '?download=true'; + }, + /** + * Process the field data to add some information (url, etc.). + * + * @private + */ + _generatedMetadata: function () { + var self = this; + _.each(this.value.data, function (record) { + // attachments are tagged `allowUnlink` because only new attachments + // will be unlinked after deletion + self.metadata[record.id] = { + allowUnlink: false, + url: self._getFileUrl(record.data), + }; + }); + }, + /** + * @private + * @override + */ + _render: function () { + // render the attachments ; as the attachments will changes after each + // _setValue, we put the rendering here to ensure they will be updated + this.$('.oe_placeholder_files, .oe_attachments') + .replaceWith($(qweb.render('FieldBinaryFileUploader.files', { + widget: this, + }))); + this.$('.oe_fileupload').show(); + + // display image thumbnail + this.$('.o_image[data-mimetype^="image"]').each(function () { + var $img = $(this); + if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) { + $img.css('background-image', "url('" + $img.data('src') + "')"); + } + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onAttach: function () { + // This widget uses a hidden form to upload files. Clicking on 'Attach' + // will simulate a click on the related input. + this.$('.o_form_input_file').click(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onDelete: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + + var fileID = $(ev.currentTarget).data('id'); + var record = _.findWhere(this.value.data, {res_id: fileID}); + if (record) { + this._setValue({ + operation: 'REMOVE', + ids: [record.id], + }); + var metadata = this.metadata[record.id]; + if (!metadata || metadata.allowUnlink) { + this._rpc({ + model: 'ir.attachment', + method: 'unlink', + args: [record.res_id], + }); + } + } + }, + /** + * @private + * @param {Event} ev + */ + _onFileChanged: function (ev) { + var self = this; + ev.stopPropagation(); + + var files = ev.target.files; + var attachment_ids = this.value.res_ids; + + _.each(files, function (file) { + var record = _.find(self.value.data, function (attachment) { + return attachment.data.name === file.name; + }); + if (record) { + var metadata = self.metadata[record.id]; + if (!metadata || metadata.allowUnlink) { + // there is a existing attachment with the same name so we + // replace it + attachment_ids = _.without(attachment_ids, record.res_id); + self._rpc({ + model: 'ir.attachment', + method: 'unlink', + args: [record.res_id], + }); + } + } + self.uploadingFiles.push(file); + }); + + this._setValue({ + operation: 'REPLACE_WITH', + ids: attachment_ids, + }); + + this.$('form.o_form_binary_form').submit(); + this.$('.oe_fileupload').hide(); + }, + /** + * @private + */ + _onFileLoaded: function () { + var self = this; + // the first argument isn't a file but the jQuery.Event + var files = Array.prototype.slice.call(arguments, 1); + // files has been uploaded, clear uploading + this.uploadingFiles = []; + + var attachment_ids = this.value.res_ids; + _.each(files, function (file) { + if (file.error) { + self.do_warn(_t('Uploading Error'), file.error); + } else { + attachment_ids.push(file.id); + } + }); + + this._setValue({ + operation: 'REPLACE_WITH', + ids: attachment_ids, + }); + }, +}); + var FieldMany2ManyTags = AbstractField.extend({ tag_template: "FieldMany2ManyTag", className: "o_form_field o_form_field_many2manytags", @@ -1526,6 +1715,7 @@ return { FieldOne2Many: FieldOne2Many, FieldMany2Many: FieldMany2Many, + FieldMany2ManyBinaryMultiFiles: FieldMany2ManyBinaryMultiFiles, FieldMany2ManyCheckBoxes: FieldMany2ManyCheckBoxes, FieldMany2ManyTags: FieldMany2ManyTags, FormFieldMany2ManyTags: FormFieldMany2ManyTags, diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index ce4e8eeefd3..c044002a039 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -984,6 +984,7 @@ <span class="o_form_binary_progress">Uploading...</span> <t t-call="HiddenInputFile"> + <t t-set="image_only" t-value="true"/> <t t-set="fileupload_id" t-value="widget.fileupload_id"/> </t> </div> @@ -1031,49 +1032,47 @@ <input type="hidden" name="csrf_token" t-att-value="csrf_token"/> <input type="hidden" name="session_id" value="" t-if="widget.getSession().override_session"/> <input type="hidden" name="callback" t-att-value="fileupload_id"/> - <input t-if="widget.widget!='image'" type="file" class="o_form_input_file" name="ufile" t-att="{'multiple': multi_upload ? 'multiple' : null}"/> - <input t-if="widget.widget=='image'" type="file" class="o_form_input_file" name="ufile" accept="image/*"/> + <input t-if="widget.image_only" type="file" class="o_form_input_file" name="ufile" accept="image/*"/> + <input t-else="1" type="file" class="o_form_input_file" name="ufile" t-att="{'multiple': multi_upload ? 'multiple' : null}"/> <t t-raw="0"/> </form> <iframe t-att-id="fileupload_id" t-att-name="fileupload_id" style="display: none"/> </div> </t> -<div t-name="FieldBinaryFileUploader" t-attf-class="oe_fileupload #{widget.node.attrs.class ? widget.node.attrs.class :''}"> +<div t-name="FieldBinaryFileUploader" t-attf-class="oe_fileupload #{widget.attrs.class ? widget.attrs.class :''}"> <div class="oe_placeholder_files"/> - <div t-if="!widget.get('effective_readonly')" class="oe_add"> + <div t-if="widget.mode !== 'readonly'" class="oe_add"> <button class="btn btn-default o_attach"><span class="fa fa-paperclip"/> <t t-esc="widget.string"/></button> <t t-call="HiddenInputFile"> <t t-set="fileupload_id" t-value="widget.fileupload_id"/> <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t> <t t-set="multi_upload" t-value="true"/> - <input type="hidden" name="model" t-att-value="widget.view.model"/> + <input type="hidden" name="model" t-att-value="widget.model"/> <input type="hidden" name="id" value="0"/> - <input t-if="widget.getSession().override_session" type="hidden" name="session_id" t-att-value="widget.session.session_id"/> + <input t-if="widget.getSession().override_session" type="hidden" name="session_id" t-att-value="widget.getSession().session_id"/> </t> </div> </div> <div t-name="FieldBinaryFileUploader.files" class="oe_attachments"> - <t t-if="widget.get('effective_readonly')"> - <div t-foreach="widget.get('value')" t-as="id"> - <t t-set="file" t-value="widget.data[id]"/> - <a target="_blank" t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"><t t-raw="file.name || file.filename"/></a> + <t t-if="widget.mode === 'readonly'"> + <div t-foreach="widget.value.data" t-as="file"> + <a target="_blank" t-att-href="widget.metadata[file.id].url" t-attf-title="#{(file.data.name || file.data.filename) + (file.data.date?' \n('+file.data.date+')':'' )}"><t t-raw="file.data.name || file.data.filename"/></a> </div> </t> - <t t-if="!widget.get('effective_readonly')"> - <div t-foreach="values" t-as="id" class="oe_attachment"> - <t t-set="file" t-value="widget.data[id]"/> - <t t-if="!file.upload"> + <t t-else="1"> + <div t-foreach="widget.value.data" t-as="file" class="oe_attachment"> + <t t-if="!file.data.upload"> <div> - <a href="#" class="fa fa-times pull-right oe_delete" title="Delete this file" t-att-data-id="file.id"/> - <t t-raw="file.name || file.filename"/> + <a href="#" class="fa fa-times pull-right oe_delete" title="Delete this file" t-att-data-id="file.data.id"/> + <t t-raw="file.data.name || file.data.filename"/> </div> - <a class="o_image" target="_blank" t-att-href="file.url" - t-att-title="file.name" - t-att-data-mimetype="file.mimetype" - t-attf-data-src="/web/image/#{file.id}/100x80"/> + <a class="o_image" target="_blank" t-att-href="widget.metadata[file.id] ? widget.metadata[file.id].url : false" + t-att-title="file.data.name" + t-att-data-mimetype="file.data.mimetype" + t-attf-data-src="/web/image/#{file.data.id}/100x80"/> </t> </div> - <div t-foreach="widget.files_uploading" t-as="file" class="oe_attachment"> + <div t-foreach="widget.uploadingFiles" t-as="file" class="oe_attachment"> <div>Uploading...</div> <a t-att-title="file.name" t-att-name="file.name" class="o_image"> <i class="fa fa-spinner fa-spin fa-5x fa-fw"></i> diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 11433bff92c..47043fda90e 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -3954,6 +3954,86 @@ QUnit.module('relational_fields', { form.destroy(); }); + + QUnit.module('FieldMany2ManyBinaryMultiFiles'); + + QUnit.test('widget many2many_binary', function (assert) { + assert.expect(14); + this.data['ir.attachment'] = { + fields: { + name: {string:"Name", type: "char"}, + mimetype: {string: "Mimetype", type: "char"}, + }, + records: [{ + id: 17, + name: 'Marley&Me.jpg', + mimetype: 'jpg', + }], + }; + this.data.turtle.fields.picture_ids = { + string: "Pictures", + type: "many2many", + relation: 'ir.attachment', + }; + this.data.turtle.records[0].picture_ids = [17]; + + var form = createView({ + View: FormView, + model: 'turtle', + data: this.data, + arch:'<form string="Turtles">' + + '<group><field name="picture_ids" widget="many2many_binary"/></group>' + + '</form>', + archs: { + 'ir.attachment,false,list': '<tree string="Pictures"><field name="name"/></tree>', + }, + res_id: 1, + mockRPC: function (route, args) { + assert.step(route); + if (route === '/web/dataset/call_kw/ir.attachment/read') { + assert.deepEqual(args.args[1], ['name', 'datas_fname', 'mimetype']); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('div.o_form_field.oe_fileupload').length, 1, + "there should be the attachment widget"); + assert.strictEqual(form.$('div.o_form_field.oe_fileupload .oe_attachments').children().length, 1, + "there should be no attachment"); + assert.strictEqual(form.$('div.o_form_field.oe_fileupload .o_attach').length, 0, + "there should not be an Add button (readonly)"); + assert.strictEqual(form.$('div.o_form_field.oe_fileupload .oe_attachment .oe_delete').length, 0, + "there should not be a Delete button (readonly)"); + + // to edit mode + form.$buttons.find('.o_form_button_edit').click(); + assert.strictEqual(form.$('div.o_form_field.oe_fileupload .o_attach').length, 1, + "there should be an Add button"); + assert.strictEqual(form.$('div.o_form_field.oe_fileupload .o_attach').text().trim(), "Pictures", + "the button should be correctly named"); + assert.strictEqual(form.$('div.o_form_field.oe_fileupload .o_hidden_input_file form').length, 1, + "there should be a hidden form to upload attachments"); + + // TODO: add an attachment + // no idea how to test this + + // delete the attachment + form.$('div.o_form_field.oe_fileupload .oe_attachment .oe_delete').click(); + + + assert.verifySteps([ + '/web/dataset/call_kw/turtle/read', + '/web/dataset/call_kw/ir.attachment/read', + ]); + + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('div.o_form_field.oe_fileupload .oe_attachments').children().length, 0, + "there should be no attachment"); + + form.destroy(); + }); }); }); }); |
