[MERGE] Miscellaneous SEO improvments

- Compress images using pngquant
- Added http.send_file() helper
- Move javascripts in the bottom of the page (better google pagespeed ranking)
- Added website.get_image_url() helper that uses a hash allowing the usage of
  a more aggressive cache
- Misc. tools/image improvements
This commit is contained in:
Fabien Meghazi
2014-09-16 22:10:31 +02:00
36 changed files with 250 additions and 98 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 540 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 33 KiB

+10 -11
View File
@@ -32,6 +32,7 @@ except ImportError:
import openerp
import openerp.modules.registry
from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound
from openerp.modules import get_module_resource
from openerp.tools import topological_sort
from openerp.tools.translate import _
from openerp import http
@@ -1180,7 +1181,8 @@ class Binary(http.Controller):
'/logo.png',
], type='http', auth="none")
def company_logo(self, dbname=None, **kw):
# TODO add etag, refactor to use /image code for etag
imgname = 'logo.png'
placeholder = functools.partial(get_module_resource, 'web', 'static', 'src', 'img')
uid = None
if request.session.db:
dbname = request.session.db
@@ -1192,13 +1194,13 @@ class Binary(http.Controller):
uid = openerp.SUPERUSER_ID
if not dbname:
image_data = self.placeholder('logo.png')
response = http.send_file(placeholder(imgname))
else:
try:
# create an empty registry
registry = openerp.modules.registry.Registry(dbname)
with registry.cursor() as cr:
cr.execute("""SELECT c.logo_web
cr.execute("""SELECT c.logo_web, c.write_date
FROM res_users u
LEFT JOIN res_company c
ON c.id = u.company_id
@@ -1206,17 +1208,14 @@ class Binary(http.Controller):
""", (uid,))
row = cr.fetchone()
if row and row[0]:
image_data = str(row[0]).decode('base64')
image_data = StringIO(str(row[0]).decode('base64'))
response = http.send_file(image_data, filename=imgname, mtime=row[1])
else:
image_data = self.placeholder('nologo.png')
response = http.send_file(placeholder('nologo.png'))
except Exception:
image_data = self.placeholder('logo.png')
response = http.send_file(placeholder(imgname))
headers = [
('Content-Type', 'image/png'),
('Content-Length', len(image_data)),
]
return request.make_response(image_data, headers)
return response
class Action(http.Controller):
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+12 -7
View File
@@ -8,8 +8,6 @@ import xml.etree.ElementTree as ET
import logging
import re
from sys import maxint
import werkzeug.utils
import urllib2
import werkzeug.wrappers
@@ -17,7 +15,8 @@ from PIL import Image
import openerp
from openerp.addons.web import http
from openerp.http import request, Response
from openerp.http import request, STATIC_CACHE
from openerp.tools import image_save_for_web
logger = logging.getLogger(__name__)
@@ -316,7 +315,7 @@ class Website(openerp.addons.web.controllers.main.Home):
return True
@http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
def attach(self, func, upload=None, url=None):
def attach(self, func, upload=None, url=None, disable_optimization=None):
Attachments = request.registry['ir.attachment']
website_url = message = None
@@ -339,6 +338,9 @@ class Website(openerp.addons.web.controllers.main.Home):
u"Image size excessive, uploaded images must be smaller "
u"than 42 million pixel")
if not disable_optimization and image.format in ('PNG', 'JPEG'):
image_data = image_save_for_web(image)
attachment_id = Attachments.create(request.cr, request.uid, {
'name': upload.filename,
'datas': image_data.encode('base64'),
@@ -397,8 +399,8 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route([
'/website/image',
'/website/image/<model>/<id>/<field>',
'/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>'
'/website/image/<model>-<id>-<field>',
'/website/image/<model>-<id>-<field>-<int:max_width>x<int:max_height>'
], auth="public", website=True)
def website_image(self, model, id, field, max_width=None, max_height=None):
""" Fetches the requested field and ensures it does not go above
@@ -416,9 +418,12 @@ class Website(openerp.addons.web.controllers.main.Home):
all cases.
"""
try:
idsha = id.split('_')
id = idsha[0]
response = werkzeug.wrappers.Response()
return request.registry['website']._image(
request.cr, request.uid, model, id, field, response, max_width, max_height)
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)
+7 -11
View File
@@ -286,21 +286,17 @@ class Image(orm.AbstractModel):
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
if options is None: options = {}
classes = ['img', 'img-responsive'] + options.get('class', '').split()
aclasses = ['img', 'img-responsive'] + options.get('class', '').split()
classes = ' '.join(itertools.imap(escape, aclasses))
url_frags = {
'classes': ' '.join(itertools.imap(escape, classes)),
'model': record._model._name,
'id': record.id,
'field': field_name,
'max_size': '',
}
max_size = None
max_width, max_height = options.get('max_width', 0), options.get('max_height', 0)
if max_width or max_height:
url_frags['max_size'] = '/%sx%s' % (max_width, max_height)
max_size = '%sx%s' % (max_width, max_height)
img = '<img class="%(classes)s" src="/website/image/%(model)s/%(id)s/%(field)s%(max_size)s"/>'
return ir_qweb.HTMLSafe(img % url_frags)
src = self.pool['website'].image_url(cr, uid, record, field_name, max_size)
img = '<img class="%s" src="%s"/>' % (classes, src)
return ir_qweb.HTMLSafe(img)
local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
def from_html(self, cr, uid, model, column, element, context=None):
+18 -11
View File
@@ -4,13 +4,13 @@ import contextlib
import datetime
import hashlib
import inspect
import itertools
import logging
import math
import mimetypes
import unicodedata
import os
import re
import time
import urlparse
from PIL import Image
@@ -25,8 +25,7 @@ except ImportError:
import openerp
from openerp.osv import orm, osv, fields
from openerp.tools import html_escape as escape
from openerp.tools import ustr as ustr
from openerp.tools import html_escape as escape, ustr, image_resize_and_sharpen, image_save_for_web
from openerp.tools.safe_eval import safe_eval
from openerp.addons.web.http import request
@@ -513,7 +512,7 @@ class website(osv.osv):
response.data = f.read()
return response.make_conditional(request.httprequest)
def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, context=None):
def _image(self, cr, uid, model, id, 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.
@@ -565,6 +564,10 @@ class website(osv.osv):
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
@@ -585,14 +588,22 @@ class website(osv.osv):
if w < max_w and h < max_h:
response.data = data
else:
image.thumbnail((max_w, max_h), Image.ANTIALIAS)
image.save(response.stream, image.format)
size = (max_w, max_h)
img = image_resize_and_sharpen(image, size)
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
id = '%s_%s' % (record.id, hashlib.sha1(record.sudo().write_date).hexdigest()[0:7])
size = '' if size is None else '-%s' % size
return '/website/image/%s-%s-%s%s' % (model, id, field, size)
class website_menu(osv.osv):
_name = "website.menu"
@@ -668,11 +679,7 @@ class ir_attachment(osv.osv):
if attach.url:
result[attach.id] = attach.url
else:
result[attach.id] = urlplus('/website/image', {
'model': 'ir.attachment',
'field': 'datas',
'id': attach.id
})
result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas')
return result
def _datas_checksum(self, cr, uid, ids, name, arg, context=None):
return dict(
@@ -1387,6 +1387,10 @@
'click button.filepicker': function () {
this.$('input[type=file]').click();
},
'click .js_disable_optimization': function () {
this.$('input[name="disable_optimization"]').val('1');
this.$('button.filepicker').click();
},
'change input[type=file]': 'file_selection',
'submit form': 'form_submit',
'change input.url': "change_input",
@@ -204,7 +204,17 @@
<div class="well">
<div class="form-group pull-left">
<input type="file" name="upload" accept="image/*" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/>
<button type="button" class="btn btn-primary filepicker">Upload an image from your computer</button>
<input type="hidden" name="disable_optimization" value=""/>
<div class="btn-group">
<button type="button" class="btn btn-primary filepicker">Upload an image from your computer</button>
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Alternate Upload</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="#" class="js_disable_optimization">Upload image without optimization</a></li>
</ul>
</div>
<button type="button" class="btn hidden wait" disabled="disabled">Uploading...</button>
</div>
<div>
+10 -4
View File
@@ -88,8 +88,8 @@
</t>
</t>
<t t-call-assets="web.assets_common"/>
<t t-call-assets="website.assets_frontend"/>
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="website.assets_frontend" t-js="false"/>
<t t-raw="head or ''" name='layout_head'/>
<t t-if="website and website.google_analytics_key">
@@ -150,6 +150,9 @@
</div>
</footer>
</div>
<t t-call-assets="web.assets_common" t-css="false"/>
<t t-call-assets="website.assets_frontend" t-css="false"/>
</body>
</html>
</template>
@@ -177,8 +180,11 @@
</template>
<template id="editor_head" inherit_id="website.layout" name="Editor" groups="base.group_website_publisher,base.group_website_designer">
<xpath expr='//t[@t-call-assets="website.assets_frontend"]' position="after">
<t t-call-assets="website.assets_editor"/>
<xpath expr='//t[@t-call-assets="website.assets_frontend"][@t-js]' position="after">
<t t-call-assets="website.assets_editor" t-js="false"/>
</xpath>
<xpath expr='//t[@t-call-assets="website.assets_frontend"][@t-css]' position="after">
<t t-call-assets="website.assets_editor" t-css="false"/>
</xpath>
</template>
@@ -123,7 +123,7 @@
<div t-foreach="blog_posts" t-as="blog_post" class="mb32">
<img class="img-circle pull-right mt16"
t-attf-src="/website/image/res.partner/{{ blog_post.author_id.id }}/image_small"
t-att-src="website.image_url(blog_post.author_id, 'image_small')"
style="width: 50px;"/>
<a t-attf-href="/blog/#{ slug(blog_post.blog_id) }/post/#{ slug(blog_post) }">
@@ -205,7 +205,7 @@
<h2 t-field="blog_post.subtitle"/>
<p class="post-meta text-muted text-center" name="blog_post_data"/>
<div>
<img class="img-circle" t-attf-src="/website/image/res.partner/{{ blog_post.author_id.id }}/image_small" style="width: 30px; margin-right: 10px;"/>
<img class="img-circle" t-att-src="website.image_url(blog_post.author_id, 'image_small')" style="width: 30px; margin-right: 10px;"/>
<span t-field="blog_post.author_id" style="display: inline-block;" t-field-options='{
"widget": "contact",
"fields": ["name"]
@@ -224,7 +224,7 @@
<ul class="media-list" id="comments-list">
<li t-foreach="comments" t-as="message" class="media">
<span class="pull-left">
<img class="media-object img img-circle" t-attf-src="/website/image/mail.message/{{ message.id }}/author_avatar" style="width: 30px"/>
<img class="media-object img img-circle" t-att-src="website.image_url(message, 'author_avatar')" style="width: 30px"/>
</span>
<div class="media-body">
<t t-call="website.publish_short">
@@ -249,7 +249,7 @@
<h1 t-field="next_post.name"/>
<h2 t-field="next_post.subtitle"/>
<div>
<img class="img-circle" t-attf-src="'/website/image/res.partner/{{ next_post.author_id.id }}/image_small" style="width: 30px; margin-right: 10px;"/>
<img class="img-circle" t-att-src="website.image_url(next_post.author_id, 'image_small')" style="width: 30px; margin-right: 10px;"/>
<span t-field="next_post.author_id" style="display: inline-block;" t-field-options='{
"widget": "contact",
"fields": ["name"]
@@ -292,7 +292,7 @@
<form id="comment" t-attf-action="/blogpost/comment" method="POST">
<div class="media">
<span class="pull-left">
<img class="img img-circle media-object" t-attf-src="/website/image/res.partner/{{ user_id.partner_id.id }}/image_small" style="width: 30px"/>
<img class="img img-circle media-object" t-att-src="website.image_url(user_id.partner_id, 'image_small')" style="width: 30px"/>
</span>
<div class="media-body">
<input name="blog_post_id" t-att-value="blog_post.id" type="hidden"/>
+1 -1
View File
@@ -404,7 +404,7 @@
<div class="country_events_list">
<div>
<t t-if="country">
<img class="img-rounded img-responsive" t-attf-src="/website/image/res.country/{{ country.id }}/image"></img>
<img class="img-rounded img-responsive" t-att-src="website.image_url(country, 'image')"></img>
<h4><b>Events: <span t-esc="country.name"></span></b></h4>
</t>
<t t-if="not country">
-3
View File
@@ -12,6 +12,3 @@ class hr(osv.osv):
_defaults = {
'website_published': False
}
def img(self, cr, uid, ids, field='image_small', context=None):
return "/website/image/%s/%s/%s" % (self._name, ids[0], field)
+1 -1
View File
@@ -12,7 +12,7 @@
<div t-foreach="employee_ids" t-as="employee" class="col-sm-3 col-lg-2 mt16 text-center colsize">
<t t-call="website.publish_management"><t t-set="object" t-value="employee"/></t>
<div class="clearfix"/>
<img t-att-src="employee.img('image_medium')" class="img shadow img-rounded"/>
<img t-att-src="website.image_url(employee, 'image_medium')" class="img shadow img-rounded"/>
<div class="mt8">
<strong t-field="employee.name"></strong>
</div>
@@ -9,7 +9,7 @@
</template>
<template id="instant_click_init" inherit_id="website.layout" name="Instant Click Init" groups="base.group_public">
<xpath expr="//div[@id='wrapwrap']" position="after">
<xpath expr="//body" position="inside">
<script data-no-instant="">
InstantClick.init();
</script>
@@ -3,7 +3,7 @@
<data>
<template id="header" inherit_id="website.layout" name="LiveChat Import (internal) Scripts">
<xpath expr="//head" position="inside">
<xpath expr="//body" position="inside">
<t t-if="website and website.channel_id">
<t t-raw="website.channel_id.script_internal"/>
</t>
@@ -10,6 +10,9 @@
<template id="head" inherit_id="website.assets_frontend" name="Mail Group Snippet">
<xpath expr="/t" position="inside">
<link rel="stylesheet" href="/website_mail_group/static/src/css/website_mail_group.css"/>
<script type="text/javascript" src="/website_mail_group/static/src/js/website_mail_group.js"/>
<script type="text/javascript" src="/website_mail_group/static/src/js/website_mail_group.snippet.js"></script>
</xpath>
</template>
@@ -36,7 +39,7 @@
</div>
<div class="row mt8" t-foreach="groups" t-as="group">
<div class="col-md-3">
<img t-attf-src="/website/image/mail.group/{{ group['id'] }}/image_small" class="pull-left"/>
<img t-att-src="website.image_url(group, 'image_small')" class="pull-left"/>
<strong><a t-attf-href="/groups/#{ slug(group) }" t-esc="group.name"/></strong><br />
<t t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain">
<i class='fa fa-envelope-o'/>
@@ -60,10 +63,6 @@
<template id="group_messages" name="Message Threads">
<t t-call="website.layout">
<t t-set="head">
<link rel='stylesheet' href="/website_mail_group/static/src/css/website_mail_group.css"/>
<script type="text/javascript" src="/website_mail_group/static/src/js/website_mail_group.js"/>
</t>
<section class="container">
<div class="mt8">
<ol class="breadcrumb pull-left">
@@ -123,10 +122,6 @@
<template id="group_message">
<t t-call="website.layout">
<t t-set="head">
<link rel='stylesheet' href="/website_mail_group/static/src/css/website_mail_group.css"/>
<script type="text/javascript" src="/website_mail_group/static/src/js/website_mail_group.js"/>
</t>
<section class="container">
<div class="row mt8">
<ol class="breadcrumb pull-left">
@@ -182,7 +177,7 @@
</div>
<div class="media">
<img class="img-rounded pull-left mt0 media-object o_mg_avatar"
t-attf-src="/website/image/mail.message/{{ message.id }}/author_avatar"/>
t-att-src="website.image_url(message, 'author_avatar')"/>
<div class="media-body">
<h4 class="media-heading" t-esc="message.description"/>
<small>
@@ -252,7 +247,7 @@
<ul class="media-list">
<li t-foreach="messages" t-as="thread" class="media">
<img class="img-rounded pull-left mt0 media-object o_mg_avatar"
t-attf-src="/website/image/mail.message/{{ thread.id }}/author_avatar"/>
t-att-src="website.image_url(thread, 'author_avatar')"/>
<div class="media-body">
<h4 class="media-heading">
<a t-attf-href="/groups/#{slug(group)}/#{slug(thread)}?mode=#{mode}&amp;date_begin=#{date_begin}&amp;date_end=#{date_end}" t-esc="thread.description"/>
@@ -116,7 +116,7 @@
<t t-foreach="quotation.message_ids" t-as="message">
<li class="media" t-if="message.type &lt;&gt; 'comment' or message.subtype_id">
<div class="media-body">
<img class="media-object pull-left" t-attf-src="/website/image/res.partner/{{ message.author_id.id }}/image_small" style="width: 50px; margin-right: 10px;"/>
<img class="media-object pull-left" t-att-src="website.image_url(message.author_id, 'image_small')" style="width: 50px; margin-right: 10px;"/>
<div class="media-body">
<h5 class="media-heading">
<span t-field="message.author_id"/> <small>on <span t-field="message.date"/></small>
@@ -134,7 +134,7 @@
<xpath expr="//h1" position="after">
<section class="mb32 css_editable_mode_hidden hidden-print">
<form id="comment" t-attf-action="/quote/#{quotation.id}/#{quotation.access_token}/post" method="POST">
<img class="img pull-left img-rounded" t-attf-src="/website/image/res.partner/{{ user_id.partner_id.id }}/image_small" style="width: 50px; margin-right: 10px;"/>
<img class="img pull-left img-rounded" t-att-src="website.image_url(user_id.partner_id, 'image_small')" style="width: 50px; margin-right: 10px;"/>
<div class="pull-left mb32" style="width: 75%%">
<textarea rows="4" name="comment" class="form-control" placeholder="Send us a note..."></textarea>
<button type="submit" class="btn btn-primary mt8">Send</button>
-7
View File
@@ -178,9 +178,6 @@ class product_template(osv.Model):
else:
return self.set_sequence_bottom(cr, uid, ids, context=context)
def img(self, cr, uid, ids, field='image_small', context=None):
return "/website/image/%s/%s/%s" % (self._name, ids[0], field)
class product_product(osv.Model):
_inherit = "product.product"
@@ -194,10 +191,6 @@ class product_product(osv.Model):
'website_url': fields.function(_website_url, string="Website url", type="char"),
}
def img(self, cr, uid, ids, field='image_small', context=None):
temp_id = self.browse(cr, uid, ids[0], context=context).product_tmpl_id.id
return "/website/image/product.template/%s/%s" % (temp_id, field)
class product_attribute(osv.Model):
_inherit = "product.attribute"
_columns = {
@@ -121,7 +121,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');
$img.attr("src", "/website/image/product.product/" + product_id + "/image");
$img.attr("src", "/website/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);
}
+5 -9
View File
@@ -12,6 +12,7 @@
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_payment.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_validate.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_buy.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
</xpath>
</template>
@@ -76,7 +77,7 @@
</div>
<div class="oe_product_image">
<a itemprop="url" t-att-href="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']>1 else None))">
<img itemprop="image" class="img img-responsive" t-attf-src="/website/image/product.template/#{product.id}/image{{'' if product_image_big else '/300x300' }}"/>
<img itemprop="image" class="img img-responsive" t-att-src="website.image_url(product, 'image', None if product_image_big else '300x300')"/>
</a>
</div>
<section>
@@ -345,7 +346,6 @@
<template id="product" name="Product">
<t t-call="website.layout">
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
<t t-set="additional_title" t-value="product.name"/>
<div itemscope="itemscope" itemtype="http://schema.org/Product" id="wrap" class="js_sale">
@@ -374,7 +374,7 @@
<section t-attf-class="container oe_website_sale #{(compute_currency(product.lst_price) - product.price) &gt; 0.1 and 'discount'}" id="product_detail">
<div class="row">
<div class="col-sm-7 col-md-7 col-lg-7">
<span itemprop="image" t-field="product.image" t-field-options='{"widget": "image", "class": "product_detail_img"}'/>
<span itemprop="image" class="agr" t-field="product.image" t-field-options='{"widget": "image", "class": "product_detail_img"}'/>
</div><div class="col-sm-5 col-md-5 col-lg-4 col-lg-offset-1">
<h1 itemprop="name" t-field="product.name">Product Name</h1>
<span itemprop="url" style="display:none;" t-esc="'/shop/product/%s' % slug(product)"/>
@@ -603,7 +603,7 @@
<ul class="media-list" id="comments-list" t-if="product.website_message_ids">
<li t-foreach="product.website_message_ids" t-as="message" class="media">
<div class="media-body oe_msg">
<img class="media-object pull-left oe_msg_avatar" t-attf-src="/website/image/res.partner/{{ message.author_id.id }}/image_small" style="width: 50px; margin-right: 10px;"/>
<img class="media-object pull-left oe_msg_avatar" t-att-src="website.image_url(message.author_id, 'image_small')" style="width: 50px; margin-right: 10px;"/>
<div class="media-body oe_msg_content">
<t t-call="website.publish_short">
<t t-set="object" t-value="message"/>
@@ -633,7 +633,7 @@
</ul>
<div class="css_editable_mode_hidden">
<form id="comment" t-attf-action="/shop/product/comment/#{product.id}" method="POST">
<img class="img pull-left img-rounded" t-attf-src="/website/image/res.partner/{{ user_id.partner_id.id }}/image_small" style="width: 50px; margin-right: 10px;"/>
<img class="img pull-left img-rounded" t-att-src="website.image_url(user_id.partner_id, 'image_small')" style="width: 50px; margin-right: 10px;"/>
<div class="pull-left mb32" style="width: 75%%">
<textarea rows="3" name="comment" class="form-control" placeholder="Write a comment..."></textarea>
<a class="btn btn-primary mt8 a-submit">Post</a>
@@ -649,7 +649,6 @@
<template id="cart" name="Shopping Cart">
<t t-call="website.layout">
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
<div id="wrap">
<div class="container oe_website_sale">
@@ -837,7 +836,6 @@
<template id="checkout">
<t t-call="website.layout">
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
<t t-set="additional_title">Shop - Checkout</t>
<div id="wrap">
<div class="container oe_website_sale">
@@ -1014,7 +1012,6 @@
<template id="payment">
<t t-call="website.layout">
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
<t t-set="additional_title">Shop - Select Payment Mode</t>
<div id="wrap">
<div class="container oe_website_sale">
@@ -1133,7 +1130,6 @@
<template id="confirmation">
<t t-call="website.layout">
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
<t t-set="additional_title">Shop - Confirmed</t>
<div id="wrap">
<div class="container oe_website_sale">
+12 -4
View File
@@ -265,14 +265,15 @@ class QWeb(orm.AbstractModel):
for attribute in self._render_att:
if attribute_name[2:].startswith(attribute):
att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
generated_attributes += val and ' %s="%s"' % (att, escape(val)) or " "
if val:
generated_attributes += self.render_attribute(element, att, val, qwebcontext)
break
else:
if attribute_name[2:] in self._render_tag:
t_render = attribute_name[2:]
template_attributes[attribute_name[2:]] = attribute_value
else:
generated_attributes += ' %s="%s"' % (attribute_name, escape(attribute_value))
generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
if 'debug' in template_attributes:
debugger = template_attributes.get('debug', 'pdb')
@@ -328,6 +329,9 @@ class QWeb(orm.AbstractModel):
else:
return "<%s%s/>" % (name, generated_attributes)
def render_attribute(self, element, name, value, qwebcontext):
return ' %s="%s"' % (name, escape(value))
# Attributes
def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
if attribute_name.startswith("t-attf-"):
@@ -1030,6 +1034,7 @@ class AssetsBundle(object):
context = self.context.copy()
context['inherit_branding'] = False
context['rendering_bundle'] = True
self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
self.parse()
@@ -1082,10 +1087,13 @@ class AssetsBundle(object):
for jscript in self.javascripts:
response.append(jscript.to_html())
else:
url_for = self.context.get('url_for', lambda url: url)
if css and self.stylesheets:
response.append('<link href="/web/css/%s/%s" rel="stylesheet"/>' % (self.xmlid, self.version))
href = '/web/css/%s/%s' % (self.xmlid, self.version)
response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
if js:
response.append('<script type="text/javascript" src="/web/js/%s/%s"></script>' % (self.xmlid, self.version))
src = '/web/js/%s/%s' % (self.xmlid, self.version)
response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
response.extend(self.remains)
return sep + sep.join(response)
+100
View File
@@ -5,6 +5,7 @@
import ast
import collections
import contextlib
import datetime
import errno
import functools
import getpass
@@ -21,6 +22,7 @@ import time
import traceback
import urlparse
import warnings
from zlib import adler32
import babel.core
import psutil
@@ -33,6 +35,7 @@ import werkzeug.local
import werkzeug.routing
import werkzeug.wrappers
import werkzeug.wsgi
from werkzeug.wsgi import wrap_file
import openerp
from openerp import SUPERUSER_ID
@@ -1404,6 +1407,103 @@ def db_monodb(httprequest=None):
return dbs[0]
return None
def send_file(filepath_or_fp, mimetype=None, as_attachment=False, filename=None, mtime=None,
add_etags=True, cache_timeout=STATIC_CACHE, conditional=True):
"""This is a modified version of Flask's send_file()
Sends the contents of a file to the client. This will use the
most efficient method available and configured. By default it will
try to use the WSGI server's file_wrapper support.
By default it will try to guess the mimetype for you, but you can
also explicitly provide one. For extra security you probably want
to send certain files as attachment (HTML for instance). The mimetype
guessing requires a `filename` or an `attachment_filename` to be
provided.
Please never pass filenames to this function from user sources without
checking them first.
:param filepath_or_fp: the filename of the file to send.
Alternatively a file object might be provided
in which case `X-Sendfile` might not work and
fall back to the traditional method. Make sure
that the file pointer is positioned at the start
of data to send before calling :func:`send_file`.
:param mimetype: the mimetype of the file if provided, otherwise
auto detection happens.
:param as_attachment: set to `True` if you want to send this file with
a ``Content-Disposition: attachment`` header.
:param filename: the filename for the attachment if it differs from the file's filename or
if using file object without 'name' attribute (eg: E-tags with StringIO).
:param mtime: last modification time to use for contitional response.
:param add_etags: set to `False` to disable attaching of etags.
:param conditional: set to `False` to disable conditional responses.
:param cache_timeout: the timeout in seconds for the headers.
"""
if isinstance(filepath_or_fp, (str, unicode)):
if not filename:
filename = os.path.basename(filepath_or_fp)
file = open(filepath_or_fp, 'rb')
if not mtime:
mtime = os.path.getmtime(filepath_or_fp)
else:
file = filepath_or_fp
if not filename:
filename = getattr(file, 'name', None)
file.seek(0, 2)
size = file.tell()
file.seek(0)
if mimetype is None and filename:
mimetype = mimetypes.guess_type(filename)[0]
if mimetype is None:
mimetype = 'application/octet-stream'
headers = werkzeug.datastructures.Headers()
if as_attachment:
if filename is None:
raise TypeError('filename unavailable, required for sending as attachment')
headers.add('Content-Disposition', 'attachment', filename=filename)
headers['Content-Length'] = size
data = wrap_file(request.httprequest.environ, file)
rv = Response(data, mimetype=mimetype, headers=headers,
direct_passthrough=True)
if isinstance(mtime, str):
try:
server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
mtime = datetime.datetime.strptime(mtime.split('.')[0], server_format)
except Exception:
mtime = None
if mtime is not None:
rv.last_modified = mtime
rv.cache_control.public = True
if cache_timeout:
rv.cache_control.max_age = cache_timeout
rv.expires = int(time.time() + cache_timeout)
if add_etags and filename and mtime:
rv.set_etag('odoo-%s-%s-%s' % (
mtime,
size,
adler32(
filename.encode('utf-8') if isinstance(filename, unicode)
else filename
) & 0xffffffff
))
if conditional:
rv = rv.make_conditional(request.httprequest)
# make sure we don't send x-sendfile for servers that
# ignore the 304 status code for x-sendfile.
if rv.status_code == 304:
rv.headers.pop('x-sendfile', None)
return rv
#----------------------------------------------------------
# RPC controller
#----------------------------------------------------------
+43 -7
View File
@@ -88,13 +88,7 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
return base64_source
if image.size != size:
# create a thumbnail: will resize and keep ratios, then sharpen for better looking result
image.thumbnail(size, Image.ANTIALIAS)
sharpener = ImageEnhance.Sharpness(image.convert('RGBA'))
resized_image = sharpener.enhance(2.0)
# create a transparent image for background and paste the image on it
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))
image = image_resize_and_sharpen(image, size)
if image.mode not in ["1", "L", "P", "RGB", "RGBA"]:
image = image.convert("RGB")
@@ -102,6 +96,48 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
image.save(background_stream, filetype)
return background_stream.getvalue().encode(encoding)
def image_resize_and_sharpen(image, size, factor=2.0):
"""
Create a thumbnail by resizing while keeping ratio.
A sharpen filter is applied for a better looking result.
:param image: PIL.Image.Image()
:param size: 2-tuple(width, height)
:param factor: Sharpen factor (default: 2.0)
"""
if image.mode != 'RGBA':
image = image.convert('RGBA')
image.thumbnail(size, Image.ANTIALIAS)
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))
image.paste(resized_image, ((size[0] - resized_image.size[0]) / 2, (size[1] - resized_image.size[1]) / 2))
return image
def image_save_for_web(image, fp=None, format=None):
"""
Save image optimized for web usage.
:param image: PIL.Image.Image()
:param fp: File name or file object. If not specified, a bytestring is returned.
:param format: File format if could not be deduced from image.
"""
opt = dict(format=image.format or format)
if image.format == 'PNG':
opt.update(optimize=True)
if image.mode != 'P':
# Floyd Steinberg dithering by default
image = image.convert('RGBA').convert('P', palette=Image.WEB, colors=256)
elif image.format == 'JPEG':
opt.update(optimize=True, quality=80)
if fp:
image.save(fp, **opt)
else:
img = StringIO.StringIO()
image.save(img, **opt)
return img.getvalue()
def image_resize_image_big(base64_source, size=(1204, 1024), encoding='base64', filetype=None, avoid_if_small=True):
""" Wrapper on image_resize_image, to resize images larger than the standard
'big' image size: 1024x1024px.