# -*- coding: utf-8 -*-
import collections
+import copy
import cStringIO
import datetime
import hashlib
import json
+import itertools
import logging
import math
import os
import re
import sys
-import xml # FIXME use lxml and etree
-import itertools
-import lxml.html
+import textwrap
+import uuid
+from subprocess import Popen, PIPE
from urlparse import urlparse
import babel
import babel.dates
import werkzeug
+from lxml import etree, html
from PIL import Image
import openerp.http
import openerp.tools
-import openerp.tools.func
+from openerp.tools.func import lazy_property
import openerp.tools.lru
from openerp.http import request
from openerp.tools.safe_eval import safe_eval as eval
from openerp.osv import osv, orm, fields
+from openerp.tools import html_escape as escape, which
from openerp.tools.translate import _
-from openerp import SUPERUSER_ID
_logger = logging.getLogger(__name__)
def __init__(self, message, **kw):
Exception.__init__(self, message)
self.qweb = dict(kw)
+ def pretty_xml(self):
+ if 'node' not in self.qweb:
+ return ''
+ return etree.tostring(self.qweb['node'], pretty_print=True)
class QWebTemplateNotFound(QWebException):
pass
e.qweb['cause'] = original
raise
+class FileSystemLoader(object):
+ def __init__(self, path):
+ # TODO: support multiple files #add_file() + add cache
+ self.path = path
+ self.doc = etree.parse(path).getroot()
+
+ def __iter__(self):
+ for node in self.doc:
+ name = node.get('t-name')
+ if name:
+ yield name
+
+ def __call__(self, name):
+ for node in self.doc:
+ if node.get('t-name') == name:
+ root = etree.Element('templates')
+ root.append(copy.deepcopy(node))
+ arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
+ return arch
+
class QWebContext(dict):
- def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
+ def __init__(self, cr, uid, data, loader=None, context=None):
self.cr = cr
self.uid = uid
self.loader = loader
- self.templates = templates or {}
self.context = context
dic = dict(data)
super(QWebContext, self).__init__(dic)
return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
def copy(self):
+ """ Clones the current context, conserving all data and metadata
+ (loader, template cache, ...)
+ """
return QWebContext(self.cr, self.uid, dict.copy(self),
loader=self.loader,
- templates=self.templates,
context=self.context)
def __copy__(self):
return self.copy()
class QWeb(orm.AbstractModel):
- """QWeb Xml templating engine
-
- The templating engine use a very simple syntax based "magic" xml
- attributes, to produce textual output (even non-xml).
-
- The core magic attributes are:
-
- flow attributes:
- t-if t-foreach t-call
-
- output attributes:
- t-att t-raw t-esc t-trim
+ """ Base QWeb rendering engine
- assignation attribute:
- t-set
-
- QWeb can be extended like any OpenERP model and new attributes can be
- added.
-
- If you need to customize t-fields rendering, subclass the ir.qweb.field
- model (and its sub-models) then override :meth:`~.get_converter_for` to
- fetch the right field converters for your qweb model.
+ * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
+ create new models called :samp:`ir.qweb.field.{widget}`
+ * alternatively, override :meth:`~.get_converter_for` and return an
+ arbitrary model to use as field converter
Beware that if you need extensions or alterations which could be
incompatible with other subsystems, you should create a local object
_name = 'ir.qweb'
- node = xml.dom.Node
_void_elements = frozenset([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
def register_tag(self, tag, func):
self._render_tag[tag] = func
- def add_template(self, qwebcontext, name, node):
- """Add a parsed template in the context. Used to preprocess templates."""
- qwebcontext.templates[name] = node
+ def get_template(self, name, qwebcontext):
+ origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
+ try:
+ document = qwebcontext.loader(name)
+ except ValueError:
+ raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
- def load_document(self, document, res_id, qwebcontext):
- """
- Loads an XML document and installs any contained template in the engine
- """
if hasattr(document, 'documentElement'):
dom = document
elif document.startswith("<?xml"):
- dom = xml.dom.minidom.parseString(document)
+ dom = etree.fromstring(document)
else:
- dom = xml.dom.minidom.parse(document)
+ dom = etree.parse(document).getroot()
- for node in dom.documentElement.childNodes:
- if node.nodeType == self.node.ELEMENT_NODE:
- if node.getAttribute('t-name'):
- name = str(node.getAttribute("t-name"))
- self.add_template(qwebcontext, name, node)
- if res_id and node.tagName == "t":
- self.add_template(qwebcontext, res_id, node)
- res_id = None
-
- def get_template(self, name, qwebcontext):
- origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
- if qwebcontext.loader and name not in qwebcontext.templates:
- try:
- xml_doc = qwebcontext.loader(name)
- except ValueError:
- raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
- self.load_document(xml_doc, isinstance(name, (int, long)) and name or None, qwebcontext=qwebcontext)
-
- if name in qwebcontext.templates:
- return qwebcontext.templates[name]
+ res_id = isinstance(name, (int, long)) and name or None
+ for node in dom:
+ if node.get('t-name') or (res_id and node.tag == "t"):
+ return node
raise QWebTemplateNotFound("Template %r not found" % name, template=origin_template)
return int(bool(self.eval(expr, qwebcontext)))
def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
+ """ render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
+
+ Renders the template specified by the provided template name
+
+ :param qwebcontext: context for rendering the template
+ :type qwebcontext: dict or :class:`QWebContext` instance
+ :param loader: if ``qwebcontext`` is a dict, loader set into the
+ context instantiated for rendering
+ """
if qwebcontext is None:
qwebcontext = {}
return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
def render_node(self, element, qwebcontext):
- result = ""
- if element.nodeType == self.node.TEXT_NODE or element.nodeType == self.node.CDATA_SECTION_NODE:
- result = element.data.encode("utf8")
- elif element.nodeType == self.node.ELEMENT_NODE:
- generated_attributes = ""
- t_render = None
- template_attributes = {}
- for (attribute_name, attribute_value) in element.attributes.items():
- attribute_name = str(attribute_name)
- if attribute_name == "groups":
- cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
- uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
- can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
- if not can_see:
- return ''
- continue
-
- if isinstance(attribute_value, unicode):
- attribute_value = attribute_value.encode("utf8")
- else:
- attribute_value = attribute_value.nodeValue.encode("utf8")
-
- if attribute_name.startswith("t-"):
- 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, werkzeug.utils.escape(val)) or " "
- 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, werkzeug.utils.escape(attribute_value))
+ generated_attributes = ""
+ t_render = None
+ template_attributes = {}
- if 'debug' in template_attributes:
- debugger = template_attributes.get('debug', 'pdb')
+ debugger = element.get('t-debug')
+ if debugger is not None:
+ if openerp.tools.config['dev_mode']:
__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)
+ _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":
+ cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
+ uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
+ can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
+ if not can_see:
+ return ''
+
+ attribute_value = attribute_value.encode("utf8")
+
+ if attribute_name.startswith("t-"):
+ for attribute in self._render_att:
+ if attribute_name[2:].startswith(attribute):
+ attrs = self._render_att[attribute](
+ self, element, attribute_name, attribute_value, qwebcontext)
+ for att, val in attrs:
+ if not val: continue
+ if not isinstance(val, str):
+ val = unicode(val).encode('utf-8')
+ 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 += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
+
+ 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 += self.render_tail(element.tail, element, qwebcontext)
+
if isinstance(result, unicode):
return result.encode('utf-8')
return result
# qwebcontext: values
# inner: optional innerXml
if inner:
- g_inner = inner
+ g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
else:
- g_inner = []
- for current_node in element.childNodes:
+ 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))
except QWebException:
raise
except Exception:
template = qwebcontext.get('__template__')
- raise_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template)
- name = str(element.nodeName)
+ raise_qweb_exception(message="Could not render element %r" % element.tag, node=element, template=template)
+ name = str(element.tag)
inner = "".join(g_inner)
trim = template_attributes.get("trim", 0)
if trim == 0:
else:
return "<%s%s/>" % (name, generated_attributes)
+ 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-"):
- att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
- elif attribute_name.startswith("t-att-"):
- att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
- if isinstance(val, unicode):
- val = val.encode("utf8")
- else:
- att, val = self.eval_object(attribute_value, qwebcontext)
- return att, val
+ return [(attribute_name[7:], self.eval_format(attribute_value, qwebcontext))]
+
+ if attribute_name.startswith("t-att-"):
+ return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))]
+
+ result = self.eval_object(attribute_value, qwebcontext)
+ if isinstance(result, collections.Mapping):
+ return result.iteritems()
+ # assume tuple
+ return [result]
# Tags
def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
options = json.loads(template_attributes.get('esc-options') or '{}')
- widget = self.get_widget_for(options.get('widget', ''))
+ widget = self.get_widget_for(options.get('widget'))
inner = widget.format(template_attributes['esc'], options, qwebcontext)
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
+ def _iterate(self, iterable):
+ if isinstance (iterable, collections.Mapping):
+ return iterable.iteritems()
+
+ return itertools.izip(*itertools.tee(iterable))
+
def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
expr = template_attributes["foreach"]
enum = self.eval_object(expr, qwebcontext)
- if enum is not None:
- var = template_attributes.get('as', expr).replace('.', '_')
- copy_qwebcontext = qwebcontext.copy()
- size = -1
- if isinstance(enum, (list, tuple)):
- size = len(enum)
- elif hasattr(enum, 'count'):
- size = enum.count()
- copy_qwebcontext["%s_size" % var] = size
- copy_qwebcontext["%s_all" % var] = enum
- index = 0
- ru = []
- for i in enum:
- copy_qwebcontext["%s_value" % var] = i
- copy_qwebcontext["%s_index" % var] = index
- copy_qwebcontext["%s_first" % var] = index == 0
- copy_qwebcontext["%s_even" % var] = index % 2
- copy_qwebcontext["%s_odd" % var] = (index + 1) % 2
- copy_qwebcontext["%s_last" % var] = index + 1 == size
- if index % 2:
- copy_qwebcontext["%s_parity" % var] = 'odd'
- else:
- copy_qwebcontext["%s_parity" % var] = 'even'
- if 'as' in template_attributes:
- copy_qwebcontext[var] = i
- elif isinstance(i, dict):
- copy_qwebcontext.update(i)
- ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
- index += 1
- return "".join(ru)
- else:
+ if enum is None:
template = qwebcontext.get('__template__')
raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
+ if isinstance(enum, int):
+ enum = range(enum)
+
+ varname = template_attributes['as'].replace('.', '_')
+ copy_qwebcontext = qwebcontext.copy()
+
+ size = None
+ if isinstance(enum, collections.Sized):
+ size = len(enum)
+ copy_qwebcontext["%s_size" % varname] = size
+
+ copy_qwebcontext["%s_all" % varname] = enum
+ ru = []
+ for index, (item, value) in enumerate(self._iterate(enum)):
+ copy_qwebcontext.update({
+ varname: item,
+ '%s_value' % varname: value,
+ '%s_index' % varname: index,
+ '%s_first' % varname: index == 0,
+ })
+ if size is not None:
+ copy_qwebcontext['%s_last' % varname] = index + 1 == size
+ if index % 2:
+ copy_qwebcontext.update({
+ '%s_parity' % varname: 'odd',
+ '%s_even' % varname: False,
+ '%s_odd' % varname: True,
+ })
+ else:
+ copy_qwebcontext.update({
+ '%s_parity' % varname: 'even',
+ '%s_even' % varname: True,
+ '%s_odd' % varname: False,
+ })
+ ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
+ return "".join(ru)
def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
if self.eval_bool(template_attributes["if"], qwebcontext):
def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
""" This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
- name = template_attributes['call-assets']
-
- # Backward compatibility hack for manifest usage
- qwebcontext['manifest_list'] = openerp.addons.web.controllers.main.manifest_list
-
- d = qwebcontext.copy()
- d.context['inherit_branding'] = False
- content = self.render_tag_call(
- element, {'call': name}, generated_attributes, d)
- bundle = AssetsBundle(name, html=content)
+ if len(element):
+ # An asset bundle is rendered in two differents contexts (when genereting html and
+ # when generating the bundle itself) so they must be qwebcontext free
+ # even '0' variable is forbidden
+ template = qwebcontext.get('__template__')
+ raise QWebException("t-call-assets cannot contain children nodes", template=template)
+ xmlid = template_attributes['call-assets']
+ cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
+ bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
css = self.get_attr_bool(template_attributes.get('css'), default=True)
js = self.get_attr_bool(template_attributes.get('js'), default=True)
return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
""" eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
- node_name = element.nodeName
+ node_name = element.tag
assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
"li", "ul", "ol", "dl", "dt", "dd"),\
"RTE widgets do not work correctly on %r elements" % node_name
record, field_name = template_attributes["field"].rsplit('.', 1)
record = self.eval_object(record, qwebcontext)
- column = record._model._all_columns[field_name].column
+ field = record._fields[field_name]
options = json.loads(template_attributes.get('field-options') or '{}')
- field_type = get_field_type(column, options)
+ field_type = get_field_type(field, options)
converter = self.get_converter_for(field_type)
element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
def get_converter_for(self, field_type):
+ """ returns a :class:`~openerp.models.Model` used to render a
+ ``t-field``.
+
+ By default, tries to get the model named
+ :samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
+
+ :param str field_type: type or widget of field to render
+ """
return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
def get_widget_for(self, widget):
- return self.pool.get('ir.qweb.widget.' + widget, self.pool['ir.qweb.widget'])
+ """ returns a :class:`~openerp.models.Model` used to render a
+ ``t-esc``
+
+ :param str widget: name of the widget to use, or ``None``
+ """
+ widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
+ return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
def get_attr_bool(self, attr, default=False):
if attr:
def attributes(self, cr, uid, field_name, record, options,
source_element, g_att, t_att, qweb_context,
context=None):
- """
+ """ attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None)
+
Generates the metadata attributes (prefixed by ``data-oe-`` for the
root node of the field conversion. Attribute values are escaped by the
- parent using ``werkzeug.utils.escape``.
+ parent.
The default attributes are:
* ``model``, the name of the record's model
* ``id`` the id of the record to which the field belongs
* ``field`` the name of the converted field
- * ``type`` the logical field type (widget, may not match the column's
- ``type``, may not be any _column subclass name)
+ * ``type`` the logical field type (widget, may not match the field's
+ ``type``, may not be any Field subclass name)
* ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
- column is translatable
+ field is translatable
* ``expression``, the original expression
:returns: iterable of (attribute name, attribute value) pairs.
"""
- column = record._model._all_columns[field_name].column
- field_type = get_field_type(column, options)
+ field = record._fields[field_name]
+ field_type = get_field_type(field, options)
return [
- ('data-oe-model', record._model._name),
+ ('data-oe-model', record._name),
('data-oe-id', record.id),
('data-oe-field', field_name),
('data-oe-type', field_type),
('data-oe-expression', t_att['field']),
]
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
- """ Converts a single value to its HTML version/output
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
+ """ value_to_html(cr, uid, value, field, options=None, context=None)
+
+ Converts a single value to its HTML version/output
"""
if not value: return ''
return value
- def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
- """ Converts the specified field of the browse_record ``record`` to
- HTML
+ def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
+ """ record_to_html(cr, uid, field_name, record, options=None, context=None)
+
+ Converts the specified field of the browse_record ``record`` to HTML
"""
+ field = record._fields[field_name]
return self.value_to_html(
- cr, uid, record[field_name], column, options=options, context=context)
+ cr, uid, record[field_name], field, options=options, context=context)
def to_html(self, cr, uid, field_name, record, options,
source_element, t_att, g_att, qweb_context, context=None):
- """ Converts a ``t-field`` to its HTML output. A ``t-field`` may be
+ """ to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None)
+
+ Converts a ``t-field`` to its HTML output. A ``t-field`` may be
extended by a ``t-field-options``, which is a JSON-serialized mapping
of configuration values.
field's own ``_type``.
"""
try:
- content = self.record_to_html(
- cr, uid, field_name, record,
- record._model._all_columns[field_name].column,
- options, context=context)
+ content = self.record_to_html(cr, uid, field_name, record, options, context=context)
if options.get('html-escape', True):
- content = werkzeug.utils.escape(content)
+ content = escape(content)
elif hasattr(content, '__html__'):
content = content.__html__()
except Exception:
_logger.warning("Could not get field %s for model %s",
- field_name, record._model._name, exc_info=True)
+ field_name, record._name, exc_info=True)
content = None
- if context and context.get('inherit_branding'):
+ inherit_branding = context and context.get('inherit_branding')
+ if not inherit_branding and context and context.get('inherit_branding_auto'):
+ inherit_branding = self.pool['ir.model.access'].check(cr, uid, record._name, 'write', False, context=context)
+
+ if inherit_branding:
# add branding attributes
g_att += ''.join(
- ' %s="%s"' % (name, werkzeug.utils.escape(value))
+ ' %s="%s"' % (name, escape(value))
for name, value in self.attributes(
cr, uid, field_name, record, options,
source_element, g_att, t_att, qweb_context)
def render_element(self, cr, uid, source_element, t_att, g_att,
qweb_context, content):
- """ Final rendering hook, by default just calls ir.qweb's ``render_element``
+ """ render_element(cr, uid, source_element, t_att, g_att, qweb_context, content)
+
+ Final rendering hook, by default just calls ir.qweb's ``render_element``
"""
return self.qweb_object().render_element(
source_element, t_att, g_att, qweb_context, content or '')
def user_lang(self, cr, uid, context):
- """
+ """ user_lang(cr, uid, context)
+
Fetches the res.lang object corresponding to the language code stored
in the user's context. Fallbacks to en_US if no lang is present in the
context *or the language code is not valid*.
_name = 'ir.qweb.field.float'
_inherit = 'ir.qweb.field'
- def precision(self, cr, uid, column, options=None, context=None):
- _, precision = column.digits or (None, None)
+ def precision(self, cr, uid, field, options=None, context=None):
+ _, precision = field.digits or (None, None)
return precision
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
if context is None:
context = {}
- precision = self.precision(cr, uid, column, options=options, context=context)
+ precision = self.precision(cr, uid, field, options=options, context=context)
fmt = '%f' if precision is None else '%.{precision}f'
lang_code = context.get('lang') or 'en_US'
# it to switch to scientific notation starting at a million *and* to
# strip decimals. So use %f and if no precision was specified manually
# strip trailing 0.
- if not precision:
+ if precision is None:
formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
return formatted
_name = 'ir.qweb.field.date'
_inherit = 'ir.qweb.field'
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
- if not value: return ''
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
+ if not value or len(value)<10: return ''
lang = self.user_lang(cr, uid, context=context)
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(
- value, openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
+ value[:10], openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
if options and 'format' in options:
pattern = options['format']
strftime_pattern = lang.date_format
pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
- return babel.dates.format_datetime(
+ return babel.dates.format_date(
value, format=pattern,
locale=locale)
_name = 'ir.qweb.field.datetime'
_inherit = 'ir.qweb.field'
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
if not value: return ''
lang = self.user_lang(cr, uid, context=context)
locale = babel.Locale.parse(lang.code)
if options and options.get('hide_seconds'):
pattern = pattern.replace(":ss", "").replace(":s", "")
-
+
return babel.dates.format_datetime(value, format=pattern, locale=locale)
class TextConverter(osv.AbstractModel):
_name = 'ir.qweb.field.text'
_inherit = 'ir.qweb.field'
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
"""
Escapes the value and converts newlines to br. This is bullshit.
"""
_name = 'ir.qweb.field.selection'
_inherit = 'ir.qweb.field'
- def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+ def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
value = record[field_name]
if not value: return ''
- selection = dict(fields.selection.reify(
- cr, uid, record._model, column))
+ field = record._fields[field_name]
+ selection = dict(field.get_description(record.env)['selection'])
return self.value_to_html(
- cr, uid, selection[value], column, options=options)
+ cr, uid, selection[value], field, options=options)
class ManyToOneConverter(osv.AbstractModel):
_name = 'ir.qweb.field.many2one'
_inherit = 'ir.qweb.field'
- def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+ def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
[read] = record.read([field_name])
if not read[field_name]: return ''
_, value = read[field_name]
_name = 'ir.qweb.field.html'
_inherit = 'ir.qweb.field'
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
return HTMLSafe(value or '')
class ImageConverter(osv.AbstractModel):
_name = 'ir.qweb.field.image'
_inherit = 'ir.qweb.field'
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
try:
image = Image.open(cStringIO.StringIO(value.decode('base64')))
image.verify()
cr, uid, field_name, record, options,
source_element, t_att, g_att, qweb_context, context=context)
- def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
+ def record_to_html(self, cr, uid, field_name, record, options, context=None):
if context is None:
context = {}
Currency = self.pool['res.currency']
- display = self.display_currency(cr, uid, options)
+ display_currency = self.display_currency(cr, uid, options['display_currency'], options)
# lang.format mandates a sprintf-style format. These formats are non-
# minimal (they have a default fixed precision instead), and
# The log10 of the rounding should be the number of digits involved if
# negative, if positive clamp to 0 digits and call it a day.
# nb: int() ~ floor(), we want nearest rounding instead
- precision = int(round(math.log10(display.rounding)))
+ precision = int(math.floor(math.log10(display_currency.rounding)))
fmt = "%.{0}f".format(-precision if precision < 0 else 0)
+ from_amount = record[field_name]
+
+ if options.get('from_currency'):
+ from_currency = self.display_currency(cr, uid, options['from_currency'], options)
+ from_amount = Currency.compute(cr, uid, from_currency.id, display_currency.id, from_amount)
+
lang_code = context.get('lang') or 'en_US'
lang = self.pool['res.lang']
- formatted_amount = lang.format(cr, uid, [lang_code],
- fmt, Currency.round(cr, uid, display, record[field_name]),
+ formatted_amount = lang.format(cr, uid, [lang_code],
+ fmt, Currency.round(cr, uid, display_currency, from_amount),
grouping=True, monetary=True)
pre = post = u''
- if display.position == 'before':
+ if display_currency.position == 'before':
pre = u'{symbol} '
else:
post = u' {symbol}'
formatted_amount,
pre=pre, post=post,
).format(
- symbol=display.symbol,
+ symbol=display_currency.symbol,
))
- def display_currency(self, cr, uid, options):
+ def display_currency(self, cr, uid, currency, options):
return self.qweb_object().eval_object(
- options['display_currency'], options['_qweb_context'])
+ currency, options['_qweb_context'])
TIMEDELTA_UNITS = (
('year', 3600 * 24 * 365),
_name = 'ir.qweb.field.duration'
_inherit = 'ir.qweb.field'
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
units = dict(TIMEDELTA_UNITS)
if value < 0:
raise ValueError(_("Durations can't be negative"))
v*secs_per_unit, threshold=1, locale=locale)
if section:
sections.append(section)
- return u' '.join(sections)
+ return ' '.join(sections)
+
class RelativeDatetimeConverter(osv.AbstractModel):
_name = 'ir.qweb.field.relative'
_inherit = 'ir.qweb.field'
- def value_to_html(self, cr, uid, value, column, options=None, context=None):
+ def value_to_html(self, cr, uid, value, field, options=None, context=None):
parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
locale = babel.Locale.parse(
self.user_lang(cr, uid, context=context).code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(value, parse_format)
- # value should be a naive datetime in UTC. So is fields.datetime.now()
- reference = datetime.datetime.strptime(column.now(), parse_format)
+ # value should be a naive datetime in UTC. So is fields.Datetime.now()
+ reference = datetime.datetime.strptime(field.now(), parse_format)
return babel.dates.format_timedelta(
value - reference, add_direction=True, locale=locale)
_name = 'ir.qweb.field.contact'
_inherit = 'ir.qweb.field.many2one'
- def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+ def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
+ if context is None:
+ context = {}
+
if options is None:
options = {}
opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
- if not getattr(record, field_name):
+ value_rec = record[field_name]
+ if not value_rec:
return None
-
- id = getattr(record, field_name).id
- field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context={"show_address": True})
- value = field_browse.name_get()[0][1]
+ value_rec = value_rec.sudo().with_context(show_address=True)
+ value = value_rec.name_get()[0][1]
val = {
'name': value.split("\n")[0],
- 'address': werkzeug.utils.escape("\n".join(value.split("\n")[1:])),
- 'phone': field_browse.phone,
- 'mobile': field_browse.mobile,
- 'fax': field_browse.fax,
- 'city': field_browse.city,
- 'country_id': field_browse.country_id and field_browse.country_id.name_get()[0][1],
- 'email': field_browse.email,
+ 'address': escape("\n".join(value.split("\n")[1:])).strip(),
+ 'phone': value_rec.phone,
+ 'mobile': value_rec.mobile,
+ 'fax': value_rec.fax,
+ 'city': value_rec.city,
+ 'country_id': value_rec.country_id.display_name,
+ 'website': value_rec.website,
+ 'email': value_rec.email,
'fields': opf,
- 'object': field_browse,
+ 'object': value_rec,
'options': options
}
_name = 'ir.qweb.field.qweb'
_inherit = 'ir.qweb.field.many2one'
- def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
+ def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
if not getattr(record, field_name):
return None
return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
def format(self, inner, options, qwebcontext):
- return werkzeug.utils.escape(self._format(inner, options, qwebcontext))
+ return escape(self._format(inner, options, qwebcontext))
class QwebWidgetMonetary(osv.AbstractModel):
_name = 'ir.qweb.widget.monetary'
if options is None: options = {}
if options.get('html-escape', True):
- string = werkzeug.utils.escape(string)
+ string = escape(string)
return HTMLSafe(string.replace('\n', '<br>\n'))
-def get_field_type(column, options):
- """ Gets a t-field's effective type from the field's column and its options
- """
- return options.get('widget', column._type)
+def get_field_type(field, options):
+ """ Gets a t-field's effective type from the field definition and its options """
+ return options.get('widget', field.type)
+
+class AssetError(Exception):
+ pass
+class AssetNotFound(AssetError):
+ pass
class AssetsBundle(object):
- cache = openerp.tools.lru.LRU(32)
rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
+ rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
+ rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
- def __init__(self, xmlid, html=None, debug=False):
- self.debug = debug
+ def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
self.xmlid = xmlid
+ self.cr = request.cr if cr is None else cr
+ self.uid = request.uid if uid is None else uid
+ self.context = request.context if context is None else context
+ self.registry = request.registry if registry is None else registry
self.javascripts = []
self.stylesheets = []
+ self.css_errors = []
self.remains = []
self._checksum = None
- if html:
- self.parse(html)
- def parse(self, html):
- fragments = lxml.html.fragments_fromstring(html)
+ 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()
+
+ def parse(self):
+ fragments = html.fragments_fromstring(self.html)
for el in fragments:
if isinstance(el, basestring):
self.remains.append(el)
- elif isinstance(el, lxml.html.HtmlElement):
- src = el.get('src')
- href = el.get('href')
+ elif isinstance(el, html.HtmlElement):
+ src = el.get('src', '')
+ href = el.get('href', '')
+ atype = el.get('type')
+ media = el.get('media')
if el.tag == 'style':
- self.stylesheets.append(StylesheetAsset(source=el.text))
+ if atype == 'text/sass' or src.endswith('.sass'):
+ 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):
- self.stylesheets.append(StylesheetAsset(url=href))
+ if href.endswith('.sass') or atype == 'text/sass':
+ 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:
- self.javascripts.append(JavascriptAsset(source=el.text))
+ self.javascripts.append(JavascriptAsset(self, inline=el.text))
elif el.tag == 'script' and self.can_aggregate(src):
- self.javascripts.append(JavascriptAsset(url=src))
+ self.javascripts.append(JavascriptAsset(self, url=src))
else:
- self.remains.append(lxml.html.tostring(el))
+ self.remains.append(html.tostring(el))
else:
try:
- self.remains.append(lxml.html.tostring(el))
+ self.remains.append(html.tostring(el))
except Exception:
# notYETimplementederror
raise NotImplementedError
def can_aggregate(self, url):
return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
- def to_html(self, sep='\n ', css=True, js=True, debug=False):
+ def to_html(self, sep=None, css=True, js=True, debug=False):
+ if sep is None:
+ sep = '\n '
response = []
if debug:
- if css:
+ if css and self.stylesheets:
+ 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:
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" rel="stylesheet"/>' % self.xmlid)
- if js and self.javascripts:
- response.append('<script type="text/javascript" src="/web/js/%s"></script>' % self.xmlid)
+ href = '/web/css/%s/%s' % (self.xmlid, self.version)
+ response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
+ if js:
+ 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)
- @openerp.tools.func.lazy_property
+ @lazy_property
def last_modified(self):
+ """Returns last modified date of linked files"""
return max(itertools.chain(
(asset.last_modified for asset in self.javascripts),
(asset.last_modified for asset in self.stylesheets),
- [datetime.datetime(1970, 1, 1)],
))
- @openerp.tools.func.lazy_property
+ @lazy_property
+ def version(self):
+ return self.checksum[0:7]
+
+ @lazy_property
def checksum(self):
- checksum = hashlib.new('sha1')
- for asset in itertools.chain(self.javascripts, self.stylesheets):
- checksum.update(asset.content.encode("utf-8"))
- return checksum.hexdigest()
+ """
+ Not really a full checksum.
+ We compute a SHA1 on the rendered bundle + max linked files last_modified date
+ """
+ check = self.html + str(self.last_modified)
+ return hashlib.sha1(check).hexdigest()
def js(self):
- key = 'js_' + self.checksum
- if key not in self.cache:
- content =';\n'.join(asset.minify() for asset in self.javascripts)
- self.cache[key] = content
- if self.debug:
- return "/*\n%s\n*/\n" % '\n'.join(
- [asset.url for asset in self.javascripts if asset.url]) + self.cache[key]
- return self.cache[key]
+ content = self.get_cache('js')
+ if content is None:
+ content = ';\n'.join(asset.minify() for asset in self.javascripts)
+ self.set_cache('js', content)
+ return content
def css(self):
- key = 'css_' + self.checksum
- if key not in self.cache:
- content = '\n'.join(asset.minify() for asset in self.stylesheets)
+ """Generate css content from given bundle"""
+ content = self.get_cache('css')
+ if content is None:
+ content = self.preprocess_css()
+
+ if self.css_errors:
+ msg = '\n'.join(self.css_errors)
+ content += self.css_message(msg)
+
# move up all @import rules to the top
matches = []
def push(matchobj):
matches.append(content)
content = u'\n'.join(matches)
- self.cache[key] = content
- if self.debug:
- return "/*\n%s\n*/\n" % '\n'.join(
- [asset.url for asset in self.javascripts if asset.url]) + self.cache[key]
- return self.cache[key]
+ if self.css_errors:
+ return content
+ self.set_cache('css', content)
+
+ return content
+
+ def get_cache(self, type):
+ content = None
+ domain = [('url', '=', '/web/%s/%s/%s' % (type, self.xmlid, self.version))]
+ bundle = self.registry['ir.attachment'].search_read(self.cr, openerp.SUPERUSER_ID, domain, ['datas'], context=self.context)
+ if bundle and bundle[0]['datas']:
+ content = bundle[0]['datas'].decode('base64')
+ return content
+
+ def set_cache(self, type, content):
+ ira = self.registry['ir.attachment']
+ 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',
+ name=url,
+ url=url,
+ ), context=self.context)
+
+ def css_message(self, message):
+ # '\A' == css content carriage return
+ message = message.replace('\n', '\\A ').replace('"', '\\"')
+ return """
+ body:before {
+ background: #ffc;
+ width: 100%%;
+ font-size: 14px;
+ font-family: monospace;
+ white-space: pre;
+ content: "%s";
+ }
+ """ % message
+
+ def preprocess_css(self):
+ """
+ Checks if the bundle contains any sass/less content, then compiles it to css.
+ Returns the bundle's flat css.
+ """
+ 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 sanitize(matchobj):
+ ref = matchobj.group(2)
+ 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_preprocess_imports, sanitize, source)
+
+ try:
+ compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+ except Exception:
+ msg = "Could not execute command %r" % cmd[0]
+ _logger.error(msg)
+ self.css_errors.append(msg)
+ return ''
+ result = compiler.communicate(input=source.encode('utf-8'))
+ if compiler.returncode:
+ error = self.get_preprocessor_error(''.join(result), source=source)
+ _logger.warning(error)
+ self.css_errors.append(error)
+ return ''
+ compiled = result[0].strip().decode('utf8')
+ 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, PreprocessedCSS):
+ error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
+ return error
class WebAsset(object):
- def __init__(self, source=None, url=None):
- self.source = source
+ html_url = '%s'
+
+ def __init__(self, bundle, inline=None, url=None):
+ self.id = str(uuid.uuid4())
+ self.bundle = bundle
+ self.inline = inline
self.url = url
- self._irattach = None
+ self.cr = bundle.cr
+ self.uid = bundle.uid
+ self.registry = bundle.registry
+ self.context = bundle.context
self._content = None
- self.filename = None
- self.last_modified = None
- if source:
- self.last_modified = datetime.datetime(1970, 1, 1)
- if url:
- module = filter(None, self.url.split('/'))[0]
+ self._filename = None
+ self._ir_attach = None
+ name = '<inline asset>' if inline else url
+ self.name = "%s defined in bundle '%s'" % (name, bundle.xmlid)
+ if not inline and not url:
+ raise Exception("An asset should either be inlined or url linked")
+
+ def stat(self):
+ if not (self.inline or self._filename or self._ir_attach):
+ addon = filter(None, self.url.split('/'))[0]
try:
# Test url against modules static assets
- mpath = openerp.http.addons_manifest[module]['addons_path']
- self.filename = mpath + self.url.replace('/', os.path.sep)
- self.last_modified = datetime.datetime.fromtimestamp(os.path.getmtime(self.filename))
+ mpath = openerp.http.addons_manifest[addon]['addons_path']
+ self._filename = mpath + self.url.replace('/', os.path.sep)
except Exception:
try:
# Test url against ir.attachments
+ fields = ['__last_update', 'datas', 'mimetype']
domain = [('type', '=', 'binary'), ('url', '=', self.url)]
- attach = request.registry['ir.attachment'].search_read(request.cr, SUPERUSER_ID, domain, ['__last_update', 'datas', 'mimetype'], context=request.context)
- self._irattach = attach[0]
- server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
- try:
- self.last_modified = datetime.datetime.strptime(attach[0]['__last_update'], server_format + '.%f')
- except ValueError:
- self.last_modified = datetime.datetime.strptime(attach[0]['__last_update'], server_format)
+ ira = self.registry['ir.attachment']
+ attach = ira.search_read(self.cr, openerp.SUPERUSER_ID, domain, fields, context=self.context)
+ self._ir_attach = attach[0]
except Exception:
- raise KeyError("Could not find asset '%s' for '%s' addon" % (self.url, module))
+ raise AssetNotFound("Could not find %s" % self.name)
+
+ def to_html(self):
+ raise NotImplementedError()
- @openerp.tools.func.lazy_property
+ @lazy_property
+ def last_modified(self):
+ try:
+ self.stat()
+ if self._filename:
+ return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
+ elif self._ir_attach:
+ server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+ last_update = self._ir_attach['__last_update']
+ try:
+ return datetime.datetime.strptime(last_update, server_format + '.%f')
+ except ValueError:
+ return datetime.datetime.strptime(last_update, server_format)
+ except Exception:
+ pass
+ return datetime.datetime(1970, 1, 1)
+
+ @property
def content(self):
- if self.source:
- return self.source
- if self._irattach:
- return self._irattach['datas'].decode('base64')
- return self.get_content()
+ if self._content is None:
+ self._content = self.inline or self._fetch_content()
+ return self._content
- def get_content(self):
- with open(self.filename, 'rb') as fp:
- return fp.read().decode('utf-8')
+ def _fetch_content(self):
+ """ Fetch content from file or database"""
+ try:
+ self.stat()
+ if self._filename:
+ with open(self._filename, 'rb') as fp:
+ return fp.read().decode('utf-8')
+ else:
+ return self._ir_attach['datas'].decode('base64')
+ except UnicodeDecodeError:
+ raise AssetError('%s is not utf-8 encoded.' % self.name)
+ except IOError:
+ raise AssetNotFound('File %s does not exist.' % self.name)
+ except:
+ raise AssetError('Could not get content for %s.' % self.name)
def minify(self):
return self.content
+ def with_header(self, content=None):
+ if content is None:
+ content = self.content
+ return '\n/* %s */\n%s' % (self.name, content)
+
class JavascriptAsset(WebAsset):
def minify(self):
- return rjsmin(self.content)
+ return self.with_header(rjsmin(self.content))
+
+ def _fetch_content(self):
+ try:
+ return super(JavascriptAsset, self)._fetch_content()
+ except AssetError, e:
+ return "console.error(%s);" % json.dumps(e.message)
def to_html(self):
if self.url:
- return '<script type="text/javascript" src="%s"></script>' % self.url
+ return '<script type="text/javascript" src="%s"></script>' % (self.html_url % self.url)
else:
- return '<script type="text/javascript" charset="utf-8">%s</script>' % self.source
+ return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
class StylesheetAsset(WebAsset):
rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
+ rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
- def _get_content(self):
- with open(self.filename, 'rb') as fp:
- firstline = fp.readline()
- m = re.match(r'@charset "([^"]+)";', firstline)
- if m:
- encoding = m.group(1)
- else:
- encoding = "utf-8"
- # "reinject" first line as it's not @charset
- fp.seek(0)
+ def __init__(self, *args, **kw):
+ self.media = kw.pop('media', None)
+ super(StylesheetAsset, self).__init__(*args, **kw)
- return fp.read().decode(encoding)
+ @property
+ def content(self):
+ content = super(StylesheetAsset, self).content
+ if self.media:
+ content = '@media %s { %s }' % (self.media, content)
+ return content
- def get_content(self):
- content = self._get_content()
- if self.url:
+ def _fetch_content(self):
+ try:
+ 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,
- )
-
- content = self.rx_url.sub(
- r"url(\1%s/" % (web_dir,),
- content,
- )
- return content
+ if self.rx_import:
+ content = self.rx_import.sub(
+ r"""@import \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)
+
+ return content
+ except AssetError, e:
+ self.bundle.css_errors.append(e.message)
+ return ''
def minify(self):
# remove existing sourcemaps, make no sense after re-mini
# space
content = re.sub(r'\s+', ' ', content)
content = re.sub(r' *([{}]) *', r'\1', content)
- return content
+ return self.with_header(content)
def to_html(self):
+ media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
if self.url:
- return '<link rel="stylesheet" href="%s" type="text/css"/>' % self.url
+ href = self.html_url % self.url
+ return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
+ else:
+ return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
+
+class PreprocessedCSS(StylesheetAsset):
+ html_url = '%s.css'
+ rx_import = None
+
+ def minify(self):
+ return self.with_header()
+
+ def to_html(self):
+ if self.url:
+ ira = self.registry['ir.attachment']
+ url = self.html_url % self.url
+ domain = [('type', '=', 'binary'), ('url', '=', url)]
+ ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, 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': datas}, context=self.context)
+ else:
+ ira.create(self.cr, openerp.SUPERUSER_ID, dict(
+ datas=datas,
+ mimetype='text/css',
+ type='binary',
+ name=url,
+ url=url,
+ ), context=self.context)
+ 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
+ if self.indent == self.reindent:
+ # Don't reindent the file if identation is the final one (reindent)
+ raise StopIteration()
+ return ind.replace(self.indent, self.reindent)
+
+ try:
+ content = self.rx_indent.sub(fix_indent, content)
+ except StopIteration:
+ pass
+ return "/*! %s */\n%s" % (self.id, content)
+
+ def get_command(self):
+ defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
+ sass = which('sass', path=os.pathsep.join(defpath))
+ return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
+ '-r', 'bootstrap-sass']
+
+class LessStylesheetAsset(PreprocessedCSS):
+ def get_command(self):
+ defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
+ if os.name == 'nt':
+ lessc = which('lessc.cmd', path=os.pathsep.join(defpath))
else:
- return '<style type="text/css">%s</style>' % self.source
+ lessc = which('lessc', path=os.pathsep.join(defpath))
+ 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.