~openerp-dev/openobject-server/trunk-staging-mat

« back to all changes in this revision

Viewing changes to openerp/addons/base/ir/ir_qweb.py

[MERGE] Web assets moved from manifests to ir.ui.view bundles

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
import collections
3
3
import cStringIO
4
4
import datetime
 
5
import hashlib
5
6
import json
6
7
import logging
7
8
import math
 
9
import os
8
10
import re
9
11
import sys
10
12
import xml  # FIXME use lxml and etree
 
13
import itertools
 
14
import lxml.html
 
15
from urlparse import urlparse
11
16
 
12
17
import babel
13
18
import babel.dates
14
 
import werkzeug.utils
 
19
import werkzeug
15
20
from PIL import Image
16
21
 
 
22
import openerp.http
17
23
import openerp.tools
 
24
import openerp.tools.func
 
25
import openerp.tools.lru
18
26
from openerp.tools.safe_eval import safe_eval as eval
19
27
from openerp.osv import osv, orm, fields
20
28
from openerp.tools.translate import _
396
404
            pass
397
405
        return self.render(cr, uid, template, d)
398
406
 
 
407
    def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
 
408
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
 
409
        name = template_attributes['call-assets']
 
410
 
 
411
        # Backward compatibility hack for manifest usage
 
412
        qwebcontext['manifest_list'] = openerp.addons.web.controllers.main.manifest_list
 
413
 
 
414
        d = qwebcontext.copy()
 
415
        d.context['inherit_branding'] = False
 
416
        content = self.render_tag_call(
 
417
            element, {'call': name}, generated_attributes, d)
 
418
        if qwebcontext.get('debug'):
 
419
            return content
 
420
        bundle = AssetsBundle(name, html=content)
 
421
        return bundle.to_html()
 
422
 
399
423
    def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
400
424
        if "value" in template_attributes:
401
425
            qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
821
845
        return babel.dates.format_timedelta(
822
846
            value - reference, add_direction=True, locale=locale)
823
847
 
824
 
 
825
848
class Contact(orm.AbstractModel):
826
849
    _name = 'ir.qweb.field.contact'
827
850
    _inherit = 'ir.qweb.field.many2one'
