[IMP] base,mail,web: attachments adaptation for documents
This commit is the counter-part of an enteprise commit introducing
the Documents app.
Here is a summary of what has been done:
- tweak unlink of ir_attachment to prevent unlink recursivity
(when attachments are attached to ir_attachments)
- improve ir_attachment kanban view
- add several arguments to binary_content controller:
- 'force_ext': to force the extension in the filename, base on
mimetype
- 'share_token' and 'share_id': to autorize download from a
share link
- add 'thumbnail' field on ir_attachment to optimize Kanban view
- add 'upper_limit' argument to image to allow to bypass the
500*500 size limit
- DocumentViewer now handles text files
More information available on task 1853490
Co-authored-by: Pierre Paridans <app@odoo.com>
Co-authored-by: sri-odoo <sri@odoo.com>
Co-authored-by: ThanhDodeurOdoo <tso@odoo.com>
This commit is contained in:
committed by
Aaron Bohy
co-authored by
Pierre Paridans
sri-odoo
parent
ad2e2cc8b5
commit
60aa9ffd01
@@ -964,6 +964,8 @@ class Message(models.Model):
|
||||
def unlink(self):
|
||||
# cascade-delete attachments that are directly attached to the message (should only happen
|
||||
# for mail.messages that act as parent for a standalone mail.mail record).
|
||||
if not self:
|
||||
return True
|
||||
self.check_access_rule('unlink')
|
||||
self.mapped('attachment_ids').filtered(
|
||||
lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0)
|
||||
|
||||
@@ -320,6 +320,8 @@ class MailThread(models.AbstractModel):
|
||||
def unlink(self):
|
||||
""" Override unlink to delete messages and followers. This cannot be
|
||||
cascaded, because link is done through (res_model, res_id). """
|
||||
if not self:
|
||||
return True
|
||||
self.env['mail.message'].search([('model', '=', self._name), ('res_id', 'in', self.ids)]).unlink()
|
||||
res = super(MailThread, self).unlink()
|
||||
self.env['mail.followers'].sudo().search(
|
||||
|
||||
@@ -43,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.mimetype.match("(image|video|application/pdf)");
|
||||
var match = attachment.mimetype.match("(image|video|application/pdf|text)");
|
||||
|
||||
if (match) {
|
||||
attachment.type = match[1];
|
||||
@@ -116,7 +116,7 @@ var DocumentViewer = Widget.extend({
|
||||
* @param {float} angle
|
||||
*/
|
||||
_getTransform: function(scale, angle) {
|
||||
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)'
|
||||
return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)';
|
||||
},
|
||||
/**
|
||||
* Rotate image clockwise by provided angle
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
$o-kanban-attachement-image-size: 80px;
|
||||
|
||||
.o_kanban_view {
|
||||
align-items: flex-start;
|
||||
|
||||
.o_kanban_record.o_kanban_attachment {
|
||||
padding: 0;
|
||||
|
||||
.o_kanban_image {
|
||||
width: $o-kanban-attachement-image-size;
|
||||
|
||||
+ div {
|
||||
padding-left: $o-kanban-attachement-image-size + $o-kanban-inside-hgutter;
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding-left: $o-kanban-attachement-image-size + $o-kanban-inside-hgutter-mobile;
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_image_wrapper {
|
||||
min-height: $o-kanban-attachement-image-size;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.o_attachment_image {
|
||||
@include size($o-kanban-attachement-image-size);
|
||||
}
|
||||
|
||||
.o_image {
|
||||
@include size($o-kanban-attachement-image-size*0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_details {
|
||||
.o_kanban_details_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: $o-kanban-attachement-image-size;
|
||||
padding: $o-kanban-inside-vgutter $o-kanban-inside-hgutter;
|
||||
|
||||
.o_kanban_record_title {
|
||||
margin-bottom: $o-kanban-inside-vgutter*0.5;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.o_kanban_record_body {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.oe_kanban_avatar {
|
||||
border-radius: 4px;
|
||||
border: 1px solid $component-active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_message_search"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Add menu entry in Settings/Email -->
|
||||
<menuitem name="Messages" id="menu_mail_message" parent="base.menu_email" action="action_view_mail_message"/>
|
||||
|
||||
@@ -140,23 +140,54 @@
|
||||
<field name="mimetype"/>
|
||||
<field name="type"/>
|
||||
<field name="datas_fname"/>
|
||||
<field name="create_uid"/>
|
||||
<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">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="oe_kanban_vignette">
|
||||
<div class="oe_attachment">
|
||||
<div class="oe_name">
|
||||
<field name="name"/>
|
||||
<div class="oe_kanban_global_area o_kanban_attachment">
|
||||
<div class="o_kanban_image">
|
||||
<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"/>
|
||||
<div t-else="!webimage" class="o_image o_image_thumbnail" t-att-data-mimetype="record.mimetype.value"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_details">
|
||||
<div class="o_kanban_details_wrapper">
|
||||
<div t-att-title="record.datas_fname.raw_value" class="o_kanban_record_title">
|
||||
<t t-if="record.type.raw_value == 'url'">
|
||||
<field name="name" class="o_text_overflow"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<field name="datas_fname" class="o_text_overflow"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="record.type.raw_value == 'binary'">
|
||||
<a t-attf-href="/web/content/#{record.id.raw_value}?download=1">
|
||||
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/>
|
||||
<div t-if="!webimage" class="o_image" t-att-data-mimetype="record.mimetype.value"/>
|
||||
<div t-if="webimage" class="o_attachment_image">
|
||||
<img t-attf-src="/web/image/#{record.id.raw_value}?download=1" alt="Download"/>
|
||||
</div>
|
||||
</a>
|
||||
<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'">
|
||||
<span class="o_document_url"><i class="fa fa-globe"/> <field name="url" widget="url"/></span>
|
||||
</t>
|
||||
<samp t-else="" class="text-muted"> </samp>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<time class="oe_kanban_bottom_left">
|
||||
<field name="create_date" widget="date"/>
|
||||
</time>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<img t-att-src="kanban_image('res.users', 'image_small', record.create_uid.raw_value)" t-att-data-member_id="record.create_uid.raw_value" t-att-alt="record.create_uid.raw_value" width="32" height="32" class="oe_kanban_avatar"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/systray.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/mail_activity.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/activity_view.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/kanban_view.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -424,11 +424,13 @@ 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', access_token=None, env=None):
|
||||
default_mimetype='application/octet-stream', share_id=None, share_token=None, access_token=None,
|
||||
force_ext=False, 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, access_token=access_token, env=env)
|
||||
default_mimetype=default_mimetype, share_id=share_id, share_token=share_token, access_token=access_token,
|
||||
force_ext=force_ext, env=env)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Odoo Web web Controllers
|
||||
@@ -1004,11 +1006,12 @@ class Binary(http.Controller):
|
||||
'/web/content/<string:model>/<int:id>/<string:field>/<string:filename>'], type='http', auth="public")
|
||||
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, access_token=None, **kw):
|
||||
download=None, data=None, token=None, share_id=None, share_token=None, access_token=None,
|
||||
force_ext=False, **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)
|
||||
access_token=access_token, share_id=share_id, share_token=share_token, force_ext=force_ext)
|
||||
if status == 304:
|
||||
response = werkzeug.wrappers.Response(status=status, headers=headers)
|
||||
elif status == 301:
|
||||
@@ -1042,11 +1045,12 @@ class Binary(http.Controller):
|
||||
'/web/image/<int:id>-<string:unique>/<int:width>x<int:height>/<string:filename>'], type='http', auth="public")
|
||||
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, access_token=None, avoid_if_small=False):
|
||||
download=None, width=0, height=0, crop=False, share_id=None, share_token=None, access_token=None, avoid_if_small=False,
|
||||
upper_limit=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,
|
||||
default_mimetype='image/png', access_token=access_token)
|
||||
default_mimetype='image/png', share_id=share_id, share_token=share_token, access_token=access_token)
|
||||
if status == 304:
|
||||
return werkzeug.wrappers.Response(status=304, headers=headers)
|
||||
elif status == 301:
|
||||
@@ -1065,12 +1069,15 @@ class Binary(http.Controller):
|
||||
content = crop_image(content, type='center', size=(width, height), ratio=(1, 1))
|
||||
|
||||
elif content and (width or height):
|
||||
# resize maximum 500*500
|
||||
if width > 500:
|
||||
width = 500
|
||||
if height > 500:
|
||||
height = 500
|
||||
content = odoo.tools.image_resize_image(base64_source=content, size=(width or None, height or None), encoding='base64', filetype='PNG', avoid_if_small=avoid_if_small)
|
||||
if not upper_limit:
|
||||
# resize maximum 500*500
|
||||
if width > 500:
|
||||
width = 500
|
||||
if height > 500:
|
||||
height = 500
|
||||
content = odoo.tools.image_resize_image(base64_source=content, size=(width or None, height or None),
|
||||
encoding='base64', filetype='PNG', upper_limit=upper_limit,
|
||||
avoid_if_small=avoid_if_small)
|
||||
# resize force png as filetype
|
||||
headers = self.force_contenttype(headers, contenttype='image/png')
|
||||
|
||||
|
||||
@@ -225,8 +225,20 @@
|
||||
}
|
||||
|
||||
.o_attachment_image > img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
padding-top: 75%;
|
||||
|
||||
.o_attachment_image {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
transform: scale(1.05);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&:hover .o_attachment_image {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.o_progressbar {
|
||||
|
||||
@@ -235,7 +235,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, env=None):
|
||||
access_token=None, share_id=None, share_token=None, force_ext=False, env=None):
|
||||
env = env or request.env
|
||||
obj = None
|
||||
if xmlid:
|
||||
@@ -248,7 +248,8 @@ class Http(models.AbstractModel):
|
||||
return super(Http, cls).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, access_token=access_token, env=env)
|
||||
default_mimetype=default_mimetype, access_token=access_token, share_id=share_id, share_token=share_token,
|
||||
force_ext=force_ext, env=env)
|
||||
|
||||
|
||||
class ModelConverter(ModelConverter):
|
||||
|
||||
@@ -6,14 +6,16 @@ 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, _
|
||||
from odoo import api, fields, models, tools, SUPERUSER_ID, exceptions, _
|
||||
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
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,6 +44,14 @@ 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')
|
||||
@@ -272,15 +282,16 @@ class IrAttachment(models.Model):
|
||||
"""
|
||||
return ['base.group_system']
|
||||
|
||||
name = fields.Char('Attachment Name', required=True)
|
||||
datas_fname = fields.Char('File Name')
|
||||
name = fields.Char('Name', required=True)
|
||||
datas_fname = fields.Char('Filename')
|
||||
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.")
|
||||
create_date = fields.Datetime('Date Created', readonly=True)
|
||||
create_uid = fields.Many2one('res.users', string='Owner', readonly=True)
|
||||
create_uid = fields.Many2one('res.users', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', change_default=True,
|
||||
default=lambda self: self.env['res.company']._company_default_get('ir.attachment'))
|
||||
type = fields.Selection([('url', 'URL'), ('binary', 'File')],
|
||||
@@ -300,6 +311,18 @@ 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(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')
|
||||
|
||||
@api.model_cr_context
|
||||
def _auto_init(self):
|
||||
@@ -438,6 +461,8 @@ class IrAttachment(models.Model):
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
if not self:
|
||||
return True
|
||||
self.check('unlink')
|
||||
|
||||
# First delete in the database, *then* in the filesystem if the
|
||||
@@ -472,3 +497,31 @@ class IrAttachment(models.Model):
|
||||
@api.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):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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,
|
||||
}
|
||||
|
||||
values.update(custom_values)
|
||||
created_attachment = self.env['ir.attachment'].create(values)
|
||||
ids.append(created_attachment.id)
|
||||
return ids
|
||||
|
||||
@@ -23,6 +23,7 @@ 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__)
|
||||
@@ -246,7 +247,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, env=None):
|
||||
access_token=None, share_id=None, share_token=None, force_ext=False, env=None):
|
||||
""" Get file, attachment or downloadable content
|
||||
|
||||
If the ``xmlid`` and ``id`` parameter is omitted, fetches the default value for the
|
||||
@@ -262,9 +263,13 @@ class IrHttp(models.AbstractModel):
|
||||
:param str filename_field: if not create an filename with model-id-field
|
||||
:param bool download: apply headers to download the file
|
||||
:param str mimetype: mintype of the field (for headers)
|
||||
:param share_id: the id of the documents.share that contains the attachment
|
||||
:param share_token: the token of the documents.share that contains the attachment
|
||||
: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)
|
||||
"""
|
||||
@@ -277,6 +282,24 @@ class IrHttp(models.AbstractModel):
|
||||
obj = env[model].sudo().browse(int(id))
|
||||
if not consteq(obj.access_token, access_token):
|
||||
return (403, [], None)
|
||||
elif id and share_id and share_token:
|
||||
share = env['documents.share'].sudo().browse(int(share_id))
|
||||
if share:
|
||||
if share.state == 'expired':
|
||||
return (403, [], None)
|
||||
if not consteq(share.access_token, share_token):
|
||||
return (403, [], None)
|
||||
elif share.type == 'ids' and (id in share.attachment_ids.ids):
|
||||
obj = env[model].sudo().browse(int(id))
|
||||
elif share.type == 'domain':
|
||||
obj = env[model].sudo().browse(int(id))
|
||||
share_domain = []
|
||||
if share.domain:
|
||||
share_domain = literal_eval(share.domain)
|
||||
domain = [['folder_id', '=', share.folder_id.id]] + share_domain
|
||||
attachments_check = http.request.env['ir.attachment'].sudo().search(domain)
|
||||
if obj not in attachments_check:
|
||||
return (403, [], None)
|
||||
elif id and model in env.registry:
|
||||
obj = env[model].browse(int(id))
|
||||
|
||||
@@ -338,6 +361,14 @@ 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
|
||||
|
||||
@@ -79,3 +79,84 @@ 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'))
|
||||
|
||||
+23
-5
@@ -18,7 +18,7 @@ Image._initialized = 2
|
||||
# Image resizing
|
||||
# ----------------------------------------
|
||||
|
||||
def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype=None, avoid_if_small=False):
|
||||
def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', filetype=None, avoid_if_small=False, upper_limit=False):
|
||||
""" Function to resize an image. The image will be resized to the given
|
||||
size, while keeping the aspect ratios, and holes in the image will be
|
||||
filled with transparent background. The image will not be stretched if
|
||||
@@ -63,18 +63,32 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
|
||||
}.get(filetype, filetype)
|
||||
|
||||
asked_width, asked_height = size
|
||||
if upper_limit:
|
||||
if asked_width:
|
||||
if asked_width >= image.size[0]:
|
||||
asked_width = image.size[0]
|
||||
if asked_height:
|
||||
if asked_height >= image.size[1]:
|
||||
asked_height = image.size[1]
|
||||
|
||||
if image.size[0] >= image.size[1]:
|
||||
asked_height = None
|
||||
else:
|
||||
asked_width = None
|
||||
if asked_width is None and asked_height is None:
|
||||
return base64_source
|
||||
|
||||
if asked_width is None:
|
||||
asked_width = int(image.size[0] * (float(asked_height) / image.size[1]))
|
||||
if asked_height is None:
|
||||
asked_height = int(image.size[1] * (float(asked_width) / image.size[0]))
|
||||
size = asked_width, asked_height
|
||||
|
||||
# check image size: do not create a thumbnail if avoiding smaller images
|
||||
if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]:
|
||||
return base64_source
|
||||
|
||||
if image.size != size:
|
||||
image = image_resize_and_sharpen(image, size)
|
||||
image = image_resize_and_sharpen(image, size, upper_limit=upper_limit)
|
||||
if image.mode not in ["1", "L", "P", "RGB", "RGBA"] or (filetype == 'JPEG' and image.mode == 'RGBA'):
|
||||
image = image.convert("RGB")
|
||||
|
||||
@@ -82,7 +96,7 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
|
||||
image.save(background_stream, filetype)
|
||||
return codecs.encode(background_stream.getvalue(), encoding)
|
||||
|
||||
def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0):
|
||||
def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0, upper_limit=False):
|
||||
"""
|
||||
Create a thumbnail by resizing while keeping ratio.
|
||||
A sharpen filter is applied for a better looking result.
|
||||
@@ -101,8 +115,12 @@ def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.
|
||||
sharpener = ImageEnhance.Sharpness(image)
|
||||
resized_image = sharpener.enhance(factor)
|
||||
# create a transparent image for background and paste the image on it
|
||||
image = Image.new('RGBA', size, (255, 255, 255, 0))
|
||||
if upper_limit:
|
||||
image = Image.new('RGBA', (size[0], size[1]-3), (255, 255, 255, 0)) # FIXME temporary fix for trimming the ghost border.
|
||||
else:
|
||||
image = Image.new('RGBA', size, (255, 255, 255, 0))
|
||||
image.paste(resized_image, ((size[0] - resized_image.size[0]) // 2, (size[1] - resized_image.size[1]) // 2))
|
||||
|
||||
if image.mode != origin_mode:
|
||||
image = image.convert(origin_mode)
|
||||
return image
|
||||
|
||||
Reference in New Issue
Block a user