[FIX] web: reintroduce many2many_binary field widget

The field has been adapated to the new views.
This commit also introduces some tests.
This commit is contained in:
Martin Geubelle
2017-04-24 15:28:57 +02:00
parent eafa14d3bc
commit 9def2009ee
4 changed files with 291 additions and 21 deletions
@@ -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)
@@ -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,
+20 -21
View File
@@ -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>
@@ -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();
});
});
});
});