[WIP] AssetsBundle
authorFabien Meghazi <fme@openerp.com>
Wed, 9 Apr 2014 16:17:58 +0000 (18:17 +0200)
committerFabien Meghazi <fme@openerp.com>
Wed, 9 Apr 2014 16:17:58 +0000 (18:17 +0200)
bzr revid: fme@openerp.com-20140409161758-3hye6u21rjmoiio9

openerp/addons/base/ir/ir_qweb.py
openerp/addons/base/ir/ir_ui_view.py
openerp/http.py

index 391dbf6..da58455 100644 (file)
@@ -5,10 +5,12 @@ import datetime
 import json
 import logging
 import math
+import os
 import re
 import sys
 import xml  # FIXME use lxml and etree
 import lxml
+from urlparse import urlparse
 
 import babel
 import babel.dates
@@ -388,42 +390,18 @@ class QWeb(orm.AbstractModel):
         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):
-        template_attributes['call'] = template_attributes['call-assets']
-        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
-
-        tags = dict()
-        current_style = None
-        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)
+        """ 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
 
-        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
+        # 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)
+        if qwebcontext.get('debug'):
+            return content
+        bundle = AssetsBundle(name, html=content)
+        return bundle.to_html()
 
     def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
         if "value" in template_attributes:
@@ -848,7 +826,6 @@ class RelativeDatetimeConverter(osv.AbstractModel):
         return babel.dates.format_timedelta(
             value - reference, add_direction=True, locale=locale)
 
-
 class Contact(orm.AbstractModel):
     _name = 'ir.qweb.field.contact'
     _inherit = 'ir.qweb.field.many2one'
@@ -923,4 +900,82 @@ def get_field_type(column, options):
     """
     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:
index 7c857e7..b6d785f 100644 (file)
@@ -858,7 +858,8 @@ class view(osv.osv):
             values = dict()
         qcontext = dict(
             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,
             quote_plus=werkzeug.url_quote_plus,
         )
index ccff43e..2c3e8d9 100644 (file)
@@ -3,7 +3,6 @@
 # OpenERP HTTP layer
 #----------------------------------------------------------
 import ast
-import base64
 import collections
 import contextlib
 import errno
@@ -138,51 +137,6 @@ def redirect_with_hash(url, code=303):
         return werkzeug.utils.redirect(url, code)
     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):
     """ Parent class for all OpenERP Web request types, mostly deals with
     initialization and setup of the request object (the dispatching itself has
@@ -613,7 +567,7 @@ class HttpRequest(WebRequest):
                 response.set_cookie(k, v)
         return response
 
-    def render(self, template, qcontext=None, **kw):
+    def render(self, template, qcontext=None, lazy=True, **kw):
         """ Lazy render of QWeb template.
 
         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 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):
         """ Helper for 404 response, return its result from the method