[MERGE]Merge lp:~openerp-dev/openobject-server/trunk-website-al.

bzr revid: bth@tinyerp.com-20131015142737-75xztjk49ka53ll2
This commit is contained in:
bth-openerp
2013-10-15 19:57:37 +05:30
18 changed files with 1015 additions and 670 deletions
+7 -1
View File
@@ -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
+1
View File
@@ -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:
-77
View File
@@ -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', '<br>\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', '<br>\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 ('<img src="data:%s;base64,%s">' % (mime, value)), []
PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', }
+655
View File
@@ -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("<?xml"):
dom = xml.dom.minidom.parseString(x)
else:
dom = xml.dom.minidom.parse(x)
for n in dom.documentElement.childNodes:
if n.nodeType == self.node.ELEMENT_NODE and n.getAttribute('t-name'):
self._t[str(n.getAttribute("t-name"))] = n
def get_template(self, name, context):
if context.loader and name not in self._t:
xml_doc = context.loader(name)
self.load_document(xml_doc)
if name in self._t:
return self._t[name]
raise KeyError('qweb: template "%s" not found' % name)
def eval(self, expr, v):
try:
return v.safe_eval(expr)
except (osv.except_osv, orm.except_orm), err:
raise orm.except_orm("QWeb Error", "Invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), err[1]))
except Exception:
raise SyntaxError("QWeb: invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), traceback.format_exc()))
def eval_object(self, expr, v):
return self.eval(expr, v)
def eval_str(self, expr, v):
if expr == "0":
return v.get(0, '')
val = self.eval(expr, v)
if isinstance(val, unicode):
return val.encode("utf8")
return str(val)
def eval_format(self, expr, v):
expr, replacements = self._format_regex.subn(
lambda m: self.eval_str(m.group(1) or m.group(2), v),
expr)
if replacements:
return expr
try:
return str(expr % v)
except:
raise Exception("QWeb: format error '%s' " % expr)
def eval_bool(self, expr, v):
val = self.eval(expr, v)
if val:
return 1
else:
return 0
def render(self, tname, v=None, out=None, loader=None, undefined_handler=None):
if v is None:
v = {}
if not isinstance(v, QWebContext):
v = QWebContext(v, undefined_handler=undefined_handler, loader=loader)
v['__template__'] = tname
stack = v.get('__stack__', [])
if stack:
v['__caller__'] = stack[-1]
stack.append(tname)
v['__stack__'] = stack
v['xmlid'] = str(stack[0]) # Temporary fix
return self.render_node(self.get_template(tname, v), v)
def render_node(self, e, v):
r = ""
if e.nodeType == self.node.TEXT_NODE or e.nodeType == self.node.CDATA_SECTION_NODE:
r = e.data.encode("utf8")
elif e.nodeType == self.node.ELEMENT_NODE:
g_att = ""
t_render = None
t_att = {}
for (an, av) in e.attributes.items():
an = str(an)
if isinstance(av, unicode):
av = av.encode("utf8")
else:
av = av.nodeValue.encode("utf8")
if an.startswith("t-"):
for i in self._render_att:
if an[2:].startswith(i):
g_att += self._render_att[i](self, e, an, av, v)
break
else:
if an[2:] in self._render_tag:
t_render = an[2:]
t_att[an[2:]] = av
else:
g_att += ' %s="%s"' % (an, werkzeug.utils.escape(av))
if 'debug' in t_att:
debugger = t_att.get('debug', 'pdb')
__import__(debugger).set_trace() # pdb, ipdb, pudb, ...
if t_render:
r = self._render_tag[t_render](self, e, t_att, g_att, v)
else:
r = self.render_element(e, t_att, g_att, v)
if isinstance(r, unicode):
return r.encode('utf-8')
return r
def render_element(self, e, t_att, g_att, v, inner=None):
# e: element
# t_att: t-* attributes
# g_att: generated attributes
# v: values
# inner: optional innerXml
if inner:
g_inner = inner
else:
g_inner = []
for n in e.childNodes:
g_inner.append(self.render_node(n, v))
name = str(e.nodeName)
inner = "".join(g_inner)
trim = t_att.get("trim", 0)
if trim == 0:
pass
elif trim == 'left':
inner = inner.lstrip()
elif trim == 'right':
inner = inner.rstrip()
elif trim == 'both':
inner = inner.strip()
if name == "t":
return inner
elif len(inner) or name not in self._void_elements:
return "<%s%s>%s</%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: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
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', '<br>\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', '<br>\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 '<img src="data:%s;base64,%s">' % (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'<span class="oe_currency_value">{0}</span>' \
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:
+7 -4
View File
@@ -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 ?
+1
View File
@@ -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
+18 -9
View File
@@ -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('(<setFont.?name.?=.?)(".*?")(.)', '\g<1>"%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 = """
<header>
<pageTemplate>
@@ -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:
+9 -1
View File
@@ -80,7 +80,15 @@
<field name="custom_footer" on_change="onchange_footer(custom_footer, phone, fax, email, website, vat, company_registry, bank_ids)"/>
<field name="rml_footer" attrs="{'invisible': [('custom_footer','=',False)]}"/>
<field name="rml_footer_readonly" attrs="{'invisible': [('custom_footer','=',True)]}"/>
<field name="font" on_change="onchange_font_name(font, rml_header, rml_header2, rml_header3)"/>
</group>
<group>
<label for="font" />
<div>
<div>
<field name="font" class="oe_inline" colspan="2"/>
<button string="(reload fonts)" name="act_discover_fonts" type="object" class="oe_link" colspan="1"/>
</div>
</div>
</group>
</page>
<page string="Header/Footer" groups="base.group_no_one">
+9 -8
View File
@@ -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:
+96
View File
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
111 access_ir_mail_server ir_mail_server model_ir_mail_server group_system 1 1 1 1
112 access_ir_actions_client ir_actions_client all model_ir_actions_client 1 0 0 0
113 access_ir_needaction_mixin ir_needaction_mixin model_ir_needaction_mixin 1 1 1 1
114 access_ir_qweb access_ir_qweb model_ir_qweb 0 0 0 0
115 access_ir_qweb_field access_ir_qweb_field model_ir_qweb_field 0 0 0 0
116 access_ir_qweb_field_float access_ir_qweb_field_float model_ir_qweb_field_float 0 0 0 0
117 access_ir_qweb_field_text access_ir_qweb_field_text model_ir_qweb_field_text 0 0 0 0
118 access_ir_qweb_field_selection access_ir_qweb_field_selection model_ir_qweb_field_selection 0 0 0 0
119 access_ir_qweb_field_many2one access_ir_qweb_field_many2one model_ir_qweb_field_many2one 0 0 0 0
120 access_ir_qweb_field_html access_ir_qweb_field_html model_ir_qweb_field_html 0 0 0 0
121 access_ir_qweb_field_image access_ir_qweb_field_image model_ir_qweb_field_image 0 0 0 0
122 access_ir_qweb_field_monetary access_ir_qweb_field_monetary model_ir_qweb_field_monetary 0 0 0 0
+19 -79
View File
@@ -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)
+5 -1
View File
@@ -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:
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_converter_model access_converter_model model_test_converter_test_model 1 1 1 1
3 access_test_converter_test_model_sub access_test_converter_test_model_sub model_test_converter_test_model_sub 1 1 1 1
4 access_test_converter_monetary access_test_converter_monetary model_test_converter_monetary 1 1 1 1
@@ -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)),
}
@@ -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<bar>")
value = converter("foo<bar>")
self.assertEqual(value, "foo&lt;bar&gt;")
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,
'<span data-oe-model="{obj._model._name}" data-oe-id="{obj.id}" '
'data-oe-field="value" data-oe-type="monetary" '
'data-oe-expression="obj.value">'
'<span class="oe_currency_value">0.12</span>'
' {symbol}</span>'.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,
'<span data-oe-model="{obj._model._name}" data-oe-id="{obj.id}" '
'data-oe-field="value" data-oe-type="monetary" '
'data-oe-expression="obj.value">'
'{symbol} '
'<span class="oe_currency_value">0.12</span>'
'</span>'.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,
'<span data-oe-model="{obj._model._name}" data-oe-id="{obj.id}" '
'data-oe-field="value" data-oe-type="monetary" '
'data-oe-expression="obj.value">'
'<span class="oe_currency_value">0.12</span>'
' {symbol}</span>'.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.<br>
+ The next line. This is equivalent to +1 and may be repeated with cumulative effect.<br>
""")
self.assertEqual(warnings, [])
value, warnings = converter("""
value = converter("""
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>
""")
@@ -99,65 +190,78 @@ class TestHTMLExport(common.TransactionCase):
fgdkls;hjas;lj &lt;b&gt;fdslkj&lt;/b&gt; d;lasjfa lkdja &lt;a href=http://spam.com&gt;lfks&lt;/a&gt;<br>
fldkjsfhs &lt;i style=&quot;color: red&quot;&gt;&lt;a href=&quot;http://spamspam.com&quot;&gt;fldskjh&lt;/a&gt;&lt;/i&gt;<br>
""")
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': "Fo<b>o</b>"})
value, warnings = converter(Sub.browse(self.cr, self.uid, id1))
value = converter(Sub.browse(self.cr, self.uid, id1))
self.assertEqual(value, "Fo&lt;b&gt;o&lt;/b&gt;")
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, '<img src="data:image/jpeg;base64,%s">' % (
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, '<img src="data:image/jpeg;base64,%s">' % (
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>span</span>'
value, warnings = converter(input)
value = converter(input)
self.assertEqual(value, input)
self.assertEqual(warnings, [])
# o2m, m2m?
# reference?
# o2m, m2m?
# reference?
+9 -21
View File
@@ -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,
'<span data-oe-model="res.company" data-oe-id="%d" '
'data-oe-field="name" data-oe-type="char" '
'data-oe-translate="0" '
'data-oe-expression="company.name">%s</span>' % (
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,
'<span data-oe-model="res.company" data-oe-id="%d" '
'data-oe-field="name" data-oe-type="char" '
'data-oe-translate="0" '
'data-oe-expression="company.name">%s</span>' % (
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}))
-411
View File
@@ -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("<?xml"):
dom = xml.dom.minidom.parseString(x)
else:
dom = xml.dom.minidom.parse(x)
for n in dom.documentElement.childNodes:
if n.nodeType == 1 and n.getAttribute('t-name'):
self._t[str(n.getAttribute("t-name"))] = n
def get_template(self, name):
if name in self._t:
return self._t[name]
elif self.loader:
xml = self.loader(name)
self.add_template(xml)
if name in self._t:
return self._t[name]
raise KeyError('qweb: template "%s" not found' % name)
def eval(self, expr, v):
try:
return v.safe_eval(expr)
except (osv.except_osv, orm.except_orm), err:
raise orm.except_orm("QWeb Error", "Invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), err[1]))
except Exception:
raise SyntaxError("QWeb: invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), traceback.format_exc()))
def eval_object(self, expr, v):
return self.eval(expr, v)
def eval_str(self, expr, v):
if expr == "0":
return v.get(0, '')
val = self.eval(expr, v)
if isinstance(val, unicode):
return val.encode("utf8")
return str(val)
def eval_format(self, expr, v):
use_native = True
for m in self._format_regex.finditer(expr):
use_native = False
expr = expr.replace(m.group(), self.eval_str(m.groups()[1] or m.groups()[3], v))
if not use_native:
return expr
else:
try:
return str(expr % v)
except:
raise Exception("QWeb: format error '%s' " % expr)
def eval_bool(self, expr, v):
val = self.eval(expr, v)
if val:
return 1
else:
return 0
def render(self, tname, v=None, out=None):
if v is None:
v = {}
v['__template__'] = tname
stack = v.get('__stack__', [])
if stack:
v['__caller__'] = stack[-1]
stack.append(tname)
v['__stack__'] = stack
v = QWebContext(v, self.undefined_handler)
return self.render_node(self.get_template(tname), v)
def render_node(self, e, v):
r = ""
if e.nodeType == self.node.TEXT_NODE or e.nodeType == self.node.CDATA_SECTION_NODE:
r = e.data.encode("utf8")
elif e.nodeType == self.node.ELEMENT_NODE:
g_att = ""
t_render = None
t_att = {}
for (an, av) in e.attributes.items():
an = str(an)
if isinstance(av, unicode):
av = av.encode("utf8")
else:
av = av.nodeValue.encode("utf8")
if an.startswith("t-"):
for i in self._render_att:
if an[2:].startswith(i):
g_att += self._render_att[i](self, e, an, av, v)
break
else:
if an[2:] in self._render_tag:
t_render = an[2:]
t_att[an[2:]] = av
else:
g_att += ' %s="%s"' % (an, werkzeug.utils.escape(av))
if 'debug' in t_att:
debugger = t_att.get('debug', 'pdb')
__import__(debugger).set_trace() # pdb, ipdb, pudb, ...
if t_render:
if t_render in self._render_tag:
r = self._render_tag[t_render](self, e, t_att, g_att, v)
else:
r = self.render_element(e, t_att, g_att, v)
if isinstance(r, unicode):
return r.encode('utf-8')
return r
def render_element(self, e, t_att, g_att, v, inner=None):
# e: element
# t_att: t-* attributes
# g_att: generated attributes
# v: values
# inner: optional innerXml
if inner:
g_inner = inner
else:
g_inner = []
for n in e.childNodes:
g_inner.append(self.render_node(n, v))
name = str(e.nodeName)
inner = "".join(g_inner)
trim = t_att.get("trim", 0)
if trim == 0:
pass
elif trim == 'left':
inner = inner.lstrip()
elif trim == 'right':
inner = inner.rstrip()
elif trim == 'both':
inner = inner.strip()
if name == "t":
return inner
elif len(inner) or name not in self._void_elements:
return "<%s%s>%s</%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: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
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: