diff --git a/debian/control b/debian/control index a4c1b050c0a..74cd027427a 100644 --- a/debian/control +++ b/debian/control @@ -47,7 +47,13 @@ Depends: Conflicts: tinyerp-server, openerp-server, openerp-web Replaces: tinyerp-server, openerp-server, openerp-web Recommends: - graphviz, ghostscript, postgresql, python-matplotlib, poppler-utils + antiword, + graphviz, + ghostscript, + postgresql, + python-gevent, + python-matplotlib, + poppler-utils Description: OpenERP Enterprise Resource Management OpenERP, previously known as TinyERP, is a complete ERP and CRM. The main features are accounting (analytic and financial), stock management, sales and diff --git a/openerp/addons/base/ir/__init__.py b/openerp/addons/base/ir/__init__.py index 35184f7a565..6fb675d23c2 100644 --- a/openerp/addons/base/ir/__init__.py +++ b/openerp/addons/base/ir/__init__.py @@ -37,6 +37,7 @@ import ir_config_parameter import osv_memory_autovacuum import ir_mail_server import ir_fields +import ir_qweb # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index de45d2d1f1f..39f7f99b5fa 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -149,30 +149,6 @@ class ir_fields_converter(orm.Model): return functools.partial( converter, cr, uid, model, column, context=context) - def from_field(self, cr, uid, model, column, totype='str', context=None): - """ Fetches a converter for the provided column object, to the - specified type. - - A converter is a callable taking a value as returned by a read on the - column, and returning a formatted value of type ``totype``. - - By default, tries to get a method on itself with a name matching the - pattern ``_$totype_from_${column._type}`` and returns it. - - Error conditions are similar to :meth:`~.to_field` - - :param column: column object to generate a value from - :param str totype: - :return: a function (column.read_type -> totype) - :rtype: Callable | None - """ - converter = getattr( - self, '_%s_from_%s' % (totype, column._type), None) - if not converter: return None - - return functools.partial( - converter, cr, uid, model, column, context=context) - def _str_to_boolean(self, cr, uid, model, column, value, context=None): # all translatables used for booleans true, yes, false, no = _(u"true"), _(u"yes"), _(u"false"), _(u"no") @@ -460,56 +436,3 @@ class ir_fields_converter(orm.Model): commands.append(CREATE(writable)) return commands, warnings - - def _html_from_passthrough(self, cr, uid, model, column, value, context=None): - """ - escapes the value and returns it directly - """ - return werkzeug.utils.escape(value), [] - - _html_from_char = _html_from_integer = \ - _html_from_date = _html_from_datetime = _html_from_passthrough - - def _html_from_float(self, cr, uid, model, column, value, context=None): - width, precision = column.digits or (None, None) - if precision is None: - fmt = '{value}' - else: - fmt = '{value:.{precision}f}' - - return werkzeug.utils.escape( - fmt.format(value=value, width=width, precision=precision, )), [] - - def _html_from_text(self, cr, uid, model, column, value, context=None): - """ - Escapes the value and converts newlines to br. This is bullshit. - """ - return werkzeug.utils.escape(value).replace('\n', '
\n'), [] - - def _html_from_selection(self, cr, uid, model, column, value, context=None): - selection = dict(openerp.osv.fields.selection.reify( - cr, uid, model, column, context=context)) - return werkzeug.utils.escape(selection[value]), [] - - def _html_from_html(self, cr, uid, model, column, value, context=None): - return value, [] - - def _html_from_many2one(self, cr, uid, model, column, value, context=None): - return werkzeug.utils.escape(value.name_get()[0][1]).replace('\n', '
\n'), [] - - def _html_from_binary(self, cr, uid, model, column, value, context=None): - try: - image = Image.open(cStringIO.StringIO(value.decode('base64'))) - except IOError: - raise ValueError("Non-image binary fields can not be converted to HTML") - try: image.verify() - except: # no idea what "suitable exceptions" are - raise ValueError("Invalid image content") - - mime = PIL_MIME_MAPPING.get(image.format) - if mime is None: - raise ValueError("Unknown PIL format %s" % image.format) - - return ('' % (mime, value)), [] - -PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', } diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py new file mode 100644 index 00000000000..897cda37f61 --- /dev/null +++ b/openerp/addons/base/ir/ir_qweb.py @@ -0,0 +1,655 @@ +# -*- coding: utf-8 -*- +import cStringIO +import json +import datetime +import dateutil +import logging +import re +import traceback +import urllib +import xml # FIXME use lxml and etree + +import Image +import werkzeug.utils + +from openerp.osv import osv, orm, fields + +_logger = logging.getLogger(__name__) + +BUILTINS = { + 'False': False, + 'None': None, + 'True': True, + 'abs': abs, + 'bool': bool, + 'dict': dict, + 'filter': filter, + 'len': len, + 'list': list, + 'map': map, + 'max': max, + 'min': min, + 'reduce': reduce, + 'repr': repr, + 'round': round, + 'set': set, + 'str': str, + 'tuple': tuple, + 'quote': urllib.quote, + 'urlencode': urllib.urlencode, + 'datetime': datetime, + # dateutil.relativedelta is an old-style class and cannot be directly + # instanciated wihtin a jinja2 expression, so a lambda "proxy" is + # is needed, apparently. + 'relativedelta': lambda *a, **kw : dateutil.relativedelta.relativedelta(*a, **kw), +} + +## We use a jinja2 sandboxed environment to render qWeb templates. +#from openerp.tools.safe_eval import safe_eval as eval +#from jinja2.sandbox import SandboxedEnvironment +#from jinja2.exceptions import SecurityError, UndefinedError +#UNSAFE = ["browse", "search", "read", "unlink", "read_group"] +#SAFE = ["_name"] + +class QWebContext(dict): + def __init__(self, data, undefined_handler=None, loader=None): + self.loader = loader + self.undefined_handler = undefined_handler + dic = BUILTINS.copy() + dic.update(data) + super(QWebContext, self).__init__(dic) + self['defined'] = lambda key: key in self + + def __getitem__(self, key): + if key in self: + return self.get(key) + elif not self.undefined_handler: + raise NameError("QWeb: name %r is not defined while rendering template %r" % (key, self.get('__template__'))) + else: + return self.get(key, self.undefined_handler(key, self)) + + def safe_eval(self, expr): + # This is too slow, we should cached compiled expressions attribute of + # qweb to will be changed into a model object ir.qweb. + # + # The cache should be on qweb, and qweb context contructor take qweb as + # argument to store the cache. + # + #class QWebSandboxedEnvironment(SandboxedEnvironment): + # def is_safe_attribute(self, obj, attr, value): + # if str(attr) in SAFE: + # res = True + # else: + # res = super(QWebSandboxedEnvironment, self).is_safe_attribute(obj, attr, value) + # if str(attr) in UNSAFE or not res: + # raise SecurityError("access to attribute '%s' of '%s' object is unsafe." % (attr,obj)) + # return res + #env = qWebSandboxedEnvironment(variable_start_string="${", variable_end_string="}") + #env.globals.update(context) + #env.compile_expression(expr)() + return eval(expr, None, self) + + def copy(self): + return QWebContext(dict.copy(self), + undefined_handler=self.undefined_handler, + loader=self.loader) + + def __copy__(self): + return self.copy() + +class QWeb(orm.AbstractModel): + """QWeb Xml templating engine + + The templating engine use a very simple syntax, "magic" xml attributes, to + produce any kind of texutal output (even non-xml). + + QWebXml: + the template engine core implements the basic magic attributes: + + t-att t-raw t-esc t-if t-foreach t-set t-call t-trim + + + - loader: function that return a template + + QWeb rendering can be used for many tasks. As a result, customizations + made by one task context (to either the main qweb rendering or to specific + fields rendering) could break other tasks. + + To avoid that, ``ir.qweb`` was consciously made inheritable and the "root" + of an object hierarchy. If you need extensions or alterations which could + be incompatible with other subsystems, you should create a local object + inheriting from ``ir.qweb`` and customize that. + + If you need to customize t-fields rendering, subclass the ir.qweb.field + model (and its sub-models) then override :meth:`~.get_converter_for` to + fetch the right field converters for your qweb model. + """ + + _name = 'ir.qweb' + + node = xml.dom.Node + _void_elements = frozenset([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', + 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']) + _format_regex = re.compile( + '(?:' + # ruby-style pattern + '#\{(.+?)\}' + ')|(?:' + # jinja-style pattern + '\{\{(.+?)\}\}' + ')') + + def __init__(self, pool, cr): + super(QWeb, self).__init__(pool, cr) + self._t = {} + + self._render_tag = self.prefixed_methods('render_tag_') + self._render_att = self.prefixed_methods('render_att_') + + def prefixed_methods(self, prefix): + """ Extracts all methods prefixed by ``prefix``, and returns a mapping + of (t-name, method) where the t-name is the method name with prefix + removed and underscore converted to dashes + + :param str prefix: + :return: dict + """ + return dict( + (name[len(prefix):].replace('_', '-'), getattr(type(self), name)) + for name in dir(self) + if name.startswith(prefix)) + + def register_tag(self, tag, func): + self._render_tag[tag] = func + + def load_document(self, x): + """ + Loads an XML document and installs any contained template in the engine + """ + if hasattr(x, 'documentElement'): + dom = x + elif x.startswith("%s" % tuple( + v if isinstance(v, str) else v.encode('utf-8') + for v in (name, g_att, inner, name)) + else: + return "<%s%s/>" % (name, g_att) + + # Attributes + def render_att_att(self, e, an, av, v): + if an.startswith("t-attf-"): + att, val = an[7:], self.eval_format(av, v) + elif an.startswith("t-att-"): + att, val = an[6:], self.eval(av, v) + if isinstance(val, unicode): + val = val.encode("utf8") + else: + att, val = self.eval_object(av, v) + return val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " " + + def render_att_href(self, e, an, av, v): + return self.url_for(e, an, av, v) + def render_att_src(self, e, an, av, v): + return self.url_for(e, an, av, v) + def render_att_action(self, e, an, av, v): + return self.url_for(e, an, av, v) + def url_for(self, e, an, av, v): + if 'url_for' not in v: + raise KeyError("qweb: no 'url_for' found in context") + # Temporary implementation of t-keep-query until qweb py v2 + keep_query = e.attributes.get('t-keep-query') + if keep_query: + params = self.eval_format(keep_query.value, v) + keep_query = [q.strip() for q in params.split(',')] + path = str(v['url_for'](self.eval_format(av, v), keep_query=keep_query)) + return ' %s="%s"' % (an[2:], werkzeug.utils.escape(path)) + + # Tags + def render_tag_raw(self, e, t_att, g_att, v): + inner = self.eval_str(t_att["raw"], v) + return self.render_element(e, t_att, g_att, v, inner) + + def render_tag_rawf(self, e, t_att, g_att, v): + inner = self.eval_format(t_att["rawf"], v) + return self.render_element(e, t_att, g_att, v, inner) + + def render_tag_esc(self, e, t_att, g_att, v): + inner = werkzeug.utils.escape(self.eval_str(t_att["esc"], v)) + return self.render_element(e, t_att, g_att, v, inner) + + def render_tag_escf(self, e, t_att, g_att, v): + inner = werkzeug.utils.escape(self.eval_format(t_att["escf"], v)) + return self.render_element(e, t_att, g_att, v, inner) + + def render_tag_foreach(self, e, t_att, g_att, v): + expr = t_att["foreach"] + enum = self.eval_object(expr, v) + if enum is not None: + var = t_att.get('as', expr).replace('.', '_') + d = v.copy() + size = -1 + if isinstance(enum, (list, tuple)): + size = len(enum) + elif hasattr(enum, 'count'): + size = enum.count() + d["%s_size" % var] = size + d["%s_all" % var] = enum + index = 0 + ru = [] + for i in enum: + d["%s_value" % var] = i + d["%s_index" % var] = index + d["%s_first" % var] = index == 0 + d["%s_even" % var] = index % 2 + d["%s_odd" % var] = (index + 1) % 2 + d["%s_last" % var] = index + 1 == size + if index % 2: + d["%s_parity" % var] = 'odd' + else: + d["%s_parity" % var] = 'even' + if 'as' in t_att: + d[var] = i + elif isinstance(i, dict): + d.update(i) + ru.append(self.render_element(e, t_att, g_att, d)) + index += 1 + return "".join(ru) + else: + raise NameError("QWeb: foreach enumerator %r is not defined while rendering template %r" % (expr, v.get('__template__'))) + + def render_tag_if(self, e, t_att, g_att, v): + if self.eval_bool(t_att["if"], v): + return self.render_element(e, t_att, g_att, v) + else: + return "" + + def render_tag_call(self, e, t_att, g_att, v): + d = v if 'import' in t_att else v.copy() + d[0] = self.render_element(e, t_att, g_att, d) + + return self.render(self.eval_format(t_att["call"], d), d) + + def render_tag_set(self, e, t_att, g_att, v): + if "value" in t_att: + v[t_att["set"]] = self.eval_object(t_att["value"], v) + elif "valuef" in t_att: + v[t_att["set"]] = self.eval_format(t_att["valuef"], v) + else: + v[t_att["set"]] = self.render_element(e, t_att, g_att, v) + return "" + + def render_tag_field(self, e, t_att, g_att, v): + """ eg: +1 555 555 8069""" + node_name = e.nodeName + assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td", + "ol", "ul", "ol", "dl", "dt", "dd"),\ + "RTE widgets do not work correctly on %r elements" % node_name + assert node_name != 't',\ + "t-field can not be used on a t element, provide an actual HTML node" + + record, field_name = t_att["field"].rsplit('.', 1) + record = self.eval_object(record, v) + + column = record._model._all_columns[field_name].column + options = json.loads(t_att.get('field-options') or '{}') + field_type = get_field_type(column, options) + + converter = self.get_converter_for(field_type) + + return converter.to_html(record._cr, record._uid, field_name, record, options, + e, t_att, g_att, v) + + def get_converter_for(self, field_type): + return self.pool.get('ir.qweb.field.' + field_type, + self.pool['ir.qweb.field']) + + +class FieldConverter(osv.AbstractModel): + """ Used to convert a t-field specification into an output HTML field. + + :meth:`~.to_html` is the entry point of this conversion from QWeb, it: + + * converts the record value to html using :meth:`~.record_to_html` + * generates the metadata attributes (``data-oe-``) to set on the root + result node + * generates the root result node itself through :meth:`~.render_element` + """ + _name = 'ir.qweb.field' + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context): + """ + Generates the metadata attributes (prefixed by ``data-oe-`` for the + root node of the field conversion. Attribute values are escaped by the + parent using ``werkzeug.utils.escape``. + + The default attributes are: + + * ``model``, the name of the record's model + * ``id`` the id of the record to which the field belongs + * ``field`` the name of the converted field + * ``type`` the logical field type (widget, may not match the column's + ``type``, may not be any _column subclass name) + * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the + column is translatable + * ``expression``, the original expression + + :returns: iterable of (attribute name, attribute value) pairs. + """ + column = record._model._all_columns[field_name].column + field_type = get_field_type(column, options) + return [ + ('data-oe-model', record._model._name), + ('data-oe-id', record.id), + ('data-oe-field', field_name), + ('data-oe-type', field_type), + ('data-oe-expression', t_att['field']), + ] + + def value_to_html(self, cr, uid, value, column, options=None): + """ Converts a single value to its HTML version/output + """ + return werkzeug.utils.escape(value) + + def record_to_html(self, cr, uid, field_name, record, column, options=None): + """ Converts the specified field of the browse_record ``record`` to + HTML + """ + return self.value_to_html( + cr, uid, record[field_name], column, options=None) + + def to_html(self, cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context): + """ Converts a ``t-field`` to its HTML output. A ``t-field`` may be + extended by a ``t-field-options``, which is a JSON-serialized mapping + of configuration values. + + A default configuration key is ``widget`` which can override the + field's own ``_type``. + """ + content = None + try: + content = self.record_to_html( + cr, uid, field_name, record, + record._model._all_columns[field_name].column, + options) + except Exception: + _logger.warning("Could not get field %s for model %s", + field_name, record._model._name, exc_info=True) + + g_att += ''.join( + ' %s="%s"' % (name, werkzeug.utils.escape(value)) + for name, value in self.attributes( + cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context) + ) + + return self.render_element(cr, uid, source_element, t_att, g_att, + qweb_context, content) + + def qweb_object(self): + return self.pool['ir.qweb'] + + def render_element(self, cr, uid, source_element, t_att, g_att, + qweb_context, content): + """ Final rendering hook, by default just calls ir.qweb's ``render_element`` + """ + return self.qweb_object().render_element( + source_element, t_att, g_att, qweb_context, content or '') + +class FloatConverter(osv.AbstractModel): + _name = 'ir.qweb.field.float' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + width, precision = column.digits or (None, None) + fmt = '{value}' if precision is None else '{value:.{precision}f}' + + return werkzeug.utils.escape( + fmt.format(value=value, width=width, precision=precision, )) + +class TextConverter(osv.AbstractModel): + _name = 'ir.qweb.field.text' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + """ + Escapes the value and converts newlines to br. This is bullshit. + """ + return werkzeug.utils.escape(value).replace('\n', '
\n') + +class SelectionConverter(osv.AbstractModel): + _name = 'ir.qweb.field.selection' + _inherit = 'ir.qweb.field' + + def record_to_html(self, cr, uid, field_name, record, column, options=None): + # FIXME: context + value = record[field_name] + selection = dict(fields.selection.reify( + cr, uid, record._model, column)) + return self.value_to_html( + cr, uid, selection[value], column, options=options) + +class ManyToOneConverter(osv.AbstractModel): + _name = 'ir.qweb.field.many2one' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + # value may be a browse_null + if not value: return '' + return werkzeug.utils.escape(value.name_get()[0][1]).replace('\n', '
\n') + +class HTMLConverter(osv.AbstractModel): + _name = 'ir.qweb.field.html' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + return value + +class ImageConverter(osv.AbstractModel): + """ ``image`` widget rendering, inserts a data:uri-using image tag in the + document. May be overridden by e.g. the website module to generate links + instead. + + .. todo:: what happens if different output need different converters? e.g. + reports may need embedded images or FS links whereas website + needs website-aware + """ + _name = 'ir.qweb.field.image' + _inherit = 'ir.qweb.field' + + def value_to_html(self, cr, uid, value, column, options=None): + try: + image = Image.open(cStringIO.StringIO(value.decode('base64'))) + image.verify() + except IOError: + raise ValueError("Non-image binary fields can not be converted to HTML") + except: # image.verify() throws "suitable exceptions", I have no idea what they are + raise ValueError("Invalid image content") + + return '' % (Image.MIME[image.format], value) + +class MonetaryConverter(osv.AbstractModel): + """ ``monetary`` converter, has a mandatory option + ``display_currency``. + + .. note:: the monetary converter internally adds the qweb context to its + options mapping, so that the context is available to callees. + It's set under the ``_qweb_context`` key. + """ + _name = 'ir.qweb.field.monetary' + _inherit = 'ir.qweb.field' + + def to_html(self, cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context): + options['_qweb_context'] = qweb_context + return super(MonetaryConverter, self).to_html( + cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context) + + def record_to_html(self, cr, uid, field_name, record, column, options): + Currency = self.pool['res.currency'] + display = self.display_currency(cr, uid, options) + + symbol_pre = symbol_post = space_pre = space_post = u'' + if display.position == 'before': + space_pre = u' ' + symbol_pre = display.symbol + else: + space_post = u' ' + symbol_post = display.symbol + + return u'{symbol_pre}{space_pre}' \ + u'{0}' \ + u'{space_post}{symbol_post}'.format( + Currency.round(cr, uid, display, record[field_name]), + space_pre=space_pre, + symbol_pre=symbol_pre, + space_post=space_post, + symbol_post=symbol_post,) + + def display_currency(self, cr, uid, options): + return self.qweb_object().eval_object( + options['display_currency'], options['_qweb_context']) + +def get_field_type(column, options): + """ Gets a t-field's effective type from the field's column and its options + """ + return options.get('widget', column._type) + +# vim:et: diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index b77eaf38631..6fa609826f4 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -27,6 +27,7 @@ import os import time import HTMLParser +from lxml import etree import openerp from openerp import tools @@ -34,7 +35,7 @@ from openerp.osv import fields, osv, orm from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.view_validation import valid_view -from openerp.tools import misc, qweb +from openerp.tools import misc from openerp.osv.orm import browse_record, browse_record_list _logger = logging.getLogger(__name__) @@ -766,7 +767,7 @@ class view(osv.osv): self.translate_qweb(cr, uid, id_, node, lang, context) return arch - def render(self, cr, uid, id_or_xml_id, values, context=None): + def render(self, cr, uid, id_or_xml_id, values, engine='ir.qweb', context=None): if not context: context = {} @@ -783,8 +784,10 @@ class view(osv.osv): def loader(name): return self.read_template(cr, uid, name, context=context) - engine = qweb.QWebXml(loader=loader, undefined_handler=lambda key, v: None) - return engine.render(id_or_xml_id, values) + + return self.pool[engine].render( + id_or_xml_id, values, + loader=loader, undefined_handler=lambda key, v: None) # maybe used to print the workflow ? diff --git a/openerp/addons/base/res/__init__.py b/openerp/addons/base/res/__init__.py index 50807e20116..24bded0cc7f 100644 --- a/openerp/addons/base/res/__init__.py +++ b/openerp/addons/base/res/__init__.py @@ -25,6 +25,7 @@ import res_partner import res_bank import res_config import res_currency +import res_font import res_company import res_users import res_request diff --git a/openerp/addons/base/res/res_company.py b/openerp/addons/base/res/res_company.py index d61963dbce4..6e74dbf1547 100644 --- a/openerp/addons/base/res/res_company.py +++ b/openerp/addons/base/res/res_company.py @@ -112,9 +112,6 @@ class res_company(osv.osv): def _get_companies_from_partner(self, cr, uid, ids, context=None): return self.pool['res.company'].search(cr, uid, [('partner_id', 'in', ids)], context=context) - - def _get_font(self, cr, uid, context=None): - return sorted(customfonts.RegisterCustomFonts()) _columns = { 'name': fields.related('partner_id', 'name', string='Company Name', size=128, required=True, store=True, type='char'), @@ -128,7 +125,7 @@ class res_company(osv.osv): 'rml_footer': fields.text('Report Footer', help="Footer text displayed at the bottom of all reports."), 'rml_footer_readonly': fields.related('rml_footer', type='text', string='Report Footer', readonly=True), 'custom_footer': fields.boolean('Custom Footer', help="Check this to define the report footer manually. Otherwise it will be filled in automatically."), - 'font': fields.selection(_get_font, "Font",help="Set the font into the report header, will be used for every RML report of the company"), + 'font': fields.many2one('res.font', string="Font",help="Set the font into the report header, it will be used as default font in the RML reports of the user company"), 'logo': fields.related('partner_id', 'image', string="Logo", type="binary"), 'logo_web': fields.function(_get_logo_web, string="Logo Web", type="binary", store={ 'res.company': (lambda s, c, u, i, x: i, ['partner_id'], 10), @@ -192,10 +189,11 @@ class res_company(osv.osv): default_para = re.sub('fontName.?=.?".*"', 'fontName="%s"'% font,header) return re.sub('("%s"\g<3>'% font,default_para) + fontname = self.pool.get('res.font').browse(cr, uid, font, context=context).name return {'value':{ - 'rml_header': _change_header(rml_header,font), - 'rml_header2':_change_header(rml_header2,font), - 'rml_header3':_change_header(rml_header3,font) + 'rml_header': _change_header(rml_header,fontname), + 'rml_header2':_change_header(rml_header2,fontname), + 'rml_header3':_change_header(rml_header3,fontname) }} def on_change_country(self, cr, uid, ids, country_id, context=None): @@ -294,6 +292,15 @@ class res_company(osv.osv): def _get_logo(self, cr, uid, ids): return open(os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb') .read().encode('base64') + def _get_font(self, cr, uid, ids): + font_obj = self.pool.get('res.font') + res = font_obj.search(cr, uid, [('name', '=', 'Helvetica')], limit=1) + if res: + return res[0] + + font_obj.init_no_scan(cr, uid) + return font_obj.search(cr, uid, [('name', '=', 'Helvetica')], limit=1)[0] + _header = """
@@ -388,6 +395,9 @@ class res_company(osv.osv): return {'value': {'rml_header': self._header_letter}} return {'value': {'rml_header': self._header_a4}} + def act_discover_fonts(self, cr, uid, ids, context=None): + return self.pool.get("res.font").discover_fonts(cr, uid, ids, context) + _defaults = { 'currency_id': _get_euro, 'paper_format': 'a4', @@ -395,12 +405,11 @@ class res_company(osv.osv): 'rml_header2': _header2, 'rml_header3': _header3, 'logo':_get_logo, - 'font':'Helvetica', + 'font':_get_font, } _constraints = [ (osv.osv._check_recursion, 'Error! You can not create recursive companies.', ['parent_id']) ] - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/res/res_company_view.xml b/openerp/addons/base/res/res_company_view.xml index e9acf7940b3..d0f35223bba 100644 --- a/openerp/addons/base/res/res_company_view.xml +++ b/openerp/addons/base/res/res_company_view.xml @@ -80,7 +80,15 @@ - + + + diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index a496a1eccdf..61128a5ce99 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -40,20 +40,21 @@ class res_currency(osv.osv): if context is None: context = {} res = {} - if 'date' in context: - date = context['date'] - else: - date = time.strftime('%Y-%m-%d') - date = date or time.strftime('%Y-%m-%d') + + date = context.get('date') or time.strftime('%Y-%m-%d') # Convert False values to None ... currency_rate_type = context.get('currency_rate_type_id') or None # ... and use 'is NULL' instead of '= some-id'. operator = '=' if currency_rate_type else 'is' for id in ids: - cr.execute("SELECT currency_id, rate FROM res_currency_rate WHERE currency_id = %s AND name <= %s AND currency_rate_type_id " + operator +" %s ORDER BY name desc LIMIT 1" ,(id, date, currency_rate_type)) + cr.execute('SELECT rate FROM res_currency_rate ' + 'WHERE currency_id = %s ' + 'AND name <= %s ' + 'AND currency_rate_type_id ' + operator + ' %s ' + 'ORDER BY name desc LIMIT 1', + (id, date, currency_rate_type)) if cr.rowcount: - id, rate = cr.fetchall()[0] - res[id] = rate + res[id] = cr.fetchone()[0] elif not raise_on_no_rate: res[id] = 0 else: diff --git a/openerp/addons/base/res/res_font.py b/openerp/addons/base/res/res_font.py new file mode 100644 index 00000000000..4218b0b7955 --- /dev/null +++ b/openerp/addons/base/res/res_font.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from reportlab.pdfbase import ttfonts +from openerp.osv import fields, osv +from openerp.report.render.rml2pdf import customfonts + +import logging + +"""This module allows the mapping of some system-available TTF fonts to +the reportlab engine. + +This file could be customized per distro (although most Linux/Unix ones) +should have the same filenames, only need the code below). + +Due to an awful configuration that ships with reportlab at many Linux +and Ubuntu distros, we have to override the search path, too. +""" +_logger = logging.getLogger(__name__) + + +class res_font(osv.Model): + _name = "res.font" + _description = 'Fonts available' + _order = 'name' + + _columns = { + 'name': fields.char("Name", required=True), + } + + _sql_constraints = [ + ('name_font_uniq', 'unique(name)', 'You can not register two fonts with the same name'), + ] + + def discover_fonts(self, cr, uid, ids, context=None): + """Scan fonts on the file system, add them to the list of known fonts + and create font object for the new ones""" + customfonts.CustomTTFonts = customfonts.BaseCustomTTFonts + + found_fonts = {} + for font_path in customfonts.list_all_sysfonts(): + try: + font = ttfonts.TTFontFile(font_path) + _logger.debug("Found font %s at %s", font.name, font_path) + if not found_fonts.get(font.familyName): + found_fonts[font.familyName] = {'name': font.familyName} + + mode = font.styleName.lower().replace(" ", "") + + customfonts.CustomTTFonts.append((font.familyName, font.name, font_path, mode)) + except ttfonts.TTFError: + _logger.warning("Could not register Font %s", font_path) + + # add default PDF fonts + for family in customfonts.BasePDFFonts: + if not found_fonts.get(family): + found_fonts[family] = {'name': family} + + # remove deleted fonts + existing_font_ids = self.search(cr, uid, [], context=context) + existing_font_names = [] + for font in self.browse(cr, uid, existing_font_ids): + existing_font_names.append(font.name) + if font.name not in found_fonts.keys(): + self.unlink(cr, uid, font.id, context=context) + + # add unknown fonts + for family, vals in found_fonts.items(): + if family not in existing_font_names: + self.create(cr, uid, vals, context=context) + return True + + def init_no_scan(self, cr, uid, context=None): + """Add demo data for PDF fonts without scan (faster for db creation)""" + for font in customfonts.BasePDFFonts: + if not self.search(cr, uid, [('name', '=', font)], context=context): + self.create(cr, uid, {'name':font}, context=context) + return True \ No newline at end of file diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index a9266504f74..1401b188e0a 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -111,4 +111,12 @@ "access_ir_mail_server","ir_mail_server","model_ir_mail_server","group_system",1,1,1,1 "access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0 "access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1 - +access_ir_qweb,access_ir_qweb,model_ir_qweb,,0,0,0,0 +access_ir_qweb_field,access_ir_qweb_field,model_ir_qweb_field,,0,0,0,0 +access_ir_qweb_field_float,access_ir_qweb_field_float,model_ir_qweb_field_float,,0,0,0,0 +access_ir_qweb_field_text,access_ir_qweb_field_text,model_ir_qweb_field_text,,0,0,0,0 +access_ir_qweb_field_selection,access_ir_qweb_field_selection,model_ir_qweb_field_selection,,0,0,0,0 +access_ir_qweb_field_many2one,access_ir_qweb_field_many2one,model_ir_qweb_field_many2one,,0,0,0,0 +access_ir_qweb_field_html,access_ir_qweb_field_html,model_ir_qweb_field_html,,0,0,0,0 +access_ir_qweb_field_image,access_ir_qweb_field_image,model_ir_qweb_field_image,,0,0,0,0 +access_ir_qweb_field_monetary,access_ir_qweb_field_monetary,model_ir_qweb_field_monetary,,0,0,0,0 diff --git a/openerp/report/render/rml2pdf/customfonts.py b/openerp/report/render/rml2pdf/customfonts.py index ede2d91aaed..7f89f324bf7 100644 --- a/openerp/report/render/rml2pdf/customfonts.py +++ b/openerp/report/render/rml2pdf/customfonts.py @@ -37,18 +37,30 @@ should have the same filenames, only need the code below). Due to an awful configuration that ships with reportlab at many Linux and Ubuntu distros, we have to override the search path, too. """ -_fonts_cache = {'registered_fonts': [], 'total_system_fonts': 0} _logger = logging.getLogger(__name__) -# Basic fonts familly included in PDF standart, will always be in the font list +# Basic fonts family included in PDF standart, will always be in the font list BasePDFFonts = [ - ('Helvetica', 'Helvetica'), - ('Times', 'Times'), - ('Courier', 'Courier'), + 'Helvetica', + 'Times', + 'Courier' ] # List of fonts found on the disk -CustomTTFonts = [] +CustomTTFonts = BaseCustomTTFonts = [ ('Helvetica', "DejaVu Sans", "DejaVuSans.ttf", 'normal'), + ('Helvetica', "DejaVu Sans Bold", "DejaVuSans-Bold.ttf", 'bold'), + ('Helvetica', "DejaVu Sans Oblique", "DejaVuSans-Oblique.ttf", 'italic'), + ('Helvetica', "DejaVu Sans BoldOblique", "DejaVuSans-BoldOblique.ttf", 'bolditalic'), + ('Times', "Liberation Serif", "LiberationSerif-Regular.ttf", 'normal'), + ('Times', "Liberation Serif Bold", "LiberationSerif-Bold.ttf", 'bold'), + ('Times', "Liberation Serif Italic", "LiberationSerif-Italic.ttf", 'italic'), + ('Times', "Liberation Serif BoldItalic", "LiberationSerif-BoldItalic.ttf", 'bolditalic'), + ('Courier', "FreeMono", "FreeMono.ttf", 'normal'), + ('Courier', "FreeMono Bold", "FreeMonoBold.ttf", 'bold'), + ('Courier', "FreeMono Oblique", "FreeMonoOblique.ttf", 'italic'), + ('Courier', "FreeMono BoldOblique", "FreeMonoBoldOblique.ttf", 'bolditalic'), +] + # Search path for TTF files, in addition of rl_config.TTFSearchPath TTFSearchPath = [ @@ -73,7 +85,7 @@ TTFSearchPath = [ 'c:/windows/fonts' ] -def all_sysfonts_list(): +def list_all_sysfonts(): """ This function returns list of font directories of system. """ @@ -89,76 +101,6 @@ def all_sysfonts_list(): filepath.append(os.path.join(dirname, filename)) return filepath -def init_new_font(familyName, name, font_dir): - return { - 'regular':(familyName, name, font_dir, 'regular'), - 'italic':(), - 'bold':(familyName, name, font_dir, 'bold'), - 'bolditalic':(), - } - -def RegisterCustomFonts(): - """ - This function prepares a list for all system fonts to be registered - in reportlab and returns the updated list with new fonts. - """ - global CustomTTFonts - all_system_fonts = all_sysfonts_list() - if len(all_system_fonts) != _fonts_cache['total_system_fonts']: - font_modes, last_family, registered_font_list, _fonts_cache['registered_fonts'] = {}, "", [], list(BasePDFFonts) - - #Prepares a list of registered fonts. Remove such fonts those don't have cmap for Unicode. - for dirname in all_system_fonts: - try: - font_info = ttfonts.TTFontFile(dirname) - if font_info.styleName in ('Regular','Normal','Book','Medium', 'Roman'): - _fonts_cache['registered_fonts'].append((font_info.name, font_info.fullName)) - registered_font_list.append((font_info.familyName, font_info.name, dirname, font_info.styleName.lower().replace(" ", ""))) - _logger.debug("Found font %s at %s", font_info.name, dirname) - except: - _logger.warning("Could not register Font %s", dirname) - - #Prepare font list for mapping.Each font family requires four type of modes(regular,bold,italic,bolditalic). - #If all modes are not found, dummy entries are made for remaining modes. - for familyName, name, font_dir, mode in sorted(registered_font_list): - if not last_family or not font_modes: - last_family = familyName - font_modes = init_new_font(familyName, name, font_dir) - - if last_family != familyName: - # new font familly, adding previous to the list of fonts - if not font_modes['italic']: - font_modes['italic'] = font_modes['regular'][:3]+('italic',) - if not font_modes['bolditalic']: - font_modes['bolditalic'] = font_modes['bold'][:3]+('bolditalic',) - CustomTTFonts.extend(font_modes.values()) - font_modes = init_new_font(familyName, name, font_dir) - - if (mode== 'normal') or (mode == 'regular') or (mode == 'medium') or (mode == 'book') or (mode == 'roman'): - font_modes['regular'] = (familyName, name, font_dir, 'regular') - elif (mode == 'italic') or (mode == 'oblique'): - font_modes['italic'] = (familyName, name, font_dir, 'italic') - elif mode == 'bold': - font_modes['bold'] = (familyName, name, font_dir, 'bold') - elif (mode == 'bolditalic') or (mode == 'boldoblique'): - font_modes['bolditalic'] = (familyName, name, font_dir, 'bolditalic') - last_family = familyName - - # add the last one - if font_modes: - if not font_modes['italic']: - font_modes['italic'] = font_modes['regular'][:3]+('italic',) - if not font_modes['bolditalic']: - font_modes['bolditalic'] = font_modes['bold'][:3]+('bolditalic',) - CustomTTFonts.extend(font_modes.values()) - - _fonts_cache['total_system_fonts'] = len(all_system_fonts) - - # remove duplicates - CustomTTFonts = sorted(list(set(CustomTTFonts))) - _fonts_cache['registered_fonts'] = sorted(list(set(_fonts_cache['registered_fonts']))) - return _fonts_cache['registered_fonts'] - def SetCustomFonts(rmldoc): """ Map some font names to the corresponding TTF fonts @@ -167,8 +109,6 @@ def SetCustomFonts(rmldoc): This function is called once per report, so it should avoid system-wide processing (cache it, instead). """ - if not _fonts_cache['registered_fonts']: - RegisterCustomFonts() for name, font, filename, mode in CustomTTFonts: if os.path.isabs(filename) and os.path.exists(filename): rmldoc.setTTFontMapping(name, font, filename, mode) diff --git a/openerp/service/server.py b/openerp/service/server.py index 7fa6dae0015..19457341b04 100644 --- a/openerp/service/server.py +++ b/openerp/service/server.py @@ -3,7 +3,6 @@ #----------------------------------------------------------- import datetime import errno -import fcntl import logging import os import os.path @@ -21,6 +20,11 @@ import time import traceback import werkzeug.serving + +try: + import fcntl +except ImportError: + pass try: from setproctitle import setproctitle except ImportError: diff --git a/openerp/tests/addons/test_converter/ir.model.access.csv b/openerp/tests/addons/test_converter/ir.model.access.csv index 66937821c94..ff4ac975996 100644 --- a/openerp/tests/addons/test_converter/ir.model.access.csv +++ b/openerp/tests/addons/test_converter/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_converter_model,access_converter_model,model_test_converter_test_model,,1,1,1,1 access_test_converter_test_model_sub,access_test_converter_test_model_sub,model_test_converter_test_model_sub,,1,1,1,1 +access_test_converter_monetary,access_test_converter_monetary,model_test_converter_monetary,,1,1,1,1 diff --git a/openerp/tests/addons/test_converter/models.py b/openerp/tests/addons/test_converter/models.py index 9be3c9d675e..9074ba8178f 100644 --- a/openerp/tests/addons/test_converter/models.py +++ b/openerp/tests/addons/test_converter/models.py @@ -36,3 +36,11 @@ class test_model_sub(orm.Model): _columns = { 'name': fields.char() } + + +class test_model_monetary(orm.Model): + _name = 'test_converter.monetary' + + _columns = { + 'value': fields.float(digits=(16, 55)), + } diff --git a/openerp/tests/addons/test_converter/tests/test_html.py b/openerp/tests/addons/test_converter/tests/test_html.py index b4ad5c35679..d433c814958 100644 --- a/openerp/tests/addons/test_converter/tests/test_html.py +++ b/openerp/tests/addons/test_converter/tests/test_html.py @@ -1,77 +1,169 @@ # -*- encoding: utf-8 -*- -import base64 +import json import os +import xml.dom.minidom from openerp.tests import common +from openerp.addons.base.ir import ir_qweb directory = os.path.dirname(__file__) -class TestHTMLExport(common.TransactionCase): +impl = xml.dom.minidom.getDOMImplementation() +doc = impl.createDocument(None, None, None) + +class TestExport(common.TransactionCase): + _model = None + def setUp(self): - super(TestHTMLExport, self).setUp() - self.Converter = self.registry('ir.fields.converter') - self.Model = self.registry('test_converter.test_model') + super(TestExport, self).setUp() + self.Model = self.registry(self._model) + self.columns = self.Model._all_columns def get_column(self, name): return self.Model._all_columns[name].column def get_converter(self, name): - return self.Converter.from_field( - self.cr, self.uid, self.Model, self.get_column(name), - totype="html") + column = self.get_column(name) + try: + model = self.registry('ir.qweb.field.' + column._type) + except KeyError: + model = self.registry('ir.qweb.field') + return lambda value: model.value_to_html( + self.cr, self.uid, value, column) + +class TestBasicExport(TestExport): + _model = 'test_converter.test_model' + +class TestCharExport(TestBasicExport): def test_char(self): converter = self.get_converter('char') - value, warnings = converter('foo') + value = converter('foo') self.assertEqual(value, 'foo') - self.assertEqual(warnings, []) - value, warnings = converter("foo") + value = converter("foo") self.assertEqual(value, "foo<bar>") - self.assertEqual(warnings, []) +class TestIntegerExport(TestBasicExport): def test_integer(self): converter = self.get_converter('integer') - value, warnings = converter(42) + value = converter(42) self.assertEqual(value, "42") - self.assertEqual(warnings, []) +class TestFloatExport(TestBasicExport): def test_float(self): converter = self.get_converter('float') - value, warnings = converter(42.0) + value = converter(42.0) self.assertEqual(value, "42.0") - self.assertEqual(warnings, []) - value, warnings = converter(42.0100) + value = converter(42.0100) self.assertEqual(value, "42.01") - self.assertEqual(warnings, []) - value, warnings = converter(42.01234) + value = converter(42.01234) self.assertEqual(value, "42.01234") - self.assertEqual(warnings, []) def test_numeric(self): converter = self.get_converter('numeric') - value, warnings = converter(42.0) + value = converter(42.0) self.assertEqual(value, '42.00') - self.assertEqual(warnings, []) - value, warnings = converter(42.01234) + value = converter(42.01234) self.assertEqual(value, '42.01') - self.assertEqual(warnings, []) +class TestCurrencyExport(TestExport): + _model = 'test_converter.monetary' + + def setUp(self): + super(TestCurrencyExport, self).setUp() + self.Currency = self.registry('res.currency') + self.base = self.create(self.Currency, name="Source", symbol=u'source') + + def create(self, model, context=None, **values): + return model.browse( + self.cr, self.uid, + model.create(self.cr, self.uid, values, context=context), + context=context) + + def convert(self, obj, dest): + converter = self.registry('ir.qweb.field.monetary') + options = { + 'widget': 'monetary', + 'display_currency': 'c2' + } + converted = converter.to_html( + self.cr, self.uid, 'value', obj, options, + doc.createElement('span'), + {'field': 'obj.value', 'field-options': json.dumps(options)}, + '', ir_qweb.QWebContext({'obj': obj, 'c2': dest, })) + return converted + + def test_currency_post(self): + currency = self.create(self.Currency, name="Test", symbol=u"test") + obj = self.create(self.Model, value=0.12) + + converted = self.convert(obj, dest=currency) + + self.assertEqual( + converted, + '' + '0.12' + ' {symbol}'.format( + obj=obj, + symbol=currency.symbol.encode('utf-8') + ),) + + def test_currency_pre(self): + currency = self.create( + self.Currency, name="Test", symbol=u"test", position='before') + obj = self.create(self.Model, value=0.12) + + converted = self.convert(obj, dest=currency) + + self.assertEqual( + converted, + '' + '{symbol} ' + '0.12' + ''.format( + obj=obj, + symbol=currency.symbol.encode('utf-8') + ),) + + def test_currency_precision(self): + """ Precision should be the currency's, not the float field's + """ + currency = self.create(self.Currency, name="Test", symbol=u"test",) + obj = self.create(self.Model, value=0.1234567) + + converted = self.convert(obj, dest=currency) + + self.assertEqual( + converted, + '' + '0.12' + ' {symbol}'.format( + obj=obj, + symbol=currency.symbol.encode('utf-8') + ),) + +class TestTextExport(TestBasicExport): def test_text(self): converter = self.get_converter('text') - value, warnings = converter("This is my text-kai") + value = converter("This is my text-kai") self.assertEqual(value, "This is my text-kai") - self.assertEqual(warnings, []) - value, warnings = converter(""" + value = converter(""" . The current line (address) in the buffer. $ The last line in the buffer. n The nth, line in the buffer where n is a number in the range [0,$]. @@ -89,9 +181,8 @@ class TestHTMLExport(common.TransactionCase): -n The nth previous line, where n is a non-negative number.
+ The next line. This is equivalent to +1 and may be repeated with cumulative effect.
""") - self.assertEqual(warnings, []) - value, warnings = converter(""" + value = converter(""" fgdkls;hjas;lj fdslkj d;lasjfa lkdja lfks fldkjsfhs fldskjh """) @@ -99,65 +190,78 @@ class TestHTMLExport(common.TransactionCase): fgdkls;hjas;lj <b>fdslkj</b> d;lasjfa lkdja <a href=http://spam.com>lfks</a>
fldkjsfhs <i style="color: red"><a href="http://spamspam.com">fldskjh</a></i>
""") - self.assertEqual(warnings, []) +class TestMany2OneExport(TestBasicExport): def test_many2one(self): converter = self.get_converter('many2one') Sub = self.registry('test_converter.test_model.sub') id0 = Sub.create(self.cr, self.uid, {'name': "Foo"}) - value, warnings = converter(Sub.browse(self.cr, self.uid, id0)) + value = converter(Sub.browse(self.cr, self.uid, id0)) self.assertEqual(value, "Foo") - self.assertEqual(warnings, []) id1 = Sub.create(self.cr, self.uid, {'name': "Foo"}) - value, warnings = converter(Sub.browse(self.cr, self.uid, id1)) + value = converter(Sub.browse(self.cr, self.uid, id1)) self.assertEqual(value, "Fo<b>o</b>") - self.assertEqual(warnings, []) - def test_binary(self): - converter = self.get_converter('binary') +class TestBinaryExport(TestBasicExport): + def test_image(self): + column = self.get_column('binary') + converter = self.registry('ir.qweb.field.image') + with open(os.path.join(directory, 'test_vectors', 'image'), 'rb') as f: content = f.read() - value, warnings = converter(content.encode('base64')) - self.assertEqual( - value, '' % ( - content.encode('base64') - )) - self.assertEqual(warnings, []) + encoded_content = content.encode('base64') + value = converter.value_to_html( + self.cr, self.uid, encoded_content, column) + self.assertEqual( + value, '' % ( + encoded_content + )) with open(os.path.join(directory, 'test_vectors', 'pdf'), 'rb') as f: content = f.read() - with self.assertRaises(ValueError): - converter(content.encode('base64')) + with self.assertRaises(ValueError): + converter.value_to_html( + self.cr, self.uid, 'binary', content.encode('base64'), column) with open(os.path.join(directory, 'test_vectors', 'pptx'), 'rb') as f: content = f.read() - with self.assertRaises(ValueError): - converter(content.encode('base64')) + with self.assertRaises(ValueError): + converter.value_to_html( + self.cr, self.uid, 'binary', content.encode('base64'), column) +class TestSelectionExport(TestBasicExport): def test_selection(self): - converter = self.get_converter('selection') + [record] = self.Model.browse(self.cr, self.uid, [self.Model.create(self.cr, self.uid, { + 'selection': 2, + 'selection_str': 'C', + })]) - value, warnings = converter(2) + column_name = 'selection' + column = self.get_column(column_name) + converter = self.registry('ir.qweb.field.selection') + + value = converter.record_to_html( + self.cr, self.uid, column_name, record, column) self.assertEqual(value, "réponse B") - self.assertEqual(warnings, []) - converter = self.get_converter('selection_str') - - value, warnings = converter('C') + column_name = 'selection_str' + column = self.get_column(column_name) + value = converter.record_to_html( + self.cr, self.uid, column_name, record, column) self.assertEqual(value, "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?") - self.assertEqual(warnings, []) +class TestHTMLExport(TestBasicExport): def test_html(self): converter = self.get_converter('html') input = 'span' - value, warnings = converter(input) + value = converter(input) self.assertEqual(value, input) - self.assertEqual(warnings, []) - # o2m, m2m? - # reference? + +# o2m, m2m? +# reference? diff --git a/openerp/tests/test_qweb.py b/openerp/tests/test_qweb.py index 96ee0cf74af..662946c175e 100644 --- a/openerp/tests/test_qweb.py +++ b/openerp/tests/test_qweb.py @@ -1,26 +1,18 @@ # -*- coding: utf-8 -*- import cgi -from collections import namedtuple from xml.dom import minidom as dom import common - -from ..tools import qweb +from openerp.addons.base.ir import ir_qweb +import openerp.addons.base.ir.ir_qweb impl = dom.getDOMImplementation() document = impl.createDocument(None, None, None) -Request = namedtuple('Request', 'cr uid registry') -class RegistryProxy(object): - def __init__(self, func): - self.func = func - def __getitem__(self, name): - return self.func(name) - class TestQWebTField(common.TransactionCase): def setUp(self): super(TestQWebTField, self).setUp() - self.engine = qweb.QWebXml() + self.engine = self.registry('ir.qweb') def test_trivial(self): field = document.createElement('span') @@ -32,16 +24,14 @@ class TestQWebTField(common.TransactionCase): }) root_company = Companies.browse(self.cr, self.uid, company_id) - result = self.engine.render_node(field, { + result = self.engine.render_node(field, ir_qweb.QWebContext({ 'company': root_company, - 'request': Request(self.cr, self.uid, RegistryProxy(self.registry)) - }) + })) self.assertEqual( result, '%s' % ( company_id, "My Test Company",)) @@ -57,15 +47,13 @@ class TestQWebTField(common.TransactionCase): }) root_company = Companies.browse(self.cr, self.uid, company_id) - result = self.engine.render_node(field, { + result = self.engine.render_node(field, ir_qweb.QWebContext({ 'company': root_company, - 'request': Request(self.cr, self.uid, RegistryProxy(self.registry)) - }) + })) self.assertEqual( result, '%s' % ( company_id, cgi.escape(s.encode('utf-8')),)) @@ -77,7 +65,7 @@ class TestQWebTField(common.TransactionCase): with self.assertRaisesRegexp( AssertionError, r'^RTE widgets do not work correctly'): - self.engine.render_node(field, {'company': None}) + self.engine.render_node(field, ir_qweb.QWebContext({'company': None})) def test_reject_t_tag(self): field = document.createElement('t') @@ -86,4 +74,4 @@ class TestQWebTField(common.TransactionCase): with self.assertRaisesRegexp( AssertionError, r'^t-field can not be used on a t element'): - self.engine.render_node(field, {'company': None}) + self.engine.render_node(field, ir_qweb.QWebContext({'company': None})) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py deleted file mode 100644 index 2683ea0b1ca..00000000000 --- a/openerp/tools/qweb.py +++ /dev/null @@ -1,411 +0,0 @@ -import datetime -import dateutil -import logging -import re -import traceback -import urllib -import xml # FIXME use lxml and etree - -import werkzeug.utils - -from openerp.osv import osv, orm - -_logger = logging.getLogger(__name__) - -BUILTINS = { - 'False': False, - 'None': None, - 'True': True, - 'abs': abs, - 'bool': bool, - 'dict': dict, - 'filter': filter, - 'len': len, - 'list': list, - 'map': map, - 'max': max, - 'min': min, - 'reduce': reduce, - 'repr': repr, - 'round': round, - 'set': set, - 'str': str, - 'tuple': tuple, - 'quote': urllib.quote, - 'urlencode': urllib.urlencode, - 'datetime': datetime, - # dateutil.relativedelta is an old-style class and cannot be directly - # instanciated wihtin a jinja2 expression, so a lambda "proxy" is - # is needed, apparently. - 'relativedelta': lambda *a, **kw : dateutil.relativedelta.relativedelta(*a, **kw), -} - -## We use a jinja2 sandboxed environment to render qWeb templates. -#from openerp.tools.safe_eval import safe_eval as eval -#from jinja2.sandbox import SandboxedEnvironment -#from jinja2.exceptions import SecurityError, UndefinedError -#UNSAFE = ["browse", "search", "read", "unlink", "read_group"] -#SAFE = ["_name"] - -class QWebContext(dict): - def __init__(self, data, undefined_handler=None): - self.undefined_handler = undefined_handler - dic = BUILTINS.copy() - dic.update(data) - super(QWebContext, self).__init__(dic) - self['defined'] = lambda key: key in self - - def __getitem__(self, key): - if key in self: - return self.get(key) - elif not self.undefined_handler: - raise NameError("QWeb: name %r is not defined while rendering template %r" % (key, self.get('__template__'))) - else: - return self.get(key, self.undefined_handler(key, self)) - - def safe_eval(self, expr): - # This is too slow, we should cached compiled expressions attribute of - # qweb to will be changed into a model object ir.qweb. - # - # The cache should be on qweb, and qweb context contructor take qweb as - # argument to store the cache. - # - #class QWebSandboxedEnvironment(SandboxedEnvironment): - # def is_safe_attribute(self, obj, attr, value): - # if str(attr) in SAFE: - # res = True - # else: - # res = super(QWebSandboxedEnvironment, self).is_safe_attribute(obj, attr, value) - # if str(attr) in UNSAFE or not res: - # raise SecurityError("access to attribute '%s' of '%s' object is unsafe." % (attr,obj)) - # return res - #env = qWebSandboxedEnvironment(variable_start_string="${", variable_end_string="}") - #env.globals.update(context) - #env.compile_expression(expr)() - return eval(expr, None, self) - -class QWebXml(object): - """QWeb Xml templating engine - - The templating engine use a very simple syntax, "magic" xml attributes, to - produce any kind of texutal output (even non-xml). - - QWebXml: - the template engine core implements the basic magic attributes: - - t-att t-raw t-esc t-if t-foreach t-set t-call t-trim - - - - loader: function that return a template - - - """ - def __init__(self, loader=None, undefined_handler=None): - self.loader = loader - self.undefined_handler = undefined_handler - self.node = xml.dom.Node - self._t = {} - self._render_tag = {} - self._format_regex = re.compile('(#\{(.*?)\})|(\{\{(.*?)\}\})') - self._void_elements = set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', - 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']) - prefix = 'render_tag_' - for i in [j for j in dir(self) if j.startswith(prefix)]: - name = i[len(prefix):].replace('_', '-') - self._render_tag[name] = getattr(self.__class__, i) - - self._render_att = {} - prefix = 'render_att_' - for i in [j for j in dir(self) if j.startswith(prefix)]: - name = i[len(prefix):].replace('_', '-') - self._render_att[name] = getattr(self.__class__, i) - - def register_tag(self, tag, func): - self._render_tag[tag] = func - - def add_template(self, x): - if hasattr(x, 'documentElement'): - dom = x - elif x.startswith("%s" % tuple( - v if isinstance(v, str) else v.encode('utf-8') - for v in (name, g_att, inner, name)) - else: - return "<%s%s/>" % (name, g_att) - - # Attributes - def render_att_att(self, e, an, av, v): - if an.startswith("t-attf-"): - att, val = an[7:], self.eval_format(av, v) - elif an.startswith("t-att-"): - att, val = an[6:], self.eval(av, v) - if isinstance(val, unicode): - val = val.encode("utf8") - else: - att, val = self.eval_object(av, v) - return val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " " - - def render_att_href(self, e, an, av, v): - return self.url_for(e, an, av, v) - def render_att_src(self, e, an, av, v): - return self.url_for(e, an, av, v) - def render_att_action(self, e, an, av, v): - return self.url_for(e, an, av, v) - def url_for(self, e, an, av, v): - if 'url_for' not in v: - raise KeyError("qweb: no 'url_for' found in context") - path = str(v['url_for'](self.eval_format(av, v))) - return ' %s="%s"' % (an[2:], werkzeug.utils.escape(path)) - - # Tags - def render_tag_raw(self, e, t_att, g_att, v): - inner = self.eval_str(t_att["raw"], v) - return self.render_element(e, t_att, g_att, v, inner) - - def render_tag_rawf(self, e, t_att, g_att, v): - inner = self.eval_format(t_att["rawf"], v) - return self.render_element(e, t_att, g_att, v, inner) - - def render_tag_esc(self, e, t_att, g_att, v): - inner = werkzeug.utils.escape(self.eval_str(t_att["esc"], v)) - return self.render_element(e, t_att, g_att, v, inner) - - def render_tag_escf(self, e, t_att, g_att, v): - inner = werkzeug.utils.escape(self.eval_format(t_att["escf"], v)) - return self.render_element(e, t_att, g_att, v, inner) - - def render_tag_foreach(self, e, t_att, g_att, v): - expr = t_att["foreach"] - enum = self.eval_object(expr, v) - if enum is not None: - var = t_att.get('as', expr).replace('.', '_') - d = QWebContext(v.copy(), self.undefined_handler) - size = -1 - if isinstance(enum, (list, tuple)): - size = len(enum) - elif hasattr(enum, 'count'): - size = enum.count() - d["%s_size" % var] = size - d["%s_all" % var] = enum - index = 0 - ru = [] - for i in enum: - d["%s_value" % var] = i - d["%s_index" % var] = index - d["%s_first" % var] = index == 0 - d["%s_even" % var] = index % 2 - d["%s_odd" % var] = (index + 1) % 2 - d["%s_last" % var] = index + 1 == size - if index % 2: - d["%s_parity" % var] = 'odd' - else: - d["%s_parity" % var] = 'even' - if 'as' in t_att: - d[var] = i - elif isinstance(i, dict): - d.update(i) - ru.append(self.render_element(e, t_att, g_att, d)) - index += 1 - return "".join(ru) - else: - raise NameError("QWeb: foreach enumerator %r is not defined while rendering template %r" % (expr, v.get('__template__'))) - - def render_tag_if(self, e, t_att, g_att, v): - if self.eval_bool(t_att["if"], v): - return self.render_element(e, t_att, g_att, v) - else: - return "" - - def render_tag_call(self, e, t_att, g_att, v): - if "import" in t_att: - d = v - else: - d = QWebContext(v.copy(), self.undefined_handler) - d[0] = self.render_element(e, t_att, g_att, d) - return self.render(self.eval_format(t_att["call"], d), d) - - def render_tag_set(self, e, t_att, g_att, v): - if "value" in t_att: - v[t_att["set"]] = self.eval_object(t_att["value"], v) - elif "valuef" in t_att: - v[t_att["set"]] = self.eval_format(t_att["valuef"], v) - else: - v[t_att["set"]] = self.render_element(e, t_att, g_att, v) - return "" - - def render_tag_field(self, e, t_att, g_att, v): - """ eg: +1 555 555 8069""" - node_name = e.nodeName - assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td", - "ol", "ul", "ol", "dl", "dt", "dd"),\ - "RTE widgets do not work correctly on %r elements" % node_name - assert node_name != 't',\ - "t-field can not be used on a t element, provide an actual HTML node" - - record, field = t_att["field"].rsplit('.', 1) - record = self.eval_object(record, v) - - column = record._model._all_columns[field].column - field_type = column._type - - req = v['request'] - converter = req.registry['ir.fields.converter'].from_field( - req.cr, req.uid, record._model, column, totype='html') - - content = None - try: - value = record[field] - if value: - content, warnings = converter(value) - assert not warnings - except KeyError: - _logger.warning("t-field no field %s for model %s", field, record._model._name) - - g_att += ''.join( - ' %s="%s"' % (name, werkzeug.utils.escape(value)) - for name, value in [ - ('data-oe-model', record._model._name), - ('data-oe-id', record.id), - ('data-oe-field', field), - ('data-oe-type', field_type), - ('data-oe-translate', '1' if column.translate else '0'), - ('data-oe-expression', t_att['field']), - ] - ) - - return self.render_element(e, t_att, g_att, v, content or "") - -# vim:et: