[IMP] web: create generic controller '/web/content' and '/web/image'

Replace deprecate controllers like /web/binary/image, /web/binary/saveas...
Use ETag for all content with 'unique' option to cache the content if the content is never changed.
This commit is contained in:
Christophe Matthieu
2015-08-21 22:06:52 +02:00
committed by Antony Lesuisse
parent 5d2c3ceb02
commit 6baf611df1
39 changed files with 239 additions and 474 deletions
@@ -113,7 +113,7 @@ var Sidebar = Widget.extend({
render_user_avatars: function(item) {
item.find(".oe_user_avatar").each(function() {
var user_id = parseInt( $(this).attr('data-id'), 10);
var url = session.url('/web/binary/image', {model: 'res.users', field: 'image_small', id: user_id});
var url = session.url('/web/image', {model: 'res.users', field: 'image_small', id: user_id});
$(this).attr("src", url);
});
}
@@ -235,7 +235,7 @@ var InstantMessaging = Widget.extend({
self.widgets = {};
self.users = [];
_.each(result, function(user) {
user.image_url = session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user.id});
user.image_url = session.url('/web/image', {model:'res.users', field: 'image_small', id: user.id});
var widget = new UserWidget(self, user);
widget.appendTo(self.$(".oe_im_users"));
widget.on("user_clicked", self, self.user_clicked);
-16
View File
@@ -13,22 +13,6 @@ import mimetypes
class MailController(http.Controller):
_cp_path = '/mail'
@http.route('/mail/download_attachment', type='http', auth='user')
def download_attachment(self, model, id, method, attachment_id, **kw):
# FIXME use /web/binary/saveas directly
Model = request.registry.get(model)
res = getattr(Model, method)(request.cr, request.uid, int(id), int(attachment_id))
if res:
filecontent = base64.b64decode(res.get('base64'))
filename = res.get('filename')
content_type = mimetypes.guess_type(filename)
if filecontent and filename:
return request.make_response(
filecontent,
headers=[('Content-Type', content_type[0] or 'application/octet-stream'),
('Content-Disposition', content_disposition(filename))])
return request.not_found()
@http.route('/mail/receive', type='json', auth='none')
def receive(self, req):
""" End-point to receive mail from an external SMTP server. """
-16
View File
@@ -142,22 +142,6 @@ class Message(models.Model):
def _needaction_domain_get(self):
return [('needaction', '=', True)]
#------------------------------------------------------
# download an attachment
#------------------------------------------------------
@api.multi
def download_attachment(self, attachment_id):
self.ensure_one()
if attachment_id in self.attachment_ids.ids:
attachment = self.env['ir.attachment'].sudo().browse(attachment_id)
if attachment.datas and attachment.datas_fname:
return {
'base64': attachment.datas,
'filename': attachment.datas_fname,
}
return False
#------------------------------------------------------
# Notification API
#------------------------------------------------------
+4 -13
View File
@@ -253,7 +253,7 @@ var Attachment = Widget.extend ({
for (var l in message.attachment_ids) {
var attach = message.attachment_ids[l];
if (!attach.formating) {
attach.url = mail_utils.get_attachment_url(session, message.id, attach.id);
attach.url = '/web/content/' + attach.id + '?download=true';
attach.name = mail_utils.breakword(attach.name || attach.filename);
attach.formating = true;
}
@@ -264,13 +264,6 @@ var Attachment = Widget.extend ({
display_attachments: function (message) {
return QWeb.render('ThreadMessageAttachments', {'record': message, 'widget': this});
},
/**
* Return the link to resized image
*/
attachments_resize_image: function (id, resize) {
return mail_utils.get_image(session, 'ir.attachment', 'datas', id, resize);
},
});
/**
@@ -907,11 +900,9 @@ var MailThread = Attachment.extend ({
avt = ('/mail/static/src/img/email_icon.png');
}
else if (author) {
avt = mail_utils.get_image(
session, 'res.partner', 'image_small', author[0]);
avt = '/web/image/res.partner/' + author[0] + '/image_small';
}else {
avt = mail_utils.get_image(
session, 'res.users', 'image_small', session.uid);
avt = '/web/image/res.partner/' + session.uid + '/image_small';
}
return avt;
},
@@ -1388,7 +1379,7 @@ var ComposeMessage = Attachment.extend ({
'id': result.id,
'name': result.name,
'filename': result.filename,
'url': mail_utils.get_attachment_url(session, this.id, result.id)
'url': '/web/content/' + result.id + '?download=true'
};
}
}
+2 -2
View File
@@ -241,7 +241,7 @@ var Followers = form_common.AbstractField.extend({
// truncate number of displayed followers
_(this.followers).each(function (record) {
$(qweb.render('mail_followers_partner', {
'record': _.extend(record, {'avatar_url': mail_utils.get_image(session, record['res_model'], 'image_small', record['res_id'])}),
'record': _.extend(record, {'avatar_url': '/web/image/' + record['res_model'] + '/' + record['res_id'] + '/image_small'}),
'widget': self})
).appendTo(node_user_list);
// On mouse-enter it will show the edit_subtype pencil.
@@ -417,4 +417,4 @@ var Followers = form_common.AbstractField.extend({
/* Add the widget to registry */
core.form_widget_registry.add('mail_followers', Followers);
});
});
-19
View File
@@ -27,23 +27,6 @@ function parse_email(text) {
return [text, false];
}
/* Get an image in /web/binary/image?... */
function get_image(session, model, field, id, resize) {
var r = resize ? encodeURIComponent(resize) : '';
id = id || '';
return session.url('/web/binary/image', {model: model, field: field, id: id, resize: r});
}
/* Get the url of an attachment {'id': id} */
function get_attachment_url(session, message_id, attachment_id) {
return session.url('/mail/download_attachment', {
'model': 'mail.message',
'id': message_id,
'method': 'download_attachment',
'attachment_id': attachment_id
});
}
/**
* Replaces some expressions
* - :name - shortcut to an image
@@ -125,8 +108,6 @@ function bindTooltipTo($el, value, position) {
return {
parse_email: parse_email,
get_image: get_image,
get_attachment_url: get_attachment_url,
do_replace_expressions: do_replace_expressions,
get_text2html: get_text2html,
expand_domain: expand_domain,
+1 -1
View File
@@ -208,7 +208,7 @@
<t t-if="attachment.file_type_icon === 'webimage'">
<div t-attf-class="oe_attachment oe_preview #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank">
<img t-att-src="widget.attachments_resize_image(attachment.id, [100,80])"></img>
<img t-attf-src="/web/image/#{attachment.id}/100x80"></img>
<div class='oe_name'><t t-raw='attachment.name'/></div>
</a>
<div class='oe_delete oe_e'>
+3 -2
View File
@@ -186,11 +186,12 @@ class TestMailMessage(TestMail):
'datas': 'My attachment'.encode('base64'),
'name': 'doc.txt',
'datas_fname': 'doc.txt'})
# attach the attachment to the message
self.message.write({'attachment_ids': [(4, attachment.id)]})
self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
self.message.sudo(self.user_employee).read()
# Test: Bert downloads attachment, ok because he can read message
self.message.sudo(self.user_employee).download_attachment(attachment.id)
# Test: Bert has access to attachment, ok because he can read message
attachment.sudo(self.user_employee).read(['name', 'datas'])
def test_mail_message_access_read_author(self):
self.message.write({'author_id': self.user_employee.partner_id.id})
@@ -624,7 +624,7 @@ var ProductCategoriesWidget = PosBaseWidget.extend({
},
get_image_url: function(category){
return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
return window.location.origin + '/web/image?model=pos.category&field=image_medium&id='+category.id;
},
render_category: function( category, with_image ){
@@ -771,7 +771,7 @@ var ProductListWidget = PosBaseWidget.extend({
this.renderElement();
},
get_product_image_url: function(product){
return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
return window.location.origin + '/web/image?model=product.product&field=image_medium&id='+product.id;
},
replace: function($target){
this.renderElement();
@@ -1109,7 +1109,7 @@ var ClientListScreenWidget = ScreenWidget.extend({
}
},
partner_icon_url: function(id){
return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
return '/web/image?model=res.partner&id='+id+'&field=image_small';
},
// ui handle for the 'edit selected customer' action
@@ -375,7 +375,7 @@ var FloorScreenWidget = screens.ScreenWidget.extend({
}
},
background_image_url: function(floor) {
return '/web/binary/image?model=restaurant.floor&id='+floor.id+'&field=background_image';
return '/web/image?model=restaurant.floor&id='+floor.id+'&field=background_image';
},
get_floor_style: function() {
var style = "";
+1 -1
View File
@@ -17,7 +17,7 @@ options.registry.many2one.include({
var $img = $('.header .row img:first');
var css = window.getComputedStyle($img[0]);
$img.css("max-height", css.height+'px');
$img.attr("src", "/web_editor/image/res.partner/"+self.ID+"/image");
$img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
setTimeout(function () { $img.removeClass('o_dirty'); },0);
}
}
+139 -126
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
import ast
import base64
import csv
import functools
@@ -39,7 +38,8 @@ from openerp.tools import topological_sort
from openerp.tools.translate import _
from openerp.tools import ustr
from openerp import http
from openerp.http import request, serialize_exception as _serialize_exception
import mimetypes
from openerp.http import request, serialize_exception as _serialize_exception, STATIC_CACHE
from openerp.exceptions import AccessError
_logger = logging.getLogger(__name__)
@@ -432,6 +432,82 @@ def content_disposition(filename):
else:
return "attachment; filename*=UTF-8''%s" % escaped
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', env=None):
""" Get file, attachment or downloadable content
If the ``xmlid`` and ``id`` parameter is omitted, fetches the default value for the
binary field (via ``default_get``), otherwise fetches the field for
that precise record.
:param str xmlid: xmlid of the record
:param str model: name of the model to fetch the binary from
:param int id: id of the record from which to fetch the binary
:param str field: binary field
:param bool unique: add a max-age for the cache control
:param str filename: choose a filename
: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 str default_mimetype: default mintype if no mintype found
:param Environment env: by default use request.env
:returns: (status, headers, content)
"""
env = env or request.env
# get object and content
obj = None
if xmlid:
obj = env.ref(xmlid, False)
elif id and model in env.registry:
obj = env[model].browse(int(id))
# obj exists
if not obj or not obj.exists() or field not in obj:
return (404, [], None)
# check read access
try:
last_update = obj['__last_update']
except AccesError, e:
return (403, [], None)
status = 200
# filename
if not filename:
if filename_field in obj:
filename = obj[filename_field]
else:
filename = "%s-%s-%s" % (obj._model._name, obj.id, field)
# mimetype
if not mimetype:
if 'mimetype' in obj and obj.mimetype:
mimetype = obj.mimetype
elif filename:
mimetype = mimetypes.guess_type(filename)[0]
if not mimetype:
mimetype = default_mimetype
headers = [('Content-Type', mimetype)]
# cache
etag = hasattr(request, 'httprequest') and request.httprequest.headers.get('If-None-Match')
retag = hashlib.md5(last_update).hexdigest()
if etag == retag:
status = 304
headers.append(('ETag', retag))
if unique:
headers.append(('Cache-Control', 'max-age=%s' % STATIC_CACHE))
else:
headers.append(('Cache-Control', 'max-age=0'))
# content-disposition default name
if download:
headers.append(('Content-Disposition', content_disposition(filename)))
# get content after cache control
content = obj[field] or ''
return (status, headers, content)
#----------------------------------------------------------
# OpenERP Web web Controllers
@@ -978,138 +1054,74 @@ class TreeView(View):
class Binary(http.Controller):
@http.route('/web/binary/image', type='http', auth="public")
def image(self, model, id, field, **kw):
last_update = '__last_update'
Model = request.registry[model]
cr, uid, context = request.cr, request.uid, request.context
headers = [('Content-Type', 'image/png')]
etag = request.httprequest.headers.get('If-None-Match')
hashed_session = hashlib.md5(request.session_id).hexdigest()
retag = hashed_session
id = None if not id else simplejson.loads(id)
if type(id) is list:
id = id[0] # m2o
try:
if etag:
if not id and hashed_session == etag:
return werkzeug.wrappers.Response(status=304)
else:
date = Model.read(cr, uid, [id], [last_update], context)[0].get(last_update)
if hashlib.md5(date).hexdigest() == etag:
return werkzeug.wrappers.Response(status=304)
if not id:
res = Model.default_get(cr, uid, [field], context).get(field)
image_base64 = res
else:
res = Model.read(cr, uid, [id], [last_update, field], context)[0]
retag = hashlib.md5(res.get(last_update)).hexdigest()
image_base64 = res.get(field)
if kw.get('resize'):
resize = kw.get('resize').split(',')
if len(resize) == 2 and int(resize[0]) and int(resize[1]):
width = int(resize[0])
height = int(resize[1])
# resize maximum 500*500
if width > 500: width = 500
if height > 500: height = 500
image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
image_data = base64.b64decode(image_base64)
except Exception:
image_data = self.placeholder()
headers.append(('ETag', retag))
headers.append(('Content-Length', len(image_data)))
try:
ncache = int(kw.get('cache'))
headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
except:
pass
return request.make_response(image_data, headers)
def placeholder(self, image='placeholder.png'):
addons_path = http.addons_manifest['web']['addons_path']
return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read()
@http.route('/web/binary/saveas', type='http', auth="public")
@serialize_exception
def saveas(self, model, field, id=None, filename_field=None, **kw):
""" Download link for files stored as binary fields.
If the ``id`` parameter is omitted, fetches the default value for the
binary field (via ``default_get``), otherwise fetches the field for
that precise record.
:param str model: name of the model to fetch the binary from
:param str field: binary field
:param str id: id of the record from which to fetch the binary
:param str filename_field: field holding the file's name, if any
:returns: :class:`werkzeug.wrappers.Response`
"""
Model = request.registry[model]
cr, uid, context = request.cr, request.uid, request.context
fields = [field]
content_type = 'application/octet-stream'
if filename_field:
fields.append(filename_field)
if id:
fields.append('file_type')
res = Model.read(cr, uid, [int(id)], fields, context)[0]
if res.get('file_type'):
content_type = res['file_type']
@http.route(['/web/content',
'/web/content/<string:xmlid>',
'/web/content/<string:xmlid>/<string:filename>',
'/web/content/<int:id>',
'/web/content/<int:id>/<string:filename>',
'/web/content/<string:model>/<int:id>/<string:field>',
'/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):
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)
if status == 304:
response = werkzeug.wrappers.Response(status=status, headers=headers)
elif status != 200:
response = request.not_found()
else:
res = Model.default_get(cr, uid, fields, context)
filecontent = base64.b64decode(res.get(field) or '')
if not filecontent:
content_base64 = base64.b64decode(content)
headers.append(('Content-Length', len(content_base64)))
response = request.make_response(content_base64, headers)
if token:
response.set_cookie('fileToken', token)
return response
@http.route(['/web/image',
'/web/image/<string:xmlid>',
'/web/image/<string:xmlid>/<string:filename>',
'/web/image/<string:xmlid>/<int:width>x<int:height>',
'/web/image/<string:xmlid>/<int:width>x<int:height>/<string:filename>',
'/web/image/<int:id>',
'/web/image/<int:id>/<string:filename>',
'/web/image/<string:model>/<int:id>/<string:field>',
'/web/image/<string:model>/<int:id>/<string:field>/<string:filename>',
'/web/image/<int:id>/<int:width>x<int:height>',
'/web/image/<int:id>/<int:width>x<int:height>/<string:filename>',
'/web/image/<string:model>/<int:id>/<string:field>/<int:width>x<int:height>',
'/web/image/<string:model>/<int:id>/<string:field>/<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):
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')
if status == 304:
return werkzeug.wrappers.Response(status=304, headers=headers)
elif status != 200 and download:
return request.not_found()
else:
filename = '%s_%s' % (model.replace('.', '_'), id)
if filename_field:
filename = res.get(filename_field, '') or filename
return request.make_response(filecontent,
[('Content-Type', content_type),
('Content-Disposition', content_disposition(filename))])
@http.route('/web/binary/saveas_ajax', type='http', auth="public")
@serialize_exception
def saveas_ajax(self, data, token):
jdata = simplejson.loads(data)
model = jdata['model']
field = jdata['field']
data = jdata['data']
id = jdata.get('id', None)
filename_field = jdata.get('filename_field', None)
context = jdata.get('context', {})
content_type = 'application/octet-stream'
if content and width and height:
# resize maximum 500*500
if width > 500:
width = 500
if height > 500:
height = 500
content = openerp.tools.image_resize_image(base64_source=content, size=(width, height), encoding='base64', filetype='PNG')
image_base64 = content and base64.b64decode(content) or self.placeholder()
headers.append(('Content-Length', len(image_base64)))
response = request.make_response(image_base64, headers)
response.status = str(status)
return response
# backward compatibility
@http.route(['/web/binary/image'], type='http', auth="public")
def content_image_backward_compatibility(self, model, id, field, resize=None, **kw):
width = None
height = None
if resize:
width, height = resize.split(",")
return self.content_image(model=model, id=id, field=field, width=width, height=height)
Model = request.session.model(model)
fields = [field]
if filename_field:
fields.append(filename_field)
if data:
res = {field: data, filename_field: jdata.get('filename', None)}
elif id:
fields.append('file_type')
res = Model.read([int(id)], fields, context)[0]
if res.get('file_type'):
content_type = res['file_type']
else:
res = Model.default_get(fields, context)
filecontent = base64.b64decode(res.get(field) or '')
if not filecontent:
raise ValueError(_("No content found for field '%s' on '%s:%s'") %
(field, model, id))
else:
filename = '%s_%s' % (model.replace('.', '_'), id)
if filename_field:
filename = res.get(filename_field, '') or filename
return request.make_response(filecontent,
headers=[('Content-Type', content_type),
('Content-Disposition', content_disposition(filename))],
cookies={'fileToken': token})
@http.route('/web/binary/upload', type='http', auth="user")
@serialize_exception
@@ -1145,6 +1157,7 @@ class Binary(http.Controller):
}, request.context)
args = {
'filename': ufile.filename,
'mimetype': ufile.content_type,
'id': attachment_id
}
except Exception:
@@ -1462,7 +1462,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractManyField.extend(common.Reinitializ
this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
},
get_file_url: function (attachment) {
return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment.id});
return '/web/content/' + attachment.id + '?download=true';
},
read_name_values : function () {
var self = this;
@@ -1474,7 +1474,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractManyField.extend(common.Reinitializ
return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
_.each(datas, function (data) {
data.no_unlink = true;
data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
data.url = '/web/content/' + data.id + '?download=true';
self.data[data.id] = data;
});
return ids;
@@ -333,6 +333,7 @@ var FormView = View.extend(common.FieldManagerMixin, {
}
var fields = _.keys(self.fields_view.fields);
fields.push('display_name');
fields.push('__last_update');
return self.dataset.read_index(fields, {
context: { 'bin_size': true }
}).then(function(r) {
+14 -14
View File
@@ -1155,18 +1155,18 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, {
var filename_fieldname = this.node.attrs.filename;
var filename_field = this.view.fields && this.view.fields[filename_fieldname];
this.session.get_file({
url: '/web/binary/saveas_ajax',
data: {data: JSON.stringify({
model: this.view.dataset.model,
id: (this.view.datarecord.id || ''),
field: this.name,
filename_field: (filename_fieldname || ''),
data: utils.is_bin_size(value) ? null : value,
filename: filename_field ? filename_field.get('value') : null,
context: this.view.dataset.get_context()
})},
complete: framework.unblockUI,
error: c.rpc_error.bind(c)
'url': '/web/content',
'data': {
'model': this.view.dataset.model,
'id': this.view.datarecord.id,
'field': this.name,
'filename_field': filename_fieldname,
'filename': filename_field ? filename_field.get('value') : null,
'download': true,
'data': utils.is_bin_size(value) ? null : value,
},
'complete': framework.unblockUI,
'error': c.rpc_error.bind(c)
});
ev.stopPropagation();
return false;
@@ -1263,11 +1263,11 @@ var FieldBinaryImage = FieldBinary.extend({
var field = this.name;
if (this.options.preview_image)
field = this.options.preview_image;
url = session.url('/web/binary/image', {
url = session.url('/web/image', {
model: this.view.dataset.model,
id: id,
field: field,
t: (new Date().getTime()),
unique: (this.view.datarecord.__last_update || '').replace(/[^0-9]/g, ''),
});
} else {
url = this.placeholder;
+1 -1
View File
@@ -1910,7 +1910,7 @@ var ColumnBinary = Column.extend({
if (value.substr(0, 10).indexOf(' ') == -1) {
download_url = "data:application/octet-stream;base64," + value;
} else {
download_url = session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
download_url = session.url('/web/content', {model: options.model, field: this.id, id: options.id, download: true});
if (this.filename) {
download_url += '&filename_field=' + this.filename;
}
+3 -5
View File
@@ -174,16 +174,14 @@ var Sidebar = Widget.extend({
}
},
on_attachments_loaded: function(attachments) {
var self = this;
var prefix = session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
_.each(attachments,function(a) {
a.label = a.name;
if(a.type === "binary") {
a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime());
a.url = '/web/content/' + a.id + '?download=true&t=' + (new Date().getTime());
}
});
self.items.files = attachments;
self.redraw();
this.items.files = attachments;
this.redraw();
this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
},
@@ -47,7 +47,7 @@ var SystrayMenu = Widget.extend({
if (!session.debug) {
topbar_name = _.str.sprintf("%s (%s)", topbar_name, session.db);
}
var avatar_src = session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: session.uid});
var avatar_src = session.url('/web/image', {model:'res.users', field: 'image_small', id: session.uid});
$avatar.attr('src', avatar_src);
core.bus.trigger('resize'); // Re-trigger the reflow logic
@@ -565,7 +565,7 @@ var CalendarView = View.extend({
if (attendee_showed<= MAX_ATTENDEES) {
if (self.avatar_model !== null) {
the_title_avatar += '<img title="' + _.escape(self.all_attendees[the_attendee_people]) + '" class="o_attendee_head" \
src="/web/binary/image?model=' + self.avatar_model + '&field=image_small&id=' + the_attendee_people + '"></img>';
src="/web/image/' + self.avatar_model + '/' + the_attendee_people + '/image_small"></img>';
}
else {
if (!self.colorIsAttendee || the_attendee_people != temp_ret[self.color_field]) {
@@ -28,7 +28,7 @@
<span><i t-attf-class="fa fa-users fa-fw o_cal_avatar"></i></span>
</t>
<t t-if="(filters_value.value != -1) &amp;&amp; filters_value.avatar_model ">
<img t-att-src="'/web/binary/image?model=' + filters_value.avatar_model + '&amp;field=image_small&amp;id=' + filters_value.value" class="o_cal_avatar"/>
<img t-att-src="'/web/image/' + filters_value.avatar_model + '/' + filters_value.value + '/image_small" class="o_cal_avatar"/>
</t>
<span t-attf-class="color_filter o_underline_color_#{filters_value.color}" ><t t-esc="filters_value.label" /></span>
</t>
+2 -65
View File
@@ -127,66 +127,6 @@ class Web_Editor(http.Controller):
return response
#------------------------------------------------------
# image route for browse record
#------------------------------------------------------
def placeholder(self, response):
return request.registry['ir.attachment']._image_placeholder(response)
@http.route([
'/web_editor/image',
'/web_editor/image/<xmlid>',
'/web_editor/image/<xmlid>/<int:max_width>x<int:max_height>',
'/web_editor/image/<xmlid>/<field>',
'/web_editor/image/<xmlid>/<field>/<int:max_width>x<int:max_height>',
'/web_editor/image/<model>/<id>/<field>',
'/web_editor/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
], type='http', auth="public")
def image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None):
""" Fetches the requested field and ensures it does not go above
(max_width, max_height), resizing it if necessary.
If the record is not found or does not have the requested field,
returns a placeholder image via :meth:`~.placeholder`.
Sets and checks conditional response parameters:
* :mailheader:`ETag` is always set (and checked)
* :mailheader:`Last-Modified is set iif the record has a concurrency
field (``__last_update``)
The requested field is assumed to be base64-encoded image data in
all cases.
xmlid can be used to load the image. But the field image must by base64-encoded
"""
if xmlid and "." in xmlid:
try:
record = request.env.ref(xmlid)
model, id = record._name, record.id
except:
raise werkzeug.exceptions.NotFound()
if model == 'ir.attachment' and not field:
if record.sudo().type == "url":
field = "url"
else:
field = "datas"
if not model or not id or not field:
raise werkzeug.exceptions.NotFound()
try:
idsha = str(id).split('_')
id = idsha[0]
response = werkzeug.wrappers.Response()
return request.registry['ir.attachment']._image(
request.cr, request.uid, model, id, field, response, max_width, max_height,
cache=STATIC_CACHE if len(idsha) > 1 else None)
except Exception:
logger.exception("Cannot render image field %r of record %s[%s] at size(%s,%s)",
field, model, id, max_width, max_height)
response = werkzeug.wrappers.Response()
return self.placeholder(response)
#------------------------------------------------------
# add attachment (images or link)
#------------------------------------------------------
@@ -200,7 +140,6 @@ class Web_Editor(http.Controller):
uploads = []
message = None
if not upload: # no image provided, storing the link and the image name
uploads.append({'website_url': url})
name = url.split("/").pop() # recover filename
attachment_id = Attachments.create(request.cr, request.uid, {
'name': name,
@@ -208,6 +147,7 @@ class Web_Editor(http.Controller):
'url': url,
'res_model': 'ir.ui.view',
}, request.context)
uploads.append({'id': attachment_id})
else: # images provided
try:
attachment_ids = []
@@ -230,10 +170,7 @@ class Web_Editor(http.Controller):
'res_model': 'ir.ui.view',
}, request.context)
attachment_ids.append(attachment_id)
uploads = Attachments.read(
request.cr, request.uid, attachment_ids, ['website_url'],
context=request.context)
uploads = [{'id': id} for id in attachment_ids]
except Exception, e:
logger.exception("Failed to upload image to attachment")
message = unicode(e)
+1 -114
View File
@@ -28,122 +28,9 @@ class ir_attachment(osv.osv):
if attach.url:
result[attach.id] = attach.url
else:
result[attach.id] = self.image_url(cr, uid, attach, 'datas')
result[attach.id] = '/web/image/%s?unique=%s' % (attach.id, attach.checksum)
return result
_columns = {
'local_url': fields.function(_local_url_get, string="Attachment URL", type='char'),
}
def _image_placeholder(self, response):
# file_open may return a StringIO. StringIO can be closed but are
# not context managers in Python 2 though that is fixed in 3
with contextlib.closing(openerp.tools.misc.file_open(
os.path.join('web', 'static', 'src', 'img', 'placeholder.png'),
mode='rb')) as f:
response.data = f.read()
return response.make_conditional(request.httprequest)
def _image(self, cr, uid, model, id_or_ids, field, response, max_width=maxint, max_height=maxint, cache=None, context=None):
""" Fetches the requested field and ensures it does not go above
(max_width, max_height), resizing it if necessary.
Resizing is bypassed if the object provides a $field_big, which will
be interpreted as a pre-resized version of the base field.
If the record is not found or does not have the requested field,
returns a placeholder image via :meth:`~._image_placeholder`.
Sets and checks conditional response parameters:
* :mailheader:`ETag` is always set (and checked)
* :mailheader:`Last-Modified is set iif the record has a concurrency
field (``__last_update``)
The requested field is assumed to be base64-encoded image data in
all cases.
"""
Model = self.pool[model]
ids = isinstance(id_or_ids, (list, tuple)) and id_or_ids or [int(id_or_ids)]
ids = Model.search(cr, uid, [('id', 'in', ids)], context=context)
if not ids:
return self._image_placeholder(response)
concurrency = '__last_update'
[record] = Model.read(cr, openerp.SUPERUSER_ID, ids, [concurrency, field], context=context)
if concurrency in record:
server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
try:
response.last_modified = datetime.datetime.strptime(
record[concurrency], server_format + '.%f')
except ValueError:
# just in case we have a timestamp without microseconds
response.last_modified = datetime.datetime.strptime(
record[concurrency], server_format)
# Field does not exist on model or field set to False
if not record.get(field):
# FIXME: maybe a field which does not exist should be a 404?
return self._image_placeholder(response)
response.set_etag(hashlib.sha1(record[field]).hexdigest())
response.make_conditional(request.httprequest)
if cache:
response.cache_control.max_age = cache
response.expires = int(time.time() + cache)
# conditional request match
if response.status_code == 304:
return response
if model == 'ir.attachment' and field == 'url' and field in record:
path = record[field].strip('/')
# Check that we serve a file from within the module
if os.path.normpath(path).startswith('..'):
return self._image_placeholder(response)
# Check that the file actually exists
path = path.split('/')
resource = openerp.modules.get_module_resource(*path)
if not resource:
return self._image_placeholder(response)
data = open(resource, 'rb').read()
else:
data = record[field].decode('base64')
image = Image.open(cStringIO.StringIO(data))
response.mimetype = Image.MIME[image.format]
filename = '%s_%s.%s' % (model.replace('.', '_'), ids[0], str(image.format).lower())
response.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
if (not max_width) and (not max_height):
response.data = data
return response
w, h = image.size
max_w = int(max_width) if max_width else maxint
max_h = int(max_height) if max_height else maxint
if w < max_w and h < max_h:
response.data = data
else:
size = (max_w, max_h)
img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True)
image_save_for_web(img, response.stream, format=image.format)
# invalidate content-length computed by make_conditional as
# writing to response.stream does not do it (as of werkzeug 0.9.3)
del response.headers['Content-Length']
return response
def image_url(self, cr, uid, record, field, size=None, context=None):
"""Returns a local url that points to the image field of a given browse record."""
model = record._name
sudo_record = record.sudo()
id = '%s_%s' % (record.id, hashlib.sha1(sudo_record.write_date or sudo_record.create_date or '').hexdigest()[0:7])
size = '' if size is None else '/%s' % size
return '/web_editor/image/%s/%s/%s%s' % (model, id, field, size)
+12 -8
View File
@@ -15,6 +15,7 @@ import urllib2
import urlparse
import re
import simplejson
import hashlib
import pytz
from dateutil import parser
@@ -329,13 +330,16 @@ class Image(orm.AbstractModel):
classes = ' '.join(itertools.imap(escape, aclasses))
max_size = None
max_width, max_height = options.get('max_width', 0), options.get('max_height', 0)
if max_width or max_height:
max_size = '%sx%s' % (max_width, max_height)
src = self.pool['ir.attachment'].image_url(cr, uid, record, field_name, max_size)
if options.get('resize'):
src = "%s/%s" % (src, options.get('resize'))
max_size = options.get('resize')
else:
max_width, max_height = options.get('max_width', 0), options.get('max_height', 0)
if max_width or max_height:
max_size = '%sx%s' % (max_width, max_height)
sha = hashlib.sha1(record.write_date).hexdigest()[0:7]
max_size = '' if max_size is None else '/%s' % max_size
src = '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field_name, max_size, sha)
img = '<img class="%s" src="%s" style="%s"/>' % (classes, src, options.get('style', ''))
return ir_qweb.HTMLSafe(img)
@@ -346,8 +350,8 @@ class Image(orm.AbstractModel):
url = element.find('img').get('src')
url_object = urlparse.urlsplit(url)
if url_object.path.startswith('/web_editor/image'):
# url might be /web_editor/image/<model>/<id>[_<checksum>]/<field>[/<width>x<height>]
if url_object.path.startswith('/web/image'):
# url might be /web/image/<model>/<id>[_<checksum>]/<field>[/<width>x<height>]
fragments = url_object.path.split('/')
query = dict(urlparse.parse_qsl(url_object.query))
model = query.get('model', fragments[3])
+1 -1
View File
@@ -366,7 +366,7 @@ var ImageDialog = Widget.extend({
self.file_selected(null, error || !attachments.length);
}
for (var i=0; i<attachments.length; i++) {
self.file_selected(attachments[i]['local_url'] || '/web_editor/image/ir.attachment/'+attachments[i].id+'_'+(new Date().getTime())+'/datas', error);
self.file_selected(attachments[i]['local_url'] || '/web/image/'+attachments[i].id, error);
}
};
},
@@ -127,7 +127,8 @@ var KanbanRecord = Widget.extend({
id = JSON.stringify(id);
if (options.preview_image)
field = options.preview_image;
url = session.url('/web/binary/image', {model: model, field: field, id: id});
var unique = this.record.__last_update.value.replace(/[^0-9]/g, '');
url = session.url('/web/image', {model: model, field: field, id: id, unique: unique});
if (cache !== undefined) {
// Set the cache duration in seconds.
url += '&cache=' + parseInt(cache, 10);
@@ -77,7 +77,7 @@
</t>
<t t-name="KanbanAttachmentImage">
<div class="o_kanban_attachment_image">
<img t-att-src="'/web/binary/image?model=ir.attachment&amp;field=datas&amp;id=' + widget.field.raw_value[0]"/>
<img t-att-src="'/web/image/' + widget.field.raw_value[0]"/>
</div>
</t>
</template>
-14
View File
@@ -369,17 +369,3 @@ class Website(openerp.addons.web.controllers.main.Home):
if res:
return res
return request.redirect('/')
#------------------------------------------------------
# image route for browse record
#------------------------------------------------------
@http.route([
'/website/image',
'/website/image/<xmlid>',
'/website/image/<xmlid>/<field>',
'/website/image/<model>/<id>/<field>',
'/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
], type='http', auth="public")
def website_image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None):
logger.warning("Deprecated image controller, please use /web_editor/image/")
return Web_Editor().image(model=model, id=id, field=field, xmlid=xmlid, max_width=max_width, max_height=max_height)
+8 -14
View File
@@ -5,6 +5,7 @@ import math
import unicodedata
import re
import urlparse
import hashlib
from sys import maxint
@@ -122,8 +123,8 @@ _UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[A-Za-z0-9-_]+?\w)-)?(-?\d+)(?=$|/)')
DEFAULT_CDN_FILTERS = [
"^/[^/]+/static/",
"^/web/(css|js)/",
"^/website/image/",
"^/web_editor/image/",
"^/web/image",
"^/web/content",
]
def unslug(s):
@@ -558,7 +559,11 @@ class website(osv.osv):
return self.pool['ir.attachment']._image(cr, uid, model, id, field, response, max_width=max_width, max_height=max_height, cache=cache, context=context)
def image_url(self, cr, uid, record, field, size=None, context=None):
return self.pool['ir.attachment'].image_url(cr, uid, record, field, size=size, context=context)
"""Returns a local url that points to the image field of a given browse record."""
sudo_record = record.sudo()
sha = hashlib.sha1(sudo_record.write_date).hexdigest()[0:7]
size = '' if size is None else '/%s' % size
return '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field, size, sha)
class website_menu(osv.osv):
_name = "website.menu"
@@ -637,17 +642,6 @@ class ir_attachment(osv.osv):
'website_url': fields.related("local_url", string="Attachment URL", type='char', deprecated=True), # related for backward compatibility with saas-6
}
def _image(self, cr, uid, model, id_or_ids, field, response, max_width=maxint, max_height=maxint, cache=None, context=None):
Model = self.pool[model]
ids = isinstance(id_or_ids, (list, tuple)) and id_or_ids or [int(id_or_ids)]
ids = Model.search(cr, uid, [('id', 'in', ids)], context=context)
if not ids and 'website_published' in Model._fields:
ids = Model.search(cr, openerp.SUPERUSER_ID, [('id', 'in', ids), ('website_published', '=', True)], context=context)
return super(ir_attachment, self)._image(cr, openerp.SUPERUSER_ID, model, id_or_ids, field, response,
max_width=max_width, max_height=max_height, cache=cache, context=context)
class res_partner(osv.osv):
_inherit = "res.partner"
@@ -73,7 +73,7 @@ odoo.define('website_blog.editor', function (require) {
var $img = $(this).find("img");
var css = window.getComputedStyle($img[0]);
$img.css({ width: css.width, height: css.height });
$img.attr("src", "/web_editor/image/res.partner/"+self.ID+"/image");
$img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
});
setTimeout(function () { $nodes.removeClass('o_dirty'); },0);
}
+11 -8
View File
@@ -6,12 +6,14 @@ import werkzeug.wrappers
import simplejson
import lxml
from urllib2 import urlopen, URLError
import base64
import openerp
from openerp import tools, _
from openerp.addons.web import http
from openerp.addons.web.controllers.main import binary_content
from openerp.addons.web.http import request
from openerp.addons.website.models.website import slug
from openerp.tools.translate import _
class WebsiteForum(http.Controller):
@@ -455,13 +457,14 @@ class WebsiteForum(http.Controller):
@http.route(['/forum/user/<int:user_id>/avatar'], type='http', auth="public", website=True)
def user_avatar(self, user_id=0, **post):
response = werkzeug.wrappers.Response()
User = request.env['res.users']
Website = request.env['website']
user = User.sudo().search([('id', '=', user_id)])
if not user.exists() or (user_id != request.session.uid and user.karma < 1):
return Website._image_placeholder(response)
return Website._image('res.users', user.id, 'image', response)
status, headers, content = binary_content(model='res.users', id=user_id, field='image', default_mimetype='image/png', env=request.env(openerp.SUPERUSER_ID))
if status == 304:
return werkzeug.wrappers.Response(status=304)
image_base64 = base64.b64decode(content)
headers.append(('Content-Length', len(image_base64)))
response = request.make_response(image_base64, headers)
response.status = str(status)
return response
@http.route(['/forum/<model("forum.forum"):forum>/user/<int:user_id>'], type='http', auth="public", website=True)
def open_user(self, forum, user_id=0, **post):
+1 -1
View File
@@ -42,7 +42,7 @@
<section id="discussion" class="hidden-print oe_website_portal">
<section class="mb32 css_editable_mode_hidden hidden-print">
<form class="o_website_chatter_form" t-attf-action="/website_mail/post/#{chatter_mode}" method="POST" t-if="website.env.user != request.env['ir.model.data'].xmlid_to_object('base.public_user') or token or sha_in">
<img class="img pull-left img-circle" t-att-src="website.image_url(user_id.partner_id, 'image_small')" style="width: 50px; margin-right: 10px;"/>
<img class="img pull-left img-circle" t-attf-src="/web/image/res.partner/#{user_id.partner_id.id}/image_small/50x50" style="width: 50px; margin-right: 10px;"/>
<div class="pull-left mb32" style="width: 75%%">
<textarea rows="4" name="message" class="form-control" placeholder="Write a message..."></textarea>
<input type='hidden' name="res_model" t-att-value="object._model"/>
@@ -205,9 +205,9 @@
</p>
<div class="o_mg_link_content">
<div class="col-md-2 col-sm-3 text-center" t-foreach='message.attachment_ids' t-as='attachment'>
<a t-att-href="'/mail/download_attachment?model=mail.message&amp;id='+str(message.id)+'&amp;method=download_attachment&amp;attachment_id='+str(attachment.id)" target="_blank">
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
<t t-if="attachment.file_type == 'webimage'">
<img t-att-src="'/web/binary/image?model=ir.attachment&amp;field=datas&amp;id=' + str(attachment.id) + '&amp;resize=100,80'"
<img t-att-src="'/web/image/' + str(attachment.id) + '/100x80'"
class='oe_attachment_embedded'
t-att-title="attachment.name"/>
</t>
@@ -253,7 +253,7 @@ $('.oe_website_sale').each(function () {
$parent.find(".oe_price:first .oe_currency_value").html(price_to_str(+$(this).data('price')) );
var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
$img.attr("src", "/web_editor/image/product.product/" + $(this).val() + "/image");
$img.attr("src", "/web/image/product.product/" + $(this).val() + "/image");
});
$(oe_website_sale).on('change', 'input.js_variant_change, select.js_variant_change, ul[data-attribute_value_ids]', function (ev) {
@@ -290,7 +290,7 @@ $('.oe_website_sale').each(function () {
if (product_id) {
var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
$img.attr("src", "/web_editor/image/product.product/" + product_id + "/image");
$img.attr("src", "/web/image/product.product/" + product_id + "/image");
$img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
.data('oe-model', 'product.product').data('oe-id', product_id);
}
@@ -26,7 +26,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
}).then(function (modal) {
var $modal = $(modal);
$modal.find('img:first').attr("src", "/web_editor/image/product.product/" + product_id + "/image_medium");
$modal.find('img:first').attr("src", "/web/image/product.product/" + product_id + "/image_medium");
$modal.appendTo($form)
.modal()
+1 -1
View File
@@ -258,7 +258,7 @@ class website_slides(http.Controller):
def slide_mapped_dict(slide):
return {
'img_src': '/web_editor/image/slide.slide/%s/image_thumb' % (slide.id),
'img_src': '/web/image/slide.slide/%s/image_thumb' % (slide.id),
'caption': slide.name,
'url': slide.website_url
}
@@ -43,7 +43,7 @@
</tr><tr>
<td style="width: 600px; vertical-align:top; text-align:center; background-color:#FFFFF; color:#414141">
<a href="${object.website_url}" style="color: #a24689;">
<img alt="${object.name}" src="${ctx['base_url']}/web_editor/image/slide.slide/${object.id}/image" style="height:auto; width:500px; background-color: #cccccc; margin: 16px;">
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image" style="height:auto; width:500px; background-color: #cccccc; margin: 16px;">
</a>
<p style="font-size:24px; font-weight:bold"><a href="${object.website_url}">${object.name}</a></p>
<p style="color: #ccc;">Published on ${object.write_date}</p>
@@ -94,7 +94,7 @@
</tr><tr>
<td style="width: 600px; vertical-align:top; text-align:center; background-color:#FFFFF; color:#414141">
<a href="${object.website_url}" style="color: #a24689;">
<img alt="${object.name}" src="${ctx['base_url']}/web_editor/image/slide.slide/${object.id}/image"
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image"
style="height:auto; width:500px; background-color: #cccccc; margin: 16px;">
</a>
<p style="font-size:24px; font-weight:bold"><a href="${object.website_url}">${object.name}</a></p>
@@ -49,7 +49,7 @@
<div class="well well-sm">
<div>
<a t-attf-href="/slides/#{slug(channel)}">
<img t-if="channel.promoted_slide_id" t-attf-src="/web_editor/image/slide.slide/#{channel.promoted_slide_id.id}/image_medium" class="img-responsive oe_slides_channel_thumbnail"/>
<img t-if="channel.promoted_slide_id" t-attf-src="/web/image/slide.slide/#{channel.promoted_slide_id.id}/image_medium" class="img-responsive oe_slides_channel_thumbnail"/>
<img t-if="not channel.promoted_slide_id" src="/website_slides/static/description/channel-default.jpg" class="img-responsive oe_slides_opacity"/>
</a>
</div>
@@ -215,7 +215,7 @@
<div class="row">
<div class="col-md-5 mt16 mb16">
<a t-attf-href="/slides/slide/#{slug(channel.promoted_slide_id)}">
<img t-attf-src="/web_editor/image/slide.slide/#{channel.promoted_slide_id.id}/image" class="img-responsive shadow oe_slides_promote_image"/>
<img t-attf-src="/web/image/slide.slide/#{channel.promoted_slide_id.id}/image" class="img-responsive shadow oe_slides_promote_image"/>
<span t-if="channel.promote_strategy in ['latest', 'most_viewed', 'most_voted', 'custom']"
class="label label-primary pull-right" style="position: absolute;right: 50px;top: 7px;"
t-field="channel.promote_strategy"/>
@@ -333,7 +333,7 @@
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 mt16 mb16" t-foreach="slides" t-as="slide">
<div class="well well-sm oe_slides_thumbnail_container">
<a t-attf-href="/slides/slide/#{slug(slide)}">
<img t-attf-src="/web_editor/image/slide.slide/#{slide.id}/image_thumb" class="img-responsive img-thumbnail oe_slides_grid_thumbnail"/>
<img t-attf-src="/web/image/slide.slide/#{slide.id}/image_thumb" class="img-responsive img-thumbnail oe_slides_grid_thumbnail"/>
</a>
<span t-if="not slide.website_published" class="label label-danger" style="position: absolute;right: 20px;top: 7px;">Not Published</span>
<a t-if="not is_public_user and slide.website_published and slide.channel_id.promote_strategy == 'custom' and slide.channel_id.promoted_slide_id.id != slide.id"
@@ -374,7 +374,7 @@
<meta property="og:title" t-att-content="slide.name" />
<meta property="og:type" content="website" />
<meta property="og:url" t-att-content="slide.website_url" />
<meta property="og:image" t-attf-content="#{request.httprequest.host_url}/web_editor/image/slide.slide/#{slide.id}/image_thumb"/>
<meta property="og:image" t-attf-content="#{request.httprequest.host_url}/web/image/slide.slide/#{slide.id}/image_thumb"/>
<meta property="og:description" t-att-content="slide.description" />
</t>
</t>
@@ -394,7 +394,7 @@
<div t-if="not private">
<t t-if="slide.datas and slide.slide_type == 'infographic'">
<img t-attf-src="/web_editor/image/slide.slide/#{slide.id}/datas" class="img-responsive" style="width:100%"/>
<img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-responsive" style="width:100%"/>
</t>
<div t-if="slide.slide_type in ('presentation', 'document')" class="embed-responsive embed-responsive-16by9 embed-responsive-item mb8">
<t t-raw="slide.embed_code"/>
@@ -544,7 +544,7 @@
<div class="media">
<span class="pull-left">
<img class="img img-circle media-object"
t-att-src="'/web_editor/image?model=res.partner&amp;field=image_small&amp;id='+str(user_id.partner_id.id)"
t-att-src="'/web/image?model=res.partner&amp;field=image_small&amp;id='+str(user_id.partner_id.id)"
style="width: 30px"/>
</span>
<div class="media-body">
@@ -569,7 +569,7 @@
<li t-foreach="comments" t-as="message" class="media">
<span class="pull-left">
<img class="media-object img img-circle"
t-att-src="'/web_editor/image?model=mail.message&amp;field=author_avatar&amp;id='+str(message.id)" style="width: 30px"/>
t-att-src="'/web/image?model=mail.message&amp;field=author_avatar&amp;id='+str(message.id)" style="width: 30px"/>
</span>
<div class="media-body">
<t t-call="website.publish_short">
@@ -708,7 +708,7 @@
<template id="related_slides" name="Related Slide">
<li class="media">
<a class="media-left pull-left" t-attf-href="/slides/slide/#{slug(slide)}">
<img class="media-object oe_slides_apart_small" t-attf-src="/web_editor/image/slide.slide/#{slide.id}/image_thumb"/>
<img class="media-object oe_slides_apart_small" t-attf-src="/web/image/slide.slide/#{slide.id}/image_thumb"/>
</a>
<div class="media-body">
<a t-attf-href="/slides/slide/#{slug(slide)}"><h5 class="media-heading" t-esc="slide.name"/></a>
@@ -83,7 +83,7 @@
<h4><t t-esc="suggest_slide.name"/></h4>
</div>
<span class="oe-image-thumbnail">
<img class="img-responsive" t-attf-src="/web_editor/image/slide.slide/#{suggest_slide.id}/image_thumb" alt="suggest_slide.name"/>
<img class="img-responsive" t-attf-src="/web/image/slide.slide/#{suggest_slide.id}/image_thumb" alt="suggest_slide.name"/>
</span>
</a>
@@ -92,7 +92,7 @@
</div>
<t t-if="slide.slide_type in ('presentation', 'document')">
<div id="PDFViewerLoader" class="oe_slides_loader">
<img class="img-responsive" t-attf-src="/web_editor/image/slide.slide/#{slide.id}/image_medium" style="margin:auto;height:100%"/>
<img class="img-responsive" t-attf-src="/web/image/slide.slide/#{slide.id}/image_medium" style="margin:auto;height:100%"/>
<h4 class="text-center">
<i class="fa fa-spinner fa-spin"></i> Loading ...
</h4>
@@ -100,7 +100,7 @@
<canvas id="PDFViewerCanvas" class="img-responsive" style="margin:auto;height:100%;display:none;"></canvas>
</t>
<t t-if="slide.slide_type in ('infographic')">
<img t-attf-src="/web_editor/image/slide.slide/#{slide.id}/datas" class="img-responsive" style="width:100%"/>
<img t-attf-src="/web/image/slide.slide/#{slide.id}/datas" class="img-responsive" style="width:100%"/>
</t>
</div>
<!-- Fixed bottom navbar -->