[WIP] AssetsBundle

bzr revid: fme@openerp.com-20140409161758-3hye6u21rjmoiio9
This commit is contained in:
Fabien Meghazi 2014-04-09 18:17:58 +02:00
parent cbb5488f97
commit c1dc467701
3 changed files with 99 additions and 85 deletions

View File

@ -5,10 +5,12 @@ import datetime
import json import json
import logging import logging
import math import math
import os
import re import re
import sys import sys
import xml # FIXME use lxml and etree import xml # FIXME use lxml and etree
import lxml import lxml
from urlparse import urlparse
import babel import babel
import babel.dates import babel.dates
@ -388,42 +390,18 @@ class QWeb(orm.AbstractModel):
return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d) return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d)
def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext): def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
template_attributes['call'] = template_attributes['call-assets'] """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
name = template_attributes['call-assets']
template_attributes['call'] = name
# Backward compatibility hack for manifest usage
qwebcontext['manifest_list'] = openerp.addons.web.controllers.main.manifest_list
content = self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext) content = self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext)
html = lxml.html.fragment_fromstring('<s>%s</s>' % content) # avoid this hack if possible if qwebcontext.get('debug'):
return content
tags = dict() bundle = AssetsBundle(name, html=content)
current_style = None return bundle.to_html()
current_script = None
for el in html:
if el.tag == 'link' and el.attrib.get('rel') == 'stylesheet':
cssfile = el.attrib.get('href')
if current_style is None:
current_style = el
tags[current_style] = []
el.attrib['href'] = '/web/css/'
else:
html.remove(el)
tags[current_style].append(cssfile)
if el.tag == 'script':
if 'src' not in el.attrib:
current_script = None
else:
jsfile = el.attrib['src']
if current_script is None:
current_script = el
tags[current_script] = []
el.attrib['src'] = '/web/js/'
else:
html.remove(el)
tags[current_script].append(jsfile)
for tag, files in tags.items():
attr = 'href' if 'href' in tag.attrib else 'src'
tag.attrib[attr] += openerp.http.pathlist_to_base64(files)
r = lxml.html.tostring(html)[3:-4] # avoid this hack if possible
return r
def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext): def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
if "value" in template_attributes: if "value" in template_attributes:
@ -848,7 +826,6 @@ class RelativeDatetimeConverter(osv.AbstractModel):
return babel.dates.format_timedelta( return babel.dates.format_timedelta(
value - reference, add_direction=True, locale=locale) value - reference, add_direction=True, locale=locale)
class Contact(orm.AbstractModel): class Contact(orm.AbstractModel):
_name = 'ir.qweb.field.contact' _name = 'ir.qweb.field.contact'
_inherit = 'ir.qweb.field.many2one' _inherit = 'ir.qweb.field.many2one'
@ -923,4 +900,82 @@ def get_field_type(column, options):
""" """
return options.get('widget', column._type) return options.get('widget', column._type)
class AssetsBundle(object):
def __init__(self, xmlid, html=None):
self.xmlid = xmlid
self.javascripts = []
self.stylesheets = []
self.remains = []
if html:
self.parse(html)
def parse(self, html):
fragments = lxml.html.fragments_fromstring(html)
for el in fragments:
if type(el) is basestring:
self.remains.append(el)
elif type(el) is lxml.html.HtmlElement:
src = el.attrib.get('src')
href = el.attrib.get('href')
if el.tag == 'style':
self.stylesheets.append(StylesheetAsset(inline=el.text))
elif el.tag == 'link' and el.attrib['rel'] == 'stylesheet' and self.is_internal_url(href):
self.stylesheets.append(StylesheetAsset(url=href))
elif el.tag == 'script' and not src:
self.javascripts.append(JavascriptAsset(inline=el.text))
elif el.tag == 'script' and self.is_internal_url(src):
self.javascripts.append(JavascriptAsset(url=src))
else:
self.remains.append(lxml.html.tostring(el))
else:
try:
self.remains.append(lxml.html.tostring(el))
except Exception:
raise NotImplementedError
def is_internal_url(self, url):
return not urlparse(url).netloc
def to_html(self, sep='\n'):
response = list(self.remains)
if self.stylesheets:
response.insert(0, '<link href="/web/css/%s" rel="stylesheet"/>' % self.xmlid)
if self.javascripts:
response.insert(0, '<script type="text/javascript" src="/web/js/%s"></script>' % self.xmlid)
return sep.join(response)
def js(self):
content = '\n'.join([asset.get_content() for asset in self.javascripts])
return content
def css(self):
content = '\n'.join([asset.get_content() for asset in self.stylesheets])
return content
class WebAsset(object):
def __init__(self, source=None, url=None, filename=None):
self.source = source
self.filename = filename
self.url = url
def get_content(self):
if self.source:
return self.source
if self.url:
module = filter(bool, self.url.split('/'))[0]
mpath = openerp.http.addons_manifest[module]['addons_path']
self.filename = mpath + self.url.replace('/', os.path.sep)
with open(self.filename, 'rb') as fp:
data = fp.read().decode('utf-8')
return data
class JavascriptAsset(WebAsset):
def get_content(self):
content = super(JavascriptAsset, self).get_content()
return content + ';'
class StylesheetAsset(WebAsset):
pass
# vim:et: # vim:et:

View File

@ -858,7 +858,8 @@ class view(osv.osv):
values = dict() values = dict()
qcontext = dict( qcontext = dict(
keep_query=keep_query, keep_query=keep_query,
request=request, request=request, # might be unbound if we're not in an httprequest context
debug=request.debug if request else False,
json=simplejson, json=simplejson,
quote_plus=werkzeug.url_quote_plus, quote_plus=werkzeug.url_quote_plus,
) )

View File

@ -3,7 +3,6 @@
# OpenERP HTTP layer # OpenERP HTTP layer
#---------------------------------------------------------- #----------------------------------------------------------
import ast import ast
import base64
import collections import collections
import contextlib import contextlib
import errno import errno
@ -138,51 +137,6 @@ def redirect_with_hash(url, code=303):
return werkzeug.utils.redirect(url, code) return werkzeug.utils.redirect(url, code)
return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
def pathlist_to_base64(files):
""" Serialize a path list to an url-safe string trying to avoid path
duplication while keeping files order.
"""
seen = []
concat = []
current_path = None
for f in files:
if f in seen:
continue
seen.append(f)
path, fname = os.path.split(f)
if not path:
path = '/'
if path != current_path:
current_path = path
concat.append(path)
concat.append(fname)
r = '|'.join(concat)
return base64.urlsafe_b64encode(r.encode('zlib'))
def base64_to_pathlist(s, fspath=False):
""" Deserialize a string generated by filelist_to_base64
:param fspath: returns tuple(filesystem path, url path) if True
"""
files = []
decoded = base64.urlsafe_b64decode(str(s)).decode('zlib')
items = decoded.split('|')
current_path = '/'
while items:
item = items.pop(0)
if item.startswith('/'):
current_path = item
else:
path = current_path + '/' + item
if fspath:
module = filter(bool, path.split('/'))[0]
mpath = addons_manifest[module]['addons_path']
fs_path = mpath + path.replace('/', os.path.sep)
files.append((fs_path, path))
else:
files.append(path)
return files
class WebRequest(object): class WebRequest(object):
""" Parent class for all OpenERP Web request types, mostly deals with """ Parent class for all OpenERP Web request types, mostly deals with
initialization and setup of the request object (the dispatching itself has initialization and setup of the request object (the dispatching itself has
@ -613,7 +567,7 @@ class HttpRequest(WebRequest):
response.set_cookie(k, v) response.set_cookie(k, v)
return response return response
def render(self, template, qcontext=None, **kw): def render(self, template, qcontext=None, lazy=True, **kw):
""" Lazy render of QWeb template. """ Lazy render of QWeb template.
The actual rendering of the given template will occur at then end of The actual rendering of the given template will occur at then end of
@ -622,8 +576,12 @@ class HttpRequest(WebRequest):
:param basestring template: template to render :param basestring template: template to render
:param dict qcontext: Rendering context to use :param dict qcontext: Rendering context to use
:param dict lazy: Lazy rendering is processed later in wsgi response layer (default True)
""" """
return Response(template=template, qcontext=qcontext, **kw) response = Response(template=template, qcontext=qcontext, **kw)
if not lazy:
return response.render()
return response
def not_found(self, description=None): def not_found(self, description=None):
""" Helper for 404 response, return its result from the method """ Helper for 404 response, return its result from the method