1 # -*- coding: utf-8 -*-
15 from subprocess import Popen, PIPE
16 from urlparse import urlparse
21 from lxml import etree, html
26 from openerp.tools.func import lazy_property
27 import openerp.tools.lru
28 from openerp.http import request
29 from openerp.tools.safe_eval import safe_eval as eval
30 from openerp.osv import osv, orm, fields
31 from openerp.tools import html_escape as escape
32 from openerp.tools.translate import _
34 _logger = logging.getLogger(__name__)
36 #--------------------------------------------------------------------
37 # QWeb template engine
38 #--------------------------------------------------------------------
39 class QWebException(Exception):
40 def __init__(self, message, **kw):
41 Exception.__init__(self, message)
44 if 'node' not in self.qweb:
46 return etree.tostring(self.qweb['node'], pretty_print=True)
48 class QWebTemplateNotFound(QWebException):
51 def raise_qweb_exception(etype=None, **kw):
54 orig_type, original, tb = sys.exc_info()
56 raise etype, original, tb
58 for k, v in kw.items():
60 # Will use `raise foo from bar` in python 3 and rename cause to __cause__
61 e.qweb['cause'] = original
64 class QWebContext(dict):
65 def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
69 self.templates = templates or {}
70 self.context = context
72 super(QWebContext, self).__init__(dic)
73 self['defined'] = lambda key: key in self
75 def safe_eval(self, expr):
76 locals_dict = collections.defaultdict(lambda: None)
77 locals_dict.update(self)
78 locals_dict.pop('cr', None)
79 locals_dict.pop('loader', None)
80 return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
83 """ Clones the current context, conserving all data and metadata
84 (loader, template cache, ...)
86 return QWebContext(self.cr, self.uid, dict.copy(self),
88 templates=self.templates,
94 class QWeb(orm.AbstractModel):
95 """ Base QWeb rendering engine
97 * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
98 create new models called :samp:`ir.qweb.field.{widget}`
99 * alternatively, override :meth:`~.get_converter_for` and return an
100 arbitrary model to use as field converter
102 Beware that if you need extensions or alterations which could be
103 incompatible with other subsystems, you should create a local object
104 inheriting from ``ir.qweb`` and customize that.
109 _void_elements = frozenset([
110 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
111 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
112 _format_regex = re.compile(
117 # jinja-style pattern
121 def __init__(self, pool, cr):
122 super(QWeb, self).__init__(pool, cr)
124 self._render_tag = self.prefixed_methods('render_tag_')
125 self._render_att = self.prefixed_methods('render_att_')
127 def prefixed_methods(self, prefix):
128 """ Extracts all methods prefixed by ``prefix``, and returns a mapping
129 of (t-name, method) where the t-name is the method name with prefix
130 removed and underscore converted to dashes
135 n_prefix = len(prefix)
137 (name[n_prefix:].replace('_', '-'), getattr(type(self), name))
138 for name in dir(self)
139 if name.startswith(prefix)
142 def register_tag(self, tag, func):
143 self._render_tag[tag] = func
145 def add_template(self, qwebcontext, name, node):
146 """Add a parsed template in the context. Used to preprocess templates."""
147 qwebcontext.templates[name] = node
149 def load_document(self, document, res_id, qwebcontext):
151 Loads an XML document and installs any contained template in the engine
153 :type document: a parsed lxml.etree element, an unparsed XML document
154 (as a string) or the path of an XML file to load
156 if not isinstance(document, basestring):
157 # assume lxml.etree.Element
159 elif document.startswith("<?xml"):
160 dom = etree.fromstring(document)
162 dom = etree.parse(document).getroot()
165 if node.get('t-name'):
166 name = str(node.get("t-name"))
167 self.add_template(qwebcontext, name, node)
168 if res_id and node.tag == "t":
169 self.add_template(qwebcontext, res_id, node)
172 def get_template(self, name, qwebcontext):
173 """ Tries to fetch the template ``name``, either gets it from the
174 context's template cache or loads one with the context's loader (if
177 :raises QWebTemplateNotFound: if the template can not be found or loaded
179 origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
180 if qwebcontext.loader and name not in qwebcontext.templates:
182 xml_doc = qwebcontext.loader(name)
184 raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
185 self.load_document(xml_doc, isinstance(name, (int, long)) and name or None, qwebcontext=qwebcontext)
187 if name in qwebcontext.templates:
188 return qwebcontext.templates[name]
190 raise QWebTemplateNotFound("Template %r not found" % name, template=origin_template)
192 def eval(self, expr, qwebcontext):
194 return qwebcontext.safe_eval(expr)
196 template = qwebcontext.get('__template__')
197 raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
199 def eval_object(self, expr, qwebcontext):
200 return self.eval(expr, qwebcontext)
202 def eval_str(self, expr, qwebcontext):
204 return qwebcontext.get(0, '')
205 val = self.eval(expr, qwebcontext)
206 if isinstance(val, unicode):
207 return val.encode("utf8")
208 if val is False or val is None:
212 def eval_format(self, expr, qwebcontext):
213 expr, replacements = self._format_regex.subn(
214 lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
222 return str(expr % qwebcontext)
224 template = qwebcontext.get('__template__')
225 raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
227 def eval_bool(self, expr, qwebcontext):
228 return int(bool(self.eval(expr, qwebcontext)))
230 def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
231 """ render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
233 Renders the template specified by the provided template name
235 :param qwebcontext: context for rendering the template
236 :type qwebcontext: dict or :class:`QWebContext` instance
237 :param loader: if ``qwebcontext`` is a dict, loader set into the
238 context instantiated for rendering
240 if qwebcontext is None:
243 if not isinstance(qwebcontext, QWebContext):
244 qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
246 qwebcontext['__template__'] = id_or_xml_id
247 stack = qwebcontext.get('__stack__', [])
249 qwebcontext['__caller__'] = stack[-1]
250 stack.append(id_or_xml_id)
251 qwebcontext['__stack__'] = stack
252 qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
253 return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
255 def render_node(self, element, qwebcontext):
256 generated_attributes = ""
258 template_attributes = {}
260 debugger = element.get('t-debug')
261 if debugger is not None:
262 if openerp.tools.config['dev_mode']:
263 __import__(debugger).set_trace() # pdb, ipdb, pudb, ...
265 _logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
267 for (attribute_name, attribute_value) in element.attrib.iteritems():
268 attribute_name = str(attribute_name)
269 if attribute_name == "groups":
270 cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
271 uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
272 can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
276 attribute_value = attribute_value.encode("utf8")
278 if attribute_name.startswith("t-"):
279 for attribute in self._render_att:
280 if attribute_name[2:].startswith(attribute):
281 attrs = self._render_att[attribute](
282 self, element, attribute_name, attribute_value, qwebcontext)
283 for att, val in attrs:
285 if not isinstance(val, str):
286 val = unicode(val).encode('utf-8')
287 generated_attributes += self.render_attribute(element, att, val, qwebcontext)
290 if attribute_name[2:] in self._render_tag:
291 t_render = attribute_name[2:]
292 template_attributes[attribute_name[2:]] = attribute_value
294 generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
297 result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
299 result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
302 result += self.render_tail(element.tail, element, qwebcontext)
304 if isinstance(result, unicode):
305 return result.encode('utf-8')
308 def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
310 # template_attributes: t-* attributes
311 # generated_attributes: generated attributes
312 # qwebcontext: values
313 # inner: optional innerXml
315 g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
317 g_inner = [] if element.text is None else [self.render_text(element.text, element, qwebcontext)]
318 for current_node in element.iterchildren(tag=etree.Element):
320 g_inner.append(self.render_node(current_node, qwebcontext))
321 except QWebException:
324 template = qwebcontext.get('__template__')
325 raise_qweb_exception(message="Could not render element %r" % element.tag, node=element, template=template)
326 name = str(element.tag)
327 inner = "".join(g_inner)
328 trim = template_attributes.get("trim", 0)
332 inner = inner.lstrip()
333 elif trim == 'right':
334 inner = inner.rstrip()
336 inner = inner.strip()
339 elif len(inner) or name not in self._void_elements:
340 return "<%s%s>%s</%s>" % tuple(
341 qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
342 for qwebcontext in (name, generated_attributes, inner, name)
345 return "<%s%s/>" % (name, generated_attributes)
347 def render_attribute(self, element, name, value, qwebcontext):
348 return ' %s="%s"' % (name, escape(value))
350 def render_text(self, text, element, qwebcontext):
351 return text.encode('utf-8')
353 def render_tail(self, tail, element, qwebcontext):
354 return tail.encode('utf-8')
357 def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
358 if attribute_name.startswith("t-attf-"):
359 return [(attribute_name[7:], self.eval_format(attribute_value, qwebcontext))]
361 if attribute_name.startswith("t-att-"):
362 return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))]
364 result = self.eval_object(attribute_value, qwebcontext)
365 if isinstance(result, collections.Mapping):
366 return result.iteritems()
371 def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
372 inner = self.eval_str(template_attributes["raw"], qwebcontext)
373 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
375 def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
376 options = json.loads(template_attributes.get('esc-options') or '{}')
377 widget = self.get_widget_for(options.get('widget'))
378 inner = widget.format(template_attributes['esc'], options, qwebcontext)
379 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
381 def _iterate(self, iterable):
382 if isinstance (iterable, collections.Mapping):
383 return iterable.iteritems()
385 return itertools.izip(*itertools.tee(iterable))
387 def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
388 expr = template_attributes["foreach"]
389 enum = self.eval_object(expr, qwebcontext)
391 template = qwebcontext.get('__template__')
392 raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
393 if isinstance(enum, int):
396 varname = template_attributes['as'].replace('.', '_')
397 copy_qwebcontext = qwebcontext.copy()
400 if isinstance(enum, collections.Sized):
402 copy_qwebcontext["%s_size" % varname] = size
404 copy_qwebcontext["%s_all" % varname] = enum
406 for index, (item, value) in enumerate(self._iterate(enum)):
407 copy_qwebcontext.update({
409 '%s_value' % varname: value,
410 '%s_index' % varname: index,
411 '%s_first' % varname: index == 0,
414 copy_qwebcontext['%s_last' % varname] = index + 1 == size
416 copy_qwebcontext.update({
417 '%s_parity' % varname: 'odd',
418 '%s_even' % varname: False,
419 '%s_odd' % varname: True,
422 copy_qwebcontext.update({
423 '%s_parity' % varname: 'even',
424 '%s_even' % varname: True,
425 '%s_odd' % varname: False,
427 ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
430 def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
431 if self.eval_bool(template_attributes["if"], qwebcontext):
432 return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
435 def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
436 d = qwebcontext.copy()
437 d[0] = self.render_element(element, template_attributes, generated_attributes, d)
438 cr = d.get('request') and d['request'].cr or None
439 uid = d.get('request') and d['request'].uid or None
441 template = self.eval_format(template_attributes["call"], d)
443 template = int(template)
446 return self.render(cr, uid, template, d)
448 def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
449 """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
451 # An asset bundle is rendered in two differents contexts (when genereting html and
452 # when generating the bundle itself) so they must be qwebcontext free
453 # even '0' variable is forbidden
454 template = qwebcontext.get('__template__')
455 raise QWebException("t-call-assets cannot contain children nodes", template=template)
456 xmlid = template_attributes['call-assets']
457 cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
458 bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
459 css = self.get_attr_bool(template_attributes.get('css'), default=True)
460 js = self.get_attr_bool(template_attributes.get('js'), default=True)
461 return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
463 def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
464 if "value" in template_attributes:
465 qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
466 elif "valuef" in template_attributes:
467 qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
469 qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
472 def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
473 """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
474 node_name = element.tag
475 assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
476 "li", "ul", "ol", "dl", "dt", "dd"),\
477 "RTE widgets do not work correctly on %r elements" % node_name
478 assert node_name != 't',\
479 "t-field can not be used on a t element, provide an actual HTML node"
481 record, field_name = template_attributes["field"].rsplit('.', 1)
482 record = self.eval_object(record, qwebcontext)
484 column = record._all_columns[field_name].column
485 options = json.loads(template_attributes.get('field-options') or '{}')
486 field_type = get_field_type(column, options)
488 converter = self.get_converter_for(field_type)
490 return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
491 element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
493 def get_converter_for(self, field_type):
494 """ returns a :class:`~openerp.models.Model` used to render a
497 By default, tries to get the model named
498 :samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
500 :param str field_type: type or widget of field to render
502 return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
504 def get_widget_for(self, widget):
505 """ returns a :class:`~openerp.models.Model` used to render a
508 :param str widget: name of the widget to use, or ``None``
510 widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
511 return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
513 def get_attr_bool(self, attr, default=False):
516 if attr in ('false', '0'):
518 elif attr in ('true', '1'):
522 #--------------------------------------------------------------------
523 # QWeb Fields converters
524 #--------------------------------------------------------------------
526 class FieldConverter(osv.AbstractModel):
527 """ Used to convert a t-field specification into an output HTML field.
529 :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
531 * converts the record value to html using :meth:`~.record_to_html`
532 * generates the metadata attributes (``data-oe-``) to set on the root
534 * generates the root result node itself through :meth:`~.render_element`
536 _name = 'ir.qweb.field'
538 def attributes(self, cr, uid, field_name, record, options,
539 source_element, g_att, t_att, qweb_context,
541 """ attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None)
543 Generates the metadata attributes (prefixed by ``data-oe-`` for the
544 root node of the field conversion. Attribute values are escaped by the
547 The default attributes are:
549 * ``model``, the name of the record's model
550 * ``id`` the id of the record to which the field belongs
551 * ``field`` the name of the converted field
552 * ``type`` the logical field type (widget, may not match the column's
553 ``type``, may not be any _column subclass name)
554 * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
555 column is translatable
556 * ``expression``, the original expression
558 :returns: iterable of (attribute name, attribute value) pairs.
560 column = record._all_columns[field_name].column
561 field_type = get_field_type(column, options)
563 ('data-oe-model', record._name),
564 ('data-oe-id', record.id),
565 ('data-oe-field', field_name),
566 ('data-oe-type', field_type),
567 ('data-oe-expression', t_att['field']),
570 def value_to_html(self, cr, uid, value, column, options=None, context=None):
571 """ value_to_html(cr, uid, value, column, options=None, context=None)
573 Converts a single value to its HTML version/output
575 if not value: return ''
578 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
579 """ record_to_html(cr, uid, field_name, record, column, options=None, context=None)
581 Converts the specified field of the browse_record ``record`` to HTML
583 return self.value_to_html(
584 cr, uid, record[field_name], column, options=options, context=context)
586 def to_html(self, cr, uid, field_name, record, options,
587 source_element, t_att, g_att, qweb_context, context=None):
588 """ to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None)
590 Converts a ``t-field`` to its HTML output. A ``t-field`` may be
591 extended by a ``t-field-options``, which is a JSON-serialized mapping
592 of configuration values.
594 A default configuration key is ``widget`` which can override the
595 field's own ``_type``.
598 content = self.record_to_html(
599 cr, uid, field_name, record,
600 record._all_columns[field_name].column,
601 options, context=context)
602 if options.get('html-escape', True):
603 content = escape(content)
604 elif hasattr(content, '__html__'):
605 content = content.__html__()
607 _logger.warning("Could not get field %s for model %s",
608 field_name, record._name, exc_info=True)
611 inherit_branding = context and context.get('inherit_branding')
612 if not inherit_branding and context and context.get('inherit_branding_auto'):
613 inherit_branding = self.pool['ir.model.access'].check(cr, uid, record._name, 'write', False, context=context)
616 # add branding attributes
618 ' %s="%s"' % (name, escape(value))
619 for name, value in self.attributes(
620 cr, uid, field_name, record, options,
621 source_element, g_att, t_att, qweb_context)
624 return self.render_element(cr, uid, source_element, t_att, g_att,
625 qweb_context, content)
627 def qweb_object(self):
628 return self.pool['ir.qweb']
630 def render_element(self, cr, uid, source_element, t_att, g_att,
631 qweb_context, content):
632 """ render_element(cr, uid, source_element, t_att, g_att, qweb_context, content)
634 Final rendering hook, by default just calls ir.qweb's ``render_element``
636 return self.qweb_object().render_element(
637 source_element, t_att, g_att, qweb_context, content or '')
639 def user_lang(self, cr, uid, context):
640 """ user_lang(cr, uid, context)
642 Fetches the res.lang object corresponding to the language code stored
643 in the user's context. Fallbacks to en_US if no lang is present in the
644 context *or the language code is not valid*.
646 :returns: res.lang browse_record
648 if context is None: context = {}
650 lang_code = context.get('lang') or 'en_US'
651 Lang = self.pool['res.lang']
653 lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
654 or Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
656 return Lang.browse(cr, uid, lang_ids[0], context=context)
658 class FloatConverter(osv.AbstractModel):
659 _name = 'ir.qweb.field.float'
660 _inherit = 'ir.qweb.field'
662 def precision(self, cr, uid, column, options=None, context=None):
663 _, precision = column.digits or (None, None)
666 def value_to_html(self, cr, uid, value, column, options=None, context=None):
669 precision = self.precision(cr, uid, column, options=options, context=context)
670 fmt = '%f' if precision is None else '%.{precision}f'
672 lang_code = context.get('lang') or 'en_US'
673 lang = self.pool['res.lang']
674 formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
676 # %f does not strip trailing zeroes. %g does but its precision causes
677 # it to switch to scientific notation starting at a million *and* to
678 # strip decimals. So use %f and if no precision was specified manually
680 if precision is None:
681 formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
684 class DateConverter(osv.AbstractModel):
685 _name = 'ir.qweb.field.date'
686 _inherit = 'ir.qweb.field'
688 def value_to_html(self, cr, uid, value, column, options=None, context=None):
689 if not value or len(value)<10: return ''
690 lang = self.user_lang(cr, uid, context=context)
691 locale = babel.Locale.parse(lang.code)
693 if isinstance(value, basestring):
694 value = datetime.datetime.strptime(
695 value[:10], openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
697 if options and 'format' in options:
698 pattern = options['format']
700 strftime_pattern = lang.date_format
701 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
703 return babel.dates.format_date(
704 value, format=pattern,
707 class DateTimeConverter(osv.AbstractModel):
708 _name = 'ir.qweb.field.datetime'
709 _inherit = 'ir.qweb.field'
711 def value_to_html(self, cr, uid, value, column, options=None, context=None):
712 if not value: return ''
713 lang = self.user_lang(cr, uid, context=context)
714 locale = babel.Locale.parse(lang.code)
716 if isinstance(value, basestring):
717 value = datetime.datetime.strptime(
718 value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
719 value = fields.datetime.context_timestamp(
720 cr, uid, timestamp=value, context=context)
722 if options and 'format' in options:
723 pattern = options['format']
725 strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
726 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
728 if options and options.get('hide_seconds'):
729 pattern = pattern.replace(":ss", "").replace(":s", "")
731 return babel.dates.format_datetime(value, format=pattern, locale=locale)
733 class TextConverter(osv.AbstractModel):
734 _name = 'ir.qweb.field.text'
735 _inherit = 'ir.qweb.field'
737 def value_to_html(self, cr, uid, value, column, options=None, context=None):
739 Escapes the value and converts newlines to br. This is bullshit.
741 if not value: return ''
743 return nl2br(value, options=options)
745 class SelectionConverter(osv.AbstractModel):
746 _name = 'ir.qweb.field.selection'
747 _inherit = 'ir.qweb.field'
749 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
750 value = record[field_name]
751 if not value: return ''
752 selection = dict(fields.selection.reify(
753 cr, uid, record._model, column, context=context))
754 return self.value_to_html(
755 cr, uid, selection[value], column, options=options)
757 class ManyToOneConverter(osv.AbstractModel):
758 _name = 'ir.qweb.field.many2one'
759 _inherit = 'ir.qweb.field'
761 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
762 [read] = record.read([field_name])
763 if not read[field_name]: return ''
764 _, value = read[field_name]
765 return nl2br(value, options=options)
767 class HTMLConverter(osv.AbstractModel):
768 _name = 'ir.qweb.field.html'
769 _inherit = 'ir.qweb.field'
771 def value_to_html(self, cr, uid, value, column, options=None, context=None):
772 return HTMLSafe(value or '')
774 class ImageConverter(osv.AbstractModel):
775 """ ``image`` widget rendering, inserts a data:uri-using image tag in the
776 document. May be overridden by e.g. the website module to generate links
779 .. todo:: what happens if different output need different converters? e.g.
780 reports may need embedded images or FS links whereas website
783 _name = 'ir.qweb.field.image'
784 _inherit = 'ir.qweb.field'
786 def value_to_html(self, cr, uid, value, column, options=None, context=None):
788 image = Image.open(cStringIO.StringIO(value.decode('base64')))
791 raise ValueError("Non-image binary fields can not be converted to HTML")
792 except: # image.verify() throws "suitable exceptions", I have no idea what they are
793 raise ValueError("Invalid image content")
795 return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
797 class MonetaryConverter(osv.AbstractModel):
798 """ ``monetary`` converter, has a mandatory option
799 ``display_currency``.
801 The currency is used for formatting *and rounding* of the float value. It
802 is assumed that the linked res_currency has a non-empty rounding value and
803 res.currency's ``round`` method is used to perform rounding.
805 .. note:: the monetary converter internally adds the qweb context to its
806 options mapping, so that the context is available to callees.
807 It's set under the ``_qweb_context`` key.
809 _name = 'ir.qweb.field.monetary'
810 _inherit = 'ir.qweb.field'
812 def to_html(self, cr, uid, field_name, record, options,
813 source_element, t_att, g_att, qweb_context, context=None):
814 options['_qweb_context'] = qweb_context
815 return super(MonetaryConverter, self).to_html(
816 cr, uid, field_name, record, options,
817 source_element, t_att, g_att, qweb_context, context=context)
819 def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
822 Currency = self.pool['res.currency']
823 display_currency = self.display_currency(cr, uid, options['display_currency'], options)
825 # lang.format mandates a sprintf-style format. These formats are non-
826 # minimal (they have a default fixed precision instead), and
827 # lang.format will not set one by default. currency.round will not
828 # provide one either. So we need to generate a precision value
829 # (integer > 0) from the currency's rounding (a float generally < 1.0).
831 # The log10 of the rounding should be the number of digits involved if
832 # negative, if positive clamp to 0 digits and call it a day.
833 # nb: int() ~ floor(), we want nearest rounding instead
834 precision = int(round(math.log10(display_currency.rounding)))
835 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
837 from_amount = record[field_name]
839 if options.get('from_currency'):
840 from_currency = self.display_currency(cr, uid, options['from_currency'], options)
841 from_amount = Currency.compute(cr, uid, from_currency.id, display_currency.id, from_amount)
843 lang_code = context.get('lang') or 'en_US'
844 lang = self.pool['res.lang']
845 formatted_amount = lang.format(cr, uid, [lang_code],
846 fmt, Currency.round(cr, uid, display_currency, from_amount),
847 grouping=True, monetary=True)
850 if display_currency.position == 'before':
855 return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
859 symbol=display_currency.symbol,
862 def display_currency(self, cr, uid, currency, options):
863 return self.qweb_object().eval_object(
864 currency, options['_qweb_context'])
867 ('year', 3600 * 24 * 365),
868 ('month', 3600 * 24 * 30),
869 ('week', 3600 * 24 * 7),
875 class DurationConverter(osv.AbstractModel):
876 """ ``duration`` converter, to display integral or fractional values as
877 human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
879 Can be used on any numerical field.
881 Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
882 ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
883 field value before converting it.
885 Sub-second values will be ignored.
887 _name = 'ir.qweb.field.duration'
888 _inherit = 'ir.qweb.field'
890 def value_to_html(self, cr, uid, value, column, options=None, context=None):
891 units = dict(TIMEDELTA_UNITS)
893 raise ValueError(_("Durations can't be negative"))
894 if not options or options.get('unit') not in units:
895 raise ValueError(_("A unit must be provided to duration widgets"))
897 locale = babel.Locale.parse(
898 self.user_lang(cr, uid, context=context).code)
899 factor = units[options['unit']]
903 for unit, secs_per_unit in TIMEDELTA_UNITS:
904 v, r = divmod(r, secs_per_unit)
906 section = babel.dates.format_timedelta(
907 v*secs_per_unit, threshold=1, locale=locale)
909 sections.append(section)
910 return ' '.join(sections)
913 class RelativeDatetimeConverter(osv.AbstractModel):
914 _name = 'ir.qweb.field.relative'
915 _inherit = 'ir.qweb.field'
917 def value_to_html(self, cr, uid, value, column, options=None, context=None):
918 parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
919 locale = babel.Locale.parse(
920 self.user_lang(cr, uid, context=context).code)
922 if isinstance(value, basestring):
923 value = datetime.datetime.strptime(value, parse_format)
925 # value should be a naive datetime in UTC. So is fields.datetime.now()
926 reference = datetime.datetime.strptime(column.now(), parse_format)
928 return babel.dates.format_timedelta(
929 value - reference, add_direction=True, locale=locale)
931 class Contact(orm.AbstractModel):
932 _name = 'ir.qweb.field.contact'
933 _inherit = 'ir.qweb.field.many2one'
935 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
941 opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
942 if not getattr(record, field_name):
945 id = getattr(record, field_name).id
946 context.update(show_address=True)
947 field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context=context)
948 value = field_browse.name_get()[0][1]
951 'name': value.split("\n")[0],
952 'address': escape("\n".join(value.split("\n")[1:])).strip(),
953 'phone': field_browse.phone,
954 'mobile': field_browse.mobile,
955 'fax': field_browse.fax,
956 'city': field_browse.city,
957 'country_id': field_browse.country_id.display_name,
958 'website': field_browse.website,
959 'email': field_browse.email,
961 'object': field_browse,
965 html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
967 return HTMLSafe(html)
969 class QwebView(orm.AbstractModel):
970 _name = 'ir.qweb.field.qweb'
971 _inherit = 'ir.qweb.field.many2one'
973 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
974 if not getattr(record, field_name):
977 view = getattr(record, field_name)
979 if view._model._name != "ir.ui.view":
980 _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
983 ctx = (context or {}).copy()
984 ctx['object'] = record
985 html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
987 return HTMLSafe(html)
989 class QwebWidget(osv.AbstractModel):
990 _name = 'ir.qweb.widget'
992 def _format(self, inner, options, qwebcontext):
993 return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
995 def format(self, inner, options, qwebcontext):
996 return escape(self._format(inner, options, qwebcontext))
998 class QwebWidgetMonetary(osv.AbstractModel):
999 _name = 'ir.qweb.widget.monetary'
1000 _inherit = 'ir.qweb.widget'
1002 def _format(self, inner, options, qwebcontext):
1003 inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
1004 display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
1005 precision = int(round(math.log10(display.rounding)))
1006 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
1007 lang_code = qwebcontext.context.get('lang') or 'en_US'
1008 formatted_amount = self.pool['res.lang'].format(
1009 qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
1012 if display.position == 'before':
1017 return u'{pre}{0}{post}'.format(
1018 formatted_amount, pre=pre, post=post
1019 ).format(symbol=display.symbol,)
1021 class HTMLSafe(object):
1022 """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
1023 objects with a ``__html__`` methods but AFAIK does not provide any such
1026 Wrapping a string in HTML will prevent its escaping
1028 __slots__ = ['string']
1029 def __init__(self, string):
1030 self.string = string
1035 if isinstance(s, unicode):
1036 return s.encode('utf-8')
1038 def __unicode__(self):
1040 if isinstance(s, str):
1041 return s.decode('utf-8')
1044 def nl2br(string, options=None):
1045 """ Converts newlines to HTML linebreaks in ``string``. Automatically
1046 escapes content unless options['html-escape'] is set to False, and returns
1047 the result wrapped in an HTMLSafe object.
1050 :param dict options:
1053 if options is None: options = {}
1055 if options.get('html-escape', True):
1056 string = escape(string)
1057 return HTMLSafe(string.replace('\n', '<br>\n'))
1059 def get_field_type(column, options):
1060 """ Gets a t-field's effective type from the field's column and its options
1062 return options.get('widget', column._type)
1064 class AssetError(Exception):
1066 class AssetNotFound(AssetError):
1069 class AssetsBundle(object):
1070 rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
1071 rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
1072 rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
1074 def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
1076 self.cr = request.cr if cr is None else cr
1077 self.uid = request.uid if uid is None else uid
1078 self.context = request.context if context is None else context
1079 self.registry = request.registry if registry is None else registry
1080 self.javascripts = []
1081 self.stylesheets = []
1082 self.css_errors = []
1084 self._checksum = None
1086 context = self.context.copy()
1087 context['inherit_branding'] = False
1088 context['rendering_bundle'] = True
1089 self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
1093 fragments = html.fragments_fromstring(self.html)
1094 for el in fragments:
1095 if isinstance(el, basestring):
1096 self.remains.append(el)
1097 elif isinstance(el, html.HtmlElement):
1098 src = el.get('src', '')
1099 href = el.get('href', '')
1100 atype = el.get('type')
1101 media = el.get('media')
1102 if el.tag == 'style':
1103 if atype == 'text/sass' or src.endswith('.sass'):
1104 self.stylesheets.append(SassStylesheetAsset(self, inline=el.text, media=media))
1105 elif atype == 'text/less' or src.endswith('.less'):
1106 self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
1108 self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
1109 elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
1110 if href.endswith('.sass') or atype == 'text/sass':
1111 self.stylesheets.append(SassStylesheetAsset(self, url=href, media=media))
1112 elif href.endswith('.less') or atype == 'text/less':
1113 self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
1115 self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
1116 elif el.tag == 'script' and not src:
1117 self.javascripts.append(JavascriptAsset(self, inline=el.text))
1118 elif el.tag == 'script' and self.can_aggregate(src):
1119 self.javascripts.append(JavascriptAsset(self, url=src))
1121 self.remains.append(html.tostring(el))
1124 self.remains.append(html.tostring(el))
1126 # notYETimplementederror
1127 raise NotImplementedError
1129 def can_aggregate(self, url):
1130 return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
1132 def to_html(self, sep=None, css=True, js=True, debug=False):
1137 if css and self.stylesheets:
1138 self.preprocess_css()
1140 msg = '\n'.join(self.css_errors)
1141 self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
1142 for style in self.stylesheets:
1143 response.append(style.to_html())
1145 for jscript in self.javascripts:
1146 response.append(jscript.to_html())
1148 url_for = self.context.get('url_for', lambda url: url)
1149 if css and self.stylesheets:
1150 href = '/web/css/%s/%s' % (self.xmlid, self.version)
1151 response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
1153 src = '/web/js/%s/%s' % (self.xmlid, self.version)
1154 response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
1155 response.extend(self.remains)
1156 return sep + sep.join(response)
1159 def last_modified(self):
1160 """Returns last modified date of linked files"""
1161 return max(itertools.chain(
1162 (asset.last_modified for asset in self.javascripts),
1163 (asset.last_modified for asset in self.stylesheets),
1168 return self.checksum[0:7]
1173 Not really a full checksum.
1174 We compute a SHA1 on the rendered bundle + max linked files last_modified date
1176 check = self.html + str(self.last_modified)
1177 return hashlib.sha1(check).hexdigest()
1180 content = self.get_cache('js')
1182 content = ';\n'.join(asset.minify() for asset in self.javascripts)
1183 self.set_cache('js', content)
1187 """Generate css content from given bundle"""
1188 content = self.get_cache('css')
1190 content = self.preprocess_css()
1193 msg = '\n'.join(self.css_errors)
1194 content += self.css_message(msg)
1196 # move up all @import rules to the top
1199 matches.append(matchobj.group(0))
1202 content = re.sub(self.rx_css_import, push, content)
1204 matches.append(content)
1205 content = u'\n'.join(matches)
1208 self.set_cache('css', content)
1212 def get_cache(self, type):
1214 domain = [('url', '=', '/web/%s/%s/%s' % (type, self.xmlid, self.version))]
1215 bundle = self.registry['ir.attachment'].search_read(self.cr, openerp.SUPERUSER_ID, domain, ['datas'], context=self.context)
1216 if bundle and bundle[0]['datas']:
1217 content = bundle[0]['datas'].decode('base64')
1220 def set_cache(self, type, content):
1221 ira = self.registry['ir.attachment']
1222 ira.invalidate_bundle(self.cr, openerp.SUPERUSER_ID, type=type, xmlid=self.xmlid)
1223 url = '/web/%s/%s/%s' % (type, self.xmlid, self.version)
1224 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1225 datas=content.encode('utf8').encode('base64'),
1229 ), context=self.context)
1231 def css_message(self, message):
1232 # '\A' == css content carriage return
1233 message = message.replace('\n', '\\A ').replace('"', '\\"')
1239 font-family: monospace;
1245 def preprocess_css(self):
1247 Checks if the bundle contains any sass/less content, then compiles it to css.
1248 Returns the bundle's flat css.
1250 for atype in (SassStylesheetAsset, LessStylesheetAsset):
1251 assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
1253 cmd = assets[0].get_command()
1254 source = '\n'.join([asset.get_source() for asset in assets])
1255 compiled = self.compile_css(cmd, source)
1257 fragments = self.rx_css_split.split(compiled)
1258 at_rules = fragments.pop(0)
1260 # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
1261 self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
1263 asset_id = fragments.pop(0)
1264 asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
1265 asset._content = fragments.pop(0)
1267 return '\n'.join(asset.minify() for asset in self.stylesheets)
1269 def compile_css(self, cmd, source):
1270 """Sanitizes @import rules, remove duplicates @import rules, then compile"""
1272 def sanitize(matchobj):
1273 ref = matchobj.group(2)
1274 line = '@import "%s"%s' % (ref, matchobj.group(3))
1275 if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
1276 imports.append(line)
1278 msg = "Local import '%s' is forbidden for security reasons." % ref
1279 _logger.warning(msg)
1280 self.css_errors.append(msg)
1282 source = re.sub(self.rx_preprocess_imports, sanitize, source)
1285 compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
1287 msg = "Could not execute command %r" % cmd[0]
1289 self.css_errors.append(msg)
1291 result = compiler.communicate(input=source.encode('utf-8'))
1292 if compiler.returncode:
1293 error = self.get_preprocessor_error(''.join(result), source=source)
1294 _logger.warning(error)
1295 self.css_errors.append(error)
1297 compiled = result[0].strip().decode('utf8')
1300 def get_preprocessor_error(self, stderr, source=None):
1301 """Improve and remove sensitive information from sass/less compilator error messages"""
1302 error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
1303 if 'Cannot load compass' in error:
1304 error += "Maybe you should install the compass gem using this extra argument:\n\n" \
1305 " $ sudo gem install compass --pre\n"
1306 error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
1307 for asset in self.stylesheets:
1308 if isinstance(asset, PreprocessedCSS):
1309 error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
1312 class WebAsset(object):
1315 def __init__(self, bundle, inline=None, url=None):
1316 self.id = str(uuid.uuid4())
1317 self.bundle = bundle
1318 self.inline = inline
1321 self.uid = bundle.uid
1322 self.registry = bundle.registry
1323 self.context = bundle.context
1324 self._content = None
1325 self._filename = None
1326 self._ir_attach = None
1327 name = '<inline asset>' if inline else url
1328 self.name = "%s defined in bundle '%s'" % (name, bundle.xmlid)
1329 if not inline and not url:
1330 raise Exception("An asset should either be inlined or url linked")
1333 if not (self.inline or self._filename or self._ir_attach):
1334 addon = filter(None, self.url.split('/'))[0]
1336 # Test url against modules static assets
1337 mpath = openerp.http.addons_manifest[addon]['addons_path']
1338 self._filename = mpath + self.url.replace('/', os.path.sep)
1341 # Test url against ir.attachments
1342 fields = ['__last_update', 'datas', 'mimetype']
1343 domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1344 ira = self.registry['ir.attachment']
1345 attach = ira.search_read(self.cr, openerp.SUPERUSER_ID, domain, fields, context=self.context)
1346 self._ir_attach = attach[0]
1348 raise AssetNotFound("Could not find %s" % self.name)
1351 raise NotImplementedError()
1354 def last_modified(self):
1358 return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
1359 elif self._ir_attach:
1360 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1361 last_update = self._ir_attach['__last_update']
1363 return datetime.datetime.strptime(last_update, server_format + '.%f')
1365 return datetime.datetime.strptime(last_update, server_format)
1368 return datetime.datetime(1970, 1, 1)
1372 if self._content is None:
1373 self._content = self.inline or self._fetch_content()
1374 return self._content
1376 def _fetch_content(self):
1377 """ Fetch content from file or database"""
1381 with open(self._filename, 'rb') as fp:
1382 return fp.read().decode('utf-8')
1384 return self._ir_attach['datas'].decode('base64')
1385 except UnicodeDecodeError:
1386 raise AssetError('%s is not utf-8 encoded.' % self.name)
1388 raise AssetNotFound('File %s does not exist.' % self.name)
1390 raise AssetError('Could not get content for %s.' % self.name)
1395 def with_header(self, content=None):
1397 content = self.content
1398 return '\n/* %s */\n%s' % (self.name, content)
1400 class JavascriptAsset(WebAsset):
1402 return self.with_header(rjsmin(self.content))
1404 def _fetch_content(self):
1406 return super(JavascriptAsset, self)._fetch_content()
1407 except AssetError, e:
1408 return "console.error(%s);" % json.dumps(e.message)
1412 return '<script type="text/javascript" src="%s"></script>' % (self.html_url % self.url)
1414 return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
1416 class StylesheetAsset(WebAsset):
1417 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
1418 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
1419 rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
1420 rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
1422 def __init__(self, *args, **kw):
1423 self.media = kw.pop('media', None)
1424 super(StylesheetAsset, self).__init__(*args, **kw)
1428 content = super(StylesheetAsset, self).content
1430 content = '@media %s { %s }' % (self.media, content)
1433 def _fetch_content(self):
1435 content = super(StylesheetAsset, self)._fetch_content()
1436 web_dir = os.path.dirname(self.url)
1439 content = self.rx_import.sub(
1440 r"""@import \1%s/""" % (web_dir,),
1445 content = self.rx_url.sub(
1446 r"url(\1%s/" % (web_dir,),
1451 # remove charset declarations, we only support utf-8
1452 content = self.rx_charset.sub('', content)
1455 except AssetError, e:
1456 self.bundle.css_errors.append(e.message)
1460 # remove existing sourcemaps, make no sense after re-mini
1461 content = self.rx_sourceMap.sub('', self.content)
1463 content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
1465 content = re.sub(r'\s+', ' ', content)
1466 content = re.sub(r' *([{}]) *', r'\1', content)
1467 return self.with_header(content)
1470 media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
1472 href = self.html_url % self.url
1473 return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
1475 return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
1477 class PreprocessedCSS(StylesheetAsset):
1482 return self.with_header()
1486 ira = self.registry['ir.attachment']
1487 url = self.html_url % self.url
1488 domain = [('type', '=', 'binary'), ('url', '=', url)]
1489 ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
1490 datas = self.content.encode('utf8').encode('base64')
1492 # TODO: update only if needed
1493 ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
1495 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1497 mimetype='text/css',
1501 ), context=self.context)
1502 return super(PreprocessedCSS, self).to_html()
1504 def get_source(self):
1505 content = self.inline or self._fetch_content()
1506 return "/*! %s */\n%s" % (self.id, content)
1508 def get_command(self):
1509 raise NotImplementedError
1511 class SassStylesheetAsset(PreprocessedCSS):
1512 rx_indent = re.compile(r'^( +|\t+)', re.M)
1516 def get_source(self):
1517 content = textwrap.dedent(self.inline or self._fetch_content())
1520 # Indentation normalization
1522 if self.indent is None:
1524 if self.indent == self.reindent:
1525 # Don't reindent the file if identation is the final one (reindent)
1526 raise StopIteration()
1527 return ind.replace(self.indent, self.reindent)
1530 content = self.rx_indent.sub(fix_indent, content)
1531 except StopIteration:
1533 return "/*! %s */\n%s" % (self.id, content)
1535 def get_command(self):
1536 return ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
1537 '-r', 'bootstrap-sass']
1539 class LessStylesheetAsset(PreprocessedCSS):
1540 def get_command(self):
1541 webpath = openerp.http.addons_manifest['web']['addons_path']
1542 lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less')
1543 return ['lessc', '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
1546 """ Minify js with a clever regex.
1547 Taken from http://opensource.perlig.de/rjsmin
1548 Apache License, Version 2.0 """
1550 """ Substitution callback """
1551 groups = match.groups()
1557 (groups[4] and '\n') or
1558 (groups[5] and ' ') or
1559 (groups[6] and ' ') or
1560 (groups[7] and ' ') or
1565 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
1566 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
1567 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
1568 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
1569 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
1570 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
1571 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
1572 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
1573 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
1574 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
1575 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
1576 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
1577 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
1578 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
1579 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
1580 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
1581 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
1582 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
1583 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
1584 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
1585 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script