[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
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 540 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 33 KiB |
@@ -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):
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}&date_begin=#{date_begin}&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 <> '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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) > 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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
#----------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||