[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:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user