diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js index e2c03e24d6a8819fa8a677e8271bbd78a36f71f6..9affc3f41cf12548337110efbe513d5198766501 100644 --- a/addons/mail/static/src/js/document_viewer.js +++ b/addons/mail/static/src/js/document_viewer.js @@ -13,7 +13,6 @@ 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', @@ -44,7 +43,7 @@ var DocumentViewer = Widget.extend({ init: function (parent, attachments, activeAttachmentID) { this._super.apply(this, arguments); this.attachment = _.filter(attachments, function (attachment) { - var match = attachment.type == 'url' ? attachment.url.match("(youtu|.png|.jpg|.gif)") : 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]; @@ -67,6 +66,7 @@ var DocumentViewer = Widget.extend({ } }); this.activeAttachment = _.findWhere(attachments, {id: activeAttachmentID}); + this.modelName = 'ir.attachment'; this._reset(); }, /** @@ -325,24 +325,6 @@ 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 bd9307bdd2fb0ebe6af3601938dfc2cc4f63e195..aa5131b0de5b80d7e174c3e895ccde7539592502 100644 --- a/addons/mail/static/src/scss/composer.scss +++ b/addons/mail/static/src/scss/composer.scss @@ -485,27 +485,6 @@ $o-mail-attachment-margin: 5px; .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; - } - } } } diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index fcfe8ba7de2c64a129f68ac0c0f08ecaeb3f1f65..914168306b8375d944961fa869c5863fbf73fd5f 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -35,6 +35,7 @@ --> <t t-name="DocumentViewer.Content"> <div class="o_viewer_content"> + <t t-set="model" t-value="widget.modelName"/> <div class="o_viewer-header"> <span class="o_image_caption"> <i class="fa fa-picture-o mr8" t-if="widget.activeAttachment.type == 'image'" role="img" aria-label="Image" title="Image"/> @@ -42,11 +43,6 @@ <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> @@ -55,12 +51,12 @@ <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&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}"/> + <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}&model=#{model}" alt="Viewer"/> + <iframe t-if="widget.activeAttachment.type == 'application/pdf'" class="mt32 o_viewer_pdf" t-attf-src="/web/static/lib/pdfjs/web/viewer.html?file=/web/content/#{widget.activeAttachment.id}?model%3D#{model}" /> + <iframe t-if="(widget.activeAttachment.type || '').indexOf('text') !== -1" class="mt32 o_viewer_text" t-attf-src="/web/content/#{widget.activeAttachment.id}?model%3D#{model}" /> + <iframe t-if="widget.activeAttachment.type == 'youtu'" class="mt32 o_viewer_text" 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"/> + <source t-attf-src="/web/image/#{widget.activeAttachment.id}?model=#{model}" t-att-data-type="widget.activeAttachment.mimetype"/> </video> </div> </div> @@ -86,7 +82,7 @@ --> <t t-name="DocumentViewer"> <div class="modal o_modal_fullscreen" tabindex="-1" data-keyboard="false" role="dialog"> - <t t-call="DocumentViewer.Content"/> + <t class="o_document_viewer_content_call" t-call="DocumentViewer.Content"/> <t t-if="widget.attachment.length !== 1"> <a class="arrow arrow-left move_previous" href="#"> diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 470b6045797b12ae7eb11bc8525396e4e6312cc9..ac9f715be5ddeedfcf231915e1fb663df884552f 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -1445,11 +1445,11 @@ QUnit.test('chatter: Attachment viewer', function (assert) { "image caption should have correct download link"); // click on first image attachement testUtils.dom.click(form.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first()); - assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/1?unique=1&signature=999"]').length, 1, + assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/1?unique=1&signature=999&model=ir.attachment"]').length, 1, "Modal popup should open with first image src"); // click on next button testUtils.dom.click($('.modal .arrow.arrow-right.move_next span')); - assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/2?unique=1&signature=999"]').length, 1, + assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/2?unique=1&signature=999&model=ir.attachment"]').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 index ef825f84e2a1353d1bbe96136f939ac7327bdfb2..87fa2dbed9427c119e812657cd727c87b7686ff5 100644 --- a/addons/mail/static/tests/document_viewer_tests.js +++ b/addons/mail/static/tests/document_viewer_tests.js @@ -19,16 +19,16 @@ var createViewer = function (params) { 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') { + if (route === '/web/static/lib/pdfjs/web/viewer.html?file=/web/content/1?model%3Dir.attachment') { return $.when(); } if (route === 'https://www.youtube.com/embed/FYqW0Gdwbzk') { return $.when(); } - if (route === '/web/content/4') { + if (route === '/web/content/4?model%3Dir.attachment') { return $.when(); } - if (route === '/web/image/6?unique=1&signature=999') { + if (route === '/web/image/6?unique=1&signature=999&model=ir.attachment') { return $.when(); } }; @@ -100,38 +100,6 @@ QUnit.module('DocumentViewer', { 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.containsOnce(viewer, '.o_page_number_input', - "pdf should have a page input"); - assert.containsOnce(viewer, '.o_remainder_input', - "pdf should have a remainder checkbox"); - assert.containsOnce(viewer, '.o_split_btn', - "pdf should have a split button"); - - testUtils.dom.click(viewer.$('.o_split_btn')); - - assert.ok(viewer.isDestroyed(), 'viewer should be destroyed'); - }); - QUnit.test('Document Viewer Youtube', function (assert) { assert.expect(3); @@ -165,7 +133,7 @@ QUnit.module('DocumentViewer', { assert.strictEqual(viewer.$(".o_image_caption:contains('text.html')").length, 1, "the viewer be on the right attachment"); - assert.containsOnce(viewer, 'iframe[data-src="/web/content/4"]', + assert.containsOnce(viewer, 'iframe[data-src="/web/content/4?model%3Dir.attachment"]', "there should be an iframe with the right src"); viewer.destroy(); @@ -197,7 +165,7 @@ QUnit.module('DocumentViewer', { assert.strictEqual(viewer.$(".o_image_caption:contains('image.jpg')").length, 1, "the viewer be on the right attachment"); - assert.containsOnce(viewer, 'img[data-src="/web/image/6?unique=1&signature=999"]', + assert.containsOnce(viewer, 'img[data-src="/web/image/6?unique=1&signature=999&model=ir.attachment"]', "there should be a video player"); viewer.destroy(); diff --git a/addons/mail/views/mail_message_views.xml b/addons/mail/views/mail_message_views.xml index d224fb2ccc047ee1f7c155f8a2f5645b3965f0d7..df7643f7f36f64e2776b9ce006603c8876e6df97 100644 --- a/addons/mail/views/mail_message_views.xml +++ b/addons/mail/views/mail_message_views.xml @@ -144,9 +144,7 @@ <field name="url"/> <field name="type"/> <field name="create_date"/> - <field name="active"/> <field name="name"/> - <field name="res_model_name"/> <field name="res_name"/> <templates> <t t-name="kanban-box"> @@ -155,7 +153,7 @@ <div class="o_kanban_image_wrapper"> <t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/> <div t-if="record.type.raw_value == 'url'" class="o_url_image fa fa-link fa-3x text-muted"/> - <img t-elif="webimage" t-attf-src="/web/image/#{record.id.raw_value}?field=thumbnail" width="100" height="100" alt="Document" class="o_attachment_image"/> + <img t-elif="webimage" t-attf-src="/web/image/#{record.id.raw_value}" width="100" height="100" alt="Document" class="o_attachment_image"/> <div t-else="!webimage" class="o_image o_image_thumbnail" t-att-data-mimetype="record.mimetype.value"/> </div> </div> @@ -170,13 +168,7 @@ </t> </div> <div class="o_kanban_record_body"> - <t t-if="record.res_model_name.raw_value"> - <strong><field name="res_model_name"/></strong> - <t t-if="record.res_name.raw_value"> - <span t-att-title="record.res_name.raw_value">: <field name="res_name"/></span> - </t> - </t> - <t t-elif="record.type.raw_value == 'url'"> + <t t-if="record.type.raw_value == 'url'"> <span class="o_document_url"><i class="fa fa-globe"/> <field name="url" widget="url"/></span> </t> <samp t-else="" class="text-muted"> </samp> diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py index 535605d9fe789078efb6fd59117ea333ecd61d8d..967f93a33e205c07885fadad4041900edd920d5d 100644 --- a/odoo/addons/base/models/ir_attachment.py +++ b/odoo/addons/base/models/ir_attachment.py @@ -6,17 +6,14 @@ import itertools import logging import mimetypes import os -import io import re from collections import defaultdict import uuid -from odoo import api, fields, models, tools, SUPERUSER_ID, exceptions, _ +from odoo import api, fields, models, tools, SUPERUSER_ID, _ 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__) @@ -46,14 +43,6 @@ class IrAttachment(models.Model): record = self.env[attachment.res_model].browse(attachment.res_id) attachment.res_name = record.display_name - @api.depends('res_model') - def _compute_res_model_name(self): - for record in self: - if record.res_model: - model = self.env['ir.model'].search([('model', '=', record.res_model)], limit=1) - if model: - record.res_model_name = model[0].name - @api.model def _storage(self): return self.env['ir.config_parameter'].sudo().get_param('ir_attachment.location', 'file') @@ -290,7 +279,6 @@ class IrAttachment(models.Model): description = fields.Text('Description') res_name = fields.Char('Resource Name', compute='_compute_res_name', store=True) res_model = fields.Char('Resource Model', readonly=True, help="The database object this attachment will be attached to.") - res_model_name = fields.Char(compute='_compute_res_model_name', store=True, index=True) res_field = fields.Char('Resource Field', readonly=True) res_id = fields.Integer('Resource ID', readonly=True, help="The record id this is attached to.") company_id = fields.Many2one('res.company', string='Company', change_default=True, @@ -312,8 +300,6 @@ class IrAttachment(models.Model): checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True) 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(readonly=1, attachment=True) @api.model_cr_context def _auto_init(self): @@ -449,18 +435,6 @@ 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) @@ -469,8 +443,6 @@ class IrAttachment(models.Model): vals.pop(field, False) if 'mimetype' in vals or 'datas' in vals: vals = self._check_contents(vals) - if all([not attachment.res_field for attachment in self]): - vals = self._make_thumbnail(vals) return super(IrAttachment, self).write(vals) @api.multi @@ -502,7 +474,6 @@ 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) @@ -518,87 +489,6 @@ class IrAttachment(models.Model): def action_get(self): return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment') - def _make_pdf(self, output, name_ext): - """ - :param output: PdfFileWriter object. - :param name_ext: the additional name of the new attachment (page count). - :return: the id of the attachment. - """ - 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=None, 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 = [] - if not pdf_groups: - pdf_groups = [] - 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) - self.write({'active': False}) - elif not len(remainder_set): - self.write({'active': False}) - return new_pdf_ids - - def split_pdf(self, indices=None, 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")) - if indices: - 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(pdf_groups=[[min(x), max(x)] for x in pages], remainder=remainder) - return self._split_pdf_groups(remainder=remainder) - @api.model def get_serve_attachment(self, url, extra_domain=None, extra_fields=None, order=None): domain = [('type', '=', 'binary'), ('url', '=', url)] + (extra_domain or []) diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index 63d12137d7a3a89874eb3421b5720de5b4473c56..5d6e84a59b3c0891416a54efaf7aaf892d8bbdb6 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -3,7 +3,6 @@ # ir_http modular http routing #---------------------------------------------------------- import base64 -import datetime import hashlib import logging import mimetypes @@ -23,7 +22,6 @@ from odoo.exceptions import AccessDenied, AccessError from odoo.http import request, STATIC_CACHE, content_disposition from odoo.tools import pycompat, consteq from odoo.tools.mimetypes import guess_mimetype -from ast import literal_eval from odoo.modules.module import get_resource_path, get_module_path _logger = logging.getLogger(__name__) @@ -260,6 +258,13 @@ class IrHttp(models.AbstractModel): """ return False + @classmethod + def _get_special_models(cls): + """ + :return: the set of models that have to pass through several specific fields checks: + used for the 'url' and the 'access_token' fields. + """ + return {'ir.attachment'} @classmethod def binary_content(cls, xmlid=None, model='ir.attachment', id=None, field='datas', @@ -290,6 +295,7 @@ class IrHttp(models.AbstractModel): :returns: (status, headers, content) """ env = env or request.env + attachment_models = cls._get_special_models() # get object and content obj = None if xmlid: @@ -301,7 +307,7 @@ class IrHttp(models.AbstractModel): return (404, [], None) # access token grant access - if model == 'ir.attachment' and access_token: + if model in attachment_models and access_token: obj = obj.sudo() if access_mode: if not cls._check_access_mode(env, id, access_mode, model, access_token=access_token, @@ -320,7 +326,7 @@ class IrHttp(models.AbstractModel): # attachment by url check module_resource_path = None - if model == 'ir.attachment' and obj.type == 'url' and obj.url: + if model in attachment_models and obj.type == 'url' and obj.url: url_match = re.match("^/(\w+)/(.+)$", obj.url) if url_match: module = url_match.group(1)