[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:
ThanhDodeurOdoo
2018-09-07 16:06:47 +02:00
co-authored by Pierre Paridans
parent 469d7acb89
commit 16aeecc73d
12 changed files with 441 additions and 146 deletions
+50 -8
View File
@@ -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
+34
View File
@@ -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%;
}
+8 -1
View File
@@ -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:&amp;nbsp;</label><input class="o_page_number_input" name="Indices" placeholder="e.g. 1-5, 7, 8-9"/>
<label for="remainder">All pages:&amp;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&amp;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>
+5 -2
View File
@@ -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');
});
});
});
+1
View File
@@ -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>
+5 -5
View File
@@ -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,
+2 -2
View File
@@ -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):
+91 -35
View File
@@ -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
+1 -11
View File
@@ -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
View File
@@ -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'}"/>