1 # -*- coding: utf-8 -*-
16 from subprocess import Popen, PIPE
17 from urlparse import urlparse
22 from lxml import etree, html
27 from openerp.tools.func import lazy_property
28 import openerp.tools.lru
29 from openerp.http import request
30 from openerp.tools.safe_eval import safe_eval as eval
31 from openerp.osv import osv, orm, fields
32 from openerp.tools import html_escape as escape, which
33 from openerp.tools.translate import _
35 _logger = logging.getLogger(__name__)
37 #--------------------------------------------------------------------
38 # QWeb template engine
39 #--------------------------------------------------------------------
40 class QWebException(Exception):
41 def __init__(self, message, **kw):
42 Exception.__init__(self, message)
45 if 'node' not in self.qweb:
47 return etree.tostring(self.qweb['node'], pretty_print=True)
49 class QWebTemplateNotFound(QWebException):
52 def raise_qweb_exception(etype=None, **kw):
55 orig_type, original, tb = sys.exc_info()
57 raise etype, original, tb
59 for k, v in kw.items():
61 # Will use `raise foo from bar` in python 3 and rename cause to __cause__
62 e.qweb['cause'] = original
65 class FileSystemLoader(object):
66 def __init__(self, path):
67 # TODO: support multiple files #add_file() + add cache
69 self.doc = etree.parse(path).getroot()
73 name = node.get('t-name')
77 def __call__(self, name):
79 if node.get('t-name') == name:
80 root = etree.Element('templates')
81 root.append(copy.deepcopy(node))
82 arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
85 class QWebContext(dict):
86 def __init__(self, cr, uid, data, loader=None, context=None):
90 self.context = context
92 super(QWebContext, self).__init__(dic)
93 self['defined'] = lambda key: key in self
95 def safe_eval(self, expr):
96 locals_dict = collections.defaultdict(lambda: None)
97 locals_dict.update(self)
98 locals_dict.pop('cr', None)
99 locals_dict.pop('loader', None)
100 return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
103 """ Clones the current context, conserving all data and metadata
104 (loader, template cache, ...)
106 return QWebContext(self.cr, self.uid, dict.copy(self),
108 context=self.context)
113 class QWeb(orm.AbstractModel):
114 """ Base QWeb rendering engine
116 * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
117 create new models called :samp:`ir.qweb.field.{widget}`
118 * alternatively, override :meth:`~.get_converter_for` and return an
119 arbitrary model to use as field converter
121 Beware that if you need extensions or alterations which could be
122 incompatible with other subsystems, you should create a local object
123 inheriting from ``ir.qweb`` and customize that.
128 _void_elements = frozenset([
129 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
130 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
131 _format_regex = re.compile(
136 # jinja-style pattern
140 def __init__(self, pool, cr):
141 super(QWeb, self).__init__(pool, cr)
143 self._render_tag = self.prefixed_methods('render_tag_')
144 self._render_att = self.prefixed_methods('render_att_')
146 def prefixed_methods(self, prefix):
147 """ Extracts all methods prefixed by ``prefix``, and returns a mapping
148 of (t-name, method) where the t-name is the method name with prefix
149 removed and underscore converted to dashes
154 n_prefix = len(prefix)
156 (name[n_prefix:].replace('_', '-'), getattr(type(self), name))
157 for name in dir(self)
158 if name.startswith(prefix)
161 def register_tag(self, tag, func):
162 self._render_tag[tag] = func
164 def get_template(self, name, qwebcontext):
165 origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
167 document = qwebcontext.loader(name)
169 raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
171 if hasattr(document, 'documentElement'):
173 elif document.startswith("<?xml"):
174 dom = etree.fromstring(document)
176 dom = etree.parse(document).getroot()
178 res_id = isinstance(name, (int, long)) and name or None
180 if node.get('t-name') or (res_id and node.tag == "t"):
183 raise QWebTemplateNotFound("Template %r not found" % name, template=origin_template)
185 def eval(self, expr, qwebcontext):
187 return qwebcontext.safe_eval(expr)
189 template = qwebcontext.get('__template__')
190 raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
192 def eval_object(self, expr, qwebcontext):
193 return self.eval(expr, qwebcontext)
195 def eval_str(self, expr, qwebcontext):
197 return qwebcontext.get(0, '')
198 val = self.eval(expr, qwebcontext)
199 if isinstance(val, unicode):
200 return val.encode("utf8")
201 if val is False or val is None:
205 def eval_format(self, expr, qwebcontext):
206 expr, replacements = self._format_regex.subn(
207 lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
215 return str(expr % qwebcontext)
217 template = qwebcontext.get('__template__')
218 raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
220 def eval_bool(self, expr, qwebcontext):
221 return int(bool(self.eval(expr, qwebcontext)))
223 def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
224 """ render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
226 Renders the template specified by the provided template name
228 :param qwebcontext: context for rendering the template
229 :type qwebcontext: dict or :class:`QWebContext` instance
230 :param loader: if ``qwebcontext`` is a dict, loader set into the
231 context instantiated for rendering
233 if qwebcontext is None:
236 if not isinstance(qwebcontext, QWebContext):
237 qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
239 qwebcontext['__template__'] = id_or_xml_id
240 stack = qwebcontext.get('__stack__', [])
242 qwebcontext['__caller__'] = stack[-1]
243 stack.append(id_or_xml_id)
244 qwebcontext['__stack__'] = stack
245 qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
246 return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
248 def render_node(self, element, qwebcontext):
249 generated_attributes = ""
251 template_attributes = {}
253 debugger = element.get('t-debug')
254 if debugger is not None:
255 if openerp.tools.config['dev_mode']:
256 __import__(debugger).set_trace() # pdb, ipdb, pudb, ...
258 _logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
260 for (attribute_name, attribute_value) in element.attrib.iteritems():
261 attribute_name = str(attribute_name)
262 if attribute_name == "groups":
263 cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
264 uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
265 can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
269 attribute_value = attribute_value.encode("utf8")
271 if attribute_name.startswith("t-"):
272 for attribute in self._render_att:
273 if attribute_name[2:].startswith(attribute):
274 attrs = self._render_att[attribute](
275 self, element, attribute_name, attribute_value, qwebcontext)
276 for att, val in attrs:
278 if not isinstance(val, str):
279 val = unicode(val).encode('utf-8')
280 generated_attributes += self.render_attribute(element, att, val, qwebcontext)
283 if attribute_name[2:] in self._render_tag:
284 t_render = attribute_name[2:]
285 template_attributes[attribute_name[2:]] = attribute_value
287 generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
290 result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
292 result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
295 result += self.render_tail(element.tail, element, qwebcontext)
297 if isinstance(result, unicode):
298 return result.encode('utf-8')
301 def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
303 # template_attributes: t-* attributes
304 # generated_attributes: generated attributes
305 # qwebcontext: values
306 # inner: optional innerXml
308 g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
310 g_inner = [] if element.text is None else [self.render_text(element.text, element, qwebcontext)]
311 for current_node in element.iterchildren(tag=etree.Element):
313 g_inner.append(self.render_node(current_node, qwebcontext))
314 except QWebException:
317 template = qwebcontext.get('__template__')
318 raise_qweb_exception(message="Could not render element %r" % element.tag, node=element, template=template)
319 name = str(element.tag)
320 inner = "".join(g_inner)
321 trim = template_attributes.get("trim", 0)
325 inner = inner.lstrip()
326 elif trim == 'right':
327 inner = inner.rstrip()
329 inner = inner.strip()
332 elif len(inner) or name not in self._void_elements:
333 return "<%s%s>%s</%s>" % tuple(
334 qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
335 for qwebcontext in (name, generated_attributes, inner, name)
338 return "<%s%s/>" % (name, generated_attributes)
340 def render_attribute(self, element, name, value, qwebcontext):
341 return ' %s="%s"' % (name, escape(value))
343 def render_text(self, text, element, qwebcontext):
344 return text.encode('utf-8')
346 def render_tail(self, tail, element, qwebcontext):
347 return tail.encode('utf-8')
350 def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
351 if attribute_name.startswith("t-attf-"):
352 return [(attribute_name[7:], self.eval_format(attribute_value, qwebcontext))]
354 if attribute_name.startswith("t-att-"):
355 return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))]
357 result = self.eval_object(attribute_value, qwebcontext)
358 if isinstance(result, collections.Mapping):
359 return result.iteritems()
364 def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
365 inner = self.eval_str(template_attributes["raw"], qwebcontext)
366 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
368 def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
369 options = json.loads(template_attributes.get('esc-options') or '{}')
370 widget = self.get_widget_for(options.get('widget'))
371 inner = widget.format(template_attributes['esc'], options, qwebcontext)
372 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
374 def _iterate(self, iterable):
375 if isinstance (iterable, collections.Mapping):
376 return iterable.iteritems()
378 return itertools.izip(*itertools.tee(iterable))
380 def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
381 expr = template_attributes["foreach"]
382 enum = self.eval_object(expr, qwebcontext)
384 template = qwebcontext.get('__template__')
385 raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
386 if isinstance(enum, int):
389 varname = template_attributes['as'].replace('.', '_')
390 copy_qwebcontext = qwebcontext.copy()
393 if isinstance(enum, collections.Sized):
395 copy_qwebcontext["%s_size" % varname] = size
397 copy_qwebcontext["%s_all" % varname] = enum
399 for index, (item, value) in enumerate(self._iterate(enum)):
400 copy_qwebcontext.update({
402 '%s_value' % varname: value,
403 '%s_index' % varname: index,
404 '%s_first' % varname: index == 0,
407 copy_qwebcontext['%s_last' % varname] = index + 1 == size
409 copy_qwebcontext.update({
410 '%s_parity' % varname: 'odd',
411 '%s_even' % varname: False,
412 '%s_odd' % varname: True,
415 copy_qwebcontext.update({
416 '%s_parity' % varname: 'even',
417 '%s_even' % varname: True,
418 '%s_odd' % varname: False,
420 ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
423 def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
424 if self.eval_bool(template_attributes["if"], qwebcontext):
425 return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
428 def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
429 d = qwebcontext.copy()
430 d[0] = self.render_element(element, template_attributes, generated_attributes, d)
431 cr = d.get('request') and d['request'].cr or None
432 uid = d.get('request') and d['request'].uid or None
434 template = self.eval_format(template_attributes["call"], d)
436 template = int(template)
439 return self.render(cr, uid, template, d)
441 def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
442 """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
444 # An asset bundle is rendered in two differents contexts (when genereting html and
445 # when generating the bundle itself) so they must be qwebcontext free
446 # even '0' variable is forbidden
447 template = qwebcontext.get('__template__')
448 raise QWebException("t-call-assets cannot contain children nodes", template=template)
449 xmlid = template_attributes['call-assets']
450 cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
451 bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
452 css = self.get_attr_bool(template_attributes.get('css'), default=True)
453 js = self.get_attr_bool(template_attributes.get('js'), default=True)
454 return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
456 def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
457 if "value" in template_attributes:
458 qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
459 elif "valuef" in template_attributes:
460 qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
462 qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
465 def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
466 """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
467 node_name = element.tag
468 assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
469 "li", "ul", "ol", "dl", "dt", "dd"),\
470 "RTE widgets do not work correctly on %r elements" % node_name
471 assert node_name != 't',\
472 "t-field can not be used on a t element, provide an actual HTML node"
474 record, field_name = template_attributes["field"].rsplit('.', 1)
475 record = self.eval_object(record, qwebcontext)
477 field = record._fields[field_name]
478 options = json.loads(template_attributes.get('field-options') or '{}')
479 field_type = get_field_type(field, options)
481 converter = self.get_converter_for(field_type)
483 return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
484 element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
486 def get_converter_for(self, field_type):
487 """ returns a :class:`~openerp.models.Model` used to render a
490 By default, tries to get the model named
491 :samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
493 :param str field_type: type or widget of field to render
495 return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
497 def get_widget_for(self, widget):
498 """ returns a :class:`~openerp.models.Model` used to render a
501 :param str widget: name of the widget to use, or ``None``
503 widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
504 return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
506 def get_attr_bool(self, attr, default=False):
509 if attr in ('false', '0'):
511 elif attr in ('true', '1'):
515 #--------------------------------------------------------------------
516 # QWeb Fields converters
517 #--------------------------------------------------------------------
519 class FieldConverter(osv.AbstractModel):
520 """ Used to convert a t-field specification into an output HTML field.
522 :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
524 * converts the record value to html using :meth:`~.record_to_html`
525 * generates the metadata attributes (``data-oe-``) to set on the root
527 * generates the root result node itself through :meth:`~.render_element`
529 _name = 'ir.qweb.field'
531 def attributes(self, cr, uid, field_name, record, options,
532 source_element, g_att, t_att, qweb_context,
534 """ attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None)
536 Generates the metadata attributes (prefixed by ``data-oe-`` for the
537 root node of the field conversion. Attribute values are escaped by the
540 The default attributes are:
542 * ``model``, the name of the record's model
543 * ``id`` the id of the record to which the field belongs
544 * ``field`` the name of the converted field
545 * ``type`` the logical field type (widget, may not match the field's
546 ``type``, may not be any Field subclass name)
547 * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
548 field is translatable
549 * ``expression``, the original expression
551 :returns: iterable of (attribute name, attribute value) pairs.
553 field = record._fields[field_name]
554 field_type = get_field_type(field, options)
556 ('data-oe-model', record._name),
557 ('data-oe-id', record.id),
558 ('data-oe-field', field_name),
559 ('data-oe-type', field_type),
560 ('data-oe-expression', t_att['field']),
563 def value_to_html(self, cr, uid, value, field, options=None, context=None):
564 """ value_to_html(cr, uid, value, field, options=None, context=None)
566 Converts a single value to its HTML version/output
568 if not value: return ''
571 def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
572 """ record_to_html(cr, uid, field_name, record, options=None, context=None)
574 Converts the specified field of the browse_record ``record`` to HTML
576 field = record._fields[field_name]
577 return self.value_to_html(
578 cr, uid, record[field_name], field, options=options, context=context)
580 def to_html(self, cr, uid, field_name, record, options,
581 source_element, t_att, g_att, qweb_context, context=None):
582 """ to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None)
584 Converts a ``t-field`` to its HTML output. A ``t-field`` may be
585 extended by a ``t-field-options``, which is a JSON-serialized mapping
586 of configuration values.
588 A default configuration key is ``widget`` which can override the
589 field's own ``_type``.
592 content = self.record_to_html(cr, uid, field_name, record, options, context=context)
593 if options.get('html-escape', True):
594 content = escape(content)
595 elif hasattr(content, '__html__'):
596 content = content.__html__()
598 _logger.warning("Could not get field %s for model %s",
599 field_name, record._name, exc_info=True)
602 inherit_branding = context and context.get('inherit_branding')
603 if not inherit_branding and context and context.get('inherit_branding_auto'):
604 inherit_branding = self.pool['ir.model.access'].check(cr, uid, record._name, 'write', False, context=context)
607 # add branding attributes
609 ' %s="%s"' % (name, escape(value))
610 for name, value in self.attributes(
611 cr, uid, field_name, record, options,
612 source_element, g_att, t_att, qweb_context)
615 return self.render_element(cr, uid, source_element, t_att, g_att,
616 qweb_context, content)
618 def qweb_object(self):
619 return self.pool['ir.qweb']
621 def render_element(self, cr, uid, source_element, t_att, g_att,
622 qweb_context, content):
623 """ render_element(cr, uid, source_element, t_att, g_att, qweb_context, content)
625 Final rendering hook, by default just calls ir.qweb's ``render_element``
627 return self.qweb_object().render_element(
628 source_element, t_att, g_att, qweb_context, content or '')
630 def user_lang(self, cr, uid, context):
631 """ user_lang(cr, uid, context)
633 Fetches the res.lang object corresponding to the language code stored
634 in the user's context. Fallbacks to en_US if no lang is present in the
635 context *or the language code is not valid*.
637 :returns: res.lang browse_record
639 if context is None: context = {}
641 lang_code = context.get('lang') or 'en_US'
642 Lang = self.pool['res.lang']
644 lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
645 or Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
647 return Lang.browse(cr, uid, lang_ids[0], context=context)
649 class FloatConverter(osv.AbstractModel):
650 _name = 'ir.qweb.field.float'
651 _inherit = 'ir.qweb.field'
653 def precision(self, cr, uid, field, options=None, context=None):
654 _, precision = field.digits or (None, None)
657 def value_to_html(self, cr, uid, value, field, options=None, context=None):
660 precision = self.precision(cr, uid, field, options=options, context=context)
661 fmt = '%f' if precision is None else '%.{precision}f'
663 lang_code = context.get('lang') or 'en_US'
664 lang = self.pool['res.lang']
665 formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
667 # %f does not strip trailing zeroes. %g does but its precision causes
668 # it to switch to scientific notation starting at a million *and* to
669 # strip decimals. So use %f and if no precision was specified manually
671 if precision is None:
672 formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
675 class DateConverter(osv.AbstractModel):
676 _name = 'ir.qweb.field.date'
677 _inherit = 'ir.qweb.field'
679 def value_to_html(self, cr, uid, value, field, options=None, context=None):
680 if not value or len(value)<10: return ''
681 lang = self.user_lang(cr, uid, context=context)
682 locale = babel.Locale.parse(lang.code)
684 if isinstance(value, basestring):
685 value = datetime.datetime.strptime(
686 value[:10], openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
688 if options and 'format' in options:
689 pattern = options['format']
691 strftime_pattern = lang.date_format
692 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
694 return babel.dates.format_date(
695 value, format=pattern,
698 class DateTimeConverter(osv.AbstractModel):
699 _name = 'ir.qweb.field.datetime'
700 _inherit = 'ir.qweb.field'
702 def value_to_html(self, cr, uid, value, field, options=None, context=None):
703 if not value: return ''
704 lang = self.user_lang(cr, uid, context=context)
705 locale = babel.Locale.parse(lang.code)
707 if isinstance(value, basestring):
708 value = datetime.datetime.strptime(
709 value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
710 value = fields.datetime.context_timestamp(
711 cr, uid, timestamp=value, context=context)
713 if options and 'format' in options:
714 pattern = options['format']
716 strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
717 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
719 if options and options.get('hide_seconds'):
720 pattern = pattern.replace(":ss", "").replace(":s", "")
722 return babel.dates.format_datetime(value, format=pattern, locale=locale)
724 class TextConverter(osv.AbstractModel):
725 _name = 'ir.qweb.field.text'
726 _inherit = 'ir.qweb.field'
728 def value_to_html(self, cr, uid, value, field, options=None, context=None):
730 Escapes the value and converts newlines to br. This is bullshit.
732 if not value: return ''
734 return nl2br(value, options=options)
736 class SelectionConverter(osv.AbstractModel):
737 _name = 'ir.qweb.field.selection'
738 _inherit = 'ir.qweb.field'
740 def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
741 value = record[field_name]
742 if not value: return ''
743 field = record._fields[field_name]
744 selection = dict(field.get_description(record.env)['selection'])
745 return self.value_to_html(
746 cr, uid, selection[value], field, options=options)
748 class ManyToOneConverter(osv.AbstractModel):
749 _name = 'ir.qweb.field.many2one'
750 _inherit = 'ir.qweb.field'
752 def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
753 [read] = record.read([field_name])
754 if not read[field_name]: return ''
755 _, value = read[field_name]
756 return nl2br(value, options=options)
758 class HTMLConverter(osv.AbstractModel):
759 _name = 'ir.qweb.field.html'
760 _inherit = 'ir.qweb.field'
762 def value_to_html(self, cr, uid, value, field, options=None, context=None):
763 return HTMLSafe(value or '')
765 class ImageConverter(osv.AbstractModel):
766 """ ``image`` widget rendering, inserts a data:uri-using image tag in the
767 document. May be overridden by e.g. the website module to generate links
770 .. todo:: what happens if different output need different converters? e.g.
771 reports may need embedded images or FS links whereas website
774 _name = 'ir.qweb.field.image'
775 _inherit = 'ir.qweb.field'
777 def value_to_html(self, cr, uid, value, field, options=None, context=None):
779 image = Image.open(cStringIO.StringIO(value.decode('base64')))
782 raise ValueError("Non-image binary fields can not be converted to HTML")
783 except: # image.verify() throws "suitable exceptions", I have no idea what they are
784 raise ValueError("Invalid image content")
786 return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
788 class MonetaryConverter(osv.AbstractModel):
789 """ ``monetary`` converter, has a mandatory option
790 ``display_currency``.
792 The currency is used for formatting *and rounding* of the float value. It
793 is assumed that the linked res_currency has a non-empty rounding value and
794 res.currency's ``round`` method is used to perform rounding.
796 .. note:: the monetary converter internally adds the qweb context to its
797 options mapping, so that the context is available to callees.
798 It's set under the ``_qweb_context`` key.
800 _name = 'ir.qweb.field.monetary'
801 _inherit = 'ir.qweb.field'
803 def to_html(self, cr, uid, field_name, record, options,
804 source_element, t_att, g_att, qweb_context, context=None):
805 options['_qweb_context'] = qweb_context
806 return super(MonetaryConverter, self).to_html(
807 cr, uid, field_name, record, options,
808 source_element, t_att, g_att, qweb_context, context=context)
810 def record_to_html(self, cr, uid, field_name, record, options, context=None):
813 Currency = self.pool['res.currency']
814 display_currency = self.display_currency(cr, uid, options['display_currency'], options)
816 # lang.format mandates a sprintf-style format. These formats are non-
817 # minimal (they have a default fixed precision instead), and
818 # lang.format will not set one by default. currency.round will not
819 # provide one either. So we need to generate a precision value
820 # (integer > 0) from the currency's rounding (a float generally < 1.0).
822 # The log10 of the rounding should be the number of digits involved if
823 # negative, if positive clamp to 0 digits and call it a day.
824 # nb: int() ~ floor(), we want nearest rounding instead
825 precision = int(math.floor(math.log10(display_currency.rounding)))
826 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
828 from_amount = record[field_name]
830 if options.get('from_currency'):
831 from_currency = self.display_currency(cr, uid, options['from_currency'], options)
832 from_amount = Currency.compute(cr, uid, from_currency.id, display_currency.id, from_amount)
834 lang_code = context.get('lang') or 'en_US'
835 lang = self.pool['res.lang']
836 formatted_amount = lang.format(cr, uid, [lang_code],
837 fmt, Currency.round(cr, uid, display_currency, from_amount),
838 grouping=True, monetary=True)
841 if display_currency.position == 'before':
846 return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
850 symbol=display_currency.symbol,
853 def display_currency(self, cr, uid, currency, options):
854 return self.qweb_object().eval_object(
855 currency, options['_qweb_context'])
858 ('year', 3600 * 24 * 365),
859 ('month', 3600 * 24 * 30),
860 ('week', 3600 * 24 * 7),
866 class DurationConverter(osv.AbstractModel):
867 """ ``duration`` converter, to display integral or fractional values as
868 human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
870 Can be used on any numerical field.
872 Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
873 ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
874 field value before converting it.
876 Sub-second values will be ignored.
878 _name = 'ir.qweb.field.duration'
879 _inherit = 'ir.qweb.field'
881 def value_to_html(self, cr, uid, value, field, options=None, context=None):
882 units = dict(TIMEDELTA_UNITS)
884 raise ValueError(_("Durations can't be negative"))
885 if not options or options.get('unit') not in units:
886 raise ValueError(_("A unit must be provided to duration widgets"))
888 locale = babel.Locale.parse(
889 self.user_lang(cr, uid, context=context).code)
890 factor = units[options['unit']]
894 for unit, secs_per_unit in TIMEDELTA_UNITS:
895 v, r = divmod(r, secs_per_unit)
897 section = babel.dates.format_timedelta(
898 v*secs_per_unit, threshold=1, locale=locale)
900 sections.append(section)
901 return ' '.join(sections)
904 class RelativeDatetimeConverter(osv.AbstractModel):
905 _name = 'ir.qweb.field.relative'
906 _inherit = 'ir.qweb.field'
908 def value_to_html(self, cr, uid, value, field, options=None, context=None):
909 parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
910 locale = babel.Locale.parse(
911 self.user_lang(cr, uid, context=context).code)
913 if isinstance(value, basestring):
914 value = datetime.datetime.strptime(value, parse_format)
916 # value should be a naive datetime in UTC. So is fields.Datetime.now()
917 reference = datetime.datetime.strptime(field.now(), parse_format)
919 return babel.dates.format_timedelta(
920 value - reference, add_direction=True, locale=locale)
922 class Contact(orm.AbstractModel):
923 _name = 'ir.qweb.field.contact'
924 _inherit = 'ir.qweb.field.many2one'
926 def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
932 opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
934 value_rec = record[field_name]
937 value_rec = value_rec.sudo().with_context(show_address=True)
938 value = value_rec.name_get()[0][1]
941 'name': value.split("\n")[0],
942 'address': escape("\n".join(value.split("\n")[1:])).strip(),
943 'phone': value_rec.phone,
944 'mobile': value_rec.mobile,
945 'fax': value_rec.fax,
946 'city': value_rec.city,
947 'country_id': value_rec.country_id.display_name,
948 'website': value_rec.website,
949 'email': value_rec.email,
955 html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
957 return HTMLSafe(html)
959 class QwebView(orm.AbstractModel):
960 _name = 'ir.qweb.field.qweb'
961 _inherit = 'ir.qweb.field.many2one'
963 def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
964 if not getattr(record, field_name):
967 view = getattr(record, field_name)
969 if view._model._name != "ir.ui.view":
970 _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
973 ctx = (context or {}).copy()
974 ctx['object'] = record
975 html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
977 return HTMLSafe(html)
979 class QwebWidget(osv.AbstractModel):
980 _name = 'ir.qweb.widget'
982 def _format(self, inner, options, qwebcontext):
983 return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
985 def format(self, inner, options, qwebcontext):
986 return escape(self._format(inner, options, qwebcontext))
988 class QwebWidgetMonetary(osv.AbstractModel):
989 _name = 'ir.qweb.widget.monetary'
990 _inherit = 'ir.qweb.widget'
992 def _format(self, inner, options, qwebcontext):
993 inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
994 display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
995 precision = int(round(math.log10(display.rounding)))
996 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
997 lang_code = qwebcontext.context.get('lang') or 'en_US'
998 formatted_amount = self.pool['res.lang'].format(
999 qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
1002 if display.position == 'before':
1007 return u'{pre}{0}{post}'.format(
1008 formatted_amount, pre=pre, post=post
1009 ).format(symbol=display.symbol,)
1011 class HTMLSafe(object):
1012 """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
1013 objects with a ``__html__`` methods but AFAIK does not provide any such
1016 Wrapping a string in HTML will prevent its escaping
1018 __slots__ = ['string']
1019 def __init__(self, string):
1020 self.string = string
1025 if isinstance(s, unicode):
1026 return s.encode('utf-8')
1028 def __unicode__(self):
1030 if isinstance(s, str):
1031 return s.decode('utf-8')
1034 def nl2br(string, options=None):
1035 """ Converts newlines to HTML linebreaks in ``string``. Automatically
1036 escapes content unless options['html-escape'] is set to False, and returns
1037 the result wrapped in an HTMLSafe object.
1040 :param dict options:
1043 if options is None: options = {}
1045 if options.get('html-escape', True):
1046 string = escape(string)
1047 return HTMLSafe(string.replace('\n', '<br>\n'))
1049 def get_field_type(field, options):
1050 """ Gets a t-field's effective type from the field definition and its options """
1051 return options.get('widget', field.type)
1053 class AssetError(Exception):
1055 class AssetNotFound(AssetError):
1058 class AssetsBundle(object):
1059 rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
1060 rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
1061 rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
1063 def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
1065 self.cr = request.cr if cr is None else cr
1066 self.uid = request.uid if uid is None else uid
1067 self.context = request.context if context is None else context
1068 self.registry = request.registry if registry is None else registry
1069 self.javascripts = []
1070 self.stylesheets = []
1071 self.css_errors = []
1073 self._checksum = None
1075 context = self.context.copy()
1076 context['inherit_branding'] = False
1077 context['rendering_bundle'] = True
1078 self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
1082 fragments = html.fragments_fromstring(self.html)
1083 for el in fragments:
1084 if isinstance(el, basestring):
1085 self.remains.append(el)
1086 elif isinstance(el, html.HtmlElement):
1087 src = el.get('src', '')
1088 href = el.get('href', '')
1089 atype = el.get('type')
1090 media = el.get('media')
1091 if el.tag == 'style':
1092 if atype == 'text/sass' or src.endswith('.sass'):
1093 self.stylesheets.append(SassStylesheetAsset(self, inline=el.text, media=media))
1094 elif atype == 'text/less' or src.endswith('.less'):
1095 self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
1097 self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
1098 elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
1099 if href.endswith('.sass') or atype == 'text/sass':
1100 self.stylesheets.append(SassStylesheetAsset(self, url=href, media=media))
1101 elif href.endswith('.less') or atype == 'text/less':
1102 self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
1104 self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
1105 elif el.tag == 'script' and not src:
1106 self.javascripts.append(JavascriptAsset(self, inline=el.text))
1107 elif el.tag == 'script' and self.can_aggregate(src):
1108 self.javascripts.append(JavascriptAsset(self, url=src))
1110 self.remains.append(html.tostring(el))
1113 self.remains.append(html.tostring(el))
1115 # notYETimplementederror
1116 raise NotImplementedError
1118 def can_aggregate(self, url):
1119 return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
1121 def to_html(self, sep=None, css=True, js=True, debug=False):
1126 if css and self.stylesheets:
1127 self.preprocess_css()
1129 msg = '\n'.join(self.css_errors)
1130 self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
1131 for style in self.stylesheets:
1132 response.append(style.to_html())
1134 for jscript in self.javascripts:
1135 response.append(jscript.to_html())
1137 url_for = self.context.get('url_for', lambda url: url)
1138 if css and self.stylesheets:
1139 href = '/web/css/%s/%s' % (self.xmlid, self.version)
1140 response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
1142 src = '/web/js/%s/%s' % (self.xmlid, self.version)
1143 response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
1144 response.extend(self.remains)
1145 return sep + sep.join(response)
1148 def last_modified(self):
1149 """Returns last modified date of linked files"""
1150 return max(itertools.chain(
1151 (asset.last_modified for asset in self.javascripts),
1152 (asset.last_modified for asset in self.stylesheets),
1157 return self.checksum[0:7]
1162 Not really a full checksum.
1163 We compute a SHA1 on the rendered bundle + max linked files last_modified date
1165 check = self.html + str(self.last_modified)
1166 return hashlib.sha1(check).hexdigest()
1169 content = self.get_cache('js')
1171 content = ';\n'.join(asset.minify() for asset in self.javascripts)
1172 self.set_cache('js', content)
1176 """Generate css content from given bundle"""
1177 content = self.get_cache('css')
1179 content = self.preprocess_css()
1182 msg = '\n'.join(self.css_errors)
1183 content += self.css_message(msg)
1185 # move up all @import rules to the top
1188 matches.append(matchobj.group(0))
1191 content = re.sub(self.rx_css_import, push, content)
1193 matches.append(content)
1194 content = u'\n'.join(matches)
1197 self.set_cache('css', content)
1201 def get_cache(self, type):
1203 domain = [('url', '=', '/web/%s/%s/%s' % (type, self.xmlid, self.version))]
1204 bundle = self.registry['ir.attachment'].search_read(self.cr, openerp.SUPERUSER_ID, domain, ['datas'], context=self.context)
1205 if bundle and bundle[0]['datas']:
1206 content = bundle[0]['datas'].decode('base64')
1209 def set_cache(self, type, content):
1210 ira = self.registry['ir.attachment']
1211 ira.invalidate_bundle(self.cr, openerp.SUPERUSER_ID, type=type, xmlid=self.xmlid)
1212 url = '/web/%s/%s/%s' % (type, self.xmlid, self.version)
1213 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1214 datas=content.encode('utf8').encode('base64'),
1218 ), context=self.context)
1220 def css_message(self, message):
1221 # '\A' == css content carriage return
1222 message = message.replace('\n', '\\A ').replace('"', '\\"')
1228 font-family: monospace;
1234 def preprocess_css(self):
1236 Checks if the bundle contains any sass/less content, then compiles it to css.
1237 Returns the bundle's flat css.
1239 for atype in (SassStylesheetAsset, LessStylesheetAsset):
1240 assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
1242 cmd = assets[0].get_command()
1243 source = '\n'.join([asset.get_source() for asset in assets])
1244 compiled = self.compile_css(cmd, source)
1246 fragments = self.rx_css_split.split(compiled)
1247 at_rules = fragments.pop(0)
1249 # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
1250 self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
1252 asset_id = fragments.pop(0)
1253 asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
1254 asset._content = fragments.pop(0)
1256 return '\n'.join(asset.minify() for asset in self.stylesheets)
1258 def compile_css(self, cmd, source):
1259 """Sanitizes @import rules, remove duplicates @import rules, then compile"""
1261 def sanitize(matchobj):
1262 ref = matchobj.group(2)
1263 line = '@import "%s"%s' % (ref, matchobj.group(3))
1264 if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
1265 imports.append(line)
1267 msg = "Local import '%s' is forbidden for security reasons." % ref
1268 _logger.warning(msg)
1269 self.css_errors.append(msg)
1271 source = re.sub(self.rx_preprocess_imports, sanitize, source)
1274 compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
1276 msg = "Could not execute command %r" % cmd[0]
1278 self.css_errors.append(msg)
1280 result = compiler.communicate(input=source.encode('utf-8'))
1281 if compiler.returncode:
1282 error = self.get_preprocessor_error(''.join(result), source=source)
1283 _logger.warning(error)
1284 self.css_errors.append(error)
1286 compiled = result[0].strip().decode('utf8')
1289 def get_preprocessor_error(self, stderr, source=None):
1290 """Improve and remove sensitive information from sass/less compilator error messages"""
1291 error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
1292 if 'Cannot load compass' in error:
1293 error += "Maybe you should install the compass gem using this extra argument:\n\n" \
1294 " $ sudo gem install compass --pre\n"
1295 error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
1296 for asset in self.stylesheets:
1297 if isinstance(asset, PreprocessedCSS):
1298 error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
1301 class WebAsset(object):
1304 def __init__(self, bundle, inline=None, url=None):
1305 self.id = str(uuid.uuid4())
1306 self.bundle = bundle
1307 self.inline = inline
1310 self.uid = bundle.uid
1311 self.registry = bundle.registry
1312 self.context = bundle.context
1313 self._content = None
1314 self._filename = None
1315 self._ir_attach = None
1316 name = '<inline asset>' if inline else url
1317 self.name = "%s defined in bundle '%s'" % (name, bundle.xmlid)
1318 if not inline and not url:
1319 raise Exception("An asset should either be inlined or url linked")
1322 if not (self.inline or self._filename or self._ir_attach):
1323 addon = filter(None, self.url.split('/'))[0]
1325 # Test url against modules static assets
1326 mpath = openerp.http.addons_manifest[addon]['addons_path']
1327 self._filename = mpath + self.url.replace('/', os.path.sep)
1330 # Test url against ir.attachments
1331 fields = ['__last_update', 'datas', 'mimetype']
1332 domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1333 ira = self.registry['ir.attachment']
1334 attach = ira.search_read(self.cr, openerp.SUPERUSER_ID, domain, fields, context=self.context)
1335 self._ir_attach = attach[0]
1337 raise AssetNotFound("Could not find %s" % self.name)
1340 raise NotImplementedError()
1343 def last_modified(self):
1347 return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
1348 elif self._ir_attach:
1349 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1350 last_update = self._ir_attach['__last_update']
1352 return datetime.datetime.strptime(last_update, server_format + '.%f')
1354 return datetime.datetime.strptime(last_update, server_format)
1357 return datetime.datetime(1970, 1, 1)
1361 if self._content is None:
1362 self._content = self.inline or self._fetch_content()
1363 return self._content
1365 def _fetch_content(self):
1366 """ Fetch content from file or database"""
1370 with open(self._filename, 'rb') as fp:
1371 return fp.read().decode('utf-8')
1373 return self._ir_attach['datas'].decode('base64')
1374 except UnicodeDecodeError:
1375 raise AssetError('%s is not utf-8 encoded.' % self.name)
1377 raise AssetNotFound('File %s does not exist.' % self.name)
1379 raise AssetError('Could not get content for %s.' % self.name)
1384 def with_header(self, content=None):
1386 content = self.content
1387 return '\n/* %s */\n%s' % (self.name, content)
1389 class JavascriptAsset(WebAsset):
1391 return self.with_header(rjsmin(self.content))
1393 def _fetch_content(self):
1395 return super(JavascriptAsset, self)._fetch_content()
1396 except AssetError, e:
1397 return "console.error(%s);" % json.dumps(e.message)
1401 return '<script type="text/javascript" src="%s"></script>' % (self.html_url % self.url)
1403 return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
1405 class StylesheetAsset(WebAsset):
1406 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
1407 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
1408 rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
1409 rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
1411 def __init__(self, *args, **kw):
1412 self.media = kw.pop('media', None)
1413 super(StylesheetAsset, self).__init__(*args, **kw)
1417 content = super(StylesheetAsset, self).content
1419 content = '@media %s { %s }' % (self.media, content)
1422 def _fetch_content(self):
1424 content = super(StylesheetAsset, self)._fetch_content()
1425 web_dir = os.path.dirname(self.url)
1428 content = self.rx_import.sub(
1429 r"""@import \1%s/""" % (web_dir,),
1434 content = self.rx_url.sub(
1435 r"url(\1%s/" % (web_dir,),
1440 # remove charset declarations, we only support utf-8
1441 content = self.rx_charset.sub('', content)
1444 except AssetError, e:
1445 self.bundle.css_errors.append(e.message)
1449 # remove existing sourcemaps, make no sense after re-mini
1450 content = self.rx_sourceMap.sub('', self.content)
1452 content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
1454 content = re.sub(r'\s+', ' ', content)
1455 content = re.sub(r' *([{}]) *', r'\1', content)
1456 return self.with_header(content)
1459 media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
1461 href = self.html_url % self.url
1462 return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
1464 return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
1466 class PreprocessedCSS(StylesheetAsset):
1471 return self.with_header()
1475 ira = self.registry['ir.attachment']
1476 url = self.html_url % self.url
1477 domain = [('type', '=', 'binary'), ('url', '=', url)]
1478 ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
1479 datas = self.content.encode('utf8').encode('base64')
1481 # TODO: update only if needed
1482 ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
1484 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1486 mimetype='text/css',
1490 ), context=self.context)
1491 return super(PreprocessedCSS, self).to_html()
1493 def get_source(self):
1494 content = self.inline or self._fetch_content()
1495 return "/*! %s */\n%s" % (self.id, content)
1497 def get_command(self):
1498 raise NotImplementedError
1500 class SassStylesheetAsset(PreprocessedCSS):
1501 rx_indent = re.compile(r'^( +|\t+)', re.M)
1505 def get_source(self):
1506 content = textwrap.dedent(self.inline or self._fetch_content())
1509 # Indentation normalization
1511 if self.indent is None:
1513 if self.indent == self.reindent:
1514 # Don't reindent the file if identation is the final one (reindent)
1515 raise StopIteration()
1516 return ind.replace(self.indent, self.reindent)
1519 content = self.rx_indent.sub(fix_indent, content)
1520 except StopIteration:
1522 return "/*! %s */\n%s" % (self.id, content)
1524 def get_command(self):
1525 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
1526 sass = which('sass', path=os.pathsep.join(defpath))
1527 return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
1528 '-r', 'bootstrap-sass']
1530 class LessStylesheetAsset(PreprocessedCSS):
1531 def get_command(self):
1532 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
1534 lessc = which('lessc.cmd', path=os.pathsep.join(defpath))
1536 lessc = which('lessc', path=os.pathsep.join(defpath))
1537 webpath = openerp.http.addons_manifest['web']['addons_path']
1538 lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less')
1539 return [lessc, '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
1542 """ Minify js with a clever regex.
1543 Taken from http://opensource.perlig.de/rjsmin
1544 Apache License, Version 2.0 """
1546 """ Substitution callback """
1547 groups = match.groups()
1553 (groups[4] and '\n') or
1554 (groups[5] and ' ') or
1555 (groups[6] and ' ') or
1556 (groups[7] and ' ') or
1561 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
1562 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
1563 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
1564 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
1565 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
1566 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
1567 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
1568 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
1569 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
1570 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
1571 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
1572 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
1573 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
1574 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
1575 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
1576 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
1577 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
1578 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
1579 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
1580 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
1581 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script