diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js
index fc8f9940d81..e2c03e24d6a 100644
--- a/addons/mail/static/src/js/document_viewer.js
+++ b/addons/mail/static/src/js/document_viewer.js
@@ -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
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index 9925813bb73..5fd0ca0e8af 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -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%;
}
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index 242e97449f5..78295a037c7 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -32,6 +32,11 @@
+
+
+
+
+
×
@@ -40,8 +45,10 @@
-
+
+
+
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index 6bfa25de433..37fb75572c3 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -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");
diff --git a/addons/mail/static/tests/document_viewer_tests.js b/addons/mail/static/tests/document_viewer_tests.js
new file mode 100644
index 00000000000..bbd2d9a6fa1
--- /dev/null
+++ b/addons/mail/static/tests/document_viewer_tests.js
@@ -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');
+ });
+});
+});
diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml
index 134b554c4b9..7e116200f43 100644
--- a/addons/mail/views/mail_templates.xml
+++ b/addons/mail/views/mail_templates.xml
@@ -107,6 +107,7 @@
+
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 1719b70f6f8..4577a21ddff 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -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,
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index e3eb27a9d77..5057dc8be1e 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -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):
diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py
index 62aa7fc82e6..d1019d800f5 100644
--- a/odoo/addons/base/models/ir_attachment.py
+++ b/odoo/addons/base/models/ir_attachment.py
@@ -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
diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py
index 71659aff1f5..292d2894a34 100644
--- a/odoo/addons/base/models/ir_http.py
+++ b/odoo/addons/base/models/ir_http.py
@@ -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
diff --git a/odoo/addons/base/tests/test_ir_http.py b/odoo/addons/base/tests/test_ir_http.py
index 00b32881cf6..313a86f308d 100644
--- a/odoo/addons/base/tests/test_ir_http.py
+++ b/odoo/addons/base/tests/test_ir_http.py
@@ -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'))
diff --git a/odoo/addons/base/views/ir_attachment_views.xml b/odoo/addons/base/views/ir_attachment_views.xml
index a954444169d..2cdfe812bd3 100644
--- a/odoo/addons/base/views/ir_attachment_views.xml
+++ b/odoo/addons/base/views/ir_attachment_views.xml
@@ -67,13 +67,13 @@
-
+
-
+