[REF] base, mail: Documents (enterprise) related changes
-removed force_ext from ir.http.
-added signature arg to web/image to differentiate cache entries by URL.
-added an embed youtube viewer for youtube URL's
-added a PDF splitter to the PDF viewer UI
-added tests for mail's Document Viewer
-added a "signature" param to web/image to go around the browser cache
-thumbnail field of ir.attachment is now stored in the filestore
as a distinct attachment.
task 1853490
Co-authored-by: Pierre Paridans <app@odoo.com>
This commit is contained in:
co-authored by
Pierre Paridans
parent
469d7acb89
commit
16aeecc73d
@@ -13,6 +13,7 @@ var DocumentViewer = Widget.extend({
|
||||
template: "DocumentViewer",
|
||||
events: {
|
||||
'click .o_download_btn': '_onDownload',
|
||||
'click .o_split_btn': '_onSplitPDF',
|
||||
'click .o_viewer_img': '_onImageClicked',
|
||||
'click .o_viewer_video': '_onVideoClicked',
|
||||
'click .move_next': '_onNext',
|
||||
@@ -43,10 +44,25 @@ var DocumentViewer = Widget.extend({
|
||||
init: function (parent, attachments, activeAttachmentID) {
|
||||
this._super.apply(this, arguments);
|
||||
this.attachment = _.filter(attachments, function (attachment) {
|
||||
var match = attachment.mimetype.match("(image|video|application/pdf|text)");
|
||||
var match = attachment.type == 'url' ? attachment.url.match("(youtu|.png|.jpg|.gif)") : attachment.mimetype.match("(image|video|application/pdf|text)");
|
||||
|
||||
if (match) {
|
||||
attachment.type = match[1];
|
||||
if (match[1].match("(.png|.jpg|.gif)")) {
|
||||
attachment.type = 'image';
|
||||
}
|
||||
if (match[1] === 'youtu') {
|
||||
var youtube_array = attachment.url.split('/');
|
||||
var youtube_token = youtube_array[youtube_array.length-1];
|
||||
if (youtube_token.indexOf('watch') !== -1) {
|
||||
youtube_token = youtube_token.split('v=')[1];
|
||||
var amp = youtube_token.indexOf('&')
|
||||
if (amp !== -1){
|
||||
youtube_token = youtube_token.substring(0, amp);
|
||||
}
|
||||
}
|
||||
attachment.youtube = youtube_token;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -64,6 +80,18 @@ var DocumentViewer = Widget.extend({
|
||||
this.$('[data-toggle="tooltip"]').tooltip({delay: 0});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
if (this.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
this.trigger_up('document_viewer_closed');
|
||||
this.$el.modal('hide');
|
||||
this.$el.remove();
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -156,19 +184,15 @@ var DocumentViewer = Widget.extend({
|
||||
*/
|
||||
_onClose: function (e) {
|
||||
e.preventDefault();
|
||||
this.$el.modal('hide');
|
||||
this.trigger_up('document_viewer_closed');
|
||||
this.destroy();
|
||||
},
|
||||
/**
|
||||
* When popup close complete destroyed modal even DOM footprint too
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onDestroy: function () {
|
||||
if (this.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
this.$el.modal('hide');
|
||||
this.$el.remove();
|
||||
this.destroy();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -301,6 +325,24 @@ var DocumentViewer = Widget.extend({
|
||||
this._zoom(scale);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
*/
|
||||
_onSplitPDF: function (e) {
|
||||
e.stopPropagation();
|
||||
var self = this;
|
||||
var indices = $("input[name=Indices]").val();
|
||||
var remainder = $("input[type=checkbox][name=remainder]").is(":checked");
|
||||
this._rpc({
|
||||
model: 'ir.attachment',
|
||||
method: 'split_pdf',
|
||||
args: [this.activeAttachment.id, indices, remainder],
|
||||
}).always(function () {
|
||||
self.trigger_up('document_viewer_attachment_changed');
|
||||
self.destroy();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} e
|
||||
|
||||
@@ -452,6 +452,32 @@ $o-mail-attachment-margin: 5px;
|
||||
font-size: $h1-font-size;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.o_image_caption {
|
||||
bottom: 20%;
|
||||
position: absolute;
|
||||
|
||||
.o_split_pdf_area {
|
||||
background-color: #3a3a3a;
|
||||
margin: 5px;
|
||||
padding: 6px;
|
||||
margin-left: 50px;
|
||||
|
||||
.o_split_btn {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.o_page_number_input {
|
||||
width: 13%;
|
||||
color: black;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.o_remainder_input {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_loading_img {
|
||||
@@ -492,6 +518,14 @@ $o-mail-attachment-margin: 5px;
|
||||
box-shadow: 1px 1px 20px 1px #000;
|
||||
}
|
||||
|
||||
.o_viewer_text {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
box-shadow: 1px 1px 20px 1px #000;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.o_viewer_video {
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
<i class="fa fa-video-camera mr8" t-if="widget.activeAttachment.type == 'video'" role="img" aria-label="Video" title="Video"/>
|
||||
<t t-esc="widget.activeAttachment.name"/>
|
||||
<a role="button" href="#" class="o_download_btn ml8 small" data-toggle="tooltip" data-placement="right" title="Download"><i class="fa fa-fw fa-download" role="img" aria-label="Download"/></a>
|
||||
<span t-if="widget.activeAttachment.type == 'application/pdf'" class="o_split_pdf_area">
|
||||
<label for="Indices">Extract pages:&nbsp;</label><input class="o_page_number_input" name="Indices" placeholder="e.g. 1-5, 7, 8-9"/>
|
||||
<label for="remainder">All pages:&nbsp;</label><input class="o_remainder_input" type="checkbox" name="remainder" value="remainder"/>
|
||||
<button class="btn btn-sm btn-primary o_split_btn" data-toggle="tooltip" title="Split">Split</button>
|
||||
</span>
|
||||
</span>
|
||||
<a role="button" class="o_close_btn float-right" href="#" aria-label="Close" title="Close">×</a>
|
||||
</div>
|
||||
@@ -40,8 +45,10 @@
|
||||
<div t-if="widget.activeAttachment.type == 'image'" class="o_loading_img text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin text-gray-light fa-3x fa-fw" role="img" aria-label="Loading" title="Loading"/>
|
||||
</div>
|
||||
<img t-if="widget.activeAttachment.type == 'image'" class="o_viewer_img" t-attf-src="/web/image/#{widget.activeAttachment.id}?unique=1" alt="Viewer"/>
|
||||
<img t-if="widget.activeAttachment.type == 'image'" class="o_viewer_img" t-attf-src="/web/image/#{widget.activeAttachment.id}?unique=1&signature=#{widget.activeAttachment.checksum}" alt="Viewer"/>
|
||||
<iframe class="mt32 o_viewer_pdf" t-if="widget.activeAttachment.type == 'application/pdf'" t-attf-src="/web/static/lib/pdfjs/web/viewer.html?file=/web/content/#{widget.activeAttachment.id}" />
|
||||
<iframe class="mt32 o_viewer_text" t-if="(widget.activeAttachment.type || '').indexOf('text') !== -1" t-attf-src="/web/content/#{widget.activeAttachment.id}" />
|
||||
<iframe class="mt32 o_viewer_text" t-if="widget.activeAttachment.type == 'youtu'" allow="autoplay; encrypted-media" width="560" height="315" t-attf-src="https://www.youtube.com/embed/#{widget.activeAttachment.youtube}"/>
|
||||
<video t-if="widget.activeAttachment.type == 'video'" class="o_viewer_video" controls="controls">
|
||||
<source t-attf-src="/web/image/#{widget.activeAttachment.id}" t-att-data-type="widget.activeAttachment.mimetype"/>
|
||||
</video>
|
||||
|
||||
@@ -989,18 +989,21 @@ QUnit.test('chatter: Attachment viewer', function (assert) {
|
||||
attachment_ids: [{
|
||||
filename: 'image1.jpg',
|
||||
id:1,
|
||||
checksum: 999,
|
||||
mimetype: 'image/jpeg',
|
||||
name: 'Test Image 1',
|
||||
url: '/web/content/1?download=true'
|
||||
},{
|
||||
filename: 'image2.jpg',
|
||||
id:2,
|
||||
checksum: 999,
|
||||
mimetype: 'image/jpeg',
|
||||
name: 'Test Image 2',
|
||||
url: '/web/content/2?download=true'
|
||||
},{
|
||||
filename: 'image3.jpg',
|
||||
id:3,
|
||||
checksum: 999,
|
||||
mimetype: 'image/jpeg',
|
||||
name: 'Test Image 3',
|
||||
url: '/web/content/3?download=true'
|
||||
@@ -1051,11 +1054,11 @@ QUnit.test('chatter: Attachment viewer', function (assert) {
|
||||
"image caption should have correct download link");
|
||||
// click on first image attachement
|
||||
form.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first().click();
|
||||
assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/1?unique=1"]').length, 1,
|
||||
assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/1?unique=1&signature=999"]').length, 1,
|
||||
"Modal popup should open with first image src");
|
||||
// click on next button
|
||||
$('.modal .arrow.arrow-right.move_next span').click();
|
||||
assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/2?unique=1"]').length, 1,
|
||||
assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/2?unique=1&signature=999"]').length, 1,
|
||||
"Modal popup should have now second image src");
|
||||
assert.strictEqual($('.o_modal_fullscreen .o_viewer_toolbar .o_download_btn').length, 1,
|
||||
"Modal popup should have download button");
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
odoo.define('mail.document_viewer_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var DocumentViewer = require('mail.DocumentViewer');
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {Object[]} params.attachments
|
||||
* @param {int} params.attachmentID
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {boolean} [params.debug]
|
||||
* @returns {DocumentViewer}
|
||||
*/
|
||||
var createViewer = function (params) {
|
||||
var parent = new Widget();
|
||||
var viewer = new DocumentViewer(parent, params.attachments, params.attachmentID);
|
||||
|
||||
var mockRPC = function (route) {
|
||||
if (route === '/web/static/lib/pdfjs/web/viewer.html?file=/web/content/1') {
|
||||
return $.when();
|
||||
}
|
||||
if (route === 'https://www.youtube.com/embed/FYqW0Gdwbzk') {
|
||||
return $.when();
|
||||
}
|
||||
if (route === '/web/content/4') {
|
||||
return $.when();
|
||||
}
|
||||
if (route === '/web/image/6?unique=1&signature=999') {
|
||||
return $.when();
|
||||
}
|
||||
};
|
||||
testUtils.addMockEnvironment(parent, {
|
||||
mockRPC: function () {
|
||||
if (params.mockRPC) {
|
||||
var _super = this._super;
|
||||
this._super = mockRPC;
|
||||
var def = params.mockRPC.apply(this, arguments);
|
||||
this._super = _super;
|
||||
return def;
|
||||
} else {
|
||||
return mockRPC.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
intercepts: params.intercepts || {},
|
||||
});
|
||||
var $target = $("#qunit-fixture");
|
||||
if (params.debug) {
|
||||
$target = $('body');
|
||||
$target.addClass('debug');
|
||||
}
|
||||
viewer.appendTo($target);
|
||||
|
||||
// actually destroy the parent when the viewer is destroyed
|
||||
viewer.destroy = function () {
|
||||
delete viewer.destroy;
|
||||
parent.destroy();
|
||||
};
|
||||
return viewer;
|
||||
};
|
||||
|
||||
QUnit.module('DocumentViewer', {
|
||||
beforeEach: function () {
|
||||
this.attachments = [
|
||||
{id: 1, datas_fname: 'filePdf.pdf', type: 'binary', mimetype: 'application/pdf', datas:'R0lGOP////ywAADs='},
|
||||
{id: 2, name: 'urlYoutubeName', type: 'url', mimetype: '', url: 'https://youtu.be/FYqW0Gdwbzk', datas_fname: 'urlYoutube'},
|
||||
{id: 3, name: 'urlGoogle', type: 'url', mimetype: '', url: 'https://www.google.com', datas_fname: 'urlRandom'},
|
||||
{id: 4, name: 'text.html', datas_fname: 'text.html', type: 'binary', mimetype: 'text/html', datas:'testee'},
|
||||
{id: 5, name: 'video.mp4', datas_fname: 'video.mp4', type: 'binary', mimetype: 'video/mp4', datas:'R0lDOP////ywAADs='},
|
||||
{id: 6, name: 'image.jpg', datas_fname: 'image.jpg', type: 'binary', mimetype: 'image/jpeg', checksum: 999, datas:'R0lVOP////ywAADs='},
|
||||
];
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test('basic rendering', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var viewer = createViewer({
|
||||
attachmentID: 1,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$('.o_viewer_content').length, 1,
|
||||
"there should be a preview");
|
||||
assert.strictEqual(viewer.$('.o_close_btn').length, 1,
|
||||
"there should be a close button");
|
||||
assert.strictEqual(viewer.$('.o_viewer-header').length, 1,
|
||||
"there should be a header");
|
||||
assert.strictEqual(viewer.$('.o_image_caption').length, 1,
|
||||
"there should be an image caption");
|
||||
assert.strictEqual(viewer.$('.o_viewer_zoomer').length, 1,
|
||||
"there should be a zoomer");
|
||||
assert.strictEqual(viewer.$('.fa-chevron-right').length, 1,
|
||||
"there should be a right nav icon");
|
||||
assert.strictEqual(viewer.$('.fa-chevron-left').length, 1,
|
||||
"there should be a left nav icon");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer PDF', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var viewer = createViewer({
|
||||
attachmentID: 1,
|
||||
attachments: this.attachments,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'split_pdf') {
|
||||
assert.deepEqual(args.args, [1, "", false], "should have the right arguments");
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts: {
|
||||
document_viewer_attachment_changed: function () {
|
||||
assert.ok(true, "should trigger document_viewer_attachment_changed event");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$('.o_page_number_input').length, 1,
|
||||
"pdf should have a page input");
|
||||
assert.strictEqual(viewer.$('.o_remainder_input').length, 1,
|
||||
"pdf should have a remainder checkbox");
|
||||
assert.strictEqual(viewer.$('.o_split_btn').length, 1,
|
||||
"pdf should have a split button");
|
||||
|
||||
viewer.$('.o_split_btn').click();
|
||||
|
||||
assert.ok(viewer.isDestroyed(), 'viewer should be destroyed');
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer Youtube', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var youtubeURL = 'https://www.youtube.com/embed/FYqW0Gdwbzk';
|
||||
var viewer = createViewer({
|
||||
attachmentID: 2,
|
||||
attachments: this.attachments,
|
||||
mockRPC: function (route) {
|
||||
if (route === youtubeURL) {
|
||||
assert.ok(true, "should have called youtube URL");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('urlYoutubeName')").length, 1,
|
||||
"the viewer should be on the right attachment");
|
||||
assert.strictEqual(viewer.$('.o_viewer_text[data-src="' + youtubeURL + '"]').length, 1,
|
||||
"there should be a video player");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer html/(txt)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var viewer = createViewer({
|
||||
attachmentID: 4,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('text.html')").length, 1,
|
||||
"the viewer be on the right attachment");
|
||||
assert.strictEqual(viewer.$('iframe[data-src="/web/content/4"]').length, 1,
|
||||
"there should be an iframe with the right src");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer mp4', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var viewer = createViewer({
|
||||
attachmentID: 5,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('video.mp4')").length, 1,
|
||||
"the viewer be on the right attachment");
|
||||
assert.strictEqual(viewer.$('.o_viewer_video').length, 1,
|
||||
"there should be a video player");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Document Viewer jpg', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var viewer = createViewer({
|
||||
attachmentID: 6,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$(".o_image_caption:contains('image.jpg')").length, 1,
|
||||
"the viewer be on the right attachment");
|
||||
assert.strictEqual(viewer.$('img[data-src="/web/image/6?unique=1&signature=999"]').length, 1,
|
||||
"there should be a video player");
|
||||
|
||||
viewer.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('is closable by button', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var viewer = createViewer({
|
||||
attachmentID: 6,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$('.o_viewer_content').length, 1,
|
||||
"should have a document viewer");
|
||||
assert.strictEqual(viewer.$('.o_close_btn').length, 1,
|
||||
"should have a close button");
|
||||
|
||||
viewer.$('.o_close_btn').click();
|
||||
|
||||
assert.ok(viewer.isDestroyed(), 'viewer should be destroyed');
|
||||
});
|
||||
|
||||
QUnit.test('is closable by clicking on the wrapper', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var viewer = createViewer({
|
||||
attachmentID: 6,
|
||||
attachments: this.attachments,
|
||||
});
|
||||
|
||||
assert.strictEqual(viewer.$('.o_viewer_content').length, 1,
|
||||
"should have a document viewer");
|
||||
assert.strictEqual(viewer.$('.o_viewer_img_wrapper').length, 1,
|
||||
"should have a wrapper");
|
||||
|
||||
viewer.$('.o_viewer_img_wrapper').click();
|
||||
|
||||
assert.ok(viewer.isDestroyed(), 'viewer should be destroyed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -107,6 +107,7 @@
|
||||
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/mail_utils_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/discuss_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/document_viewer_tests.js"></script>
|
||||
<!-- systray -->
|
||||
<script type="text/javascript" src="/mail/static/tests/systray/systray_activity_menu_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/systray/systray_messaging_menu_tests.js"></script>
|
||||
|
||||
@@ -425,12 +425,12 @@ def xml2json_from_elementtree(el, preserve_whitespaces=False):
|
||||
def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', unique=False,
|
||||
filename=None, filename_field='datas_fname', download=False, mimetype=None,
|
||||
default_mimetype='application/octet-stream', share_id=None, share_token=None, access_token=None,
|
||||
force_ext=False, env=None):
|
||||
env=None):
|
||||
return request.registry['ir.http'].binary_content(
|
||||
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
|
||||
filename_field=filename_field, download=download, mimetype=mimetype,
|
||||
default_mimetype=default_mimetype, share_id=share_id, share_token=share_token, access_token=access_token,
|
||||
force_ext=force_ext, env=env)
|
||||
env=env)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Odoo Web web Controllers
|
||||
@@ -1008,11 +1008,11 @@ class Binary(http.Controller):
|
||||
def content_common(self, xmlid=None, model='ir.attachment', id=None, field='datas',
|
||||
filename=None, filename_field='datas_fname', unique=None, mimetype=None,
|
||||
download=None, data=None, token=None, share_id=None, share_token=None, access_token=None,
|
||||
force_ext=False, **kw):
|
||||
**kw):
|
||||
status, headers, content = binary_content(
|
||||
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
|
||||
filename_field=filename_field, download=download, mimetype=mimetype,
|
||||
access_token=access_token, share_id=share_id, share_token=share_token, force_ext=force_ext)
|
||||
access_token=access_token, share_id=share_id, share_token=share_token)
|
||||
if status == 304:
|
||||
response = werkzeug.wrappers.Response(status=status, headers=headers)
|
||||
elif status == 301:
|
||||
@@ -1047,7 +1047,7 @@ class Binary(http.Controller):
|
||||
def content_image(self, xmlid=None, model='ir.attachment', id=None, field='datas',
|
||||
filename_field='datas_fname', unique=None, filename=None, mimetype=None,
|
||||
download=None, width=0, height=0, crop=False, share_id=None, share_token=None, access_token=None, avoid_if_small=False,
|
||||
upper_limit=False):
|
||||
upper_limit=False, signature=False):
|
||||
status, headers, content = binary_content(
|
||||
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
|
||||
filename_field=filename_field, download=download, mimetype=mimetype,
|
||||
|
||||
@@ -236,7 +236,7 @@ class Http(models.AbstractModel):
|
||||
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas',
|
||||
unique=False, filename=None, filename_field='datas_fname', download=False,
|
||||
mimetype=None, default_mimetype='application/octet-stream',
|
||||
access_token=None, share_id=None, share_token=None, force_ext=False, env=None):
|
||||
access_token=None, share_id=None, share_token=None, env=None):
|
||||
env = env or request.env
|
||||
obj = None
|
||||
if xmlid:
|
||||
@@ -250,7 +250,7 @@ class Http(models.AbstractModel):
|
||||
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
|
||||
filename_field=filename_field, download=download, mimetype=mimetype,
|
||||
default_mimetype=default_mimetype, access_token=access_token, share_id=share_id, share_token=share_token,
|
||||
force_ext=force_ext, env=env)
|
||||
env=env)
|
||||
|
||||
@classmethod
|
||||
def _xmlid_to_obj(cls, env, xmlid):
|
||||
|
||||
@@ -16,6 +16,7 @@ from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo.tools import config, human_size, ustr, html_escape
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
from odoo.tools import crop_image, image_resize_image
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -312,17 +313,7 @@ class IrAttachment(models.Model):
|
||||
mimetype = fields.Char('Mime Type', readonly=True)
|
||||
index_content = fields.Text('Indexed Content', readonly=True, prefetch=False)
|
||||
active = fields.Boolean(default=True, string="Active", oldname='archived')
|
||||
thumbnail = fields.Binary(compute='_get_thumbnail', store=True, string="")
|
||||
|
||||
@api.depends('mimetype', 'file_size', 'checksum')
|
||||
def _get_thumbnail(self):
|
||||
for record in self:
|
||||
if record.mimetype:
|
||||
if re.match('image.*(gif|jpeg|jpg|png)', record.mimetype):
|
||||
if not record.thumbnail:
|
||||
temp_image = crop_image(record.datas, type='center', size=(100, 100), ratio=(1, 1))
|
||||
record.thumbnail = image_resize_image(base64_source=temp_image, size=(100, 100),
|
||||
encoding='base64', filetype='PNG')
|
||||
thumbnail = fields.Binary(readonly=1, attachment=True)
|
||||
|
||||
@api.model_cr_context
|
||||
def _auto_init(self):
|
||||
@@ -444,6 +435,18 @@ class IrAttachment(models.Model):
|
||||
self.check('read')
|
||||
return super(IrAttachment, self).read(fields, load=load)
|
||||
|
||||
def _make_thumbnail(self, vals):
|
||||
if vals.get('datas') and not vals.get('res_field'):
|
||||
vals['thumbnail'] = False
|
||||
if vals.get('mimetype') and re.match('image.*(gif|jpeg|jpg|png)', vals['mimetype']):
|
||||
try:
|
||||
temp_image = crop_image(vals['datas'], type='center', size=(80, 80), ratio=(1, 1))
|
||||
vals['thumbnail'] = image_resize_image(base64_source=temp_image, size=(80, 80),
|
||||
encoding='base64')
|
||||
except Exception:
|
||||
pass
|
||||
return vals
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
self.check('write', values=vals)
|
||||
@@ -452,6 +455,9 @@ class IrAttachment(models.Model):
|
||||
vals.pop(field, False)
|
||||
if 'mimetype' in vals or 'datas' in vals:
|
||||
vals = self._check_contents(vals)
|
||||
vals = self._make_thumbnail(vals)
|
||||
if all([not attachment.res_field for attachment in self]):
|
||||
vals = self._make_thumbnail(vals)
|
||||
return super(IrAttachment, self).write(vals)
|
||||
|
||||
@api.multi
|
||||
@@ -483,6 +489,7 @@ class IrAttachment(models.Model):
|
||||
for field in ('file_size', 'checksum'):
|
||||
values.pop(field, False)
|
||||
values = self._check_contents(values)
|
||||
values = self._make_thumbnail(values)
|
||||
self.browse().check('write', values=values)
|
||||
return super(IrAttachment, self).create(vals_list)
|
||||
|
||||
@@ -498,30 +505,79 @@ class IrAttachment(models.Model):
|
||||
def action_get(self):
|
||||
return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
|
||||
|
||||
@api.model
|
||||
def upload_attachment(self, attachments, custom_values=None):
|
||||
def _make_pdf(self, output, name_ext):
|
||||
"""
|
||||
directly uploads attachments.
|
||||
|
||||
:param attachments: array of dictionaries representing attachment data:
|
||||
eg: [{'name': 'doc.zip', 'data': 'data:application/zip;base64,R0lGODdhAQBADs='}]
|
||||
:param custom_values: additional values for the attachment create dictionary.
|
||||
:return: the ids of the new attachments.
|
||||
:param output: PdfFileWriter object.
|
||||
:param name_ext: the additional name of the new attachment (page count).
|
||||
:return: the id of the attachment.
|
||||
"""
|
||||
if not custom_values:
|
||||
custom_values = {}
|
||||
ids = []
|
||||
for attachment in attachments:
|
||||
data = attachment['data']
|
||||
raw_name = attachment['name']
|
||||
values = {
|
||||
'mimetype': data[data.find(':') + 1:data.find(';')],
|
||||
'name': raw_name[:raw_name.rfind('.')],
|
||||
'datas': data[data.find(',') + 1:],
|
||||
'datas_fname': raw_name,
|
||||
}
|
||||
self.ensure_one()
|
||||
try:
|
||||
stream = io.BytesIO()
|
||||
output.write(stream)
|
||||
return self.copy({
|
||||
'name': self.name+'-'+name_ext,
|
||||
'datas_fname': os.path.splitext(self.datas_fname or self.name)[0]+'-'+name_ext+".pdf",
|
||||
'datas': base64.b64encode(stream.getvalue()),
|
||||
})
|
||||
except Exception:
|
||||
raise Exception
|
||||
|
||||
def _split_pdf_groups(self, pdf_groups, remainder=False):
|
||||
"""
|
||||
calls _make_pdf to create the a new attachment for each page section.
|
||||
:param pdf_groups: a list of lists representing the pages to split: pages = [[1,1], [4,5], [7,7]]
|
||||
:returns the list of the ID's of the new PDF attachments.
|
||||
|
||||
"""
|
||||
self.ensure_one()
|
||||
with io.BytesIO(base64.b64decode(self.datas)) as stream:
|
||||
try:
|
||||
input_pdf = PdfFileReader(stream)
|
||||
except Exception:
|
||||
raise exceptions.ValidationError(_("ERROR: Invalid PDF file!"))
|
||||
max_page = input_pdf.getNumPages()
|
||||
remainder_set = set(range(0, max_page))
|
||||
new_pdf_ids = []
|
||||
for pages in pdf_groups:
|
||||
pages[1] = min(max_page, pages[1])
|
||||
pages[0] = min(max_page, pages[0])
|
||||
if pages[0] == pages[1]:
|
||||
name_ext = "%s" % (pages[0],)
|
||||
else:
|
||||
name_ext = "%s-%s" % (pages[0], pages[1])
|
||||
output = PdfFileWriter()
|
||||
for i in range(pages[0]-1, pages[1]):
|
||||
output.addPage(input_pdf.getPage(i))
|
||||
new_pdf_id = self._make_pdf(output, name_ext)
|
||||
new_pdf_ids.append(new_pdf_id)
|
||||
remainder_set = remainder_set.difference(set(range(pages[0] - 1, pages[1])))
|
||||
if remainder:
|
||||
for i in remainder_set:
|
||||
output_page = PdfFileWriter()
|
||||
name_ext = "%s" % (i + 1,)
|
||||
output_page.addPage(input_pdf.getPage(i))
|
||||
new_pdf_id = self._make_pdf(output_page, name_ext)
|
||||
new_pdf_ids.append(new_pdf_id)
|
||||
return new_pdf_ids
|
||||
|
||||
def split_pdf(self, indices, remainder=False):
|
||||
"""
|
||||
called by the Document Viewer's Split PDF button.
|
||||
evaluates the input string and turns it into a list of lists to be processed by _split_pdf_groups
|
||||
|
||||
:param indices: the formatted string of pdf split (e.g. 1,5-10, 8-22, 29-34) o_page_number_input
|
||||
:param remainder: bool, if true splits the non specified pages, one by one. form checkbox o_remainder_input
|
||||
:returns the list of the ID's of the newly created pdf attachments.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if 'pdf' not in self.mimetype:
|
||||
raise exceptions.ValidationError(_("ERROR: the file must be a PDF"))
|
||||
try:
|
||||
pages = [[int(x) for x in x.split('-')] for x in indices.split(',')]
|
||||
except ValueError:
|
||||
raise exceptions.ValidationError(_("ERROR: Invalid list of pages to split. Example: 1,5-9,10"))
|
||||
return self._split_pdf_groups([[min(x), max(x)] for x in pages], remainder=remainder)
|
||||
|
||||
|
||||
|
||||
values.update(custom_values)
|
||||
created_attachment = self.env['ir.attachment'].create(values)
|
||||
ids.append(created_attachment.id)
|
||||
return ids
|
||||
|
||||
@@ -252,7 +252,7 @@ class IrHttp(models.AbstractModel):
|
||||
def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas',
|
||||
unique=False, filename=None, filename_field='datas_fname', download=False,
|
||||
mimetype=None, default_mimetype='application/octet-stream',
|
||||
access_token=None, share_id=None, share_token=None, force_ext=False, env=None):
|
||||
access_token=None, share_id=None, share_token=None, env=None):
|
||||
""" Get file, attachment or downloadable content
|
||||
|
||||
If the ``xmlid`` and ``id`` parameter is omitted, fetches the default value for the
|
||||
@@ -273,8 +273,6 @@ class IrHttp(models.AbstractModel):
|
||||
:param str default_mimetype: default mintype if no mintype found
|
||||
:param str access_token: optional token for unauthenticated access
|
||||
only available for ir.attachment
|
||||
:param bool force_ext: if true, adds the extension to the filename
|
||||
that corresponds to the mimetype
|
||||
:param Environment env: by default use request.env
|
||||
:returns: (status, headers, content)
|
||||
"""
|
||||
@@ -366,14 +364,6 @@ class IrHttp(models.AbstractModel):
|
||||
if not mimetype:
|
||||
mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype)
|
||||
|
||||
if force_ext and (mimetype != default_mimetype):
|
||||
dot_index = filename.rfind('.')
|
||||
if dot_index > -1:
|
||||
if mimetypes.guess_extension(mimetype) != filename[dot_index:]:
|
||||
filename = filename[:dot_index] + mimetypes.guess_extension(mimetype)
|
||||
else:
|
||||
filename = filename + mimetypes.guess_extension(mimetype)
|
||||
|
||||
headers += [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')]
|
||||
|
||||
# cache
|
||||
|
||||
@@ -80,83 +80,3 @@ class test_ir_http_mimetype(common.TransactionCase):
|
||||
mimetype = dict(headers).get('Content-Type')
|
||||
self.assertEqual(mimetype, 'image/gif')
|
||||
|
||||
def test_ir_http_binary_content_force_ext_for_wrong_ext(self):
|
||||
""" Test force mimetype for wrong extension when renamed """
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'datas': GIF,
|
||||
'name': 'Test mimetype gif',
|
||||
'datas_fname': 'file.gif'})
|
||||
|
||||
attachment.datas_fname = 'file.png'
|
||||
|
||||
status, headers, content = self.env['ir.http'].binary_content(
|
||||
id=attachment.id,
|
||||
mimetype=None,
|
||||
default_mimetype='application/octet-stream',
|
||||
env=self.env,
|
||||
download=True,
|
||||
force_ext=True,
|
||||
)
|
||||
disposition = dict(headers).get('Content-Disposition')
|
||||
self.assertTrue(disposition.endswith('file.gif'))
|
||||
|
||||
def test_ir_http_binary_content_force_ext_for_right_ext(self):
|
||||
""" Test force mimetype for right extension when renamed """
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'datas': GIF,
|
||||
'name': 'Test mimetype gif',
|
||||
'datas_fname': 'file.gif'})
|
||||
|
||||
attachment.datas_fname = 'file.gif'
|
||||
|
||||
status, headers, content = self.env['ir.http'].binary_content(
|
||||
id=attachment.id,
|
||||
mimetype=None,
|
||||
default_mimetype='application/octet-stream',
|
||||
env=self.env,
|
||||
download=True,
|
||||
force_ext=True,
|
||||
)
|
||||
disposition = dict(headers).get('Content-Disposition')
|
||||
self.assertTrue(disposition.endswith('file.gif'))
|
||||
|
||||
def test_ir_http_binary_content_force_ext_without_ext(self):
|
||||
""" Test force mimetype for right extension when renamed whith filename without extension """
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'datas': GIF,
|
||||
'name': 'Test mimetype gif',
|
||||
'datas_fname': 'file.gif'})
|
||||
|
||||
attachment.datas_fname = 'file'
|
||||
|
||||
status, headers, content = self.env['ir.http'].binary_content(
|
||||
id=attachment.id,
|
||||
mimetype=None,
|
||||
default_mimetype='application/octet-stream',
|
||||
env=self.env,
|
||||
download=True,
|
||||
force_ext=True,
|
||||
)
|
||||
disposition = dict(headers).get('Content-Disposition')
|
||||
self.assertTrue(disposition.endswith('file.gif'))
|
||||
|
||||
def test_ir_http_binary_content_force_ext_for_binary_mimetype(self):
|
||||
""" Test force mimetype for wrong extension when renamed and mimetyp is 'application/octet-stream' """
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'datas': GIF,
|
||||
'name': 'Test mimetype exe',
|
||||
'datas_fname': 'file.exe',
|
||||
})
|
||||
attachment.mimetype = 'application/octet-stream'
|
||||
attachment.datas_fname = 'file.txt'
|
||||
|
||||
status, headers, content = self.env['ir.http'].binary_content(
|
||||
id=attachment.id,
|
||||
mimetype=None,
|
||||
default_mimetype='application/octet-stream',
|
||||
env=self.env,
|
||||
download=True,
|
||||
force_ext=True,
|
||||
)
|
||||
disposition = dict(headers).get('Content-Disposition')
|
||||
self.assertTrue(disposition.endswith('file.txt'))
|
||||
|
||||
@@ -67,13 +67,13 @@
|
||||
<field name="name" filter_domain="['|', ('name','ilike',self), ('datas_fname','ilike',self)]" string="Attachment"/>
|
||||
<field name="create_date"/>
|
||||
<filter name="url_filter" string="URL" domain="[('type','=','url')]"/>
|
||||
<filter name="binary_filter" string="Binary" domain="[('type','=','binary')]"/>
|
||||
<filter name="binary_filter" string="Stored" domain="[('type','=','binary')]"/>
|
||||
<separator/>
|
||||
<filter name="my_documents_filter"
|
||||
string="My Document(s)"
|
||||
domain="[('create_uid','=',uid)]"
|
||||
help="Filter on my documents"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="create_uid" string="Created by"/>
|
||||
<field name="type"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="owner" string="Owner" domain="[]" context="{'group_by':'create_uid'}"/>
|
||||
|
||||
Reference in New Issue
Block a user