X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=openerp%2Faddons%2Fbase%2Fir%2Fir_qweb.py;h=d1e6af86c5dd6a06b73a2bc59581325d583c292f;hb=709c9868de7c4ac4985ed4e0055ae3e3e49742ad;hp=d27d15b76f44413b3d379282e00c0ba9662ec9bb;hpb=e8831932464814ed5f9e3a0af33f98891a466417;p=odoo%2Fodoo.git diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index d27d15b..d1e6af8 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -250,6 +250,14 @@ class QWeb(orm.AbstractModel): generated_attributes = "" t_render = None template_attributes = {} + + debugger = element.get('t-debug') + if debugger is not None: + if openerp.tools.config['dev_mode']: + __import__(debugger).set_trace() # pdb, ipdb, pudb, ... + else: + _logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__']) + for (attribute_name, attribute_value) in element.attrib.iteritems(): attribute_name = str(attribute_name) if attribute_name == "groups": @@ -275,16 +283,13 @@ class QWeb(orm.AbstractModel): else: generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext) - if 'debug' in template_attributes: - debugger = template_attributes.get('debug', 'pdb') - __import__(debugger).set_trace() # pdb, ipdb, pudb, ... if t_render: result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext) else: result = self.render_element(element, template_attributes, generated_attributes, qwebcontext) if element.tail: - result += element.tail.encode('utf-8') + result += self.render_tail(element.tail, element, qwebcontext) if isinstance(result, unicode): return result.encode('utf-8') @@ -299,7 +304,7 @@ class QWeb(orm.AbstractModel): if inner: g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner else: - g_inner = [] if element.text is None else [element.text.encode('utf-8')] + g_inner = [] if element.text is None else [self.render_text(element.text, element, qwebcontext)] for current_node in element.iterchildren(tag=etree.Element): try: g_inner.append(self.render_node(current_node, qwebcontext)) @@ -332,6 +337,12 @@ class QWeb(orm.AbstractModel): def render_attribute(self, element, name, value, qwebcontext): return ' %s="%s"' % (name, escape(value)) + def render_text(self, text, element, qwebcontext): + return text.encode('utf-8') + + def render_tail(self, tail, element, qwebcontext): + return tail.encode('utf-8') + # Attributes def render_att_att(self, element, attribute_name, attribute_value, qwebcontext): if attribute_name.startswith("t-attf-"): @@ -893,7 +904,7 @@ class Contact(orm.AbstractModel): val = { 'name': value.split("\n")[0], - 'address': escape("\n".join(value.split("\n")[1:])), + 'address': escape("\n".join(value.split("\n")[1:])).strip(), 'phone': field_browse.phone, 'mobile': field_browse.mobile, 'fax': field_browse.fax, @@ -1011,17 +1022,8 @@ class AssetNotFound(AssetError): pass class AssetsBundle(object): - # Sass installation: - # - # sudo gem install sass compass bootstrap-sass - # - # If the following error is encountered: - # 'ERROR: Cannot load compass.' - # Use this: - # sudo gem install compass --pre - cmd_sass = ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass', '-r', 'bootstrap-sass'] rx_css_import = re.compile("(@import[^;{]+;?)", re.M) - rx_sass_import = re.compile("""(@import\s?['"]([^'"]+)['"])""") + rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""") rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/") def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None): @@ -1054,12 +1056,16 @@ class AssetsBundle(object): media = el.get('media') if el.tag == 'style': if atype == 'text/sass' or src.endswith('.sass'): - self.stylesheets.append(SassAsset(self, inline=el.text, media=media)) + self.stylesheets.append(SassStylesheetAsset(self, inline=el.text, media=media)) + elif atype == 'text/less' or src.endswith('.less'): + self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media)) else: self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media)) elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href): if href.endswith('.sass') or atype == 'text/sass': - self.stylesheets.append(SassAsset(self, url=href, media=media)) + self.stylesheets.append(SassStylesheetAsset(self, url=href, media=media)) + elif href.endswith('.less') or atype == 'text/less': + self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media)) else: self.stylesheets.append(StylesheetAsset(self, url=href, media=media)) elif el.tag == 'script' and not src: @@ -1084,7 +1090,10 @@ class AssetsBundle(object): response = [] if debug: if css and self.stylesheets: - self.compile_sass() + self.preprocess_css() + if self.css_errors: + msg = '\n'.join(self.css_errors) + self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg))) for style in self.stylesheets: response.append(style.to_html()) if js: @@ -1130,14 +1139,14 @@ class AssetsBundle(object): return content def css(self): + """Generate css content from given bundle""" content = self.get_cache('css') if content is None: - self.compile_sass() - content = '\n'.join(asset.minify() for asset in self.stylesheets) + content = self.preprocess_css() if self.css_errors: msg = '\n'.join(self.css_errors) - content += self.css_message(msg.replace('\n', '\\A ')) + content += self.css_message(msg) # move up all @import rules to the top matches = [] @@ -1165,12 +1174,8 @@ class AssetsBundle(object): def set_cache(self, type, content): ira = self.registry['ir.attachment'] - url_prefix = '/web/%s/%s/' % (type, self.xmlid) - # Invalidate previous caches - oids = ira.search(self.cr, self.uid, [('url', '=like', url_prefix + '%')], context=self.context) - if oids: - ira.unlink(self.cr, openerp.SUPERUSER_ID, oids, context=self.context) - url = url_prefix + self.version + ira.invalidate_bundle(self.cr, openerp.SUPERUSER_ID, type=type, xmlid=self.xmlid) + url = '/web/%s/%s/%s' % (type, self.xmlid, self.version) ira.create(self.cr, openerp.SUPERUSER_ID, dict( datas=content.encode('utf8').encode('base64'), type='binary', @@ -1179,6 +1184,8 @@ class AssetsBundle(object): ), context=self.context) def css_message(self, message): + # '\A' == css content carriage return + message = message.replace('\n', '\\A ').replace('"', '\\"') return """ body:before { background: #ffc; @@ -1188,57 +1195,72 @@ class AssetsBundle(object): white-space: pre; content: "%s"; } - """ % message.replace('"', '\\"') + """ % message - def compile_sass(self): + def preprocess_css(self): """ - Checks if the bundle contains any sass content, then compiles it to css. - Css compilation is done at the bundle level and not in the assets - because they are potentially interdependant. + Checks if the bundle contains any sass/less content, then compiles it to css. + Returns the bundle's flat css. """ - sass = [asset for asset in self.stylesheets if isinstance(asset, SassAsset)] - if not sass: - return - source = '\n'.join([asset.get_source() for asset in sass]) - - # move up all @import rules to the top and exclude file imports + for atype in (SassStylesheetAsset, LessStylesheetAsset): + assets = [asset for asset in self.stylesheets if isinstance(asset, atype)] + if assets: + cmd = assets[0].get_command() + source = '\n'.join([asset.get_source() for asset in assets]) + compiled = self.compile_css(cmd, source) + + fragments = self.rx_css_split.split(compiled) + at_rules = fragments.pop(0) + if at_rules: + # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible + self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules)) + while fragments: + asset_id = fragments.pop(0) + asset = next(asset for asset in self.stylesheets if asset.id == asset_id) + asset._content = fragments.pop(0) + + return '\n'.join(asset.minify() for asset in self.stylesheets) + + def compile_css(self, cmd, source): + """Sanitizes @import rules, remove duplicates @import rules, then compile""" imports = [] - def push(matchobj): + def sanitize(matchobj): ref = matchobj.group(2) - line = '@import "%s"' % ref + line = '@import "%s"%s' % (ref, matchobj.group(3)) if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')): imports.append(line) + return line + msg = "Local import '%s' is forbidden for security reasons." % ref + _logger.warning(msg) + self.css_errors.append(msg) return '' - source = re.sub(self.rx_sass_import, push, source) - imports.append(source) - source = u'\n'.join(imports) + source = re.sub(self.rx_preprocess_imports, sanitize, source) try: - compiler = Popen(self.cmd_sass, stdin=PIPE, stdout=PIPE, stderr=PIPE) + compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) except Exception: - msg = "Could not find 'sass' program needed to compile sass/scss files" + msg = "Could not execute command %r" % cmd[0] _logger.error(msg) self.css_errors.append(msg) - return + return '' result = compiler.communicate(input=source.encode('utf-8')) if compiler.returncode: - error = self.get_sass_error(''.join(result), source=source) + error = self.get_preprocessor_error(''.join(result), source=source) _logger.warning(error) self.css_errors.append(error) - return + return '' compiled = result[0].strip().decode('utf8') - fragments = self.rx_css_split.split(compiled)[1:] - while fragments: - asset_id = fragments.pop(0) - asset = next(asset for asset in sass if asset.id == asset_id) - asset._content = fragments.pop(0) - - def get_sass_error(self, stderr, source=None): - # TODO: try to find out which asset the error belongs to + return compiled + + def get_preprocessor_error(self, stderr, source=None): + """Improve and remove sensitive information from sass/less compilator error messages""" error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '') + if 'Cannot load compass' in error: + error += "Maybe you should install the compass gem using this extra argument:\n\n" \ + " $ sudo gem install compass --pre\n" error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid for asset in self.stylesheets: - if isinstance(asset, SassAsset): + if isinstance(asset, PreprocessedCSS): error += '\n - %s' % (asset.url if asset.url else '') return error @@ -1302,7 +1324,7 @@ class WebAsset(object): @property def content(self): - if not self._content: + if self._content is None: self._content = self.inline or self._fetch_content() return self._content @@ -1368,22 +1390,26 @@ class StylesheetAsset(WebAsset): content = super(StylesheetAsset, self)._fetch_content() web_dir = os.path.dirname(self.url) - content = self.rx_import.sub( - r"""@import \1%s/""" % (web_dir,), - content, - ) + if self.rx_import: + content = self.rx_import.sub( + r"""@import \1%s/""" % (web_dir,), + content, + ) - content = self.rx_url.sub( - r"url(\1%s/" % (web_dir,), - content, - ) + if self.rx_url: + content = self.rx_url.sub( + r"url(\1%s/" % (web_dir,), + content, + ) + + if self.rx_charset: + # remove charset declarations, we only support utf-8 + content = self.rx_charset.sub('', content) - # remove charset declarations, we only support utf-8 - content = self.rx_charset.sub('', content) + return content except AssetError, e: self.bundle.css_errors.append(e.message) return '' - return content def minify(self): # remove existing sourcemaps, make no sense after re-mini @@ -1403,11 +1429,9 @@ class StylesheetAsset(WebAsset): else: return '' % (media, self.with_header()) -class SassAsset(StylesheetAsset): +class PreprocessedCSS(StylesheetAsset): html_url = '%s.css' - rx_indent = re.compile(r'^( +|\t+)', re.M) - indent = None - reindent = ' ' + rx_import = None def minify(self): return self.with_header() @@ -1416,25 +1440,39 @@ class SassAsset(StylesheetAsset): if self.url: ira = self.registry['ir.attachment'] url = self.html_url % self.url - domain = [('type', '=', 'binary'), ('url', '=', self.url)] + domain = [('type', '=', 'binary'), ('url', '=', url)] ira_id = ira.search(self.cr, self.uid, domain, context=self.context) + datas = self.content.encode('utf8').encode('base64') if ira_id: # TODO: update only if needed - ira.write(self.cr, openerp.SUPERUSER_ID, [ira_id], {'datas': self.content}, context=self.context) + ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context) else: ira.create(self.cr, openerp.SUPERUSER_ID, dict( - datas=self.content.encode('utf8').encode('base64'), + datas=datas, mimetype='text/css', type='binary', name=url, url=url, ), context=self.context) - return super(SassAsset, self).to_html() + return super(PreprocessedCSS, self).to_html() + + def get_source(self): + content = self.inline or self._fetch_content() + return "/*! %s */\n%s" % (self.id, content) + + def get_command(self): + raise NotImplementedError + +class SassStylesheetAsset(PreprocessedCSS): + rx_indent = re.compile(r'^( +|\t+)', re.M) + indent = None + reindent = ' ' def get_source(self): content = textwrap.dedent(self.inline or self._fetch_content()) def fix_indent(m): + # Indentation normalization ind = m.group() if self.indent is None: self.indent = ind @@ -1449,6 +1487,16 @@ class SassAsset(StylesheetAsset): pass return "/*! %s */\n%s" % (self.id, content) + def get_command(self): + return ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass', + '-r', 'bootstrap-sass'] + +class LessStylesheetAsset(PreprocessedCSS): + def get_command(self): + webpath = openerp.http.addons_manifest['web']['addons_path'] + lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less') + return ['lessc', '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath] + def rjsmin(script): """ Minify js with a clever regex. Taken from http://opensource.perlig.de/rjsmin