951
974
    """
952
975
    return options.get('widget', column._type)
953
976
 
 
977
class AssetsBundle(object):
 
978
    cache = openerp.tools.lru.LRU(32)
 
979
    rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
 
980
 
 
981
    def __init__(self, xmlid, html=None, debug=False):
 
982
        self.debug = debug
 
983
        self.xmlid = xmlid
 
984
        self.javascripts = []
 
985
        self.stylesheets = []
 
986
        self.remains = []
 
987
        self._checksum = None
 
988
        if html:
 
989
            self.parse(html)
 
990
 
 
991
    def parse(self, html):
 
992
        fragments = lxml.html.fragments_fromstring(html)
 
993
        for el in fragments:
 
994
            if isinstance(el, basestring):
 
995
                self.remains.append(el)
 
996
            elif isinstance(el, lxml.html.HtmlElement):
 
997
                src = el.get('src')
 
998
                href = el.get('href')
 
999
                if el.tag == 'style':
 
1000
                    self.stylesheets.append(StylesheetAsset(source=el.text))
 
1001
                elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
 
1002
                    self.stylesheets.append(StylesheetAsset(url=href))
 
1003
                elif el.tag == 'script' and not src:
 
1004
                    self.javascripts.append(JavascriptAsset(source=el.text))
 
1005
                elif el.tag == 'script' and self.can_aggregate(src):
 
1006
                    self.javascripts.append(JavascriptAsset(url=src))
 
1007
                else:
 
1008
                    self.remains.append(lxml.html.tostring(el))
 
1009
            else:
 
1010
                try:
 
1011
                    self.remains.append(lxml.html.tostring(el))
 
1012
                except Exception:
 
1013
                    # notYETimplementederror
 
1014
                    raise NotImplementedError
 
1015
 
 
1016
    def can_aggregate(self, url):
 
1017
        return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
 
1018
 
 
1019
    def to_html(self, sep='\n'):
 
1020
        response = []
 
1021
        if self.stylesheets:
 
1022
            response.append('<link href="/web/css/%s" rel="stylesheet"/>' % self.xmlid)
 
1023
        if self.javascripts:
 
1024
            response.append('<script type="text/javascript" src="/web/js/%s"></script>' % self.xmlid)
 
1025
        response.extend(self.remains)
 
1026
 
 
1027
        return sep.join(response)
 
1028
 
 
1029
    @openerp.tools.func.lazy_property
 
1030
    def last_modified(self):
 
1031
        return max(itertools.chain(
 
1032
            (asset.last_modified for asset in self.javascripts),
 
1033
            (asset.last_modified for asset in self.stylesheets),
 
1034
            [datetime.datetime(1970, 1, 1)],
 
1035
        ))
 
1036
 
 
1037
    @openerp.tools.func.lazy_property
 
1038
    def checksum(self):
 
1039
        checksum = hashlib.new('sha1')
 
1040
        for asset in itertools.chain(self.javascripts, self.stylesheets):
 
1041
            checksum.update(asset.content.encode("utf-8"))
 
1042
        return checksum.hexdigest()
 
1043
 
 
1044
    def js(self):
 
1045
        key = 'js_' + self.checksum
 
1046
        if key not in self.cache:
 
1047
            content =';\n'.join(asset.minify() for asset in self.javascripts)
 
1048
            self.cache[key] = content
 
1049
        if self.debug:
 
1050
            return "/*\n%s\n*/\n" % '\n'.join(
 
1051
                [asset.filename for asset in self.javascripts if asset.filename]) + self.cache[key]
 
1052
        return self.cache[key]
 
1053
 
 
1054
    def css(self):
 
1055
        key = 'css_' + self.checksum
 
1056
        if key not in self.cache:
 
1057
            content = '\n'.join(asset.minify() for asset in self.stylesheets)
 
1058
            # move up all @import rules to the top
 
1059
            matches = []
 
1060
            def push(matchobj):
 
1061
                matches.append(matchobj.group(0))
 
1062
                return ''
 
1063
 
 
1064
            content = re.sub(self.rx_css_import, push, content)
 
1065
 
 
1066
            matches.append(content)
 
1067
            content = u'\n'.join(matches)
 
1068
            self.cache[key] = content
 
1069
        if self.debug:
 
1070
            return "/*\n%s\n*/\n" % '\n'.join(
 
1071
                [asset.filename for asset in self.javascripts if asset.filename]) + self.cache[key]
 
1072
        return self.cache[key]
 
1073
 
 
1074
class WebAsset(object):
 
1075
    def __init__(self, source=None, url=None):
 
1076
        self.source = source
 
1077
        self.url = url
 
1078
        self._filename = None
 
1079
        self._content = None
 
1080
 
 
1081
    @property
 
1082
    def filename(self):
 
1083
        if self._filename is None and self.url:
 
1084
            module = filter(None, self.url.split('/'))[0]
 
1085
            try:
 
1086
                mpath = openerp.http.addons_manifest[module]['addons_path']
 
1087
            except Exception:
 
1088
                raise KeyError("Could not find asset '%s' for '%s' addon" % (self.url, module))
 
1089
            self._filename = mpath + self.url.replace('/', os.path.sep)
 
1090
        return self._filename
 
1091
 
 
1092
    @property
 
1093
    def content(self):
 
1094
        if self._content is None:
 
1095
            self._content = self.get_content()
 
1096
        return self._content
 
1097
 
 
1098
    def get_content(self):
 
1099
        if self.source:
 
1100
            return self.source
 
1101
 
 
1102
        with open(self.filename, 'rb') as fp:
 
1103
            return fp.read().decode('utf-8')
 
1104
 
 
1105
    def minify(self):
 
1106
        return self.content
 
1107
 
 
1108
    @property
 
1109
    def last_modified(self):
 
1110
        if self.source:
 
1111
            # TODO: return last_update of bundle's ir.ui.view
 
1112
            return datetime.datetime(1970, 1, 1)
 
1113
        return datetime.datetime.fromtimestamp(os.path.getmtime(self.filename))
 
1114
 
 
1115
class JavascriptAsset(WebAsset):
 
1116
    def minify(self):
 
1117
        return rjsmin(self.content)
 
1118
 
 
1119
class StylesheetAsset(WebAsset):
 
1120
    rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
 
1121
    rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
 
1122
    rx_comments = re.compile(r"""/\*.*\*/""", re.S)
 
1123
    rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
 
1124
 
 
1125
    def _get_content(self):
 
1126
        if self.source:
 
1127
            return self.source
 
1128
 
 
1129
        with open(self.filename, 'rb') as fp:
 
1130
            firstline = fp.readline()
 
1131
            m = re.match(r'@charset "([^"]+)";', firstline)
 
1132
            if m:
 
1133
                encoding = m.group(1)
 
1134
            else:
 
1135
                encoding = "utf-8"
 
1136
                # "reinject" first line as it's not @charset
 
1137
                fp.seek(0)
 
1138
 
 
1139
            return fp.read().decode(encoding)
 
1140
 
 
1141
    def get_content(self):
 
1142
        content = self._get_content()
 
1143
        if self.url:
 
1144
            web_dir = os.path.dirname(self.url)
 
1145
 
 
1146
            content = self.rx_import.sub(
 
1147
                r"""@import \1%s/""" % (web_dir,),
 
1148
                content,
 
1149
            )
 
1150
 
 
1151
            content = self.rx_url.sub(
 
1152
                r"url(\1%s/" % (web_dir,),
 
1153
                content,
 
1154
            )
 
1155
        return content
 
1156
 
 
1157
    def minify(self):
 
1158
        # remove existing sourcemaps, make no sense after re-mini
 
1159
        return self.rx_sourceMap.sub('', self.content)
 
1160
        # return self.rx_comments.sub('', self.content)
 
1161
 
 
1162
def rjsmin(script):
 
1163
    """ Minify js with a clever regex.
 
1164
    Taken from http://opensource.perlig.de/rjsmin
 
1165
    Apache License, Version 2.0 """
 
1166
    def subber(match):
 
1167
        """ Substitution callback """
 
1168
        groups = match.groups()
 
1169
        return (
 
1170
            groups[0] or
 
1171
            groups[1] or
 
1172
            groups[2] or
 
1173
            groups[3] or
 
1174
            (groups[4] and '\n') or
 
1175
            (groups[5] and ' ') or
 
1176
            (groups[6] and ' ') or
 
1177
            (groups[7] and ' ') or
 
1178
            ''
 
1179
        )
 
1180
 
 
1181
    result = re.sub(
 
1182
        r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
 
1183
        r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
 
1184
        r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
 
1185
        r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
 
1186
        r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
 
1187
        r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
 
1188
        r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
 
1189
        r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
 
1190
        r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
 
1191
        r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
 
1192
        r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
 
1193
        r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
 
1194
        r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
 
1195
        r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
 
1196
        r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
 
1197
        r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
 
1198
        r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
 
1199
        r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
 
1200
        r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
 
1201
        r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
 
1202
        r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
 
1203
    ).strip()
 
1204
    return result
 
1205
 
954
1206
# vim:et: