[MERGE] trunk-website-al

bzr revid: al@openerp.com-20140131005207-mn7t6tar8cywe9hz
This commit is contained in:
Antony Lesuisse 2014-01-31 01:52:07 +01:00
commit d6c1346e12
71 changed files with 4217 additions and 1362 deletions

View File

@ -172,40 +172,80 @@ is as follows:
</data>
</openerp>
Record Tag
//////////
``<record>``
////////////
**Description**
Defines a new record in a specified OpenERP model.
The addition of new data is made with the record tag. This one takes a
mandatory attribute : model. Model is the object name where the insertion has
to be done. The tag record can also take an optional attribute: id. If this
attribute is given, a variable of this name can be used later on, in the same
file, to make reference to the newly created resource ID.
``@model`` (required)
A record tag may contain field tags. They indicate the record's fields value.
If a field is not specified the default value will be used.
Name of the model in which this record will be created/inserted.
The Record Field tag
////////////////////
``@id`` (optional)
The attributes for the field tag are the following:
:term:`external ID` for the record, also allows referring to this record in
the rest of this file or in other files (through ``field/@ref`` or the
:py:func:`ref() <openerp.tools.convert._ref>` function)
name : mandatory
the field name
A record tag generally contains multiple ``field`` tags specifying the values
set on the record's fields when creating it. Fields left out will be set to
their default value unless required.
eval : optional
python expression that indicating the value to add
ref
reference to an id defined in this file
``<field>``
///////////
model
model to be looked up in the search
In its most basic use, the ``field`` tag will set its body (as a string) as
the value of the corresponding ``record``'s ``@name`` field.
search
a query
Extra attributes can either preprocess the body or replace its use entirely:
``@name`` (mandatory)
Name of the field in the containing ``record``'s model
``@type`` (optional)
One of ``char``, ``int``, ``float``, ``list``, ``tuple``, ``xml`` or
``html``, ``file`` or ``base64``. Converts the ``field``'s body to the
specified type (or validates the body's content)
* ``xml`` will join multiple XML nodes under a single ``<data>`` root
* in ``xml`` and ``html``, external ids can be referenced using
``%(id_name)s``
* ``list`` and ``tuple``'s element are specified using ``<value>``
sub-nodes with the same attributes as ``field``.
* ``file`` expects a module-local path and will save the path prefixed with
the current module's name, separated by a ``,`` (comma). For use with
:py:func:`~openerp.modules.module.get_module_resource`.
* ``base64`` expects binary data, encodes it to base64 and sets it. Mostly
useful with ``@file``
``@file``
Can be used with types ``char`` and ``base64``, sources the field's content
from the specified file instead of the field's text body.
``@model``
Model used for ``@search``'s search, or registry object put in context for
``@eval``. Required if ``@search`` but optional if ``@eval``.
``@eval`` (optional)
A Python expression evaluated to obtain the value to set on the record
``@ref`` (optional)
Links to an other record through its :term:`external id`. The module prefix
may be ommitted to link to a record defined in the same module.
``@search`` (optional)
Search domain (evaluated Python expression) into ``@model`` to get the
records to set on the field.
Sets all the matches found for m2m fields, the first id for other field
types.
**Example**

98
doc/06_ir_qweb.rst Normal file
View File

@ -0,0 +1,98 @@
.. _qweb:
====
QWeb
====
``t-field``
===========
The server version of qweb includes a directive dedicated specifically to
formatting and rendering field values from
:class:`~openerp.osv.orm.browse_record` objects.
The directive is implemented through
:meth:`~base.ir.ir_qweb.QWeb.render_tag_field` on the ``ir.qweb`` openerp
object, and generally delegates to converters for rendering. These converters
are obtained through :meth:`~base.ir.ir_qweb.QWeb.get_converter_for`.
By default, the key for obtaining a converter is the type of the field's
column, but this can be overridden by providing a ``widget`` as field option.
Field options are specified through ``t-field-options``, which must be a JSON
object (map). Custom widgets may define their own (possibly mandatory) options.
Global options
--------------
A global option ``html-escape`` is provided. It defaults to ``True``, and for
many (not all) fields it determines whether the field's output will be
html-escaped before being output.
Date and datetime converters
----------------------------
The default rendering for ``date`` and ``datetime`` fields. They render the
field's value according to the current user's ``lang.date_format`` and
``lang.time_format``. The ``datetime`` converter will also localize the value
to the user's timezone (as defined by the ``tz`` context key, or the timezone
in the user's profile if there is no ``tz`` key in the context).
A custom format can be provided to use a non-default rendering. The custom
format uses the ``format`` options key, and uses the
`ldml date format patterns`_ [#ldml]_.
For instance if one wanted a date field to be rendered as
"(month) (day of month)" rather than whatever the default is, one could use:
.. code-block:: xml
<span t-field="object.datefield" t-field-options='{"format": "MMMM d"}'/>
Monetary converter (widget: ``monetary``)
-----------------------------------------
Used to format and render monetary value, requires a ``display_currency``
options value which is a path from the rendering context to a ``res.currency``
object. This object is used to set the right currency symbol, and set it at the
right position relative to the formatted value.
The field itself should be a float field.
Relative Datetime (widget: ``relative``)
----------------------------------------
Used on a ``datetime`` field, formats it relatively to the current time
(``datetime.now()``), e.g. if the field's value is 3 hours before now and the
user's lang is english, it will render to *3 hours ago*.
.. note:: this field uses babel's ``format_timedelta`` more or less directly
and will only display the biggest unit and round up at 85% e.g.
1 hour 15 minutes will be rendered as *1 hour*, and 55 minutes will
also be rendered as *1 hour*.
.. warning:: this converter *requires* babel 1.0 or more recent.
Duration (widget: ``duration``)
-------------------------------
Renders a duration defined as a ``float`` to a human-readable localized string,
e.g. ``1.5`` as hours in an english locale will be rendered to
*1 hour 30 minutes*.
Requires a ``unit`` option which may be one of ``second``, ``minute``,
``hour``, ``day``, ``week``, ``month`` or ``year``. This specifies the unit in
which the value should be interpreted before formatting.
The duration must be a positive number, and no rounding is applied.
.. [#ldml] in part because `babel`_ is used for rendering, as ``strftime``
would require altering the process's locale on the fly in order to
get correctly localized date and time output. Babel uses the CLDR
as its core and thus uses LDML date format patterns.
.. _babel: http://babel.pocoo.org
.. _ldml date format patterns:
http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns

View File

@ -11,3 +11,4 @@ Miscellanous
06_misc_user_img_specs.rst
06_misc_import.rst
06_misc_auto_join.rst
06_ir_qweb.rst

View File

@ -252,5 +252,4 @@ texinfo_documents = [
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'python': ('http://docs.python.org/', None),
'openerpweb': ('http://doc.openerp.com/trunk/developers/web', None),
}

View File

@ -12,26 +12,20 @@ Server actions
.. currentmodule:: openerp.addons.base.ir.ir_actions
.. autoclass:: actions_server
:noindex:
.. autoclass:: ir_actions_server
:members: run, _get_states
Adding a new sever action
-------------------------
The ``state`` field holds the various available types of server action. In order
to add a new server action, the first thing to do is to override the ``_get_states``
to add a new server action, the first thing to do is to override the :meth:`~.ir_actions_server._get_states`
method that returns the list of values available for the selection field.
.. automethod:: actions_server._get_states
:noindex:
The method called when executing the server action is the ``run`` method. This
The method called when executing the server action is the :meth:`~.ir_actions_server.run` method. This
method calls ``run_action_<STATE>``. When adding a new server action type, you
have to define the related method that will be called upon execution.
.. automethod:: actions_server.run
:noindex:
Changelog
---------

View File

@ -112,18 +112,6 @@ CREATE TABLE ir_act_client (
)
INHERITS (ir_actions);
CREATE TABLE ir_ui_view (
id serial NOT NULL,
name varchar(64) DEFAULT ''::varchar NOT NULL,
model varchar(64) DEFAULT ''::varchar NOT NULL,
"type" varchar(64) DEFAULT 'form'::varchar NOT NULL,
arch text NOT NULL,
field_parent varchar(64),
priority integer DEFAULT 5 NOT NULL,
primary key(id)
);
CREATE TABLE ir_ui_menu (
id serial NOT NULL,
parent_id int references ir_ui_menu on delete set null,
@ -409,4 +397,4 @@ insert into ir_model_data (name,module,model,noupdate,res_id) VALUES ('main_comp
select setval('res_company_id_seq', 2);
select setval('res_users_id_seq', 2);
select setval('res_partner_id_seq', 2);
select setval('res_currency_id_seq', 2);
select setval('res_currency_id_seq', 2);

View File

@ -91,6 +91,25 @@ Administrator</field>
<field eval="10" name="sequence"/>
</record>
<!--
A group dedicated to the portal users, making groups
restrictions more convenient.
-->
<record id="group_portal" model="res.groups">
<field name="name">Portal</field>
<field name="comment">Portal members have specific access rights (such as record rules and restricted menus).
They usually do not belong to the usual OpenERP groups.</field>
</record>
<!--
A group dedicated to the public user only, making groups
restrictions more convenient.
-->
<record id="group_public" model="res.groups">
<field name="name">Public</field>
<field name="comment">Public users have specific access rights (such as record rules and restricted menus).
They usually do not belong to the usual OpenERP groups.</field>
</record>
<!-- Basic fonts family included in PDF standart, will always be in the font list -->
<record model="res.font" id="base.font_helvetica">
<field name="name">Helvetica</field>
@ -111,5 +130,21 @@ Administrator</field>
<field name="mode">all</field>
</record>
<record id="public_partner" model="res.partner">
<field name="name">Public user</field>
<field name="active" eval="False"/>
</record>
<record id="public_user" model="res.users">
<field name="name">Public user</field>
<field name="login">public</field>
<field name="password"></field>
<!-- Avoid auto-including this demo user in any default group -->
<field name="groups_id" eval="[(6,0,[ref('base.group_public')])]"/>
<field name="image" type="base64" file="base/static/img/public_user-image.png"/>
<field name="partner_id" ref="public_partner"/>
<field name="active" eval="False"/>
</record>
</data>
</openerp>

View File

@ -1,17 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="partner_demo" model="res.partner">
<field name="name">Demo User</field>
<field name="company_id" ref="main_company"/>
<field name="customer" eval="False"/>
<field name="email">demo@example.com</field>
<field name="email">demo@yourcompany.example.com</field>
<field name="street">Avenue des Dessus-de-Lives, 2</field>
<field name="city">Namur (Loyers)</field>
<field name="zip">5101</field>
<field name="country_id" ref="be"/>
</record>
<record id="main_partner" model="res.partner">
<field name="name">YourCompany</field>
<field name="street">1725 Slough Ave.</field>
<field name="city">Scranton</field>
<field name="zip">18540</field>
<field name="phone">+1 555 123 8069</field>
<field name="email">info@yourcompany.example.com</field>
<field name="website">www.example.com</field>
<field name="image" type="base64" file="base/static/img/main_partner-image.png"/>
</record>
<record id="main_company" model="res.company">
<field name="name">YourCompany</field>
</record>
<record id="user_demo" model="res.users">
<field name="partner_id" ref="base.partner_demo"/>
<field name="login">demo</field>
@ -24,7 +41,7 @@ Mr Demo</field>
</record>
<record model="res.partner" id="base.partner_root">
<field name="email">admin@example.com</field>
<field name="email">admin@yourcompany.example.com</field>
<field name="tz">Europe/Brussels</field>
<field name="image" type="base64" file="base/static/img/partner_root-image.jpg"/>
</record>

View File

@ -37,6 +37,7 @@ import ir_config_parameter
import osv_memory_autovacuum
import ir_mail_server
import ir_fields
import ir_qweb
import ir_http
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -911,10 +911,10 @@ class ir_actions_server(osv.osv):
self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
def run(self, cr, uid, ids, context=None):
""" Run the server action. For each server action, the condition is
checked. Note that A void (aka False) condition is considered as always
""" Runs the server action. For each server action, the condition is
checked. Note that a void (``False``) condition is considered as always
valid. If it is verified, the run_action_<STATE> method is called. This
allows easy inheritance of the server actions.
allows easy overriding of the server actions.
:param dict context: context should contain following keys

View File

@ -45,6 +45,7 @@ class ir_attachment(osv.osv):
The default implementation is the file:dirname location that stores files
on the local filesystem using name based on their sha1 hash
"""
_order = 'id desc'
def _name_get_resname(self, cr, uid, ids, object, method, context):
data = {}
for attachment in self.browse(cr, uid, ids, context=context):

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import cStringIO
import datetime
import functools
import operator
@ -128,14 +129,17 @@ class ir_fields_converter(orm.Model):
:param column: column object to generate a value for
:type column: :class:`fields._column`
:param type fromtype: type to convert to something fitting for ``column``
:param fromtype: type to convert to something fitting for ``column``
:type fromtype: type | str
:param context: openerp request context
:return: a function (fromtype -> column.write_type), if a converter is found
:rtype: Callable | None
"""
assert isinstance(fromtype, (type, str))
# FIXME: return None
typename = fromtype.__name__ if isinstance(fromtype, type) else fromtype
converter = getattr(
self, '_%s_to_%s' % (fromtype.__name__, column._type), None)
self, '_%s_to_%s' % (typename, column._type), None)
if not converter: return None
return functools.partial(

View File

@ -15,9 +15,7 @@ from openerp.osv import osv, orm
_logger = logging.getLogger(__name__)
# FIXME: replace by proxy on request.uid?
_uid = object()
UID_PLACEHOLDER = object()
class ModelConverter(werkzeug.routing.BaseConverter):
@ -29,7 +27,7 @@ class ModelConverter(werkzeug.routing.BaseConverter):
def to_python(self, value):
m = re.match(self.regex, value)
return request.registry[self.model].browse(
request.cr, _uid, int(m.group(1)), context=request.context)
request.cr, UID_PLACEHOLDER, int(m.group(1)), context=request.context)
def to_url(self, value):
return value.id
@ -43,10 +41,7 @@ class ModelsConverter(werkzeug.routing.BaseConverter):
self.regex = '([0-9,]+)'
def to_python(self, value):
# TODO:
# - raise routing.ValidationError() if no browse record can be createdm
# - support slug
return request.registry[self.model].browse(request.cr, _uid, [int(i) for i in value.split(',')], context=request.context)
return request.registry[self.model].browse(request.cr, UID_PLACEHOLDER, [int(i) for i in value.split(',')], context=request.context)
def to_url(self, value):
return ",".join(i.id for i in value)
@ -66,15 +61,15 @@ class ir_http(osv.AbstractModel):
if not request.uid:
raise http.SessionExpiredException("Session expired")
def _auth_method_admin(self):
if not request.db:
raise http.SessionExpiredException("No valid database for request %s" % request.httprequest)
request.uid = openerp.SUPERUSER_ID
def _auth_method_none(self):
request.disable_db = True
request.uid = None
def _auth_method_public(self):
if not request.session.uid:
dummy, request.uid = self.pool['ir.model.data'].get_object_reference(request.cr, openerp.SUPERUSER_ID, 'base', 'public_user')
else:
request.uid = request.session.uid
def _authenticate(self, auth_method='user'):
if request.session.uid:
try:
@ -88,16 +83,8 @@ class ir_http(osv.AbstractModel):
return auth_method
def _handle_exception(self, exception):
if isinstance(exception, openerp.exceptions.AccessError):
code = 403
else:
code = getattr(exception, 'code', 500)
fn = getattr(self, '_handle_%d' % code, self._handle_unknown_exception)
return fn(exception)
def _handle_unknown_exception(self, exception):
raise exception
# If handle exception return something different than None, it will be used as a response
raise
def _dispatch(self):
# locate the controller method
@ -108,17 +95,17 @@ class ir_http(osv.AbstractModel):
# check authentication level
try:
auth_method = self._authenticate(getattr(func, "auth", None))
auth_method = self._authenticate(func.routing["auth"])
except Exception:
# force a Forbidden exception with the original traceback
return self._handle_exception(
convert_exception_to(
werkzeug.exceptions.Forbidden))
# post process arg to set uid on browse records
for arg in arguments.itervalues():
if isinstance(arg, orm.browse_record) and arg._uid is _uid:
arg._uid = request.uid
processing = self._postprocess_args(arguments)
if processing:
return processing
# set and execute handler
try:
@ -131,6 +118,16 @@ class ir_http(osv.AbstractModel):
return result
def _postprocess_args(self, arguments):
""" post process arg to set uid on browse records """
for arg in arguments.itervalues():
if isinstance(arg, orm.browse_record) and arg._uid is UID_PLACEHOLDER:
arg._uid = request.uid
try:
arg[arg._rec_name]
except KeyError:
return self._handle_exception(werkzeug.exceptions.NotFound())
def routing_map(self):
if not hasattr(self, '_routing_map'):
_logger.info("Generating routing map")
@ -138,7 +135,7 @@ class ir_http(osv.AbstractModel):
m = request.registry.get('ir.module.module')
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')], context=request.context)
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name'], context=request.context))
mods = ['', "web"] + sorted(installed)
mods = [''] + openerp.conf.server_wide_modules + sorted(installed)
self._routing_map = http.routing_map(mods, False, converters=self._get_converters())
return self._routing_map

View File

@ -3,7 +3,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
# Copyright (C) 2004-2014 OpenERP S.A. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -29,7 +29,7 @@ import openerp.modules.registry
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.osv import fields,osv
from openerp.osv.orm import Model
from openerp.osv.orm import Model, browse_null
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools import config
from openerp.tools.translate import _
@ -737,7 +737,7 @@ class ir_model_access(osv.osv):
msg_params = (model_name,)
_logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
msg = '%s %s' % (msg_heads[mode], msg_tail)
raise except_orm(_('Access Denied'), msg % msg_params)
raise openerp.exceptions.AccessError(msg % msg_params)
return r or False
__cache_clearing_methods = []
@ -853,24 +853,58 @@ class ir_model_data(osv.osv):
if not cr.fetchone():
cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
@tools.ormcache()
# NEW V8 API
@tools.ormcache(skiparg=3)
def xmlid_lookup(self, cr, uid, xmlid):
"""Low level xmlid lookup
Return (id, res_model, res_id) or raise ValueError if not found
"""
module, name = xmlid.split('.', 1)
ids = self.search(cr, uid, [('module','=',module), ('name','=', name)])
if not ids:
raise ValueError('External ID not found in the system: %s' % (xmlid))
# the sql constraints ensure us we have only one result
res = self.read(cr, uid, ids[0], ['model', 'res_id'])
if not res['res_id']:
raise ValueError('External ID not found in the system: %s' % (xmlid))
return ids[0], res['model'], res['res_id']
def xmlid_to_res_model_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
""" Return (res_model, res_id)"""
try:
return self.xmlid_lookup(cr, uid, xmlid)[1:3]
except ValueError:
if raise_if_not_found:
raise
return (False, False)
def xmlid_to_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
""" Returns res_id """
return self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)[1]
def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None):
""" Return a browse_record
if not found and raise_if_not_found is True return the browse_null
"""
t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)
res_model, res_id = t
if res_model and res_id:
record = self.pool[res_model].browse(cr, uid, res_id, context=context)
if record.exists():
return record
if raise_if_not_found:
raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xml_id))
return browse_null()
# OLD API
def _get_id(self, cr, uid, module, xml_id):
"""Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
if not ids:
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
# the sql constraints ensure us we have only one result
return ids[0]
return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[0]
@tools.ormcache()
def get_object_reference(self, cr, uid, module, xml_id):
"""Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
data_id = self._get_id(cr, uid, module, xml_id)
#assuming data_id is not False, as it was checked upstream
res = self.read(cr, uid, data_id, ['model', 'res_id'])
if not res['res_id']:
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
return res['model'], res['res_id']
return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[1:3]
def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
"""Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
@ -885,12 +919,11 @@ class ir_model_data(osv.osv):
return model, False
def get_object(self, cr, uid, module, xml_id, context=None):
"""Returns a browsable record for the given module name and xml_id or raise ValueError if not found"""
res_model, res_id = self.get_object_reference(cr, uid, module, xml_id)
result = self.pool[res_model].browse(cr, uid, res_id, context=context)
if not result.exists():
raise ValueError('No record found for unique ID %s.%s. It may have been deleted.' % (module, xml_id))
return result
""" Returns a browsable record for the given module name and xml_id.
If not found, raise a ValueError or return a browse_null, depending
on the value of `raise_exception`.
"""
return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context)
def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
if not xml_id:
@ -907,8 +940,7 @@ class ir_model_data(osv.osv):
:returns: itself
"""
self._get_id.clear_cache(self)
self.get_object_reference.clear_cache(self)
self.xmlid_lookup.clear_cache(self)
return self
def unlink(self, cr, uid, ids, context=None):
@ -929,15 +961,17 @@ class ir_model_data(osv.osv):
return False
action_id = False
if xml_id:
cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
(module, xml_id))
results = cr.fetchall()
for imd_id2,res_id2,real_id2,real_model in results:
for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
# In update mode, do not update a record if it's ir.model.data is flagged as noupdate
if mode == 'update' and noupdate_imd:
return res_id2
if not real_id2:
self._get_id.clear_cache(self, uid, module, xml_id)
self.get_object_reference.clear_cache(self, uid, module, xml_id)
self.clear_caches()
cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
res_id = False
else:

View File

@ -0,0 +1,849 @@
# -*- coding: utf-8 -*-
import collections
import cStringIO
import datetime
import json
import logging
import math
import re
import sys
import xml # FIXME use lxml and etree
import babel
import babel.dates
import werkzeug.utils
from PIL import Image
import openerp.tools
from openerp.tools.safe_eval import safe_eval as eval
from openerp.osv import osv, orm, fields
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
#--------------------------------------------------------------------
# QWeb template engine
#--------------------------------------------------------------------
class QWebException(Exception):
def __init__(self, message, **kw):
Exception.__init__(self, message)
self.qweb = dict(kw)
class QWebTemplateNotFound(QWebException):
pass
def convert_to_qweb_exception(etype=None, **kw):
if etype is None:
etype = QWebException
orig_type, original, tb = sys.exc_info()
try:
raise etype, original, tb
except etype, e:
for k, v in kw.items():
e.qweb[k] = v
e.qweb['inner'] = original
return e
class QWebContext(dict):
def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
self.cr = cr
self.uid = uid
self.loader = loader
self.templates = templates or {}
self.context = context
dic = dict(data)
super(QWebContext, self).__init__(dic)
self['defined'] = lambda key: key in self
def safe_eval(self, expr):
locals_dict = collections.defaultdict(lambda: None)
locals_dict.update(self)
locals_dict.pop('cr', None)
locals_dict.pop('loader', None)
return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
def copy(self):
return QWebContext(self.cr, self.uid, dict.copy(self),
loader=self.loader,
templates=self.templates,
context=self.context)
def __copy__(self):
return self.copy()
class QWeb(orm.AbstractModel):
"""QWeb Xml templating engine
The templating engine use a very simple syntax based "magic" xml
attributes, to produce textual output (even non-xml).
The core magic attributes are:
flow attributes:
t-if t-foreach t-call
output attributes:
t-att t-raw t-esc t-trim
assignation attribute:
t-set
QWeb can be extended like any OpenERP model and new attributes can be
added.
If you need to customize t-fields rendering, subclass the ir.qweb.field
model (and its sub-models) then override :meth:`~.get_converter_for` to
fetch the right field converters for your qweb model.
Beware that 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.
"""
_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._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
"""
n_prefix = len(prefix)
return dict(
(name[n_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 add_template(self, qwebcontext, name, node):
"""Add a parsed template in the context. Used to preprocess templates."""
qwebcontext.templates[name] = node
def load_document(self, document, qwebcontext):
"""
Loads an XML document and installs any contained template in the engine
"""
if hasattr(document, 'documentElement'):
dom = document
elif document.startswith("<?xml"):
dom = xml.dom.minidom.parseString(document)
else:
dom = xml.dom.minidom.parse(document)
for node in dom.documentElement.childNodes:
if node.nodeType == self.node.ELEMENT_NODE and node.getAttribute('t-name'):
name = str(node.getAttribute("t-name"))
self.add_template(qwebcontext, name, node)
def get_template(self, name, qwebcontext):
origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
if qwebcontext.loader and name not in qwebcontext.templates:
try:
xml_doc = qwebcontext.loader(name)
except ValueError:
raise convert_to_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
self.load_document(xml_doc, qwebcontext=qwebcontext)
if name in qwebcontext.templates:
return qwebcontext.templates[name]
raise convert_to_qweb_exception(QWebTemplateNotFound, message="Template %r not found" % name, template=origin_template)
def eval(self, expr, qwebcontext):
try:
return qwebcontext.safe_eval(expr)
except Exception:
template = qwebcontext.get('__template__')
raise convert_to_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
def eval_object(self, expr, qwebcontext):
return self.eval(expr, qwebcontext)
def eval_str(self, expr, qwebcontext):
if expr == "0":
return qwebcontext.get(0, '')
val = self.eval(expr, qwebcontext)
if isinstance(val, unicode):
return val.encode("utf8")
if val is False or val is None:
return ''
return str(val)
def eval_format(self, expr, qwebcontext):
expr, replacements = self._format_regex.subn(
lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
expr
)
if replacements:
return expr
try:
return str(expr % qwebcontext)
except Exception:
template = qwebcontext.get('__template__')
raise convert_to_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
def eval_bool(self, expr, qwebcontext):
return int(bool(self.eval(expr, qwebcontext)))
def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
if qwebcontext is None:
qwebcontext = {}
if not isinstance(qwebcontext, QWebContext):
qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
qwebcontext['__template__'] = id_or_xml_id
stack = qwebcontext.get('__stack__', [])
if stack:
qwebcontext['__caller__'] = stack[-1]
stack.append(id_or_xml_id)
qwebcontext['__stack__'] = stack
qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
def render_node(self, element, qwebcontext):
result = ""
if element.nodeType == self.node.TEXT_NODE or element.nodeType == self.node.CDATA_SECTION_NODE:
result = element.data.encode("utf8")
elif element.nodeType == self.node.ELEMENT_NODE:
generated_attributes = ""
t_render = None
template_attributes = {}
for (attribute_name, attribute_value) in element.attributes.items():
attribute_name = str(attribute_name)
if attribute_name == "groups":
cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
can_see = self.user_has_groups(cr, uid, groups=attribute_value)
if not can_see:
return ''
continue
if isinstance(attribute_value, unicode):
attribute_value = attribute_value.encode("utf8")
else:
attribute_value = attribute_value.nodeValue.encode("utf8")
if attribute_name.startswith("t-"):
for attribute in self._render_att:
if attribute_name[2:].startswith(attribute):
att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
generated_attributes += val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " "
break
else:
if attribute_name[2:] in self._render_tag:
t_render = attribute_name[2:]
template_attributes[attribute_name[2:]] = attribute_value
else:
generated_attributes += ' %s="%s"' % (attribute_name, werkzeug.utils.escape(attribute_value))
if 'debug' in template_attributes:
debugger = template_attributes.get('debug', 'pdb')
__import__(debugger).set_trace() # pdb, ipdb, pudb, ...
if t_render:
result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
else:
result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
if isinstance(result, unicode):
return result.encode('utf-8')
return result
def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
# element: element
# template_attributes: t-* attributes
# generated_attributes: generated attributes
# qwebcontext: values
# inner: optional innerXml
if inner:
g_inner = inner
else:
g_inner = []
for current_node in element.childNodes:
try:
g_inner.append(self.render_node(current_node, qwebcontext))
except QWebException:
raise
except Exception:
template = qwebcontext.get('__template__')
raise convert_to_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template)
name = str(element.nodeName)
inner = "".join(g_inner)
trim = template_attributes.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(
qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
for qwebcontext in (name, generated_attributes, inner, name)
)
else:
return "<%s%s/>" % (name, generated_attributes)
# Attributes
def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
if attribute_name.startswith("t-attf-"):
att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
elif attribute_name.startswith("t-att-"):
att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
if isinstance(val, unicode):
val = val.encode("utf8")
else:
att, val = self.eval_object(attribute_value, qwebcontext)
return att, val
# Tags
def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
inner = self.eval_str(template_attributes["raw"], qwebcontext)
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
inner = werkzeug.utils.escape(self.eval_str(template_attributes["esc"], qwebcontext))
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
expr = template_attributes["foreach"]
enum = self.eval_object(expr, qwebcontext)
if enum is not None:
var = template_attributes.get('as', expr).replace('.', '_')
copy_qwebcontext = qwebcontext.copy()
size = -1
if isinstance(enum, (list, tuple)):
size = len(enum)
elif hasattr(enum, 'count'):
size = enum.count()
copy_qwebcontext["%s_size" % var] = size
copy_qwebcontext["%s_all" % var] = enum
index = 0
ru = []
for i in enum:
copy_qwebcontext["%s_value" % var] = i
copy_qwebcontext["%s_index" % var] = index
copy_qwebcontext["%s_first" % var] = index == 0
copy_qwebcontext["%s_even" % var] = index % 2
copy_qwebcontext["%s_odd" % var] = (index + 1) % 2
copy_qwebcontext["%s_last" % var] = index + 1 == size
if index % 2:
copy_qwebcontext["%s_parity" % var] = 'odd'
else:
copy_qwebcontext["%s_parity" % var] = 'even'
if 'as' in template_attributes:
copy_qwebcontext[var] = i
elif isinstance(i, dict):
copy_qwebcontext.update(i)
ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
index += 1
return "".join(ru)
else:
template = qwebcontext.get('__template__')
raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
if self.eval_bool(template_attributes["if"], qwebcontext):
return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
return ""
def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
d = qwebcontext.copy()
d[0] = self.render_element(element, template_attributes, generated_attributes, d)
cr = d.get('request') and d['request'].cr or None
uid = d.get('request') and d['request'].uid or None
return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d)
def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
if "value" in template_attributes:
qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
elif "valuef" in template_attributes:
qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
else:
qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
return ""
def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
""" eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
node_name = element.nodeName
assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
"li", "ul", "ol", "dl", "dt", "dd"),\
"RTE widgets do not work correctly on %r elements" % node_name
assert node_name != 't',\
"t-field can not be used on a t element, provide an actual HTML node"
record, field_name = template_attributes["field"].rsplit('.', 1)
record = self.eval_object(record, qwebcontext)
column = record._model._all_columns[field_name].column
options = json.loads(template_attributes.get('field-options') or '{}')
field_type = get_field_type(column, options)
converter = self.get_converter_for(field_type)
return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
def get_converter_for(self, field_type):
return self.pool.get('ir.qweb.field.' + field_type,
self.pool['ir.qweb.field'])
#--------------------------------------------------------------------
# QWeb Fields converters
#--------------------------------------------------------------------
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,
context=None):
"""
Generates the metadata attributes (prefixed by ``data-oe-`` for the
root node of the field conversion. Attribute values are escaped by the
parent using ``werkzeug.utils.escape``.
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, context=None):
""" Converts a single value to its HTML version/output
"""
if not value: return ''
return value
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
""" Converts the specified field of the browse_record ``record`` to
HTML
"""
return self.value_to_html(
cr, uid, record[field_name], column, options=options, context=context)
def to_html(self, cr, uid, field_name, record, options,
source_element, t_att, g_att, qweb_context, context=None):
""" Converts a ``t-field`` to its HTML output. A ``t-field`` may be
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, context=context)
if options.get('html-escape', True):
content = werkzeug.utils.escape(content)
elif hasattr(content, '__html__'):
content = content.__html__()
except Exception:
_logger.warning("Could not get field %s for model %s",
field_name, record._model._name, exc_info=True)
content = None
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 '')
def user_lang(self, cr, uid, context):
"""
Fetches the res.lang object corresponding to the language code stored
in the user's context. Fallbacks to en_US if no lang is present in the
context *or the language code is not valid*.
:returns: res.lang browse_record
"""
if context is None: context = {}
lang_code = context.get('lang') or 'en_US'
Lang = self.pool['res.lang']
lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
or Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
return Lang.browse(cr, uid, lang_ids[0], context=context)
class FloatConverter(osv.AbstractModel):
_name = 'ir.qweb.field.float'
_inherit = 'ir.qweb.field'
def precision(self, cr, uid, column, options=None, context=None):
_, precision = column.digits or (None, None)
return precision
def value_to_html(self, cr, uid, value, column, options=None, context=None):
if context is None:
context = {}
precision = self.precision(cr, uid, column, options=options, context=context)
fmt = '%f' if precision is None else '%.{precision}f'
lang_code = context.get('lang') or 'en_US'
lang = self.pool['res.lang']
formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
# %f does not strip trailing zeroes. %g does but its precision causes
# it to switch to scientific notation starting at a million *and* to
# strip decimals. So use %f and if no precision was specified manually
# strip trailing 0.
if not precision:
formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
return formatted
class DateConverter(osv.AbstractModel):
_name = 'ir.qweb.field.date'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, column, options=None, context=None):
if not value: return ''
lang = self.user_lang(cr, uid, context=context)
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(
value, openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = lang.date_format
pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
return babel.dates.format_datetime(
value, format=pattern,
locale=locale)
class DateTimeConverter(osv.AbstractModel):
_name = 'ir.qweb.field.datetime'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, column, options=None, context=None):
if not value: return ''
lang = self.user_lang(cr, uid, context=context)
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(
value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
value = column.context_timestamp(
cr, uid, timestamp=value, context=context)
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
return babel.dates.format_datetime(value, format=pattern, locale=locale)
class TextConverter(osv.AbstractModel):
_name = 'ir.qweb.field.text'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, column, options=None, context=None):
"""
Escapes the value and converts newlines to br. This is bullshit.
"""
if not value: return ''
return nl2br(value, options=options)
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, context=None):
value = record[field_name]
if not value: return ''
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 record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
[read] = record.read([field_name])
if not read[field_name]: return ''
_, value = read[field_name]
return nl2br(value, options=options)
class HTMLConverter(osv.AbstractModel):
_name = 'ir.qweb.field.html'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, column, options=None, context=None):
return HTMLSafe(value or '')
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, context=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 HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
class MonetaryConverter(osv.AbstractModel):
""" ``monetary`` converter, has a mandatory option
``display_currency``.
The currency is used for formatting *and rounding* of the float value. It
is assumed that the linked res_currency has a non-empty rounding value and
res.currency's ``round`` method is used to perform rounding.
.. 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, context=None):
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, context=context)
def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
if context is None:
context = {}
Currency = self.pool['res.currency']
display = self.display_currency(cr, uid, options)
# lang.format mandates a sprintf-style format. These formats are non-
# minimal (they have a default fixed precision instead), and
# lang.format will not set one by default. currency.round will not
# provide one either. So we need to generate a precision value
# (integer > 0) from the currency's rounding (a float generally < 1.0).
#
# The log10 of the rounding should be the number of digits involved if
# negative, if positive clamp to 0 digits and call it a day.
# nb: int() ~ floor(), we want nearest rounding instead
precision = int(round(math.log10(display.rounding)))
fmt = "%.{0}f".format(-precision if precision < 0 else 0)
lang_code = context.get('lang') or 'en_US'
lang = self.pool['res.lang']
formatted_amount = lang.format(cr, uid, [lang_code],
fmt, Currency.round(cr, uid, display, record[field_name]),
grouping=True, monetary=True)
pre = post = u''
if display.position == 'before':
pre = u'{symbol} '
else:
post = u' {symbol}'
return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
formatted_amount,
pre=pre, post=post,
).format(
symbol=display.symbol,
))
def display_currency(self, cr, uid, options):
return self.qweb_object().eval_object(
options['display_currency'], options['_qweb_context'])
TIMEDELTA_UNITS = (
('year', 3600 * 24 * 365),
('month', 3600 * 24 * 30),
('week', 3600 * 24 * 7),
('day', 3600 * 24),
('hour', 3600),
('minute', 60),
('second', 1)
)
class DurationConverter(osv.AbstractModel):
""" ``duration`` converter, to display integral or fractional values as
human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
Can be used on any numerical field.
Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
field value before converting it.
Sub-second values will be ignored.
"""
_name = 'ir.qweb.field.duration'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, column, options=None, context=None):
units = dict(TIMEDELTA_UNITS)
if value < 0:
raise ValueError(_("Durations can't be negative"))
if not options or options.get('unit') not in units:
raise ValueError(_("A unit must be provided to duration widgets"))
locale = babel.Locale.parse(
self.user_lang(cr, uid, context=context).code)
factor = units[options['unit']]
sections = []
r = value * factor
for unit, secs_per_unit in TIMEDELTA_UNITS:
v, r = divmod(r, secs_per_unit)
if not v: continue
section = babel.dates.format_timedelta(
v*secs_per_unit, threshold=1, locale=locale)
if section:
sections.append(section)
return u' '.join(sections)
class RelativeDatetimeConverter(osv.AbstractModel):
_name = 'ir.qweb.field.relative'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, column, options=None, context=None):
parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
locale = babel.Locale.parse(
self.user_lang(cr, uid, context=context).code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(value, parse_format)
# value should be a naive datetime in UTC. So is fields.datetime.now()
reference = datetime.datetime.strptime(column.now(), parse_format)
return babel.dates.format_timedelta(
value - reference, add_direction=True, locale=locale)
class HTMLSafe(object):
""" HTMLSafe string wrapper, Werkzeug's escape() has special handling for
objects with a ``__html__`` methods but AFAIK does not provide any such
object.
Wrapping a string in HTML will prevent its escaping
"""
__slots__ = ['string']
def __init__(self, string):
self.string = string
def __html__(self):
return self.string
def __str__(self):
s = self.string
if isinstance(s, unicode):
return s.encode('utf-8')
return s
def __unicode__(self):
s = self.string
if isinstance(s, str):
return s.decode('utf-8')
return s
def nl2br(string, options=None):
""" Converts newlines to HTML linebreaks in ``string``. Automatically
escapes content unless options['html-escape'] is set to False, and returns
the result wrapped in an HTMLSafe object.
:param str string:
:param dict options:
:rtype: HTMLSafe
"""
if options is None: options = {}
if options.get('html-escape', True):
string = werkzeug.utils.escape(string)
return HTMLSafe(string.replace('\n', '<br>\n'))
def get_field_type(column, options):
""" Gets a t-field's effective type from the field's column and its options
"""
return options.get('widget', column._type)
# vim:et:

View File

@ -268,13 +268,8 @@ class ir_translation(osv.osv):
return translations
def _set_ids(self, cr, uid, name, tt, lang, ids, value, src=None):
# clear the caches
tr = self._get_ids(cr, uid, name, tt, lang, ids)
for res_id in tr:
if tr[res_id]:
self._get_source.clear_cache(self, uid, name, tt, lang, tr[res_id])
self._get_ids.clear_cache(self, uid, name, tt, lang, res_id)
self._get_source.clear_cache(self, uid, name, tt, lang)
self._get_ids.clear_cache(self)
self._get_source.clear_cache(self)
cr.execute('delete from ir_translation '
'where lang=%s '
@ -294,7 +289,7 @@ class ir_translation(osv.osv):
return len(ids)
@tools.ormcache(skiparg=3)
def _get_source(self, cr, uid, name, types, lang, source=None):
def _get_source(self, cr, uid, name, types, lang, source=None, res_id=None):
"""
Returns the translation for the given combination of name, type, language
and source. All values passed to this method should be unicode (not byte strings),
@ -304,6 +299,7 @@ class ir_translation(osv.osv):
:param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
:param lang: language code of the desired translation
:param source: optional source term to translate (should be unicode)
:param res_id: optional resource id to translate (if used, ``source`` should be set)
:rtype: unicode
:return: the request translation, or an empty unicode string if no translation was
found and `source` was not passed
@ -321,6 +317,9 @@ class ir_translation(osv.osv):
AND type in %s
AND src=%s"""
params = (lang or '', types, tools.ustr(source))
if res_id:
query += "AND res_id=%s"
params += (res_id,)
if name:
query += " AND name=%s"
params += (tools.ustr(name),)
@ -342,8 +341,9 @@ class ir_translation(osv.osv):
if context is None:
context = {}
ids = super(ir_translation, self).create(cr, uid, vals, context=context)
self._get_source.clear_cache(self, uid, vals.get('name',0), vals.get('type',0), vals.get('lang',0), vals.get('src',0))
self._get_ids.clear_cache(self, uid, vals.get('name',0), vals.get('type',0), vals.get('lang',0), vals.get('res_id',0))
self._get_source.clear_cache(self)
self._get_ids.clear_cache(self)
self.pool['ir.ui.view'].clear_cache()
return ids
def write(self, cursor, user, ids, vals, context=None):
@ -356,9 +356,9 @@ class ir_translation(osv.osv):
if vals.get('value'):
vals.update({'state':'translated'})
result = super(ir_translation, self).write(cursor, user, ids, vals, context=context)
for trans_obj in self.read(cursor, user, ids, ['name','type','res_id','src','lang'], context=context):
self._get_source.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['src'])
self._get_ids.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['res_id'])
self._get_source.clear_cache(self)
self._get_ids.clear_cache(self)
self.pool['ir.ui.view'].clear_cache()
return result
def unlink(self, cursor, user, ids, context=None):
@ -366,9 +366,9 @@ class ir_translation(osv.osv):
context = {}
if isinstance(ids, (int, long)):
ids = [ids]
for trans_obj in self.read(cursor, user, ids, ['name','type','res_id','src','lang'], context=context):
self._get_source.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['src'])
self._get_ids.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['res_id'])
self._get_source.clear_cache(self)
self._get_ids.clear_cache(self)
result = super(ir_translation, self).unlink(cursor, user, ids, context=context)
return result

View File

@ -18,20 +18,28 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import collections
import copy
import logging
from lxml import etree
from operator import itemgetter
import os
import HTMLParser
import openerp
from openerp import tools
from openerp.osv import fields,osv
from openerp.tools import graph
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
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath']
class view_custom(osv.osv):
_name = 'ir.ui.view.custom'
_order = 'create_date desc' # search(limit=1) should return the last customization
@ -50,59 +58,45 @@ class view_custom(osv.osv):
class view(osv.osv):
_name = 'ir.ui.view'
def _type_field(self, cr, uid, ids, name, args, context=None):
result = {}
for record in self.browse(cr, uid, ids, context):
# Get the type from the inherited view if any.
if record.inherit_id:
result[record.id] = record.inherit_id.type
else:
result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag
def _get_model_data(self, cr, uid, ids, *args, **kwargs):
ir_model_data = self.pool.get('ir.model.data')
data_ids = ir_model_data.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)])
result = dict(zip(ids, data_ids))
return result
_columns = {
'name': fields.char('View Name', required=True),
'model': fields.char('Object', size=64, required=True, select=True),
'model': fields.char('Object', select=True),
'priority': fields.integer('Sequence', required=True),
'type': fields.function(_type_field, type='selection', selection=[
'type': fields.selection([
('tree','Tree'),
('form','Form'),
('mdx','mdx'),
('graph', 'Graph'),
('calendar', 'Calendar'),
('diagram','Diagram'),
('gantt', 'Gantt'),
('kanban', 'Kanban'),
('search','Search')], string='View Type', required=True, select=True, store=True),
('search','Search'),
('qweb', 'QWeb')], string='View Type'),
'arch': fields.text('View Architecture', required=True),
'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
'field_parent': fields.char('Child Field',size=64),
'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
'field_parent': fields.char('Child Field'),
'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data", store=True),
'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
help="ID of the view defined in xml file"),
'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
}
_defaults = {
'arch': '<?xml version="1.0"?>\n<tree string="My view">\n\t<field name="name"/>\n</tree>',
'priority': 16,
'type': 'tree',
}
_order = "priority,name"
# Holds the RNG schema
_relaxng_validator = None
def create(self, cr, uid, values, context=None):
if 'type' in values:
_logger.warning("Setting the `type` field is deprecated in the `ir.ui.view` model.")
if not values.get('name'):
if values.get('inherit_id'):
inferred_type = self.browse(cr, uid, values['inherit_id'], context).type
else:
inferred_type = etree.fromstring(values['arch'].encode('utf8')).tag
values['name'] = "%s %s" % (values['model'], inferred_type)
return super(view, self).create(cr, uid, values, context)
def _relaxng(self):
if not self._relaxng_validator:
frng = tools.file_open(os.path.join('base','rng','view.rng'))
@ -115,59 +109,37 @@ class view(osv.osv):
frng.close()
return self._relaxng_validator
def _check_render_view(self, cr, uid, view, context=None):
"""Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by
its inherited views, by rendering it using ``fields_view_get()``.
@param browse_record view: view to validate
@return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention)
if no error occurred, else False.
"""
if view.model not in self.pool:
return False
try:
fvg = self.pool[view.model].fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context)
return fvg['arch']
except Exception:
_logger.exception('cannot render view %s', view.xml_id)
return False
def _check_xml(self, cr, uid, ids, context=None):
if context is None:
context = {}
context['check_view_ids'] = ids
context = dict(context, check_view_ids=ids)
# Sanity checks: the view should not break anything upon rendering!
# Any exception raised below will cause a transaction rollback.
for view in self.browse(cr, uid, ids, context):
# Sanity check: the view should not break anything upon rendering!
view_arch_utf8 = self._check_render_view(cr, uid, view, context=context)
# always utf-8 bytestring - legacy convention
if not view_arch_utf8: return False
# RNG-based validation is not possible anymore with 7.0 forms
# TODO 7.0: provide alternative assertion-based validation of view_arch_utf8
view_docs = [etree.fromstring(view_arch_utf8)]
if view_docs[0].tag == 'data':
# A <data> element is a wrapper for multiple root nodes
view_docs = view_docs[0]
validator = self._relaxng()
for view_arch in view_docs:
if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
for error in validator.error_log:
_logger.error(tools.ustr(error))
return False
if not valid_view(view_arch):
return False
return True
def _check_model(self, cr, uid, ids, context=None):
for view in self.browse(cr, uid, ids, context):
if view.model not in self.pool:
return False
view_def = self.read_combined(cr, uid, view.id, None, context=context)
view_arch_utf8 = view_def['arch']
if view.type != 'qweb':
view_doc = etree.fromstring(view_arch_utf8)
# verify that all fields used are valid, etc.
self.postprocess_and_fields(cr, uid, view.model, view_doc, view.id, context=context)
# RNG-based validation is not possible anymore with 7.0 forms
view_docs = [view_doc]
if view_docs[0].tag == 'data':
# A <data> element is a wrapper for multiple root nodes
view_docs = view_docs[0]
validator = self._relaxng()
for view_arch in view_docs:
if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
for error in validator.error_log:
_logger.error(tools.ustr(error))
return False
if not valid_view(view_arch):
return False
return True
_constraints = [
(_check_model, 'The model name does not exist.', ['model']),
(_check_xml, 'The model name does not exist or the view architecture cannot be rendered.', ['arch', 'model']),
(_check_xml, 'Invalid view definition', ['arch'])
]
def _auto_init(self, cr, context=None):
@ -176,6 +148,73 @@ class view(osv.osv):
if not cr.fetchone():
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
def create(self, cr, uid, values, context=None):
if 'type' not in values:
if values.get('inherit_id'):
values['type'] = self.browse(cr, uid, values['inherit_id'], context).type
else:
values['type'] = etree.fromstring(values['arch']).tag
if not values.get('name'):
values['name'] = "%s %s" % (values['model'], values['type'])
self.read_template.clear_cache(self)
return super(view, self).create(cr, uid, values, context)
def write(self, cr, uid, ids, vals, context=None):
if not isinstance(ids, (list, tuple)):
ids = [ids]
if context is None:
context = {}
# drop the corresponding view customizations (used for dashboards for example), otherwise
# not all users would see the updated views
custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id', 'in', ids)])
if custom_view_ids:
self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
self.read_template.clear_cache(self)
ret = super(view, self).write(cr, uid, ids, vals, context)
# if arch is modified views become noupdatable
if 'arch' in vals and not context.get('install_mode', False):
# TODO: should be doable in a read and a write
for view_ in self.browse(cr, uid, ids, context=context):
if view_.model_data_id:
self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True})
return ret
def copy(self, cr, uid, id, default=None, context=None):
if not default:
default = {}
default.update({
'model_ids': [],
})
return super(view, self).copy(cr, uid, id, default, context=context)
# default view selection
def default_view(self, cr, uid, model, view_type, context=None):
""" Fetches the default view for the provided (model, view_type) pair:
view with no parent (inherit_id=Fase) with the lowest priority.
:param str model:
:param int view_type:
:return: id of the default view of False if none found
:rtype: int
"""
domain = [
['model', '=', model],
['type', '=', view_type],
['inherit_id', '=', False],
]
ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)
if not ids:
return False
return ids[0]
#------------------------------------------------------
# Inheritance mecanism
#------------------------------------------------------
def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
"""Retrieves the architecture of views that inherit from the given view, from the sets of
views that should currently be used in the system. During the module upgrade phase it
@ -185,43 +224,595 @@ class view(osv.osv):
after the module initialization phase is completely finished.
:param int view_id: id of the view whose inheriting views should be retrieved
:param str model: model identifier of the view's related model (for double-checking)
:param str model: model identifier of the inheriting views.
:rtype: list of tuples
:return: [(view_arch,view_id), ...]
"""
user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
check_view_ids = context and context.get('check_view_ids') or (0,)
conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
if self.pool._init:
# Module init currently in progress, only consider views from modules whose code was already loaded
check_view_ids = context and context.get('check_view_ids') or (0,)
query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
WHERE v.inherit_id=%s AND v.model=%s AND (md.module in %s OR v.id in %s)
ORDER BY priority"""
query_params = (view_id, model, tuple(self.pool._init_modules), tuple(check_view_ids))
else:
# Modules fully loaded, consider all views
query = """SELECT v.id FROM ir_ui_view v
WHERE v.inherit_id=%s AND v.model=%s
ORDER BY priority"""
query_params = (view_id, model)
cr.execute(query, query_params)
view_ids = [v[0] for v in cr.fetchall()]
# filter views based on user groups
# Module init currently in progress, only consider views from
# modules whose code is already loaded
conditions.extend([
'|',
['model_ids.module', 'in', tuple(self.pool._init_modules)],
['id', 'in', check_view_ids],
])
view_ids = self.search(cr, uid, conditions, context=context)
return [(view.arch, view.id)
for view in self.browse(cr, 1, view_ids, context)
if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
def write(self, cr, uid, ids, vals, context=None):
if not isinstance(ids, (list, tuple)):
ids = [ids]
def raise_view_error(self, cr, uid, message, view_id, context=None):
view = self.browse(cr, uid, view_id, context)
not_avail = _('n/a')
message = ("%(msg)s\n\n" +
_("Error context:\nView `%(view_name)s`") +
"\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
"model: %(model)s, parent_id: %(parent)s]") % \
{
'view_name': view.name or not_avail,
'viewid': view_id or not_avail,
'xmlid': view.xml_id or not_avail,
'model': view.model or not_avail,
'parent': view.inherit_id.id or not_avail,
'msg': message,
}
_logger.error(message)
raise AttributeError(message)
# drop the corresponding view customizations (used for dashboards for example), otherwise
# not all users would see the updated views
custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)])
if custom_view_ids:
self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
def locate_node(self, arch, spec):
""" Locate a node in a source (parent) architecture.
return super(view, self).write(cr, uid, ids, vals, context)
Given a complete source (parent) architecture (i.e. the field
`arch` in a view), and a 'spec' node (a node in an inheriting
view that specifies the location in the source view of what
should be changed), return (if it exists) the node in the
source view matching the specification.
:param arch: a parent architecture to modify
:param spec: a modifying node in an inheriting view
:return: a node in the source matching the spec
"""
if spec.tag == 'xpath':
nodes = arch.xpath(spec.get('expr'))
return nodes[0] if nodes else None
elif spec.tag == 'field':
# Only compare the field name: a field can be only once in a given view
# at a given level (and for multilevel expressions, we should use xpath
# inheritance spec anyway).
for node in arch.iter('field'):
if node.get('name') == spec.get('name'):
return node
return None
for node in arch.iter(spec.tag):
if isinstance(node, SKIPPED_ELEMENT_TYPES):
continue
if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
if attr not in ('position','version')):
# Version spec should match parent's root element's version
if spec.get('version') and spec.get('version') != arch.get('version'):
return None
return node
return None
def inherit_branding(self, specs_tree, view_id, source_id):
for node in specs_tree.iterchildren(tag=etree.Element):
xpath = node.getroottree().getpath(node)
if node.tag == 'data' or node.tag == 'xpath':
self.inherit_branding(node, view_id, source_id)
else:
node.set('data-oe-id', str(view_id))
node.set('data-oe-source-id', str(source_id))
node.set('data-oe-xpath', xpath)
node.set('data-oe-model', 'ir.ui.view')
node.set('data-oe-field', 'arch')
return specs_tree
def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None):
""" Apply an inheriting view (a descendant of the base view)
Apply to a source architecture all the spec nodes (i.e. nodes
describing where and what changes to apply to some parent
architecture) given by an inheriting view.
:param Element source: a parent architecture to modify
:param Elepect specs_tree: a modifying architecture in an inheriting view
:param inherit_id: the database id of specs_arch
:return: a modified source where the specs are applied
:rtype: Element
"""
# Queue of specification nodes (i.e. nodes describing where and
# changes to apply to some parent architecture).
specs = [specs_tree]
while len(specs):
spec = specs.pop(0)
if isinstance(spec, SKIPPED_ELEMENT_TYPES):
continue
if spec.tag == 'data':
specs += [ c for c in specs_tree ]
continue
node = self.locate_node(source, spec)
if node is not None:
pos = spec.get('position', 'inside')
if pos == 'replace':
if node.getparent() is None:
source = copy.deepcopy(spec[0])
else:
for child in spec:
node.addprevious(child)
node.getparent().remove(node)
elif pos == 'attributes':
for child in spec.getiterator('attribute'):
attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
if attribute[1]:
node.set(attribute[0], attribute[1])
elif attribute[0] in node.attrib:
del node.attrib[attribute[0]]
else:
sib = node.getnext()
for child in spec:
if pos == 'inside':
node.append(child)
elif pos == 'after':
if sib is None:
node.addnext(child)
node = child
else:
sib.addprevious(child)
elif pos == 'before':
node.addprevious(child)
else:
self.raise_view_error(cr, uid, _("Invalid position attribute: '%s'") % pos, inherit_id, context=context)
else:
attrs = ''.join([
' %s="%s"' % (attr, spec.get(attr))
for attr in spec.attrib
if attr != 'position'
])
tag = "<%s%s>" % (spec.tag, attrs)
self.raise_view_error(cr, uid, _("Element '%s' cannot be located in parent view") % tag, inherit_id, context=context)
return source
def apply_view_inheritance(self, cr, uid, source, source_id, model, context=None):
""" Apply all the (directly and indirectly) inheriting views.
:param source: a parent architecture to modify (with parent modifications already applied)
:param source_id: the database view_id of the parent view
:param model: the original model for which we create a view (not
necessarily the same as the source's model); only the inheriting
views with that specific model will be applied.
:return: a modified source where all the modifying architecture are applied
"""
if context is None: context = {}
sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
for (specs, view_id) in sql_inherit:
specs_tree = etree.fromstring(specs.encode('utf-8'))
if context.get('inherit_branding'):
self.inherit_branding(specs_tree, view_id, source_id)
source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context)
return source
def read_combined(self, cr, uid, view_id, fields=None, context=None):
"""
Utility function to get a view combined with its inherited views.
* Gets the top of the view tree if a sub-view is requested
* Applies all inherited archs on the root view
* Returns the view with all requested fields
.. note:: ``arch`` is always added to the fields list even if not
requested (similar to ``id``)
"""
if context is None: context = {}
# if view_id is not a root view, climb back to the top.
base = v = self.browse(cr, uid, view_id, context=context)
while v.inherit_id:
v = v.inherit_id
root_id = v.id
# arch and model fields are always returned
if fields:
fields = list(set(fields) | set(['arch', 'model']))
# read the view arch
[view] = self.read(cr, uid, [root_id], fields=fields, context=context)
arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
if context.get('inherit_branding'):
arch_tree.attrib.update({
'data-oe-model': 'ir.ui.view',
'data-oe-id': str(root_id),
'data-oe-field': 'arch',
})
# and apply inheritance
arch = self.apply_view_inheritance(
cr, uid, arch_tree, root_id, base.model, context=context)
return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
#------------------------------------------------------
# Postprocessing: translation, groups and modifiers
#------------------------------------------------------
# TODO:
# - split postprocess so that it can be used instead of translate_qweb
# - remove group processing from ir_qweb
#------------------------------------------------------
def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None):
"""Return the description of the fields in the node.
In a normal call to this method, node is a complete view architecture
but it is actually possible to give some sub-node (this is used so
that the method can call itself recursively).
Originally, the field descriptions are drawn from the node itself.
But there is now some code calling fields_get() in order to merge some
of those information in the architecture.
"""
if context is None:
context = {}
result = False
fields = {}
children = True
modifiers = {}
Model = self.pool.get(model)
if not Model:
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
view_id, context)
def encode(s):
if isinstance(s, unicode):
return s.encode('utf8')
return s
def check_group(node):
"""Apply group restrictions, may be set at view level or model level::
* at view level this means the element should be made invisible to
people who are not members
* at model level (exclusively for fields, obviously), this means
the field should be completely removed from the view, as it is
completely unavailable for non-members
:return: True if field should be included in the result of fields_view_get
"""
if node.tag == 'field' and node.get('name') in Model._all_columns:
column = Model._all_columns[node.get('name')].column
if column.groups and not self.user_has_groups(
cr, user, groups=column.groups, context=context):
node.getparent().remove(node)
fields.pop(node.get('name'), None)
# no point processing view-level ``groups`` anymore, return
return False
if node.get('groups'):
can_see = self.user_has_groups(
cr, user, groups=node.get('groups'), context=context)
if not can_see:
node.set('invisible', '1')
modifiers['invisible'] = True
if 'attrs' in node.attrib:
del(node.attrib['attrs']) #avoid making field visible later
del(node.attrib['groups'])
return True
if node.tag in ('field', 'node', 'arrow'):
if node.get('object'):
attrs = {}
views = {}
xml = "<form>"
for f in node:
if f.tag == 'field':
xml += etree.tostring(f, encoding="utf-8")
xml += "</form>"
new_xml = etree.fromstring(encode(xml))
ctx = context.copy()
ctx['base_model_name'] = model
xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
views['form'] = {
'arch': xarch,
'fields': xfields
}
attrs = {'views': views}
fields = xfields
if node.get('name'):
attrs = {}
try:
if node.get('name') in Model._columns:
column = Model._columns[node.get('name')]
else:
column = Model._inherit_fields[node.get('name')][2]
except Exception:
column = False
if column:
children = False
views = {}
for f in node:
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(f)
ctx = context.copy()
ctx['base_model_name'] = Model
xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
views[str(f.tag)] = {
'arch': xarch,
'fields': xfields
}
attrs = {'views': views}
fields[node.get('name')] = attrs
field = model_fields.get(node.get('name'))
if field:
orm.transfer_field_to_modifiers(field, modifiers)
elif node.tag in ('form', 'tree'):
result = Model.view_header_get(cr, user, False, node.tag, context)
if result:
node.set('string', result)
in_tree_view = node.tag == 'tree'
elif node.tag == 'calendar':
for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day', 'attendee'):
if node.get(additional_field):
fields[node.get(additional_field)] = {}
if not check_group(node):
# node must be removed, no need to proceed further with its children
return fields
# The view architeture overrides the python model.
# Get the attrs before they are (possibly) deleted by check_group below
orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
# TODO remove attrs counterpart in modifiers when invisible is true ?
# translate view
if 'lang' in context:
Translations = self.pool['ir.translation']
if node.text and node.text.strip():
trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
if trans:
node.text = node.text.replace(node.text.strip(), trans)
if node.tail and node.tail.strip():
trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
if trans:
node.tail = node.tail.replace(node.tail.strip(), trans)
if node.get('string') and not result:
trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
if trans == node.get('string') and ('base_model_name' in context):
# If translation is same as source, perhaps we'd have more luck with the alternative model name
# (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
if trans:
node.set('string', trans)
for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
attr_value = node.get(attr_name)
if attr_value:
trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
if trans:
node.set(attr_name, trans)
for f in node:
if children or (node.tag == 'field' and f.tag in ('filter','separator')):
fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
orm.transfer_modifiers_to_node(modifiers, node)
return fields
def _disable_workflow_buttons(self, cr, user, model, node):
""" Set the buttons in node to readonly if the user can't activate them. """
if model is None or user == 1:
# admin user can always activate workflow buttons
return node
# TODO handle the case of more than one workflow for a model or multiple
# transitions with different groups and same signal
usersobj = self.pool.get('res.users')
buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
for button in buttons:
user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
cr.execute("""SELECT DISTINCT t.group_id
FROM wkf
INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
INNER JOIN wkf_transition t ON (t.act_to = a.id)
WHERE wkf.osv = %s
AND t.signal = %s
AND t.group_id is NOT NULL
""", (model, button.get('name')))
group_ids = [x[0] for x in cr.fetchall() if x[0]]
can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
button.set('readonly', str(int(not can_click)))
return node
def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
""" Return an architecture and a description of all the fields.
The field description combines the result of fields_get() and
postprocess().
:param node: the architecture as as an etree
:return: a tuple (arch, fields) where arch is the given node as a
string and fields is the description of all the fields.
"""
fields = {}
Model = self.pool.get(model)
if not Model:
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
if node.tag == 'diagram':
if node.getchildren()[0].tag == 'node':
node_model = self.pool[node.getchildren()[0].get('object')]
node_fields = node_model.fields_get(cr, user, None, context)
fields.update(node_fields)
if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
node.set("create", 'false')
if node.getchildren()[1].tag == 'arrow':
arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
fields.update(arrow_fields)
else:
fields = Model.fields_get(cr, user, None, context)
fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
node = self._disable_workflow_buttons(cr, user, model, node)
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
node.set(action, 'false')
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
for k in fields.keys():
if k not in fields_def:
del fields[k]
for field in fields_def:
if field == 'id':
# sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
elif field in fields:
fields[field].update(fields_def[field])
else:
message = _("Field `%(field_name)s` does not exist") % \
dict(field_name=field)
self.raise_view_error(cr, user, message, view_id, context)
return arch, fields
#------------------------------------------------------
# QWeb template views
#------------------------------------------------------
@tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
def read_template(self, cr, uid, xml_id, context=None):
if '.' not in xml_id:
raise ValueError('Invalid template id: %r' % (xml_id,))
view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
arch_tree = etree.fromstring(arch)
if 'lang' in context:
arch_tree = self.translate_qweb(cr, uid, view_id, arch_tree, context['lang'], context)
self.distribute_branding(arch_tree)
root = etree.Element('templates')
root.append(arch_tree)
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
def clear_cache(self):
self.read_template.clear_cache(self)
def distribute_branding(self, e, branding=None, parent_xpath='',
index_map=misc.ConstantMapping(1)):
if e.get('t-ignore') or e.tag == 'head':
# TODO: find a better name and check if we have a string to boolean helper
return
node_path = e.get('data-oe-xpath')
if node_path is None:
node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
if branding and not (e.get('data-oe-model') or e.get('t-field')):
e.attrib.update(branding)
e.set('data-oe-xpath', node_path)
if not e.get('data-oe-model'): return
# if a branded element contains branded elements distribute own
# branding to children unless it's t-raw, then just remove branding
# on current element
if e.tag == 't' or 't-raw' in e.attrib or \
any(self.is_node_branded(child) for child in e.iterdescendants()):
distributed_branding = dict(
(attribute, e.attrib.pop(attribute))
for attribute in MOVABLE_BRANDING
if e.get(attribute))
if 't-raw' not in e.attrib:
# TODO: collections.Counter if remove p2.6 compat
# running index by tag type, for XPath query generation
indexes = collections.defaultdict(lambda: 0)
for child in e.iterchildren(tag=etree.Element):
indexes[child.tag] += 1
self.distribute_branding(child, distributed_branding,
parent_xpath=node_path,
index_map=indexes)
def is_node_branded(self, node):
""" Finds out whether a node is branded or qweb-active (bears a
@data-oe-model or a @t-* *which is not t-field* as t-field does not
section out views)
:param node: an etree-compatible element to test
:type node: etree._Element
:rtype: boolean
"""
return any(
(attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
for attr in node.attrib
)
def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
# TODO: this should be moved in a place before inheritance is applied
# but process() is only called on fields_view_get()
Translations = self.pool['ir.translation']
h = HTMLParser.HTMLParser()
def get_trans(text):
if not text or not text.strip():
return None
text = h.unescape(text.strip())
if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
return None
return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
if arch.tag not in ['script']:
text = get_trans(arch.text)
if text:
arch.text = arch.text.replace(arch.text.strip(), text)
tail = get_trans(arch.tail)
if tail:
arch.tail = arch.tail.replace(arch.tail.strip(), tail)
for attr_name in ('title', 'alt', 'placeholder'):
attr = get_trans(arch.get(attr_name))
if attr:
arch.set(attr_name, attr)
for node in arch.iterchildren("*"):
self.translate_qweb(cr, uid, id_, node, lang, context)
return arch
@openerp.tools.ormcache()
def get_view_xmlid(self, cr, uid, id):
imd = self.pool['ir.model.data']
domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)]
xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
return '%s.%s' % (xmlid['module'], xmlid['name'])
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
if isinstance(id_or_xml_id, list):
id_or_xml_id = id_or_xml_id[0]
tname = id_or_xml_id
if isinstance(tname, (int, long)):
tname = self.get_view_xmlid(cr, uid, tname)
if not context:
context = {}
def loader(name):
return self.read_template(cr, uid, name, context=context)
return self.pool[engine].render(cr, uid, tname, values, loader=loader, context=context)
#------------------------------------------------------
# Misc
#------------------------------------------------------
def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
nodes=[]
@ -305,5 +896,4 @@ class view(osv.osv):
ids = map(itemgetter(0), cr.fetchall())
return self._check_xml(cr, uid, ids)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
# vim:et:

View File

@ -17,6 +17,7 @@
<group>
<field name="field_parent"/>
<field name="inherit_id"/>
<field name="model_data_id"/>
<field name="xml_id"/>
</group>
</group>
@ -49,17 +50,19 @@
<field name="model">ir.ui.view</field>
<field name="arch" type="xml">
<search string="Views">
<field name="name" filter_domain="['|', ('name','ilike',self), ('model','ilike',self)]" string="View"/>
<field name="name" filter_domain="['|', '|', ('name','ilike',self), ('model','ilike',self), ('model_data_id','ilike',self)]" string="View"/>
<filter string="Form" domain="[('type', '=','form')]"/>
<filter string="Tree" domain="[('type', '=', 'tree')]"/>
<filter string="Kanban" domain="[('type', '=', 'kanban')]"/>
<filter string="Search" domain="[('type', '=', 'search')]"/>
<filter string="QWeb" domain="[('type', '=', 'qweb')]"/>
<field name="model"/>
<field name="inherit_id"/>
<field name="type"/>
<group expand="0" string="Group By...">
<filter string="Object" icon="terp-stock_align_left_24" domain="[]" context="{'group_by':'model'}"/>
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'type'}"/>
<filter string="Object" domain="[]" context="{'group_by':'model'}"/>
<filter string="Type" domain="[]" context="{'group_by':'type'}"/>
<filter string="Inherit" domain="[]" context="{'group_by':'inherit_id'}"/>
</group>
</search>
</field>

View File

@ -7,6 +7,7 @@
<field name="visible" eval="0" />
</record>
<record model="ir.module.category" id="module_category_localization">
<field name="name">Localization</field>
<field name="visible" eval="0" />
@ -113,6 +114,11 @@
<field name="sequence">15</field>
</record>
<record model="ir.module.category" id="module_category_website">
<field name="name">Website</field>
<field name="sequence">16</field>
</record>
<record model="ir.module.category" id="module_category_administration">
<field name="name">Administration</field>
<field name="sequence">100</field>

View File

@ -141,8 +141,8 @@ class res_company(osv.osv):
'state_id': fields.function(_get_address_data, fnct_inv=_set_address_data, type='many2one', relation='res.country.state', string="Fed. State", multi='address'),
'bank_ids': fields.one2many('res.partner.bank','company_id', 'Bank Accounts', help='Bank accounts related to this company'),
'country_id': fields.function(_get_address_data, fnct_inv=_set_address_data, type='many2one', relation='res.country', string="Country", multi='address'),
'email': fields.function(_get_address_data, fnct_inv=_set_address_data, size=64, type='char', string="Email", multi='address'),
'phone': fields.function(_get_address_data, fnct_inv=_set_address_data, size=64, type='char', string="Phone", multi='address'),
'email': fields.related('partner_id', 'email', size=64, type='char', string="Email", store=True),
'phone': fields.related('partner_id', 'phone', size=64, type='char', string="Phone", store=True),
'fax': fields.function(_get_address_data, fnct_inv=_set_address_data, size=64, type='char', string="Fax", multi='address'),
'website': fields.related('partner_id', 'website', string="Website", type="char", size=64),
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),

View File

@ -415,6 +415,7 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
* For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
to/from the implied groups of 'group', depending on the field's value.
By default 'group' is the group Employee. Groups are given by their xml id.
The attribute 'group' may contain several xml ids, separated by commas.
* For a boolean field like 'module_XXX', ``execute`` triggers the immediate
installation of the module named 'XXX' if the field has value ``True``.
@ -437,7 +438,7 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
""" return a dictionary with the fields classified by category::
{ 'default': [('default_foo', 'model', 'foo'), ...],
'group': [('group_bar', browse_group, browse_implied_group), ...],
'group': [('group_bar', [browse_group], browse_implied_group), ...],
'module': [('module_baz', browse_module), ...],
'other': ['other_field', ...],
}
@ -446,15 +447,15 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
ir_module = self.pool['ir.module.module']
def ref(xml_id):
mod, xml = xml_id.split('.', 1)
return ir_model_data.get_object(cr, uid, mod, xml, context)
return ir_model_data.get_object(cr, uid, mod, xml, context=context)
defaults, groups, modules, others = [], [], [], []
for name, field in self._columns.items():
if name.startswith('default_') and hasattr(field, 'default_model'):
defaults.append((name, field.default_model, name[8:]))
elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
field_group = getattr(field, 'group', 'base.group_user')
groups.append((name, ref(field_group), ref(field.implied_group)))
field_groups = getattr(field, 'group', 'base.group_user').split(',')
groups.append((name, map(ref, field_groups), ref(field.implied_group)))
elif name.startswith('module_') and isinstance(field, fields.boolean):
mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
@ -477,8 +478,8 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
res[name] = value
# groups: which groups are implied by the group Employee
for name, group, implied_group in classified['group']:
res[name] = implied_group in group.implied_ids
for name, groups, implied_group in classified['group']:
res[name] = all(implied_group in group.implied_ids for group in groups)
# modules: which modules are installed/to install
for name, module in classified['module']:
@ -497,6 +498,7 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
ir_values = self.pool['ir.values']
ir_module = self.pool['ir.module.module']
res_groups = self.pool['res.groups']
classified = self._get_classified_fields(cr, uid, context)
@ -507,12 +509,16 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
ir_values.set_default(cr, SUPERUSER_ID, model, field, config[name])
# group fields: modify group / implied groups
for name, group, implied_group in classified['group']:
for name, groups, implied_group in classified['group']:
gids = map(int, groups)
if config[name]:
group.write({'implied_ids': [(4, implied_group.id)]})
res_groups.write(cr, uid, gids, {'implied_ids': [(4, implied_group.id)]}, context=context)
else:
group.write({'implied_ids': [(3, implied_group.id)]})
implied_group.write({'users': [(3, u.id) for u in group.users]})
res_groups.write(cr, uid, gids, {'implied_ids': [(3, implied_group.id)]}, context=context)
uids = set()
for group in groups:
uids.update(map(int, group.users))
implied_group.write({'users': [(3, u) for u in uids]})
# other fields: execute all methods that start with 'set_'
for method in dir(self):

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:

View File

@ -162,9 +162,13 @@ class lang(osv.osv):
]
@tools.ormcache(skiparg=3)
def _lang_data_get(self, cr, uid, lang_id, monetary=False):
def _lang_data_get(self, cr, uid, lang, monetary=False):
if type(lang) in (str, unicode):
lang = self.search(cr, uid, [('code', '=', lang)]) or \
self.search(cr, uid, [('code', '=', 'en_US')])
lang = lang[0]
conv = localeconv()
lang_obj = self.browse(cr, uid, lang_id)
lang_obj = self.browse(cr, uid, lang)
thousands_sep = lang_obj.thousands_sep or conv[monetary and 'mon_thousands_sep' or 'thousands_sep']
decimal_point = lang_obj.decimal_point
grouping = lang_obj.grouping
@ -192,32 +196,29 @@ class lang(osv.osv):
trans_obj.unlink(cr, uid, trans_ids, context=context)
return super(lang, self).unlink(cr, uid, ids, context=context)
#
# IDS: can be a list of IDS or a list of XML_IDS
#
def format(self, cr, uid, ids, percent, value, grouping=False, monetary=False, context=None):
""" Format() will return the language-specific output for float values"""
if percent[0] != '%':
raise ValueError("format() must be given exactly one %char format specifier")
lang_grouping, thousands_sep, decimal_point = self._lang_data_get(cr, uid, ids[0], monetary)
eval_lang_grouping = eval(lang_grouping)
formatted = percent % value
# floats and decimal ints need special action!
if percent[-1] in 'eEfFgG':
seps = 0
parts = formatted.split('.')
if grouping:
lang_grouping, thousands_sep, decimal_point = \
self._lang_data_get(cr, uid, ids[0], monetary)
eval_lang_grouping = eval(lang_grouping)
if grouping:
parts[0], seps = intersperse(parts[0], eval_lang_grouping, thousands_sep)
if percent[-1] in 'eEfFgG':
parts = formatted.split('.')
parts[0], _ = intersperse(parts[0], eval_lang_grouping, thousands_sep)
formatted = decimal_point.join(parts)
while seps:
sp = formatted.find(' ')
if sp == -1: break
formatted = formatted[:sp] + formatted[sp+1:]
seps -= 1
elif percent[-1] in 'diu':
if grouping:
formatted = decimal_point.join(parts)
elif percent[-1] in 'diu':
formatted = intersperse(formatted, eval_lang_grouping, thousands_sep)[0]
return formatted

View File

@ -23,14 +23,12 @@ import datetime
from lxml import etree
import math
import pytz
import re
import openerp
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.osv import osv, fields
from openerp.tools.translate import _
from openerp.tools.yaml_import import is_comment
class format_address(object):
def fields_view_get_address(self, cr, uid, arch, context={}):
@ -210,6 +208,7 @@ class res_partner(osv.osv, format_address):
def _display_name_compute(self, cr, uid, ids, name, args, context=None):
context = dict(context or {})
context.pop('show_address', None)
context.pop('show_address_only', None)
return dict(self.name_get(cr, uid, ids, context=context))
# indirections to avoid passing a copy of the overridable method when declaring the function field
@ -217,12 +216,12 @@ class res_partner(osv.osv, format_address):
_display_name = lambda self, *args, **kwargs: self._display_name_compute(*args, **kwargs)
_commercial_partner_store_triggers = {
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
['parent_id', 'is_company'], 10)
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)], context=dict(active_test=False)),
['parent_id', 'is_company'], 10)
}
_display_name_store_triggers = {
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
['parent_id', 'is_company', 'name'], 10)
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)], context=dict(active_test=False)),
['parent_id', 'is_company', 'name'], 10)
}
_order = "display_name"
@ -232,7 +231,7 @@ class res_partner(osv.osv, format_address):
'date': fields.date('Date', select=1),
'title': fields.many2one('res.partner.title', 'Title'),
'parent_id': fields.many2one('res.partner', 'Related Company'),
'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override
'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override
'ref': fields.char('Reference', size=64, select=1),
'lang': fields.selection(_lang_get, 'Language',
help="If the selected language is loaded in the system, all documents related to this contact will be printed in this language. If not, it will be English."),
@ -560,10 +559,12 @@ class res_partner(osv.osv, format_address):
name = record.name
if record.parent_id and not record.is_company:
name = "%s, %s" % (record.parent_id.name, name)
if context.get('show_address_only'):
name = self._display_address(cr, uid, record, without_company=True, context=context)
if context.get('show_address'):
name = name + "\n" + self._display_address(cr, uid, record, without_company=True, context=context)
name = name.replace('\n\n','\n')
name = name.replace('\n\n','\n')
name = name.replace('\n\n','\n')
name = name.replace('\n\n','\n')
if context.get('show_email') and record.email:
name = "%s <%s>" % (name, record.email)
res.append((record.id, name))
@ -602,7 +603,8 @@ class res_partner(osv.osv, format_address):
""" Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
# a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in'):
if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \
and args[0][2] != [False]:
context = dict(context or {}, active_test=False)
return super(res_partner, self)._search(cr, user, args, offset=offset, limit=limit, order=order, context=context,
count=count, access_rights_uid=access_rights_uid)

View File

@ -75,7 +75,7 @@
<field name="zip">106</field>
<field name="country_id" ref="base.tw"/>
<field name="street">31 Hong Kong street</field>
<field name="email">info@asustek.com</field>
<field name="email">asusteK@yourcompany.example.com</field>
<field name="phone">(+886) (02) 4162 2023</field>
<field name="website">www.asustek.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_1-image.jpg"/>
@ -88,7 +88,7 @@
<field name="zip">1300</field>
<field name="country_id" ref="base.be"/>
<field name="street">69 rue de Namur</field>
<field name="email">info@agrolait.com</field>
<field name="email">agrolait@yourcompany.example.com</field>
<field name="phone">+32 10 588 558</field>
<field name="website">www.agrolait.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_2-image.jpg"/>
@ -103,7 +103,7 @@
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="country_id" ref="base.cn"/>
<field name="street">52 Chop Suey street</field>
<field name="email">info@chinaexport.com</field>
<field name="email">chinaexport@yourcompany.example.com</field>
<field name="phone">+86 21 6484 5671</field>
<field name="website">www.chinaexport.com/</field>
<field name="image" type="base64" file="base/static/img/res_partner_3-image.png"/>
@ -120,7 +120,7 @@
<field name="country_id" ref="base.us"/>
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
<field name="street">3661 Station Street</field>
<field name="email">info@deltapc.com</field>
<field name="email">deltapc@yourcompany.example.com</field>
<field name="phone">+1 510 340 2385</field>
<field name="website">www.distribpc.com/</field>
<field name="image" type="base64" file="base/static/img/res_partner_4-image.png"/>
@ -135,25 +135,25 @@
<field model="res.country.state" name="state_id" search="[('code','ilike','il')]"/>
<field name="zip">60610</field>
<field name="city">Chicago</field>
<field name="email">epic@tech.info</field>
<field name="email">epic@yourcompany.example.com</field>
<field name="phone">+1 312 349 2324</field>
<field name="website">www.epic-tech.info//</field>
<field name="image" type="base64" file="base/static/img/res_partner_5-image.jpg"/>
</record>
<record id="res_partner_6" model="res.partner">
<field name="name">Elec Import</field>
<field name="name">OpenElec Applications</field>
<field eval="[(6, 0, [ref('res_partner_category_12')])]" name="category_id"/>
<field eval="1" name="supplier"/>
<field eval="0" name="customer"/>
<field name="is_company">1</field>
<field name="city">Chicago</field>
<field name="zip">60623</field>
<field name="zip">90001</field>
<field name="city">Los Angeles</field>
<field name="country_id" ref="base.us"/>
<field model="res.country.state" name="state_id" search="[('code','ilike','il')]"/>
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
<field name="street">23 Rockwell Lane</field>
<field name="email">info@elecimport.com</field>
<field name="phone">+1 773 439 3000</field>
<field name="email">openelecapplications@yourcompany.example.com</field>
<field name="phone">+1 312 349 2121</field>
<field name="image" type="base64" file="base/static/img/res_partner_6-image.jpg"/>
</record>
@ -168,7 +168,7 @@
<field name="zip">B46 3AG</field>
<field name="country_id" ref="base.uk"/>
<field name="phone">+44 121 690 4596</field>
<field name="email">email@wealthyandsons.com</field>
<field name="email">wealthyandsons@yourcompany.example.com</field>
<field name="website">www.wealthyandsons.com/</field>
<field name="image" type="base64" file="base/static/img/res_partner_7-image.jpg"/>
</record>
@ -184,6 +184,7 @@
<field name="zip">80352</field>
<field name="country_id" ref="base.de"/>
<field name="phone">+49 8932 450203 </field>
<field name="email">mediapole@yourcompany.example.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_8-image.jpg"/>
</record>
<record id="res_partner_9" model="res.partner">
@ -195,7 +196,7 @@
<field name="street">203, Systems Plaza</field>
<field name="city">Mumbai</field>
<field name="country_id" ref="base.in"/>
<field name="email">info@bestdesigners.in</field>
<field name="email">bestdesigners@yourcompany.example.com</field>
<field name="phone">+91 22 3445 0349</field>
<field name="website">www.bestdesigners.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_9-image.jpg"/>
@ -209,7 +210,7 @@
<field name="zip">33169</field>
<field name="country_id" ref="base.us"/>
<field model="res.country.state" name="state_id" search="[('code','=','FL')]"/>
<field name="email">contact@jackson.com</field>
<field name="email">jackson@yourcompany.example.com</field>
<field name="phone">+1 786 525 0724</field>
<field name="street">3203 Lamberts Branch Road</field>
<field name="image" type="base64" file="base/static/img/res_partner_10-image.jpg"/>
@ -228,6 +229,7 @@
<field name="zip">08078</field>
<field name="phone">+34 934 340 230</field>
<field name="website">www.lumitech.com</field>
<field name="email">luminous@yourcompany.example.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_11-image.png"/>
</record>
@ -242,7 +244,7 @@
<field name="phone">+33 4 49 23 44 54</field>
<field name="country_id" ref="base.fr"/>
<field name="street">93, Press Avenue</field>
<field name="email">info@c2c.com</field>
<field name="email">camptocamp@yourcompany.example.com</field>
<field name="website">www.camptocamp.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_12-image.jpg"/>
</record>
@ -255,7 +257,7 @@
<field name="city">Champs sur Marne</field>
<field name="zip">77420</field>
<field name="country_id" ref="base.fr"/>
<field name="email">info@axelor.com</field>
<field name="email">axelor@yourcompany.example.com</field>
<field name="phone">+33 1 64 61 04 01</field>
<field name="street">12 rue Albert Einstein</field>
<field name="website">www.axelor.com/</field>
@ -274,7 +276,7 @@
<field name="country_id" ref="base.us"/>
<field model="res.country.state" name="state_id" search="[('code','ilike','mi')]"/>
<field name="street">60, Rosewood Court</field>
<field name="email">info@chamberworks.com</field>
<field name="email">chamberworks@yourcompany.example.com</field>
<field name="phone">+1 313 222 3456</field>
<field name="image" type="base64" file="base/static/img/res_partner_14-image.jpg"/>
</record>
@ -290,6 +292,7 @@
<field name="country_id" ref="base.uk"/>
<field name="city">London</field>
<field name="phone">+44 20 1294 2193</field>
<field name="email">millennium@yourcompany.example.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_15-image.jpg"/>
</record>
@ -304,6 +307,7 @@
<field name="phone">+55 11 2402 2045</field>
<field name="country_id" ref="base.br"/>
<field name="is_company">1</field>
<field name="email">sparksystems@yourcompany.example.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_16-image.png"/>
</record>
@ -313,7 +317,7 @@
<field eval="1" name="customer"/>
<field name="is_company">1</field>
<field name="city">Rosario</field>
<field name="email">contact@nebula.ar</field>
<field name="email">nebula@yourcompany.example.com</field>
<field name="street">34 Westwood Avenue</field>
<field name="street2">Capital Federal</field>
<field name="phone">+54 341 324 9459 </field>
@ -328,7 +332,7 @@
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
<field name="city">Boston</field>
<field name="zip">02203</field>
<field name="email">info@thinkbig.com</field>
<field name="email">thinkbig@yourcompany.example.com</field>
<field name="phone">+1 857 349 3049</field>
<field name="country_id" ref="base.us"/>
<field name="street">One Lincoln Street</field>
@ -347,7 +351,7 @@
<field name="country_id" ref="base.us"/>
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
<field name="street">10200 S. De Anza Blvd</field>
<field name="email">info@seagate.com</field>
<field name="email">seagate@yourcompany.example.com</field>
<field name="phone">+1 800 732 4283</field>
<field name="image" type="base64" file="base/static/img/res_partner_19-image.jpg"/>
</record>
@ -374,6 +378,7 @@
<field name="city">Liverpool</field>
<field name="country_id" ref="base.uk"/>
<field name="zip">L25 4RL</field>
<field name="email">globalsolutions@yourcompany.example.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_21-image.png"/>
</record>
@ -390,6 +395,7 @@
<field name="phone">+44 20 1294 2193</field>
<field name="country_id" ref="base.uk"/>
<field name="website">www.vicking-direct.com</field>
<field name="email">vickingdirect@yourcompany.example.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_22-image.jpg"/>
</record>
@ -404,12 +410,136 @@
<field name="street2">Carretera Panamericana, Km 1, Urb. Delgado Chalbaud</field>
<field name="city">Caracas</field>
<field name="zip">1090</field>
<field name="email">info@vauxoo.com</field>
<field name="email">vauxoo@yourcompany.example.com</field>
<field name="phone">+58 212 681 0538</field>
<field name="country_id" ref="base.ve"/>
<field name="website">vauxoo.com</field>
<field name="image" type="base64" file="base/static/img/res_partner_23-image.jpg"/>
</record>
<record id="res_partner_24" model="res.partner">
<field name="name">OpenCorp</field>
<field eval="[(6, 0, [ref('base.res_partner_category_7'), ref('base.res_partner_category_9')])]" name="category_id"/>
<field name="is_company">1</field>
<field name="city">Mortsel</field>
<field name="zip">2640</field>
<field name="country_id" ref="base.be"/>
<field name="street">Avenue Louise 149/24</field>
<field name="email">opencorp@yourcompany.example.com</field>
<field name="phone">+32 3 450 97 11</field>
<field name="website">http://www.opencorp.com</field>
</record>
<record id="res_partner_25" model="res.partner">
<field name="name">Míng</field>
<field name="supplier">1</field>
<field eval="[(6, 0, [ref('res_partner_category_8'), ref('res_partner_category_14')])]" name="category_id"/>
<field name="is_company">1</field>
<field name="city">Shanghai</field>
<field name="zip">201208</field>
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="country_id" ref="base.cn"/>
<field name="street">89 Dong Lu Road</field>
<field name="email">míng@yourcompany.example.com</field>
<field name="phone">+86 215 069 5177</field>
<field name="website">http://www.míng.com</field>
</record>
<record id="res_partner_26" model="res.partner">
<field name="name">Federal</field>
<field name="supplier">1</field>
<field eval="[(6, 0, [ref('res_partner_category_8'), ref('res_partner_category_16')])]" name="category_id"/>
<field name="is_company">1</field>
<field name="city">Düsseldorf</field>
<field name="zip">40227</field>
<field name="country_id" ref="base.de"/>
<field name="street">Willi-Becker-Allee 10</field>
<field name="email">federal@yourcompany.example.com</field>
<field name="website">http://www.federal.com</field>
</record>
<record id="res_partner_address_1" model="res.partner">
<field name="name">Tang Tsui</field>
<field name="parent_id" eval="ref('res_partner_1')"/>
<field name="use_parent_address">1</field>
<field name="function">Service Manager</field>
<field name="email">tang@asustek.com</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_2" model="res.partner">
<field name="name">Joseph Walters</field>
<field name="parent_id" eval="ref('res_partner_1')"/>
<field name="use_parent_address">1</field>
<field name="function">Store Manager</field>
<field name="email">joseph.walters@asustek.com</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_7" model="res.partner">
<field name="name">Richard Ellis</field>
<field name="parent_id" eval="ref('res_partner_4')"/>
<field name="use_parent_address">1</field>
<field name="function">Production Supervisor</field>
<field name="email">richard.ellis@deltapc.example.com</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_8" model="res.partner">
<field name="name">Paul Williams</field>
<field name="parent_id" eval="ref('res_partner_4')"/>
<field name="use_parent_address">1</field>
<field name="function">Line Mechanic</field>
<field name="email">paul.williams@deltapc.example.com</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_9" model="res.partner">
<field name="name">Brian Williams</field>
<field name="parent_id" eval="ref('res_partner_4')"/>
<field name="use_parent_address">1</field>
<field name="function">Computer Technician</field>
<field name="email">brian.williams@deltapc.example.com</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_12" model="res.partner">
<field name="name">James Miller</field>
<field name="parent_id" eval="ref('res_partner_6')"/>
<field name="use_parent_address">1</field>
<field name="function">Electrical Supervisor</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_31" model="res.partner">
<field name="name">Edward Foster</field>
<field name="parent_id" eval="ref('res_partner_19')"/>
<field name="use_parent_address">1</field>
<field name="function">Sales Representative</field>
<field name="email">efoster@seagate.com</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_27" model="res.partner">
<field name="name">Arthur Gomez</field>
<field name="parent_id" eval="ref('res_partner_16')"/>
<field name="use_parent_address">1</field>
<field name="function">Software Developer</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_26" model="res.partner">
<field name="name">Julia Rivero</field>
<field name="parent_id" eval="ref('res_partner_16')"/>
<field name="use_parent_address">1</field>
<field name="function">Technical Director</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_address_35" model="res.partner">
<field name="name">Peter Mitchell</field>
<field name="parent_id" eval="ref('res_partner_22')"/>
<field name="use_parent_address">1</field>
<field name="function">Store Manager</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
</data>
</openerp>

View File

@ -1,242 +1,185 @@
-
!record {model: 'res.partner', id: base.res_partner_address_1}:
name: Tang Tsui
parent_id: base.res_partner_1
use_parent_address: True
function: Service Manager
email: tang@asustek.com
-
!record {model: 'res.partner', id: base.res_partner_address_2}:
name: Joseph Walters
parent_id: base.res_partner_1
use_parent_address: True
function: Store Manager
email: joseph.walters@asustek.com
-
!record {model: 'res.partner', id: base.res_partner_address_3}:
name: Thomas Passot
parent_id: base.res_partner_2
use_parent_address: True
function: Functional Consultant
email: p.thomas@agrolait.com
email: thomas.passot@agrolait.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_4}:
name: Michel Fletcher
parent_id: base.res_partner_2
use_parent_address: True
function: Analyst
email: m.fletcher@agrolait.com
email: michel.fletcher@agrolait.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_5}:
name: Chao Wang
parent_id: base.res_partner_3
use_parent_address: True
function: Marketing Manager
email: chao_wang@chinaexport.com
email: chao.wang@chinaexport.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_6}:
name: Zhi Ch'ang
parent_id: base.res_partner_3
use_parent_address: True
function: Supervisor
email: zhi_chang@chinaexport.com
-
!record {model: 'res.partner', id: base.res_partner_address_7}:
name: Richard Ellis
parent_id: base.res_partner_4
use_parent_address: True
function: Production Supervisor
-
!record {model: 'res.partner', id: base.res_partner_address_8}:
name: Paul Williams
parent_id: base.res_partner_4
use_parent_address: True
function: Line Mechanic
email:
-
!record {model: 'res.partner', id: base.res_partner_address_9}:
name: Brian Williams
parent_id: base.res_partner_4
use_parent_address: True
function: Computer Technician
email:
email: zhi.chang@chinaexport.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_10}:
name: David Simpson
parent_id: base.res_partner_5
use_parent_address: True
function: Senior Consultant
email: david.s@tech.info
email: david.simpson@epic.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_11}:
name: John M. Brown
parent_id: base.res_partner_5
use_parent_address: True
function: Director
email: john.b@tech.info
-
!record {model: 'res.partner', id: base.res_partner_address_12}:
name: James Miller
parent_id: base.res_partner_6
use_parent_address: True
function: Electrical Supervisor
email: john.brown@epic.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_13}:
name: Charlie Bernard
parent_id: base.res_partner_7
use_parent_address: True
function: Senior Associate
email: charlie.bernard@wealthyandsons.com
email: charlie.bernard@wealthyandsons.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_14}:
name: Jessica Dupont
parent_id: base.res_partner_7
use_parent_address: True
function: Analyst
email: jessica.dupont@wealthyandsons.com
email: jessica.dupont@wealthyandsons.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_15}:
name: Phillipp Miller
parent_id: base.res_partner_8
use_parent_address: True
function: Creative Director
email: phillipp.miller@mediapole.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_16}:
name: Ayaan Agarwal
parent_id: base.res_partner_9
use_parent_address: True
function: Director
email: ayaan.agarwal@bestdesigners.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_17}:
name: Daniel Jackson
parent_id: base.res_partner_10
use_parent_address: True
function: Managing Partner
email: daniel@jackson.com
email: daniel.jackson@jackson.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_18}:
name: William Thomas
parent_id: base.res_partner_10
use_parent_address: True
function: Senior Consultant
email: william@jackson.com
email: william.jackson@jackson.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_19}:
name: Sergio Pérez
parent_id: base.res_partner_11
use_parent_address: True
function: Accountant
email:
email: sergio.perez@luminous.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_20}:
name: Laura Castro
parent_id: base.res_partner_11
use_parent_address: True
function: Goods Supervisor
email:
email: laura.castro@luminous.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_21}:
name: Luc Maurer
parent_id: base.res_partner_12
use_parent_address: True
function: Director
email: luc.maurer@camptocamp.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_22}:
name: Laith Jubair
parent_id: base.res_partner_13
use_parent_address: True
function: Director
email: laith.jubair@axelor.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_23}:
name: Angel Cook
parent_id: base.res_partner_14
use_parent_address: True
function: General Manager
email: angel.cook@chamberworks.com
email: angel.cook@chamberworks.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_24}:
name: Robert Anderson
parent_id: base.res_partner_14
use_parent_address: True
function: System Analyst
email: robert.anderson@chamberworks.com
email: robert.anderson@chamberworks.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_25}:
name: Jacob Taylor
parent_id: base.res_partner_15
use_parent_address: True
function: Order Clerk
-
!record {model: 'res.partner', id: base.res_partner_address_26}:
name: Arthur Gomez
parent_id: base.res_partner_16
use_parent_address: True
function: Software Developer
email:
-
!record {model: 'res.partner', id: base.res_partner_address_27}:
name: Julia Rivero
parent_id: base.res_partner_16
use_parent_address: True
function: Technical Director
email: jacob.taylor@millennium.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_28}:
name: Benjamin Flores
parent_id: base.res_partner_17
use_parent_address: True
function: Business Executive
email: ben@nebula.ar
email: benjamin.flores@nebula.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_29}:
name: George Wilson
parent_id: base.res_partner_18
use_parent_address: True
function: Chief Information Officer (CIO)
email:
email: george.wilson@thinkbig.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_30}:
name: Lucas Jones
parent_id: base.res_partner_18
use_parent_address: True
function: Functional Consultant
email: jones@thinkbig.com
-
!record {model: 'res.partner', id: base.res_partner_address_31}:
name: Edward Foster
parent_id: base.res_partner_19
use_parent_address: True
function: Sales Representative
email: efoster@seagate.com
email: lucas.jones@thinkbig.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_32}:
name: Robin Smith
parent_id: base.res_partner_21
use_parent_address: True
function: Sales Manager
email: robin.smith@globalsolutions.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_33}:
name: Morgan Rose
parent_id: base.res_partner_21
use_parent_address: True
function: Financial Manager
email: morgan.rose@globalsolutions.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_34}:
name: Kevin Clarke
parent_id: base.res_partner_21
use_parent_address: True
function: Knowledge Manager
-
!record {model: 'res.partner', id: base.res_partner_address_35}:
name: Peter Mitchell
parent_id: base.res_partner_22
use_parent_address: True
function: Store Manager
email: kevin.clarke@globalsolutions.example.com
-
!record {model: 'res.partner', id: base.res_partner_address_36}:
name: Nhomar Hernandez
parent_id: base.res_partner_23
use_parent_address: True
function: Chief Executive Officer
email: nhomar.hernandez@vauxoo.example.com
-
!record {model: 'res.partner', id: base.res_partner_main1}:
name: Mark Davis
@ -244,7 +187,7 @@
parent_id: base.main_partner
use_parent_address: True
function: Chief Executive Officer (CEO)
email: mark@yourcompany.com
email: mark.davis@yourcompany.example.com
-
!record {model: 'res.partner', id: base.res_partner_main2}:
name: Roger Scott
@ -252,4 +195,4 @@
parent_id: base.main_partner
use_parent_address: True
function: Chief Operations Officer (COO)
email: roger@yourcompany.com
email: roger.scott@yourcompany.example.com

View File

@ -600,6 +600,7 @@ class users_implied(osv.osv):
if groups:
# delegate addition of groups to add implied groups
self.write(cr, uid, [user_id], {'groups_id': groups}, context)
self.pool['ir.ui.view'].clear_cache()
return user_id
def write(self, cr, uid, ids, values, context=None):
@ -612,6 +613,7 @@ class users_implied(osv.osv):
gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
vals = {'groups_id': [(4, g.id) for g in gs]}
super(users_implied, self).write(cr, uid, [user.id], vals, context)
self.pool['ir.ui.view'].clear_cache()
return res
#----------------------------------------------------------
@ -681,8 +683,10 @@ class groups_view(osv.osv):
def update_user_groups_view(self, cr, uid, context=None):
# the view with id 'base.user_groups_view' inherits the user form view,
# and introduces the reified group fields
view = self.get_user_groups_view(cr, uid, context)
if view:
# we have to try-catch this, because at first init the view does not exist
# but we are already creating some basic groups
view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
if view and view.exists() and view._table_name == 'ir.ui.view':
xml1, xml2 = [], []
xml1.append(E.separator(string=_('Application'), colspan="4"))
for app, kind, gs in self.get_groups_by_application(cr, uid, context):
@ -707,14 +711,6 @@ class groups_view(osv.osv):
view.write({'arch': xml_content})
return True
def get_user_groups_view(self, cr, uid, context=None):
try:
view = self.pool['ir.model.data'].get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
assert view and view._table_name == 'ir.ui.view'
except Exception:
view = False
return view
def get_application_groups(self, cr, uid, domain=None, context=None):
return self.search(cr, uid, domain or [])

View File

@ -57,6 +57,16 @@
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
</record>
<record model="ir.rule" id="res_partner_portal_public_rule">
<field name="name">res_partner: portal/public: read access on my commercial partner</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="domain_force">[('id', 'child_of', user.commercial_partner_id.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="perm_write" eval="False"/>
</record>
<record model="ir.rule" id="multi_company_default_rule">
<field name="name">Multi_company_default company</field>
<field name="model_id" ref="model_multi_company_default"/>

View File

@ -43,8 +43,6 @@
"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,1,1,1
"access_res_company_group_erp_manager","res_company group_erp_manager","model_res_company","group_erp_manager",1,1,1,1
"access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0
"access_res_font_group_erp_manager","res_font group_erp_manager","model_res_font","group_erp_manager",1,1,1,1
"access_res_font_group_all","res_font group_all","model_res_font",,1,0,0,0
"access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0
"access_res_country_state_group_all","res_country_state group_user_all","model_res_country_state",,1,0,0,0
"access_res_country_group_user","res_country group_user","model_res_country","group_partner_manager",1,1,1,1
@ -58,6 +56,8 @@
"access_res_groups_group_user","res_groups group_user","model_res_groups",,1,0,0,0
"access_res_lang_group_all","res_lang group_all","model_res_lang",,1,0,0,0
"access_res_lang_group_user","res_lang group_user","model_res_lang","group_system",1,1,1,1
"access_res_partner_public","res_partner group_public","model_res_partner","group_public",1,0,0,0
"access_res_partner_portal","res_partner group_portal","model_res_partner","group_portal",1,0,0,0
"access_res_partner_group_partner_manager","res_partner group_partner_manager","model_res_partner","group_partner_manager",1,1,1,1
"access_res_partner_group_user","res_partner group_user","model_res_partner","group_user",1,0,0,0
"access_res_partner_bank_group_user","res_partner_bank group_user","model_res_partner_bank","group_user",1,0,0,0
@ -111,4 +111,5 @@
"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_res_font_all","res_res_font all","model_res_font",,1,0,0,0
"access_res_font_group_user","res_res_font group_user","model_res_font","group_user",1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
43 access_ir_values_group_all ir_values group_all model_ir_values 1 1 1 1
44 access_res_company_group_erp_manager res_company group_erp_manager model_res_company group_erp_manager 1 1 1 1
45 access_res_company_group_user res_company group_user model_res_company 1 0 0 0
access_res_font_group_erp_manager res_font group_erp_manager model_res_font group_erp_manager 1 1 1 1
access_res_font_group_all res_font group_all model_res_font 1 0 0 0
46 access_res_country_group_all res_country group_user_all model_res_country 1 0 0 0
47 access_res_country_state_group_all res_country_state group_user_all model_res_country_state 1 0 0 0
48 access_res_country_group_user res_country group_user model_res_country group_partner_manager 1 1 1 1
56 access_res_groups_group_user res_groups group_user model_res_groups 1 0 0 0
57 access_res_lang_group_all res_lang group_all model_res_lang 1 0 0 0
58 access_res_lang_group_user res_lang group_user model_res_lang group_system 1 1 1 1
59 access_res_partner_public res_partner group_public model_res_partner group_public 1 0 0 0
60 access_res_partner_portal res_partner group_portal model_res_partner group_portal 1 0 0 0
61 access_res_partner_group_partner_manager res_partner group_partner_manager model_res_partner group_partner_manager 1 1 1 1
62 access_res_partner_group_user res_partner group_user model_res_partner group_user 1 0 0 0
63 access_res_partner_bank_group_user res_partner_bank group_user model_res_partner_bank group_user 1 0 0 0
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_res_font_all res_res_font all model_res_font 1 0 0 0
115 access_res_font_group_user res_res_font group_user model_res_font group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1,48 +1,425 @@
# -*- encoding: utf-8 -*-
from functools import partial
import unittest2
import lxml.etree
from lxml import etree as ET
from lxml.builder import E
import openerp.tests.common as common
from openerp.osv.orm import except_orm
from openerp.tools import mute_logger
from openerp.tests import common
Field = E.field
class TestNodeLocator(common.BaseCase):
"""
The node locator returns None when it can not find a node, and the first
match when it finds something (no jquery-style node sets)
"""
def setUp(self):
super(TestNodeLocator, self).setUp()
self.Views = self.registry('ir.ui.view')
def test_no_match_xpath(self):
"""
xpath simply uses the provided @expr pattern to find a node
"""
node = self.Views.locate_node(
E.root(E.foo(), E.bar(), E.baz()),
E.xpath(expr="//qux"))
self.assertIsNone(node)
def test_match_xpath(self):
bar = E.bar()
node = self.Views.locate_node(
E.root(E.foo(), bar, E.baz()),
E.xpath(expr="//bar"))
self.assertIs(node, bar)
def test_no_match_field(self):
"""
A field spec will match by @name against all fields of the view
"""
node = self.Views.locate_node(
E.root(E.foo(), E.bar(), E.baz()),
Field(name="qux"))
self.assertIsNone(node)
node = self.Views.locate_node(
E.root(Field(name="foo"), Field(name="bar"), Field(name="baz")),
Field(name="qux"))
self.assertIsNone(node)
def test_match_field(self):
bar = Field(name="bar")
node = self.Views.locate_node(
E.root(Field(name="foo"), bar, Field(name="baz")),
Field(name="bar"))
self.assertIs(node, bar)
def test_no_match_other(self):
"""
Non-xpath non-fields are matched by node name first
"""
node = self.Views.locate_node(
E.root(E.foo(), E.bar(), E.baz()),
E.qux())
self.assertIsNone(node)
def test_match_other(self):
bar = E.bar()
node = self.Views.locate_node(
E.root(E.foo(), bar, E.baz()),
E.bar())
self.assertIs(bar, node)
def test_attribute_mismatch(self):
"""
Non-xpath non-field are filtered by matching attributes on spec and
matched nodes
"""
node = self.Views.locate_node(
E.root(E.foo(attr='1'), E.bar(attr='2'), E.baz(attr='3')),
E.bar(attr='5'))
self.assertIsNone(node)
def test_attribute_filter(self):
match = E.bar(attr='2')
node = self.Views.locate_node(
E.root(E.bar(attr='1'), match, E.root(E.bar(attr='3'))),
E.bar(attr='2'))
self.assertIs(node, match)
def test_version_mismatch(self):
"""
A @version on the spec will be matched against the view's version
"""
node = self.Views.locate_node(
E.root(E.foo(attr='1'), version='4'),
E.foo(attr='1', version='3'))
self.assertIsNone(node)
class TestViewInheritance(common.TransactionCase):
def arch_for(self, name, view_type='form', parent=None):
""" Generates a trivial view of the specified ``view_type``.
The generated view is empty but ``name`` is set as its root's ``@string``.
If ``parent`` is not falsy, generates an extension view (instead of
a root view) replacing the parent's ``@string`` by ``name``
:param str name: ``@string`` value for the view root
:param str view_type:
:param bool parent:
:return: generated arch
:rtype: str
"""
if not parent:
element = E(view_type, string=name)
else:
element = E(view_type,
E.attribute(name, name='string'),
position='attributes'
)
return ET.tostring(element)
def makeView(self, name, parent=None, arch=None):
""" Generates a basic ir.ui.view with the provided name, parent and arch.
If no parent is provided, the view is top-level.
If no arch is provided, generates one by calling :meth:`~.arch_for`.
:param str name:
:param int parent: id of the parent view, if any
:param str arch:
:returns: the created view's id.
:rtype: int
"""
view_id = self.View.create(self.cr, self.uid, {
'model': self.model,
'name': name,
'arch': arch or self.arch_for(name, parent=parent),
'inherit_id': parent,
'priority': 5, # higher than default views
})
self.ids[name] = view_id
return view_id
def setUp(self):
super(TestViewInheritance, self).setUp()
self.model = 'ir.ui.view.custom'
self.View = self.registry('ir.ui.view')
self._init = self.View.pool._init
self.View.pool._init = False
self.ids = {}
a = self.makeView("A")
a1 = self.makeView("A1", a)
a11 = self.makeView("A11", a1)
self.makeView("A111", a11)
self.makeView("A12", a1)
a2 = self.makeView("A2", a)
self.makeView("A21", a2)
a22 = self.makeView("A22", a2)
self.makeView("A221", a22)
b = self.makeView('B', arch=self.arch_for("B", 'tree'))
self.makeView('B1', b, arch=self.arch_for("B1", 'tree', parent=b))
c = self.makeView('C', arch=self.arch_for("C", 'tree'))
self.View.write(self.cr, self.uid, c, {'priority': 1})
def tearDown(self):
self.View.pool._init = self._init
super(TestViewInheritance, self).tearDown()
def test_get_inheriting_views_arch(self):
self.assertEqual(self.View.get_inheriting_views_arch(
self.cr, self.uid, self.ids['A'], self.model), [
(self.arch_for('A1', parent=True), self.ids['A1']),
(self.arch_for('A2', parent=True), self.ids['A2']),
])
self.assertEqual(self.View.get_inheriting_views_arch(
self.cr, self.uid, self.ids['A21'], self.model),
[])
self.assertEqual(self.View.get_inheriting_views_arch(
self.cr, self.uid, self.ids['A11'], self.model),
[(self.arch_for('A111', parent=True), self.ids['A111'])])
def test_default_view(self):
default = self.View.default_view(
self.cr, self.uid, model=self.model, view_type='form')
self.assertEqual(default, self.ids['A'])
default_tree = self.View.default_view(
self.cr, self.uid, model=self.model, view_type='tree')
self.assertEqual(default_tree, self.ids['C'])
def test_no_default_view(self):
self.assertFalse(
self.View.default_view(
self.cr, self.uid, model='does.not.exist', view_type='form'))
self.assertFalse(
self.View.default_view(
self.cr, self.uid, model=self.model, view_type='graph'))
class TestApplyInheritanceSpecs(common.TransactionCase):
""" Applies a sequence of inheritance specification nodes to a base
architecture. IO state parameters (cr, uid, model, context) are used for
error reporting
The base architecture is altered in-place.
"""
def setUp(self):
super(TestApplyInheritanceSpecs, self).setUp()
self.View = self.registry('ir.ui.view')
self.base_arch = E.form(
Field(name="target"),
string="Title")
def test_replace(self):
spec = Field(
Field(name="replacement"),
name="target", position="replace")
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
self.assertEqual(
ET.tostring(self.base_arch),
ET.tostring(E.form(Field(name="replacement"), string="Title")))
def test_delete(self):
spec = Field(name="target", position="replace")
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
self.assertEqual(
ET.tostring(self.base_arch),
ET.tostring(E.form(string="Title")))
def test_insert_after(self):
spec = Field(
Field(name="inserted"),
name="target", position="after")
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
self.assertEqual(
ET.tostring(self.base_arch),
ET.tostring(E.form(
Field(name="target"),
Field(name="inserted"),
string="Title"
)))
def test_insert_before(self):
spec = Field(
Field(name="inserted"),
name="target", position="before")
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
self.assertEqual(
ET.tostring(self.base_arch),
ET.tostring(E.form(
Field(name="inserted"),
Field(name="target"),
string="Title")))
def test_insert_inside(self):
default = Field(Field(name="inserted"), name="target")
spec = Field(Field(name="inserted 2"), name="target", position='inside')
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
default, None)
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
self.assertEqual(
ET.tostring(self.base_arch),
ET.tostring(E.form(
Field(
Field(name="inserted"),
Field(name="inserted 2"),
name="target"),
string="Title")))
def test_unpack_data(self):
spec = E.data(
Field(Field(name="inserted 0"), name="target"),
Field(Field(name="inserted 1"), name="target"),
Field(Field(name="inserted 2"), name="target"),
Field(Field(name="inserted 3"), name="target"),
)
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
self.assertEqual(
ET.tostring(self.base_arch),
ET.tostring(E.form(
Field(
Field(name="inserted 0"),
Field(name="inserted 1"),
Field(name="inserted 2"),
Field(name="inserted 3"),
name="target"),
string="Title")))
def test_invalid_position(self):
spec = Field(
Field(name="whoops"),
name="target", position="serious_series")
with self.assertRaises(AttributeError):
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
def test_incorrect_version(self):
# Version ignored on //field elements, so use something else
arch = E.form(E.element(foo="42"))
spec = E.element(
Field(name="placeholder"),
foo="42", version="7.0")
with self.assertRaises(AttributeError):
self.View.apply_inheritance_specs(self.cr, self.uid,
arch,
spec, None)
def test_target_not_found(self):
spec = Field(name="targut")
with self.assertRaises(AttributeError):
self.View.apply_inheritance_specs(self.cr, self.uid,
self.base_arch,
spec, None)
class TestApplyInheritedArchs(common.TransactionCase):
""" Applies a sequence of modificator archs to a base view
"""
class TestViewCombined(common.TransactionCase):
"""
Test fallback operations of View.read_combined:
* defaults mapping
* ?
"""
class TestNoModel(common.TransactionCase):
def test_create_view_nomodel(self):
View = self.registry('ir.ui.view')
view_id = View.create(self.cr, self.uid, {
'name': 'dummy',
'arch': '<template name="foo"/>',
'inherit_id': False,
'type': 'qweb',
})
fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model']
[view] = View.read(self.cr, self.uid, [view_id], fields)
self.assertEqual(view, {
'id': view_id,
'name': 'dummy',
'arch': '<template name="foo"/>',
'type': 'qweb',
'priority': 16,
'inherit_id': False,
'model': False,
})
text_para = E.p("", {'class': 'legalese'})
arch = E.body(
E.div(
E.h1("Title"),
id="header"),
E.p("Welcome!"),
E.div(
E.hr(),
text_para,
id="footer"),
{'class': "index"},)
def test_qweb_translation(self):
"""
Test if translations work correctly without a model
"""
View = self.registry('ir.ui.view')
self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
orig_text = "Copyright copyrighter"
translated_text = u"Copyrighter, tous droits réservés"
self.text_para.text = orig_text
self.registry('ir.translation').create(self.cr, self.uid, {
'name': 'website',
'type': 'view',
'lang': 'fr_FR',
'src': orig_text,
'value': translated_text,
})
sarch = View.translate_qweb(self.cr, self.uid, None, self.arch, 'fr_FR')
self.text_para.text = translated_text
self.assertEqual(
ET.tostring(sarch, encoding='utf-8'),
ET.tostring(self.arch, encoding='utf-8'))
class test_views(common.TransactionCase):
@mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view')
def test_whatever(self):
Views = self.registry('ir.ui.view')
self.assertTrue(Views.pool._init)
error_msg = "The model name does not exist or the view architecture cannot be rendered"
# test arch check is call for views without xmlid during registry initialization
with self.assertRaisesRegexp(except_orm, error_msg):
Views.create(self.cr, self.uid, {
'name': 'Test View #1',
'model': 'ir.ui.view',
'arch': """<?xml version="1.0"?>
<tree>
<field name="test_1"/>
</tree>
""",
})
# same for inherited views
with self.assertRaisesRegexp(except_orm, error_msg):
# Views.pudb = True
Views.create(self.cr, self.uid, {
'name': 'Test View #2',
'model': 'ir.ui.view',
'inherit_id': self.browse_ref('base.view_view_tree').id,
'arch': """<?xml version="1.0"?>
<xpath expr="//field[@name='name']" position="after">
<field name="test_2"/>
</xpath>
""",
})
def test_nonexistent_attribute_removal(self):
Views = self.registry('ir.ui.view')
Views.create(self.cr, self.uid, {
@ -75,47 +452,32 @@ class test_views(common.TransactionCase):
validate = partial(Views._validate_custom_views, self.cr, self.uid, model)
# validation of a single view
vid = self._insert_view(**{
'name': 'base view',
'model': model,
'priority': 1,
'arch': """<?xml version="1.0"?>
vid = self._insert_view(
name='base view',
model=model,
priority=1,
arch="""<?xml version="1.0"?>
<tree string="view">
<field name="url"/>
</tree>
""",
})
)
self.assertTrue(validate()) # single view
# validation of a inherited view
self._insert_view(**{
'name': 'inherited view',
'model': model,
'priority': 1,
'inherit_id': vid,
'arch': """<?xml version="1.0"?>
self._insert_view(
name='inherited view',
model=model,
priority=1,
inherit_id=vid,
arch="""<?xml version="1.0"?>
<xpath expr="//field[@name='url']" position="before">
<field name="name"/>
</xpath>
""",
})
)
self.assertTrue(validate()) # inherited view
# validation of a bad inherited view
self._insert_view(**{
'name': 'bad inherited view',
'model': model,
'priority': 2,
'inherit_id': vid,
'arch': """<?xml version="1.0"?>
<xpath expr="//field[@name='url']" position="after">
<field name="bad"/>
</xpath>
""",
})
with mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view'):
self.assertFalse(validate()) # bad inherited view
def test_view_inheritance(self):
Views = self.registry('ir.ui.view')
@ -172,9 +534,9 @@ class test_views(common.TransactionCase):
})
self.assertEqual(view['type'], 'form')
self.assertEqual(
lxml.etree.tostring(lxml.etree.fromstring(
ET.tostring(ET.fromstring(
view['arch'],
parser=lxml.etree.XMLParser(remove_blank_text=True)
parser=ET.XMLParser(remove_blank_text=True)
)),
'<form string="Replacement title" version="7.0">'
'<p>Replacement data</p>'
@ -239,9 +601,9 @@ class test_views(common.TransactionCase):
})
self.assertEqual(view['type'], 'form')
self.assertEqual(
lxml.etree.tostring(lxml.etree.fromstring(
ET.tostring(ET.fromstring(
view['arch'],
parser=lxml.etree.XMLParser(remove_blank_text=True)
parser=ET.XMLParser(remove_blank_text=True)
)),
'<form string="Replacement title" version="7.0">'
'<p>Replacement data</p>'
@ -250,5 +612,3 @@ class test_views(common.TransactionCase):
'</footer>'
'</form>')
if __name__ == '__main__':
unittest2.main()

View File

@ -35,6 +35,7 @@ import werkzeug.wsgi
import openerp
from openerp.service import security, model as service_model
import openerp.tools
_logger = logging.getLogger(__name__)
@ -49,6 +50,28 @@ request = _request_stack()
A global proxy that always redirect to the current request object.
"""
def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
url = path
if not query:
query = {}
if forward_debug and request and request.debug:
query['debug'] = None
if query:
url += '?' + werkzeug.url_encode(query)
if keep_hash:
return redirect_with_hash(url, code)
else:
return werkzeug.utils.redirect(url, code)
def redirect_with_hash(url, code=303):
# Most IE and Safari versions decided not to preserve location.hash upon
# redirect. And even if IE10 pretends to support it, it still fails
# inexplicably in case of multiple redirects (and we do have some).
# See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
if request.httprequest.user_agent.browser in ('firefox',):
return werkzeug.utils.redirect(url, code)
return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
class WebRequest(object):
""" Parent class for all OpenERP Web request types, mostly deals with
initialization and setup of the request object (the dispatching itself has
@ -168,7 +191,7 @@ class WebRequest(object):
if not k.startswith("_ignored_"))
self.func = func
self.func_request_type = func.exposed
self.func_request_type = func.routing['type']
self.func_arguments = arguments
self.auth_method = auth
@ -181,7 +204,7 @@ class WebRequest(object):
kwargs.update(self.func_arguments)
# Backward for 7.0
if getattr(self.func, '_first_arg_is_req', False):
if getattr(self.func.method, '_first_arg_is_req', False):
args = (request,) + args
# Correct exception handling and concurency retry
@service_model.check
@ -207,7 +230,7 @@ class WebRequest(object):
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
yield (self.registry, self.cr)
def route(route, type="http", auth="user", methods=None, cors=None):
def route(route=None, **kw):
"""
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
of ``Controller``.
@ -227,17 +250,16 @@ def route(route, type="http", auth="user", methods=None, cors=None):
:param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
:param cors: The Access-Control-Allow-Origin cors directive value.
"""
assert type in ["http", "json"]
routing = kw.copy()
assert not 'type' in routing or routing['type'] in ("http", "json")
def decorator(f):
if isinstance(route, list):
f.routes = route
else:
f.routes = [route]
f.methods = methods
f.exposed = type
f.cors = cors
if getattr(f, "auth", None) is None:
f.auth = auth
if route:
if isinstance(route, list):
routes = route
else:
routes = [route]
routing['routes'] = routes
f.routing = routing
return f
return decorator
@ -308,7 +330,7 @@ class JsonRequest(WebRequest):
# Read POST content or POST Form Data named "request"
self.jsonrequest = simplejson.loads(request)
self.params = dict(self.jsonrequest.get("params", {}))
self.context = self.params.pop('context', self.session.context)
self.context = self.params.pop('context', dict(self.session.context))
def dispatch(self):
""" Calls the method asked for by the JSON-RPC2 or JSONP request
@ -390,11 +412,10 @@ def jsonrequest(f):
Use the ``route()`` decorator instead.
"""
f.combine = True
base = f.__name__.lstrip('/')
if f.__name__ == "index":
base = ""
return route([base, base + "/<path:_ignored_path>"], type="json", auth="user")(f)
return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
class HttpRequest(WebRequest):
""" Regular GET/POST request
@ -403,9 +424,9 @@ class HttpRequest(WebRequest):
def __init__(self, *args):
super(HttpRequest, self).__init__(*args)
params = dict(self.httprequest.args)
params.update(self.httprequest.form)
params.update(self.httprequest.files)
params = self.httprequest.args.to_dict()
params.update(self.httprequest.form.to_dict())
params.update(self.httprequest.files.to_dict())
params.pop('session_id', None)
self.params = params
@ -459,11 +480,10 @@ def httprequest(f):
Use the ``route()`` decorator instead.
"""
f.combine = True
base = f.__name__.lstrip('/')
if f.__name__ == "index":
base = ""
return route([base, base + "/<path:_ignored_path>"], type="http", auth="user")(f)
return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
#----------------------------------------------------------
# Controller and route registration
@ -500,6 +520,13 @@ class ControllerType(type):
class Controller(object):
__metaclass__ = ControllerType
class EndPoint(object):
def __init__(self, method, routing):
self.method = method
self.routing = routing
def __call__(self, *args, **kw):
return self.method(*args, **kw)
def routing_map(modules, nodb_only, converters=None):
routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
for module in modules:
@ -516,13 +543,25 @@ def routing_map(modules, nodb_only, converters=None):
o = cls()
members = inspect.getmembers(o)
for mk, mv in members:
if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and (not nodb_only or nodb_only == (mv.auth == "none")):
for url in mv.routes:
if getattr(mv, "combine", False):
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
if url.endswith("/") and len(url) > 1:
url = url[: -1]
routing_map.add(werkzeug.routing.Rule(url, endpoint=mv, methods=mv.methods))
if inspect.ismethod(mv) and hasattr(mv, 'routing'):
routing = dict(type='http', auth='user', methods=None, routes=None)
methods_done = list()
for claz in reversed(mv.im_class.mro()):
fn = getattr(claz, mv.func_name, None)
if fn and hasattr(fn, 'routing') and fn not in methods_done:
methods_done.append(fn)
routing.update(fn.routing)
if not nodb_only or nodb_only == (routing['auth'] == "none"):
assert routing['routes'], "Method %r has not route defined" % mv
endpoint = EndPoint(mv, routing)
for url in routing['routes']:
if routing.get("combine", False):
# deprecated
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
if url.endswith("/") and len(url) > 1:
url = url[: -1]
routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
return routing_map
#----------------------------------------------------------
@ -641,9 +680,10 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
raise SessionExpiredException("Session expired")
security.check(self.db, self.uid, self.password)
def logout(self):
def logout(self, keep_db=False):
for k in self.keys():
del self[k]
if not (keep_db and k == 'db'):
del self[k]
self._default_values()
def _default_values(self):
@ -815,8 +855,10 @@ class LazyResponse(werkzeug.wrappers.Response):
""" Lazy werkzeug response.
API not yet frozen"""
def __init__(self, callback, **kwargs):
def __init__(self, callback, status_code=None, **kwargs):
super(LazyResponse, self).__init__(mimetype='text/html')
if status_code:
self.status_code = status_code
self.callback = callback
self.params = kwargs
def process(self):
@ -928,8 +970,13 @@ class Root(object):
return explicit_session
def setup_db(self, httprequest):
if not httprequest.session.db:
# allow "admin" routes to works without being logged in when in monodb.
db = httprequest.session.db
# Check if session.db is legit
if db and db not in db_filter([db], httprequest=httprequest):
httprequest.session.logout()
db = None
if not db:
httprequest.session.db = db_monodb(httprequest)
def setup_lang(self, httprequest):
@ -952,7 +999,10 @@ class Root(object):
try:
result.process()
except(Exception), e:
result = request.registry['ir.http']._handle_exception(e)
if request.db:
result = request.registry['ir.http']._handle_exception(e)
else:
raise
if isinstance(result, basestring):
response = werkzeug.wrappers.Response(result, mimetype='text/html')
@ -971,13 +1021,13 @@ class Root(object):
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
# Support for Cross-Origin Resource Sharing
if request.func.cors:
response.headers.set('Access-Control-Allow-Origin', request.func.cors)
if request.func and 'cors' in request.func.routing:
response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
methods = 'GET, POST'
if request.func_request_type == 'json':
methods = 'POST'
elif request.func.methods:
methods = ', '.join(request.func.methods)
elif request.func.routing.get('methods'):
methods = ', '.join(request.func.routing['methods'])
response.headers.set('Access-Control-Allow-Methods', methods)
return response
@ -988,7 +1038,6 @@ class Root(object):
"""
try:
httprequest = werkzeug.wrappers.Request(environ)
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
httprequest.app = self
explicit_session = self.setup_session(httprequest)
@ -1008,10 +1057,12 @@ class Root(object):
if db:
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
try:
ir_http = request.registry['ir.http']
with openerp.tools.mute_logger('openerp.sql_db'):
ir_http = request.registry['ir.http']
except psycopg2.OperationalError:
# psycopg2 error. At this point, that's mean the database does not exists
# anymore. We unlog the user and failback in nodb mode
# psycopg2 error. At this point, that means the
# database probably does not exists anymore. Log the
# user out and fall back to nodb
request.session.logout()
result = _dispatch_nodb()
else:
@ -1032,8 +1083,11 @@ class Root(object):
return request.registry['ir.http'].routing_map()
def db_list(force=False, httprequest=None):
httprequest = httprequest or request.httprequest
dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
return db_filter(dbs, httprequest=httprequest)
def db_filter(dbs, httprequest=None):
httprequest = httprequest or request.httprequest
h = httprequest.environ['HTTP_HOST'].split(':')[0]
d = h.split('.')[0]
r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
@ -1052,8 +1106,6 @@ def db_monodb(httprequest=None):
Returns ``None`` if the magic is not magic enough.
"""
httprequest = httprequest or request.httprequest
db = None
redirect = None
dbs = db_list(True, httprequest)

View File

@ -207,6 +207,33 @@
</rng:element>
</rng:define>
<rng:define name="template">
<rng:element name="template">
<rng:optional><rng:attribute name="id"/></rng:optional>
<rng:optional><rng:attribute name="t-name"/></rng:optional>
<rng:optional><rng:attribute name="name"/></rng:optional>
<rng:optional><rng:attribute name="forcecreate"/></rng:optional>
<rng:optional><rng:attribute name="context"/></rng:optional>
<rng:optional><rng:attribute name="priority"/></rng:optional>
<rng:choice>
<rng:group>
<rng:optional><rng:attribute name="inherit_id"/></rng:optional>
<rng:optional><rng:attribute name="inherit_option_id"/></rng:optional>
<rng:optional><rng:attribute name="groups"/></rng:optional>
</rng:group>
<rng:optional>
<rng:attribute name="page"><rng:value>True</rng:value></rng:attribute>
</rng:optional>
</rng:choice>
<rng:zeroOrMore>
<rng:choice>
<rng:text/>
<rng:ref name="any"/>
</rng:choice>
</rng:zeroOrMore>
</rng:element>
</rng:define>
<rng:define name="delete">
<rng:element name="delete">
<rng:attribute name="model" />
@ -285,6 +312,7 @@
<rng:text/>
<rng:ref name="menuitem" />
<rng:ref name="record" />
<rng:ref name="template" />
<rng:ref name="delete" />
<rng:ref name="wizard" />
<rng:ref name="act_window" />

View File

@ -70,6 +70,9 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
cr.commit()
else:
cr.rollback()
# avoid keeping stale xml_id, etc. in cache
openerp.modules.registry.RegistryManager.clear_caches(cr.dbname)
def _get_files_of_kind(kind):
if kind == 'demo':

View File

@ -37,6 +37,7 @@
import base64
import datetime as DT
import functools
import logging
import pytz
import re
@ -451,6 +452,39 @@ class selection(_column):
_column.__init__(self, string=string, **args)
self.selection = selection
@classmethod
def reify(cls, cr, uid, model, field, context=None):
""" Munges the field's ``selection`` attribute as necessary to get
something useable out of it: calls it if it's a function, applies
translations to labels if it's not.
A callable ``selection`` is considered translated on its own.
:param orm.Model model:
:param _column field:
"""
if callable(field.selection):
return field.selection(model, cr, uid, context)
if not (context and 'lang' in context):
return field.selection
# field_to_dict isn't given a field name, only a field object, we
# need to get the name back in order to perform the translation lookup
field_name = next(
name for name, column in model._columns.iteritems()
if column == field)
translation_filter = "%s,%s" % (model._name, field_name)
translate = functools.partial(
model.pool['ir.translation']._get_source,
cr, uid, translation_filter, 'selection', context['lang'])
return [
(value, translate(label))
for value, label in field.selection
]
# ---------------------------------------------------------
# Relationals fields
# ---------------------------------------------------------
@ -567,9 +601,12 @@ class one2many(_column):
domain = self._domain(obj) if callable(self._domain) else self._domain
model = obj.pool[self._obj]
ids2 = model.search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
for r in model._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
if r[self._fields_id] in res:
res[r[self._fields_id]].append(r['id'])
if len(ids) != 1:
for r in model._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
if r[self._fields_id] in res:
res[r[self._fields_id]].append(r['id'])
else:
res[ids[0]] = ids2
return res
def set(self, cr, obj, id, field, values, user=None, context=None):
@ -1562,11 +1599,7 @@ def field_to_dict(model, cr, user, field, context=None):
res[arg] = getattr(field, arg)
if hasattr(field, 'selection'):
if isinstance(field.selection, (tuple, list)):
res['selection'] = field.selection
else:
# call the 'dynamic selection' function
res['selection'] = field.selection(model, cr, user, context)
res['selection'] = selection.reify(cr, user, model, field, context=context)
if res['type'] in ('one2many', 'many2many', 'many2one'):
res['relation'] = field._obj
res['domain'] = field._domain(model) if callable(field._domain) else field._domain

View File

@ -516,7 +516,7 @@ class browse_record(object):
return self._id
def __str__(self):
return "browse_record(%s, %d)" % (self._table_name, self._id)
return "browse_record(%s, %s)" % (self._table_name, self._id)
def __eq__(self, other):
if not isinstance(other, browse_record):
@ -1544,9 +1544,16 @@ class BaseModel(object):
error_msgs = []
for constraint in self._constraints:
fun, msg, fields = constraint
# We don't pass around the context here: validation code
# must always yield the same results.
if not fun(self, cr, uid, ids):
try:
# We don't pass around the context here: validation code
# must always yield the same results.
valid = fun(self, cr, uid, ids)
extra_error = None
except Exception, e:
_logger.debug('Exception while validating constraint', exc_info=True)
valid = False
extra_error = tools.ustr(e)
if not valid:
# Check presence of __call__ directly instead of using
# callable() because it will be deprecated as of Python 3.0
if hasattr(msg, '__call__'):
@ -1558,6 +1565,8 @@ class BaseModel(object):
translated_msg = tmp_msg
else:
translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
if extra_error:
translated_msg += "\n\n%s\n%s" % (_('Error details:'), extra_error)
error_msgs.append(
_("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
)
@ -1699,265 +1708,6 @@ class BaseModel(object):
return any([self.pool.get('res.users').has_group(cr, uid, group_ext_id)
for group_ext_id in groups.split(',')])
def __view_look_dom(self, cr, user, node, view_id, in_tree_view, model_fields, context=None):
"""Return the description of the fields in the node.
In a normal call to this method, node is a complete view architecture
but it is actually possible to give some sub-node (this is used so
that the method can call itself recursively).
Originally, the field descriptions are drawn from the node itself.
But there is now some code calling fields_get() in order to merge some
of those information in the architecture.
"""
if context is None:
context = {}
result = False
fields = {}
children = True
modifiers = {}
def encode(s):
if isinstance(s, unicode):
return s.encode('utf8')
return s
def check_group(node):
"""Apply group restrictions, may be set at view level or model level::
* at view level this means the element should be made invisible to
people who are not members
* at model level (exclusively for fields, obviously), this means
the field should be completely removed from the view, as it is
completely unavailable for non-members
:return: True if field should be included in the result of fields_view_get
"""
if node.tag == 'field' and node.get('name') in self._all_columns:
column = self._all_columns[node.get('name')].column
if column.groups and not self.user_has_groups(cr, user,
groups=column.groups,
context=context):
node.getparent().remove(node)
fields.pop(node.get('name'), None)
# no point processing view-level ``groups`` anymore, return
return False
if node.get('groups'):
can_see = self.user_has_groups(cr, user,
groups=node.get('groups'),
context=context)
if not can_see:
node.set('invisible', '1')
modifiers['invisible'] = True
if 'attrs' in node.attrib:
del(node.attrib['attrs']) #avoid making field visible later
del(node.attrib['groups'])
return True
if node.tag in ('field', 'node', 'arrow'):
if node.get('object'):
attrs = {}
views = {}
xml = "<form>"
for f in node:
if f.tag == 'field':
xml += etree.tostring(f, encoding="utf-8")
xml += "</form>"
new_xml = etree.fromstring(encode(xml))
ctx = context.copy()
ctx['base_model_name'] = self._name
xarch, xfields = self.pool[node.get('object')].__view_look_dom_arch(cr, user, new_xml, view_id, ctx)
views['form'] = {
'arch': xarch,
'fields': xfields
}
attrs = {'views': views}
fields = xfields
if node.get('name'):
attrs = {}
try:
if node.get('name') in self._columns:
column = self._columns[node.get('name')]
else:
column = self._inherit_fields[node.get('name')][2]
except Exception:
column = False
if column:
relation = self.pool[column._obj] if column._obj else None
children = False
views = {}
for f in node:
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(f)
ctx = context.copy()
ctx['base_model_name'] = self._name
xarch, xfields = relation.__view_look_dom_arch(cr, user, f, view_id, ctx)
views[str(f.tag)] = {
'arch': xarch,
'fields': xfields
}
attrs = {'views': views}
if node.get('widget') and node.get('widget') == 'selection':
# Prepare the cached selection list for the client. This needs to be
# done even when the field is invisible to the current user, because
# other events could need to change its value to any of the selectable ones
# (such as on_change events, refreshes, etc.)
# If domain and context are strings, we keep them for client-side, otherwise
# we evaluate them server-side to consider them when generating the list of
# possible values
# TODO: find a way to remove this hack, by allow dynamic domains
dom = []
if column._domain and not isinstance(column._domain, basestring):
dom = list(column._domain)
dom += eval(node.get('domain', '[]'), {'uid': user, 'time': time})
search_context = dict(context)
if column._context and not isinstance(column._context, basestring):
search_context.update(column._context)
attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1)
if (node.get('required') and not int(node.get('required'))) or not column.required:
attrs['selection'].append((False, ''))
fields[node.get('name')] = attrs
field = model_fields.get(node.get('name'))
if field:
transfer_field_to_modifiers(field, modifiers)
elif node.tag in ('form', 'tree'):
result = self.view_header_get(cr, user, False, node.tag, context)
if result:
node.set('string', result)
in_tree_view = node.tag == 'tree'
elif node.tag == 'calendar':
for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day','attendee'):
if node.get(additional_field):
fields[node.get(additional_field)] = {}
if not check_group(node):
# node must be removed, no need to proceed further with its children
return fields
# The view architeture overrides the python model.
# Get the attrs before they are (possibly) deleted by check_group below
transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
# TODO remove attrs couterpart in modifiers when invisible is true ?
# translate view
if 'lang' in context:
if node.text and node.text.strip():
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.text.strip())
if trans:
node.text = node.text.replace(node.text.strip(), trans)
if node.tail and node.tail.strip():
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.tail.strip())
if trans:
node.tail = node.tail.replace(node.tail.strip(), trans)
if node.get('string') and not result:
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('string'))
if trans == node.get('string') and ('base_model_name' in context):
# If translation is same as source, perhaps we'd have more luck with the alternative model name
# (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
trans = self.pool.get('ir.translation')._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
if trans:
node.set('string', trans)
for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
attr_value = node.get(attr_name)
if attr_value:
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], attr_value)
if trans:
node.set(attr_name, trans)
for f in node:
if children or (node.tag == 'field' and f.tag in ('filter','separator')):
fields.update(self.__view_look_dom(cr, user, f, view_id, in_tree_view, model_fields, context))
transfer_modifiers_to_node(modifiers, node)
return fields
def _disable_workflow_buttons(self, cr, user, node):
""" Set the buttons in node to readonly if the user can't activate them. """
if user == 1:
# admin user can always activate workflow buttons
return node
# TODO handle the case of more than one workflow for a model or multiple
# transitions with different groups and same signal
usersobj = self.pool.get('res.users')
buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
for button in buttons:
user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
cr.execute("""SELECT DISTINCT t.group_id
FROM wkf
INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
INNER JOIN wkf_transition t ON (t.act_to = a.id)
WHERE wkf.osv = %s
AND t.signal = %s
AND t.group_id is NOT NULL
""", (self._name, button.get('name')))
group_ids = [x[0] for x in cr.fetchall() if x[0]]
can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
button.set('readonly', str(int(not can_click)))
return node
def __view_look_dom_arch(self, cr, user, node, view_id, context=None):
""" Return an architecture and a description of all the fields.
The field description combines the result of fields_get() and
__view_look_dom().
:param node: the architecture as as an etree
:return: a tuple (arch, fields) where arch is the given node as a
string and fields is the description of all the fields.
"""
fields = {}
if node.tag == 'diagram':
if node.getchildren()[0].tag == 'node':
node_model = self.pool[node.getchildren()[0].get('object')]
node_fields = node_model.fields_get(cr, user, None, context)
fields.update(node_fields)
if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
node.set("create", 'false')
if node.getchildren()[1].tag == 'arrow':
arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
fields.update(arrow_fields)
else:
fields = self.fields_get(cr, user, None, context)
fields_def = self.__view_look_dom(cr, user, node, view_id, False, fields, context=context)
node = self._disable_workflow_buttons(cr, user, node)
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
if not node.get(action) and not self.check_access_rights(cr, user, operation, raise_exception=False):
node.set(action, 'false')
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
for k in fields.keys():
if k not in fields_def:
del fields[k]
for field in fields_def:
if field == 'id':
# sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
elif field in fields:
fields[field].update(fields_def[field])
else:
cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field))
res = cr.fetchall()[:]
model = res[0][1]
res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None))
msg = "\n * ".join([r[0] for r in res])
msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model"
_logger.error(msg)
raise except_orm('View error', msg)
return arch, fields
def _get_default_form_view(self, cr, user, context=None):
""" Generates a default single-line form view using all fields
of the current model except the m2m and o2m ones.
@ -2055,18 +1805,12 @@ class BaseModel(object):
return view
#
# if view_id, view_type is not required
#
def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
"""
Get the detailed composition of the requested view like fields, model, view architecture
:param cr: database cursor
:param user: current user id
:param view_id: id of the view or None
:param view_type: type of the view to return if view_id is None ('form', tree', ...)
:param context: context arguments, like lang, time zone
:param toolbar: true to include contextual actions
:param submenu: deprecated
:return: dictionary describing the composition of the requested view (including inherited views and extensions)
@ -2074,156 +1818,22 @@ class BaseModel(object):
* if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace'
* if some tag other than 'position' is found in parent view
:raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc defined on the structure
"""
if context is None:
context = {}
View = self.pool['ir.ui.view']
def encode(s):
if isinstance(s, unicode):
return s.encode('utf8')
return s
result = {
'model': self._name,
'field_parent': False,
}
def raise_view_error(error_msg, child_view_id):
view, child_view = self.pool.get('ir.ui.view').browse(cr, user, [view_id, child_view_id], context)
error_msg = error_msg % {'parent_xml_id': view.xml_id}
raise AttributeError("View definition error for inherited view '%s' on model '%s': %s"
% (child_view.xml_id, self._name, error_msg))
def locate(source, spec):
""" Locate a node in a source (parent) architecture.
Given a complete source (parent) architecture (i.e. the field
`arch` in a view), and a 'spec' node (a node in an inheriting
view that specifies the location in the source view of what
should be changed), return (if it exists) the node in the
source view matching the specification.
:param source: a parent architecture to modify
:param spec: a modifying node in an inheriting view
:return: a node in the source matching the spec
"""
if spec.tag == 'xpath':
nodes = source.xpath(spec.get('expr'))
return nodes[0] if nodes else None
elif spec.tag == 'field':
# Only compare the field name: a field can be only once in a given view
# at a given level (and for multilevel expressions, we should use xpath
# inheritance spec anyway).
for node in source.getiterator('field'):
if node.get('name') == spec.get('name'):
return node
return None
for node in source.getiterator(spec.tag):
if isinstance(node, SKIPPED_ELEMENT_TYPES):
continue
if all(node.get(attr) == spec.get(attr) \
for attr in spec.attrib
if attr not in ('position','version')):
# Version spec should match parent's root element's version
if spec.get('version') and spec.get('version') != source.get('version'):
return None
return node
return None
def apply_inheritance_specs(source, specs_arch, inherit_id=None):
""" Apply an inheriting view.
Apply to a source architecture all the spec nodes (i.e. nodes
describing where and what changes to apply to some parent
architecture) given by an inheriting view.
:param source: a parent architecture to modify
:param specs_arch: a modifying architecture in an inheriting view
:param inherit_id: the database id of the inheriting view
:return: a modified source where the specs are applied
"""
specs_tree = etree.fromstring(encode(specs_arch))
# Queue of specification nodes (i.e. nodes describing where and
# changes to apply to some parent architecture).
specs = [specs_tree]
while len(specs):
spec = specs.pop(0)
if isinstance(spec, SKIPPED_ELEMENT_TYPES):
continue
if spec.tag == 'data':
specs += [ c for c in specs_tree ]
continue
node = locate(source, spec)
if node is not None:
pos = spec.get('position', 'inside')
if pos == 'replace':
if node.getparent() is None:
source = copy.deepcopy(spec[0])
else:
for child in spec:
node.addprevious(child)
node.getparent().remove(node)
elif pos == 'attributes':
for child in spec.getiterator('attribute'):
attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
if attribute[1]:
node.set(attribute[0], attribute[1])
elif attribute[0] in node.attrib:
del node.attrib[attribute[0]]
else:
sib = node.getnext()
for child in spec:
if pos == 'inside':
node.append(child)
elif pos == 'after':
if sib is None:
node.addnext(child)
node = child
else:
sib.addprevious(child)
elif pos == 'before':
node.addprevious(child)
else:
raise_view_error("Invalid position value: '%s'" % pos, inherit_id)
else:
attrs = ''.join([
' %s="%s"' % (attr, spec.get(attr))
for attr in spec.attrib
if attr != 'position'
])
tag = "<%s%s>" % (spec.tag, attrs)
if spec.get('version') and spec.get('version') != source.get('version'):
raise_view_error("Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \
(tag, spec.get('version'), source.get('version')), inherit_id)
raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id)
return source
def apply_view_inheritance(cr, user, source, inherit_id):
""" Apply all the (directly and indirectly) inheriting views.
:param source: a parent architecture to modify (with parent
modifications already applied)
:param inherit_id: the database view_id of the parent view
:return: a modified source where all the modifying architecture
are applied
"""
sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name, context=context)
for (view_arch, view_id) in sql_inherit:
source = apply_inheritance_specs(source, view_arch, view_id)
source = apply_view_inheritance(cr, user, source, view_id)
return source
result = {'type': view_type, 'model': self._name}
sql_res = False
parent_view_model = None
view_ref_key = view_type + '_view_ref'
view_ref = context.get(view_ref_key)
# Search for a root (i.e. without any parent) view.
while True:
if view_ref and not view_id:
# try to find a view_id if none provided
if not view_id:
# <view_type>_view_ref in context can be used to overrride the default view
view_ref_key = view_type + '_view_ref'
view_ref = context.get(view_ref_key)
if view_ref:
if '.' in view_ref:
module, view_ref = view_ref.split('.', 1)
cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref))
@ -2235,82 +1845,53 @@ class BaseModel(object):
'Please use the complete `module.view_id` form instead.', view_ref_key, view_ref,
self._name)
if view_id:
cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model
FROM ir_ui_view
WHERE id=%s""", (view_id,))
else:
cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model
FROM ir_ui_view
WHERE model=%s AND type=%s AND inherit_id IS NULL
ORDER BY priority""", (self._name, view_type))
sql_res = cr.dictfetchone()
if not view_id:
# otherwise try to find the lowest priority matching ir.ui.view
view_id = View.default_view(cr, uid, self._name, view_type, context=context)
if not sql_res:
break
view_id = sql_res['inherit_id'] or sql_res['id']
parent_view_model = sql_res['model']
if not sql_res['inherit_id']:
break
# if a view was found
if sql_res:
source = etree.fromstring(encode(sql_res['arch']))
result.update(
arch=apply_view_inheritance(cr, user, source, sql_res['id']),
type=sql_res['type'],
view_id=sql_res['id'],
name=sql_res['name'],
field_parent=sql_res['field_parent'] or False)
# context for post-processing might be overriden
ctx = context
if view_id:
# read the view with inherited views applied
root_view = View.read_combined(cr, uid, view_id, fields=['id', 'name', 'field_parent', 'type', 'model', 'arch'], context=context)
result['arch'] = root_view['arch']
result['name'] = root_view['name']
result['type'] = root_view['type']
result['view_id'] = root_view['id']
result['field_parent'] = root_view['field_parent']
# override context fro postprocessing
if root_view.get('model') != self._name:
ctx = dict(context, base_model_name=root_view.get('model'))
else:
# otherwise, build some kind of default view
# fallback on default views methods if no ir.ui.view could be found
try:
view = getattr(self, '_get_default_%s_view' % view_type)(
cr, user, context)
get_func = getattr(self, '_get_default_%s_view' % view_type)
arch_etree = get_func(cr, uid, context)
result['arch'] = etree.tostring(arch_etree, encoding='utf-8')
result['type'] = view_type
result['name'] = 'default'
except AttributeError:
# what happens here, graph case?
raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type)
raise except_orm(_('Invalid Architecture!'), _("No default view of type '%s' could be found !") % view_type)
result.update(
arch=view,
name='default',
field_parent=False,
view_id=0)
if parent_view_model != self._name:
ctx = context.copy()
ctx['base_model_name'] = parent_view_model
else:
ctx = context
xarch, xfields = self.__view_look_dom_arch(cr, user, result['arch'], view_id, context=ctx)
# Apply post processing, groups and modifiers etc...
xarch, xfields = View.postprocess_and_fields(cr, uid, self._name, etree.fromstring(result['arch']), view_id, context=ctx)
result['arch'] = xarch
result['fields'] = xfields
# Add related action information if aksed
if toolbar:
toclean = ('report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml', 'report_sxw_content_data', 'report_rml_content_data')
def clean(x):
x = x[2]
for key in ('report_sxw_content', 'report_rml_content',
'report_sxw', 'report_rml',
'report_sxw_content_data', 'report_rml_content_data'):
if key in x:
del x[key]
for key in toclean:
x.pop(key, None)
return x
ir_values_obj = self.pool.get('ir.values')
resprint = ir_values_obj.get(cr, user, 'action',
'client_print_multi', [(self._name, False)], False,
context)
resaction = ir_values_obj.get(cr, user, 'action',
'client_action_multi', [(self._name, False)], False,
context)
resrelate = ir_values_obj.get(cr, user, 'action',
'client_action_relate', [(self._name, False)], False,
context)
resaction = [clean(action) for action in resaction
if view_type == 'tree' or not action[2].get('multi')]
resprint = [clean(print_) for print_ in resprint
if view_type == 'tree' or not print_[2].get('multi')]
resprint = ir_values_obj.get(cr, uid, 'action', 'client_print_multi', [(self._name, False)], False, context)
resaction = ir_values_obj.get(cr, uid, 'action', 'client_action_multi', [(self._name, False)], False, context)
resrelate = ir_values_obj.get(cr, uid, 'action', 'client_action_relate', [(self._name, False)], False, context)
resaction = [clean(action) for action in resaction if view_type == 'tree' or not action[2].get('multi')]
resprint = [clean(print_) for print_ in resprint if view_type == 'tree' or not print_[2].get('multi')]
#When multi="True" set it will display only in More of the list view
resrelate = [clean(action) for action in resrelate
if (action[2].get('multi') and view_type == 'tree') or (not action[2].get('multi') and view_type == 'form')]
@ -2325,11 +1906,11 @@ class BaseModel(object):
}
return result
_view_look_dom_arch = __view_look_dom_arch
def _view_look_dom_arch(self, cr, uid, node, view_id, context=None):
return self['ir.ui.view'].postprocess_and_fields(
cr, uid, self._name, node, view_id, context=context)
def search_count(self, cr, user, args, context=None):
if not context:
context = {}
res = self.search(cr, user, args, context=context, count=True)
if isinstance(res, list):
return len(res)
@ -3537,31 +3118,6 @@ class BaseModel(object):
self._columns[field_name].required = True
self._columns[field_name].ondelete = "cascade"
#def __getattr__(self, name):
# """
# Proxies attribute accesses to the `inherits` parent so we can call methods defined on the inherited parent
# (though inherits doesn't use Python inheritance).
# Handles translating between local ids and remote ids.
# Known issue: doesn't work correctly when using python's own super(), don't involve inherit-based inheritance
# when you have inherits.
# """
# for model, field in self._inherits.iteritems():
# proxy = self.pool.get(model)
# if hasattr(proxy, name):
# attribute = getattr(proxy, name)
# if not hasattr(attribute, '__call__'):
# return attribute
# break
# else:
# return super(orm, self).__getattr__(name)
# def _proxy(cr, uid, ids, *args, **kwargs):
# objects = self.browse(cr, uid, ids, kwargs.get('context', None))
# lst = [obj[field].id for obj in objects if obj[field]]
# return getattr(proxy, name)(cr, uid, lst, *args, **kwargs)
# return _proxy
def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
""" Return the definition of each field.
@ -3610,16 +3166,6 @@ class BaseModel(object):
help_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'help', context['lang'])
if help_trans:
res[f]['help'] = help_trans
if 'selection' in res[f]:
if isinstance(field.selection, (tuple, list)):
sel = field.selection
sel2 = []
for key, val in sel:
val2 = None
if val:
val2 = translation_obj._get_source(cr, user, self._name + ',' + f, 'selection', context['lang'], val)
sel2.append((key, val2 or val))
res[f]['selection'] = sel2
return res
@ -4347,7 +3893,7 @@ class BaseModel(object):
for (parent_pright, parent_id) in parents:
if parent_id == id:
break
position = parent_pright + 1
position = parent_pright and parent_pright + 1 or 1
# It's the first node of the parent
if not position:
@ -5471,7 +5017,7 @@ class BaseModel(object):
:rtype: List of dictionaries.
"""
record_ids = self.search(cr, uid, domain or [], offset, limit or False, order or False, context or {})
record_ids = self.search(cr, uid, domain or [], offset=offset, limit=limit, order=order, context=context)
if not record_ids:
return []
@ -5479,14 +5025,13 @@ class BaseModel(object):
# shortcut read if we only want the ids
return [{'id': id} for id in record_ids]
result = self.read(cr, uid, record_ids, fields or [], context or {})
result = self.read(cr, uid, record_ids, fields, context=context)
if len(result) <= 1:
return result
# reorder read
if len(result) >= 1:
index = {}
for r in result:
index[r['id']] = r
result = [index[x] for x in record_ids if x in index]
return result
index = dict((r['id'], r) for r in result)
return [index[x] for x in record_ids if x in index]
def _register_hook(self, cr):
""" stuff to do right after the registry is built """
@ -5557,12 +5102,12 @@ def itemgetter_tuple(items):
if len(items) == 1:
return lambda gettable: (gettable[items[0]],)
return operator.itemgetter(*items)
class ImportWarning(Warning):
""" Used to send warnings upwards the stack during the import process
"""
pass
def convert_pgerror_23502(model, fields, info, e):
m = re.match(r'^null value in column "(?P<field>\w+)" violates '
r'not-null constraint\n',
@ -5578,6 +5123,7 @@ def convert_pgerror_23502(model, fields, info, e):
'message': message,
'field': field_name,
}
def convert_pgerror_23505(model, fields, info, e):
m = re.match(r'^duplicate key (?P<field>\w+) violates unique constraint',
str(e))

View File

@ -7,7 +7,6 @@ This module groups a few sub-modules containing unittest2 test cases.
Tests can be explicitely added to the `fast_suite` or `checks` lists or not.
See the :ref:`test-framework` section in the :ref:`features` list.
"""
import test_acl
import test_basecase
import test_db_cursor
@ -20,8 +19,9 @@ import test_misc
import test_orm
import test_osv
import test_translate
import test_uninstall
import test_view_validation
import test_qweb
import test_func
# This need a change in `oe run-tests` to only run fast_suite + checks by default.
# import test_xmlrpc
@ -42,6 +42,8 @@ checks = [
test_misc,
test_osv,
test_translate,
test_qweb,
test_func,
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,5 @@
{
'name': 'test_convert',
'description': "Data for xml conversion tests",
'version': '0.0.1',
}

View File

@ -0,0 +1 @@
nothing to see here, move along

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import test_convert
checks = [
test_convert
]

View File

@ -0,0 +1,83 @@
import collections
import unittest2
from lxml import etree as ET
from lxml.builder import E
from openerp.tests import common
from openerp.tools.convert import _eval_xml
Field = E.field
Value = E.value
class TestEvalXML(common.TransactionCase):
def eval_xml(self, node, obj=None, idref=None):
return _eval_xml(obj, node, pool=None, cr=self.cr, uid=self.uid,
idref=idref, context=None)
def test_char(self):
self.assertEqual(
self.eval_xml(Field("foo")),
"foo")
self.assertEqual(
self.eval_xml(Field("None")),
"None")
def test_int(self):
self.assertIsNone(
self.eval_xml(Field("None", type='int')),
"what the fuck?")
self.assertEqual(
self.eval_xml(Field(" 42 ", type="int")),
42)
with self.assertRaises(ValueError):
self.eval_xml(Field("4.82", type="int"))
with self.assertRaises(ValueError):
self.eval_xml(Field("Whelp", type="int"))
def test_float(self):
self.assertEqual(
self.eval_xml(Field("4.78", type="float")),
4.78)
with self.assertRaises(ValueError):
self.eval_xml(Field("None", type="float"))
with self.assertRaises(ValueError):
self.eval_xml(Field("Foo", type="float"))
def test_list(self):
self.assertEqual(
self.eval_xml(Field(type="list")),
[])
self.assertEqual(
self.eval_xml(Field(
Value("foo"),
Value("5", type="int"),
Value("4.76", type="float"),
Value("None", type="int"),
type="list"
)),
["foo", 5, 4.76, None])
def test_file(self):
Obj = collections.namedtuple('Obj', 'module')
obj = Obj('test_convert')
self.assertEqual(
self.eval_xml(Field('test_file.txt', type='file'), obj),
'test_convert,test_file.txt')
with self.assertRaises(IOError):
self.eval_xml(Field('test_nofile.txt', type='file'), obj)
@unittest2.skip("not tested")
def test_xml(self):
pass
@unittest2.skip("not tested")
def test_html(self):
pass

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
import models

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
{
'name': 'test-field-converter',
'version': '0.1',
'category': 'Tests',
'description': """Tests of field conversions""",
'author': 'OpenERP SA',
'maintainer': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': ['base'],
'data': ['ir.model.access.csv'],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +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

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
from openerp.osv import orm, fields
class test_model(orm.Model):
_name = 'test_converter.test_model'
_columns = {
'char': fields.char(),
'integer': fields.integer(),
'float': fields.float(),
'numeric': fields.float(digits=(16, 2)),
'many2one': fields.many2one('test_converter.test_model.sub'),
'binary': fields.binary(),
'date': fields.date(),
'datetime': fields.datetime(),
'selection': fields.selection([
(1, "réponse A"),
(2, "réponse B"),
(3, "réponse C"),
(4, "réponse D"),
]),
'selection_str': fields.selection([
('A', "Qu'il n'est pas arrivé à Toronto"),
('B', "Qu'il était supposé arriver à Toronto"),
('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"),
('D', "La réponse D"),
], string="Lorsqu'un pancake prend l'avion à destination de Toronto et "
"qu'il fait une escale technique à St Claude, on dit:"),
'html': fields.html(),
'text': fields.text(),
}
class test_model_sub(orm.Model):
_name = 'test_converter.test_model.sub'
_columns = {
'name': fields.char()
}
class test_model_monetary(orm.Model):
_name = 'test_converter.monetary'
_columns = {
'value': fields.float(digits=(16, 55)),
}

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
from . import test_html
fast_suite = [
]
checks = [
test_html
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,387 @@
# -*- encoding: utf-8 -*-
import json
import os
import xml.dom.minidom
import datetime
from werkzeug.utils import escape as e
from openerp.tests import common
from openerp.addons.base.ir import ir_qweb
directory = os.path.dirname(__file__)
impl = xml.dom.minidom.getDOMImplementation()
doc = impl.createDocument(None, None, None)
class TestExport(common.TransactionCase):
_model = None
def setUp(self):
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, type=None):
column = self.get_column(name)
for postfix in type, column._type, '':
fs = ['ir', 'qweb', 'field']
if postfix is None: continue
if postfix: fs.append(postfix)
try:
model = self.registry('.'.join(fs))
break
except KeyError: pass
return lambda value, options=None, context=None: e(model.value_to_html(
self.cr, self.uid, value, column, options=options, context=context))
class TestBasicExport(TestExport):
_model = 'test_converter.test_model'
class TestCharExport(TestBasicExport):
def test_char(self):
converter = self.get_converter('char')
value = converter('foo')
self.assertEqual(value, 'foo')
value = converter("foo<bar>")
self.assertEqual(value, "foo&lt;bar&gt;")
class TestIntegerExport(TestBasicExport):
def test_integer(self):
converter = self.get_converter('integer')
value = converter(42)
self.assertEqual(value, "42")
class TestFloatExport(TestBasicExport):
def setUp(self):
super(TestFloatExport, self).setUp()
self.registry('res.lang').write(self.cr, self.uid, [1], {
'grouping': '[3,0]'
})
def test_float(self):
converter = self.get_converter('float')
value = converter(42.0)
self.assertEqual(value, "42.0")
value = converter(42.0100)
self.assertEqual(value, "42.01")
value = converter(42.01234)
self.assertEqual(value, "42.01234")
value = converter(1234567.89)
self.assertEqual(value, '1,234,567.89')
def test_numeric(self):
converter = self.get_converter('numeric')
value = converter(42.0)
self.assertEqual(value, '42.00')
value = converter(42.01234)
self.assertEqual(value, '42.01')
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(self.cr, self.uid, {'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 = converter("This is my text-kai")
self.assertEqual(value, "This is my text-kai")
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,$].
$ The last line in the buffer.
- The previous line. This is equivalent to -1 and may be repeated with cumulative effect.
-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(value, """<br>
. The current line (address) in the buffer.<br>
$ The last line in the buffer.<br>
n The nth, line in the buffer where n is a number in the range [0,$].<br>
$ The last line in the buffer.<br>
- The previous line. This is equivalent to -1 and may be repeated with cumulative effect.<br>
-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>
""")
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>
""")
self.assertEqual(value, """<br>
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>
""")
class TestMany2OneExport(TestBasicExport):
def test_many2one(self):
Sub = self.registry('test_converter.test_model.sub')
id0 = self.Model.create(self.cr, self.uid, {
'many2one': Sub.create(self.cr, self.uid, {'name': "Foo"})
})
id1 = self.Model.create(self.cr, self.uid, {
'many2one': Sub.create(self.cr, self.uid, {'name': "Fo<b>o</b>"})
})
def converter(record):
column = self.get_column('many2one')
model = self.registry('ir.qweb.field.many2one')
return e(model.record_to_html(
self.cr, self.uid, 'many2one', record, column))
value = converter(self.Model.browse(self.cr, self.uid, id0))
self.assertEqual(value, "Foo")
value = converter(self.Model.browse(self.cr, self.uid, id1))
self.assertEqual(value, "Fo&lt;b&gt;o&lt;/b&gt;")
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()
encoded_content = content.encode('base64')
value = e(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):
e(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):
e(converter.value_to_html(
self.cr, self.uid, 'binary', content.encode('base64'), column))
class TestSelectionExport(TestBasicExport):
def test_selection(self):
[record] = self.Model.browse(self.cr, self.uid, [self.Model.create(self.cr, self.uid, {
'selection': 2,
'selection_str': 'C',
})])
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")
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 ?")
class TestHTMLExport(TestBasicExport):
def test_html(self):
converter = self.get_converter('html')
input = '<span>span</span>'
value = converter(input)
self.assertEqual(value, input)
class TestDatetimeExport(TestBasicExport):
def setUp(self):
super(TestDatetimeExport, self).setUp()
# set user tz to known value
Users = self.registry('res.users')
Users.write(self.cr, self.uid, self.uid, {
'tz': 'Pacific/Niue'
}, context=None)
def test_date(self):
converter = self.get_converter('date')
value = converter('2011-05-03')
# default lang/format is US
self.assertEqual(value, '05/03/2011')
def test_datetime(self):
converter = self.get_converter('datetime')
value = converter('2011-05-03 11:12:13')
# default lang/format is US
self.assertEqual(value, '05/03/2011 00:12:13')
def test_custom_format(self):
converter = self.get_converter('datetime')
converter2 = self.get_converter('date')
opts = {'format': 'MMMM d'}
value = converter('2011-03-02 11:12:13', options=opts)
value2 = converter2('2001-03-02', options=opts)
self.assertEqual(
value,
'March 2'
)
self.assertEqual(
value2,
'March 2'
)
class TestDurationExport(TestBasicExport):
def setUp(self):
super(TestDurationExport, self).setUp()
# needs to have lang installed otherwise falls back on en_US
self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
def test_negative(self):
converter = self.get_converter('float', 'duration')
with self.assertRaises(ValueError):
converter(-4)
def test_missing_unit(self):
converter = self.get_converter('float', 'duration')
with self.assertRaises(ValueError):
converter(4)
def test_basic(self):
converter = self.get_converter('float', 'duration')
result = converter(4, {'unit': 'hour'}, {'lang': 'fr_FR'})
self.assertEqual(result, u'4 heures')
result = converter(50, {'unit': 'second'}, {'lang': 'fr_FR'})
self.assertEqual(result, u'50 secondes')
def test_multiple(self):
converter = self.get_converter('float', 'duration')
result = converter(1.5, {'unit': 'hour'}, {'lang': 'fr_FR'})
self.assertEqual(result, u"1 heure 30 minutes")
result = converter(72, {'unit': 'second'}, {'lang': 'fr_FR'})
self.assertEqual(result, u"1 minute 12 secondes")
class TestRelativeDatetime(TestBasicExport):
# not sure how a test based on "current time" should be tested. Even less
# so as it would mostly be a test of babel...
def setUp(self):
super(TestRelativeDatetime, self).setUp()
# needs to have lang installed otherwise falls back on en_US
self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
def test_basic(self):
converter = self.get_converter('datetime', 'relative')
t = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
result = converter(t, context={'lang': 'fr_FR'})
self.assertEqual(result, u"il y a 1 heure")

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -17,7 +17,7 @@ class TestACL(common.TransactionCase):
self.res_currency = self.registry('res.currency')
self.res_partner = self.registry('res.partner')
self.res_users = self.registry('res.users')
self.demo_uid = 3
_, self.demo_uid = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, 'base', 'user_demo')
self.tech_group = self.registry('ir.model.data').get_object(self.cr, self.uid,
*(GROUP_TECHNICAL_FEATURES.split('.')))
@ -104,7 +104,6 @@ class TestACL(common.TransactionCase):
finally:
self.res_partner._columns['email'].groups = False
if __name__ == '__main__':
unittest2.main()

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
import functools
import unittest2
from ..tools.func import compose
class TestCompose(unittest2.TestCase):
def test_basic(self):
str_add = compose(str, lambda a, b: a + b)
self.assertEqual(
str_add(1, 2),
"3")
def test_decorator(self):
""" ensure compose() can be partially applied as a decorator
"""
@functools.partial(compose, unicode)
def mul(a, b):
return a * b
self.assertEqual(mul(5, 42), u"210")

View File

@ -4,22 +4,6 @@ import functools
from openerp import exceptions
from . import common
class Fixtures(object):
def __init__(self, *args):
self.fixtures = args
def __call__(self, fn):
@functools.wraps(fn)
def wrapper(case):
for model, vars in self.fixtures:
case.registry(model).create(
case.cr, common.ADMIN_USER_ID, vars, {})
fn(case)
return wrapper
def fixtures(*args):
return Fixtures(*args)
def noid(d):
""" Removes `id` key from a dict so we don't have to keep these things
around when trying to match
@ -27,17 +11,26 @@ def noid(d):
if 'id' in d: del d['id']
return d
class TestGetFilters(common.TransactionCase):
USER_ID = 3
USER = (3, u'Demo User')
class FiltersCase(common.TransactionCase):
def build(self, model, *args):
Model = self.registry(model)
for vars in args:
Model.create(self.cr, common.ADMIN_USER_ID, vars, {})
class TestGetFilters(FiltersCase):
def setUp(self):
super(TestGetFilters, self).setUp()
self.USER = self.registry('res.users').name_search(self.cr, self.uid, 'demo')[0]
self.USER_ID = self.USER[0]
@fixtures(
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='b', user_id=USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='c', user_id=USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='d', user_id=USER_ID, model_id='ir.filters')),
)
def test_own_filters(self):
self.build(
'ir.filters',
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
dict(name='b', user_id=self.USER_ID, model_id='ir.filters'),
dict(name='c', user_id=self.USER_ID, model_id='ir.filters'),
dict(name='d', user_id=self.USER_ID, model_id='ir.filters'))
filters = self.registry('ir.filters').get_filters(
self.cr, self.USER_ID, 'ir.filters')
@ -48,13 +41,15 @@ class TestGetFilters(common.TransactionCase):
dict(name='d', is_default=False, user_id=self.USER, domain='[]', context='{}'),
])
@fixtures(
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='b', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='c', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='d', user_id=False, model_id='ir.filters')),
)
def test_global_filters(self):
self.build(
'ir.filters',
dict(name='a', user_id=False, model_id='ir.filters'),
dict(name='b', user_id=False, model_id='ir.filters'),
dict(name='c', user_id=False, model_id='ir.filters'),
dict(name='d', user_id=False, model_id='ir.filters'),
)
filters = self.registry('ir.filters').get_filters(
self.cr, self.USER_ID, 'ir.filters')
@ -65,13 +60,14 @@ class TestGetFilters(common.TransactionCase):
dict(name='d', is_default=False, user_id=False, domain='[]', context='{}'),
])
@fixtures(
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='b', user_id=common.ADMIN_USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='c', user_id=USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='d', user_id=common.ADMIN_USER_ID, model_id='ir.filters')),
)
def test_no_third_party_filters(self):
self.build(
'ir.filters',
dict(name='a', user_id=False, model_id='ir.filters'),
dict(name='b', user_id=common.ADMIN_USER_ID, model_id='ir.filters'),
dict(name='c', user_id=self.USER_ID, model_id='ir.filters'),
dict(name='d', user_id=common.ADMIN_USER_ID, model_id='ir.filters') )
filters = self.registry('ir.filters').get_filters(
self.cr, self.USER_ID, 'ir.filters')
@ -80,9 +76,11 @@ class TestGetFilters(common.TransactionCase):
dict(name='c', is_default=False, user_id=self.USER, domain='[]', context='{}'),
])
class TestOwnDefaults(common.TransactionCase):
USER_ID = 3
USER = (3, u'Demo User')
class TestOwnDefaults(FiltersCase):
def setUp(self):
super(TestOwnDefaults, self).setUp()
self.USER = self.registry('res.users').name_search(self.cr, self.uid, 'demo')[0]
self.USER_ID = self.USER[0]
def test_new_no_filter(self):
"""
@ -103,15 +101,17 @@ class TestOwnDefaults(common.TransactionCase):
domain='[]', context='{}')
])
@fixtures(
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='b', user_id=USER_ID, model_id='ir.filters')),
)
def test_new_filter_not_default(self):
"""
When creating a @is_default filter with existing non-default filters,
the new filter gets the flag
"""
self.build(
'ir.filters',
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
dict(name='b', user_id=self.USER_ID, model_id='ir.filters'),
)
Filters = self.registry('ir.filters')
Filters.create_or_replace(self.cr, self.USER_ID, {
'name': 'c',
@ -127,15 +127,17 @@ class TestOwnDefaults(common.TransactionCase):
dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'),
])
@fixtures(
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='b', is_default=True, user_id=USER_ID, model_id='ir.filters')),
)
def test_new_filter_existing_default(self):
"""
When creating a @is_default filter where an existing filter is already
@is_default, the flag should be *moved* from the old to the new filter
"""
self.build(
'ir.filters',
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
dict(name='b', is_default=True, user_id=self.USER_ID, model_id='ir.filters'),
)
Filters = self.registry('ir.filters')
Filters.create_or_replace(self.cr, self.USER_ID, {
'name': 'c',
@ -151,15 +153,17 @@ class TestOwnDefaults(common.TransactionCase):
dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'),
])
@fixtures(
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
('ir.filters', dict(name='b', is_default=True, user_id=USER_ID, model_id='ir.filters')),
)
def test_update_filter_set_default(self):
"""
When updating an existing filter to @is_default, if an other filter
already has the flag the flag should be moved
"""
self.build(
'ir.filters',
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
dict(name='b', is_default=True, user_id=self.USER_ID, model_id='ir.filters'),
)
Filters = self.registry('ir.filters')
Filters.create_or_replace(self.cr, self.USER_ID, {
'name': 'a',
@ -174,18 +178,23 @@ class TestOwnDefaults(common.TransactionCase):
dict(name='b', user_id=self.USER, is_default=False, domain='[]', context='{}'),
])
class TestGlobalDefaults(common.TransactionCase):
USER_ID = 3
class TestGlobalDefaults(FiltersCase):
def setUp(self):
super(TestGlobalDefaults, self).setUp()
self.USER = self.registry('res.users').name_search(self.cr, self.uid, 'demo')[0]
self.USER_ID = self.USER[0]
@fixtures(
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='b', user_id=False, model_id='ir.filters')),
)
def test_new_filter_not_default(self):
"""
When creating a @is_default filter with existing non-default filters,
the new filter gets the flag
"""
self.build(
'ir.filters',
dict(name='a', user_id=False, model_id='ir.filters'),
dict(name='b', user_id=False, model_id='ir.filters'),
)
Filters = self.registry('ir.filters')
Filters.create_or_replace(self.cr, self.USER_ID, {
'name': 'c',
@ -201,15 +210,17 @@ class TestGlobalDefaults(common.TransactionCase):
dict(name='c', user_id=False, is_default=True, domain='[]', context='{}'),
])
@fixtures(
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')),
)
def test_new_filter_existing_default(self):
"""
When creating a @is_default filter where an existing filter is already
@is_default, an error should be generated
"""
self.build(
'ir.filters',
dict(name='a', user_id=False, model_id='ir.filters'),
dict(name='b', is_default=True, user_id=False, model_id='ir.filters'),
)
Filters = self.registry('ir.filters')
with self.assertRaises(exceptions.Warning):
Filters.create_or_replace(self.cr, self.USER_ID, {
@ -219,15 +230,17 @@ class TestGlobalDefaults(common.TransactionCase):
'is_default': True,
})
@fixtures(
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')),
)
def test_update_filter_set_default(self):
"""
When updating an existing filter to @is_default, if an other filter
already has the flag an error should be generated
"""
self.build(
'ir.filters',
dict(name='a', user_id=False, model_id='ir.filters'),
dict(name='b', is_default=True, user_id=False, model_id='ir.filters'),
)
Filters = self.registry('ir.filters')
with self.assertRaises(exceptions.Warning):
@ -238,14 +251,16 @@ class TestGlobalDefaults(common.TransactionCase):
'is_default': True,
})
@fixtures(
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')),
)
def test_update_default_filter(self):
"""
Replacing the current default global filter should not generate any error
"""
self.build(
'ir.filters',
dict(name='a', user_id=False, model_id='ir.filters'),
dict(name='b', is_default=True, user_id=False, model_id='ir.filters'),
)
Filters = self.registry('ir.filters')
context_value = "{'some_key': True}"
Filters.create_or_replace(self.cr, self.USER_ID, {

View File

@ -22,18 +22,21 @@
#
##############################################################################
from lxml import etree
import unittest2
from . import test_mail_examples
from lxml import etree
from openerp.tests import test_mail_examples
from openerp.tools import html_sanitize, html_email_clean, append_content_to_html, plaintext2html, email_split
class TestSanitizer(unittest2.TestCase):
""" Test the html sanitizer that filters html to remove unwanted attributes """
def test_basic_sanitizer(self):
cases = [
("yop", "<p>yop</p>"), # simple
("lala<p>yop</p>xxx", "<div><p>lala</p><p>yop</p>xxx</div>"), # trailing text
("lala<p>yop</p>xxx", "<p>lala</p><p>yop</p>xxx"), # trailing text
("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci",
u"<p>Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci</p>"), # unicode
]
@ -276,6 +279,40 @@ class TestCleaner(unittest2.TestCase):
for ext in test_mail_examples.THUNDERBIRD_1_OUT:
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase signature / quoted content')
def test_70_read_more_and_shorten(self):
expand_options = {
'oe_expand_container_class': 'span_class',
'oe_expand_container_content': 'Herbert Einstein',
'oe_expand_separator_node': 'br_lapin',
'oe_expand_a_class': 'a_class',
'oe_expand_a_content': 'read mee',
}
new_html = html_email_clean(test_mail_examples.OERP_WEBSITE_HTML_1, remove=True, shorten=True, max_length=100, expand_options=expand_options)
for ext in test_mail_examples.OERP_WEBSITE_HTML_1_IN:
self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed not quoted content')
for ext in test_mail_examples.OERP_WEBSITE_HTML_1_OUT:
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase overlimit content')
for ext in ['<span class="span_class">Herbert Einstein<br_lapin></br_lapin><a href="#" class="a_class">read mee</a></span>']:
self.assertIn(ext, new_html, 'html_email_cleaner wrongly take into account specific expand options')
new_html = html_email_clean(test_mail_examples.OERP_WEBSITE_HTML_2, remove=True, shorten=True, max_length=200, expand_options=expand_options, protect_sections=False)
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_IN:
self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed not quoted content')
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_OUT:
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase overlimit content')
for ext in ['<span class="span_class">Herbert Einstein<br_lapin></br_lapin><a href="#" class="a_class">read mee</a></span>']:
self.assertIn(ext, new_html, 'html_email_cleaner wrongly take into account specific expand options')
new_html = html_email_clean(test_mail_examples.OERP_WEBSITE_HTML_2, remove=True, shorten=True, max_length=200, expand_options=expand_options, protect_sections=True)
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_IN:
self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed not quoted content')
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_OUT:
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase overlimit content')
for ext in [
'<span class="span_class">Herbert Einstein<br_lapin></br_lapin><a href="#" class="a_class">read mee</a></span>',
'tasks using the gantt chart and control deadlines']:
self.assertIn(ext, new_html, 'html_email_cleaner wrongly take into account specific expand options')
def test_70_read_more(self):
new_html = html_email_clean(test_mail_examples.BUG1, remove=True, shorten=True, max_length=100)
for ext in test_mail_examples.BUG_1_IN:

View File

@ -60,6 +60,154 @@ EDI_LIKE_HTML_SOURCE = """<div style="font-family: 'Lucica Grande', Ubuntu, Aria
</div>
</div></body></html>"""
OERP_WEBSITE_HTML_1 = """
<div>
<div class="container">
<div class="row">
<div class="col-md-12 text-center mt16 mb16" data-snippet-id="colmd">
<h2>OpenERP HR Features</h2>
<h3 class="text-muted">Manage your company most important asset: People</h3>
</div>
<div class="col-md-4" data-snippet-id="colmd">
<img class="img-rounded img-responsive" src="/website/static/src/img/china_thumb.jpg">
<h4 class="mt16">Streamline Recruitments</h4>
<p>Post job offers and keep track of each application received. Follow applicants in your recruitment process with the smart kanban view.</p>
<p>Save time by automating some communications with email templates. Resumes are indexed automatically, allowing you to easily find for specific profiles.</p>
</div>
<div class="col-md-4" data-snippet-id="colmd">
<img class="img-rounded img-responsive" src="/website/static/src/img/desert_thumb.jpg">
<h4 class="mt16">Enterprise Social Network</h4>
<p>Break down information silos. Share knowledge and best practices amongst all employees. Follow specific people or documents and join groups of interests to share expertise and documents.</p>
<p>Interact with your collegues in real time with live chat.</p>
</div>
<div class="col-md-4" data-snippet-id="colmd">
<img class="img-rounded img-responsive" src="/website/static/src/img/deers_thumb.jpg">
<h4 class="mt16">Leaves Management</h4>
<p>Keep track of the vacation days accrued by each employee. Employees enter their requests (paid holidays, sick leave, etc), for managers to approve and validate. It's all done in just a few clicks. The agenda of each employee is updated accordingly.</p>
</div>
</div>
</div>
</div>"""
OERP_WEBSITE_HTML_1_IN = [
'Manage your company most important asset: People',
'img class="img-rounded img-responsive" src="/website/static/src/img/china_thumb.jpg"',
]
OERP_WEBSITE_HTML_1_OUT = [
'Break down information silos.',
'Keep track of the vacation days accrued by each employee',
'img class="img-rounded img-responsive" src="/website/static/src/img/deers_thumb.jpg',
]
OERP_WEBSITE_HTML_2 = """
<div class="mt16 cke_widget_editable cke_widget_element oe_editable oe_dirty" data-oe-model="blog.post" data-oe-id="6" data-oe-field="content" data-oe-type="html" data-oe-translate="0" data-oe-expression="blog_post.content" data-cke-widget-data="{}" data-cke-widget-keep-attr="0" data-widget="oeref" contenteditable="true" data-cke-widget-editable="text">
<section class="mt16 mb16" data-snippet-id="text-block">
<div class="container">
<div class="row">
<div class="col-md-12 text-center mt16 mb32" data-snippet-id="colmd">
<h2>
OpenERP Project Management
</h2>
<h3 class="text-muted">Infinitely flexible. Incredibly easy to use.</h3>
</div>
<div class="col-md-12 mb16 mt16" data-snippet-id="colmd">
<p>
OpenERP's <b>collaborative and realtime</b> project
management helps your team get work done. Keep
track of everything, from the big picture to the
minute details, from the customer contract to the
billing.
</p><p>
Organize projects around <b>your own processes</b>. Work
on tasks and issues using the kanban view, schedule
tasks using the gantt chart and control deadlines
in the calendar view. Every project may have it's
own stages allowing teams to optimize their job.
</p>
</div>
</div>
</div>
</section>
<section class="" data-snippet-id="image-text">
<div class="container">
<div class="row">
<div class="col-md-6 mt16 mb16" data-snippet-id="colmd">
<img class="img-responsive shadow" src="/website/static/src/img/image_text.jpg">
</div>
<div class="col-md-6 mt32" data-snippet-id="colmd">
<h3>Manage Your Shops</h3>
<p>
OpenERP's Point of Sale introduces a super clean
interface with no installation required that runs
online and offline on modern hardwares.
</p><p>
It's full integration with the company inventory
and accounting, gives you real time statistics and
consolidations amongst all shops without the hassle
of integrating several applications.
</p>
</div>
</div>
</div>
</section>
<section class="" data-snippet-id="text-image">
<div class="container">
<div class="row">
<div class="col-md-6 mt32" data-snippet-id="colmd">
<h3>Enterprise Social Network</h3>
<p>
Make every employee feel more connected and engaged
with twitter-like features for your own company. Follow
people, share best practices, 'like' top ideas, etc.
</p><p>
Connect with experts, follow what interests you, share
documents and promote best practices with OpenERP
Social application. Get work done with effective
collaboration across departments, geographies
and business applications.
</p>
</div>
<div class="col-md-6 mt16 mb16" data-snippet-id="colmd">
<img class="img-responsive shadow" src="/website/static/src/img/text_image.png">
</div>
</div>
</div>
</section><section class="" data-snippet-id="portfolio">
<div class="container">
<div class="row">
<div class="col-md-12 text-center mt16 mb32" data-snippet-id="colmd">
<h2>Our Porfolio</h2>
<h4 class="text-muted">More than 500 successful projects</h4>
</div>
<div class="col-md-4" data-snippet-id="colmd">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/deers.jpg">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/desert.jpg">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/china.jpg">
</div>
<div class="col-md-4" data-snippet-id="colmd">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/desert.jpg">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/china.jpg">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/deers.jpg">
</div>
<div class="col-md-4" data-snippet-id="colmd">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/landscape.jpg">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/china.jpg">
<img class="img-thumbnail img-responsive" src="/website/static/src/img/desert.jpg">
</div>
</div>
</div>
</section>
</div>
"""
OERP_WEBSITE_HTML_2_IN = [
'management helps your team get work done',
]
OERP_WEBSITE_HTML_2_OUT = [
'Make every employee feel more connected',
'img class="img-responsive shadow" src="/website/static/src/img/text_image.png',
]
TEXT_1 = """I contact you about our meeting tomorrow. Here is the schedule I propose:
9 AM: brainstorming about our new amazing business app
9.45 AM: summary

View File

@ -1,7 +1,12 @@
# This test can be run stand-alone with something like:
# > PYTHONPATH=. python2 openerp/tests/test_misc.py
import datetime
import locale
import unittest2
import babel
import babel.dates
from ..tools import misc

View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
import cgi
from xml.dom import minidom as dom
import common
from openerp.addons.base.ir import ir_qweb
impl = dom.getDOMImplementation()
document = impl.createDocument(None, None, None)
class TestQWebTField(common.TransactionCase):
def setUp(self):
super(TestQWebTField, self).setUp()
self.engine = self.registry('ir.qweb')
def context(self, values):
return ir_qweb.QWebContext(self.cr, self.uid, values)
def test_trivial(self):
field = document.createElement('span')
field.setAttribute('t-field', u'company.name')
Companies = self.registry('res.company')
company_id = Companies.create(self.cr, self.uid, {
'name': "My Test Company"
})
result = self.engine.render_node(field, self.context({
'company': Companies.browse(self.cr, self.uid, company_id),
}))
self.assertEqual(
result,
'<span data-oe-model="res.company" data-oe-id="%d" '
'data-oe-field="name" data-oe-type="char" '
'data-oe-expression="company.name">%s</span>' % (
company_id,
"My Test Company",))
def test_i18n(self):
field = document.createElement('span')
field.setAttribute('t-field', u'company.name')
Companies = self.registry('res.company')
s = u"Testing «ταБЬℓσ»: 1<2 & 4+1>3, now 20% off!"
company_id = Companies.create(self.cr, self.uid, {
'name': s,
})
result = self.engine.render_node(field, self.context({
'company': Companies.browse(self.cr, self.uid, company_id),
}))
self.assertEqual(
result,
'<span data-oe-model="res.company" data-oe-id="%d" '
'data-oe-field="name" data-oe-type="char" '
'data-oe-expression="company.name">%s</span>' % (
company_id,
cgi.escape(s.encode('utf-8')),))
def test_reject_crummy_tags(self):
field = document.createElement('td')
field.setAttribute('t-field', u'company.name')
with self.assertRaisesRegexp(
AssertionError,
r'^RTE widgets do not work correctly'):
self.engine.render_node(field, self.context({
'company': None
}))
def test_reject_t_tag(self):
field = document.createElement('t')
field.setAttribute('t-field', u'company.name')
with self.assertRaisesRegexp(
AssertionError,
r'^t-field can not be used on a t element'):
self.engine.render_node(field, self.context({
'company': None
}))

View File

@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
# This assumes an existing but uninitialized database.
import unittest2
import openerp
from openerp import SUPERUSER_ID
import common
DB = common.DB
ADMIN_USER_ID = common.ADMIN_USER_ID
def registry(model):
return openerp.modules.registry.RegistryManager.get(DB)[model]
def cursor():
return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
def model_exists(model_name):
registry = openerp.modules.registry.RegistryManager.get(DB)
return model_name in registry
def reload_registry():
openerp.modules.registry.RegistryManager.new(
DB, update_module=True)
def search_registry(model_name, domain):
cr = cursor()
model = registry(model_name)
record_ids = model.search(cr, SUPERUSER_ID, domain, {})
cr.close()
return record_ids
def install_module(module_name):
ir_module_module = registry('ir.module.module')
cr = cursor()
module_ids = ir_module_module.search(cr, SUPERUSER_ID,
[('name', '=', module_name)], {})
assert len(module_ids) == 1
ir_module_module.button_install(cr, SUPERUSER_ID, module_ids, {})
cr.commit()
cr.close()
reload_registry()
def uninstall_module(module_name):
ir_module_module = registry('ir.module.module')
cr = cursor()
module_ids = ir_module_module.search(cr, SUPERUSER_ID,
[('name', '=', module_name)], {})
assert len(module_ids) == 1
ir_module_module.button_uninstall(cr, SUPERUSER_ID, module_ids, {})
cr.commit()
cr.close()
reload_registry()
class test_uninstall(unittest2.TestCase):
"""
Test the install/uninstall of a test module. The module is available in
`openerp.tests` which should be present in the addons-path.
"""
def test_01_install(self):
""" Check a few things showing the module is installed. """
install_module('test_uninstall')
assert model_exists('test_uninstall.model')
assert search_registry('ir.model.data',
[('module', '=', 'test_uninstall')])
assert search_registry('ir.model.fields',
[('model', '=', 'test_uninstall.model')])
def test_02_uninstall(self):
""" Check a few things showing the module is uninstalled. """
uninstall_module('test_uninstall')
assert not model_exists('test_uninstall.model')
assert not search_registry('ir.model.data',
[('module', '=', 'test_uninstall')])
assert not search_registry('ir.model.fields',
[('model', '=', 'test_uninstall.model')])
if __name__ == '__main__':
unittest2.main()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -4,7 +4,10 @@ from lxml import etree
from StringIO import StringIO
import unittest2
from openerp.tools.view_validation import *
from openerp.tools.view_validation import (valid_page_in_book, valid_att_in_form, valid_type_in_colspan,
valid_type_in_col, valid_att_in_field, valid_att_in_label,
valid_field_in_graph, valid_field_in_tree
)
invalid_form = etree.parse(StringIO('''\
<form>
@ -79,7 +82,7 @@ invalid_tree = etree.parse(StringIO('''\
</tree>
''')).getroot()
valid_tree= etree.parse(StringIO('''\
valid_tree = etree.parse(StringIO('''\
<tree string="">
<field name=""></field>
<field name=""></field>
@ -97,15 +100,14 @@ class test_view_validation(unittest2.TestCase):
assert valid_page_in_book(valid_form)
def test_all_field_validation(self):
assert not valid_att_in_field(invalid_form)
assert valid_att_in_field(valid_form)
assert not valid_att_in_field(invalid_form)
assert valid_att_in_field(valid_form)
def test_all_label_validation(self):
assert not valid_att_in_label(invalid_form)
assert valid_att_in_label(valid_form)
assert not valid_att_in_label(invalid_form)
assert valid_att_in_label(valid_form)
def test_form_string_validation(self):
assert not valid_att_in_form(invalid_form)
assert valid_att_in_form(valid_form)
def test_graph_validation(self):

View File

@ -1,4 +1,7 @@
import lru
import logging
logger = logging.getLogger(__name__)
class ormcache(object):
""" LRU cache decorator for orm methods,
@ -14,8 +17,8 @@ class ormcache(object):
def __call__(self,m):
self.method = m
def lookup(self2, cr, *args):
r = self.lookup(self2, cr, *args)
def lookup(self2, cr, *args, **argv):
r = self.lookup(self2, cr, *args, **argv)
return r
lookup.clear_cache = self.clear
return lookup
@ -34,7 +37,7 @@ class ormcache(object):
d = ormcache[self.method] = lru.LRU(self.size)
return d
def lookup(self, self2, cr, *args):
def lookup(self, self2, cr, *args, **argv):
d = self.lru(self2)
key = args[self.skiparg-2:]
try:
@ -54,22 +57,45 @@ class ormcache(object):
"""
d = self.lru(self2)
if args:
try:
key = args[self.skiparg-2:]
del d[key]
self2.pool._any_cache_cleared = True
except KeyError:
pass
else:
d.clear()
self2.pool._any_cache_cleared = True
logger.warn("ormcache.clear arguments are deprecated and ignored "
"(while clearing caches on (%s).%s)",
self2._name, self.method.__name__)
d.clear()
self2.pool._any_cache_cleared = True
class ormcache_context(ormcache):
def __init__(self, skiparg=2, size=8192, accepted_keys=()):
super(ormcache_context,self).__init__(skiparg,size)
self.accepted_keys = accepted_keys
def lookup(self, self2, cr, *args, **argv):
d = self.lru(self2)
context = argv.get('context', {})
ckey = filter(lambda x: x[0] in self.accepted_keys, context.items())
ckey.sort()
d = self.lru(self2)
key = args[self.skiparg-2:]+tuple(ckey)
try:
r = d[key]
self.stat_hit += 1
return r
except KeyError:
self.stat_miss += 1
value = d[key] = self.method(self2, cr, *args, **argv)
return value
except TypeError:
self.stat_err += 1
return self.method(self2, cr, *args, **argv)
class ormcache_multi(ormcache):
def __init__(self, skiparg=2, size=8192, multi=3):
super(ormcache_multi,self).__init__(skiparg,size)
self.multi = multi - 2
def lookup(self, self2, cr, *args):
def lookup(self, self2, cr, *args, **argv):
d = self.lru(self2)
args = list(args)
multi = self.multi
@ -130,6 +156,7 @@ if __name__ == '__main__':
print r
for i in a._ormcache:
print a._ormcache[i].d
a.m.clear_cache()
a.n.clear_cache(a,1,1)
r=a.n("cr",1,[1,2])
print r

View File

@ -50,7 +50,7 @@ except:
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from lxml import etree
from lxml import etree, builder
import misc
from config import config
from translate import _
@ -854,6 +854,48 @@ form: module.record_id""" % (xml_id,)
cr.commit()
return rec_model, id
def _tag_template(self, cr, el, data_node=None):
# This helper transforms a <template> element into a <record> and forwards it
tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
module = self.module
if '.' in tpl_id:
module, tpl_id = tpl_id.split('.', 1)
# set the full template name for qweb <module>.<id>
if not (el.get('inherit_id') or el.get('inherit_option_id')):
el.set('t-name', '%s.%s' % (module, tpl_id))
el.tag = 't'
else:
el.tag = 'data'
el.attrib.pop('id', None)
record_attrs = {
'id': tpl_id,
'model': 'ir.ui.view',
}
for att in ['forcecreate', 'context']:
if att in el.keys():
record_attrs[att] = el.attrib.pop(att)
Field = builder.E.field
name = el.get('name', tpl_id)
record = etree.Element('record', attrib=record_attrs)
record.append(Field(name, name='name'))
record.append(Field("qweb", name='type'))
record.append(Field(el.get('priority', "16"), name='priority'))
record.append(Field(el, name="arch", type="xml"))
for field_name in ('inherit_id','inherit_option_id'):
value = el.attrib.pop(field_name, None)
if value: record.append(Field(name=field_name, ref=value))
groups = el.attrib.pop('groups', None)
if groups:
grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
if el.attrib.pop('page', None) == 'True':
record.append(Field(name="page", eval="True"))
return self._tag_record(cr, record, data_node)
def id_get(self, cr, id_str):
if id_str in self.idref:
return self.idref[id_str]
@ -898,6 +940,7 @@ form: module.record_id""" % (xml_id,)
self._tags = {
'menuitem': self._tag_menuitem,
'record': self._tag_record,
'template': self._tag_template,
'assert': self._tag_assert,
'report': self._tag_report,
'wizard': self._tag_wizard,

View File

@ -57,4 +57,19 @@ def frame_codeinfo(fframe, back=0):
except Exception:
return "<unknown>", ''
def compose(a, b):
""" Composes the callables ``a`` and ``b``. ``compose(a, b)(*args)`` is
equivalent to ``a(b(*args))``.
Can be used as a decorator by partially applying ``a``::
@partial(compose, a)
def b():
...
"""
@wraps(b)
def wrapper(*args, **kwargs):
return a(b(*args, **kwargs))
return wrapper
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (C) 2012-2013 OpenERP S.A. (<http://openerp.com>).
# Copyright (C) 2012-TODAY OpenERP S.A. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -45,10 +45,15 @@ tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "if
tags_to_remove = ['html', 'body', 'font']
# allow new semantic HTML5 tags
allowed_tags = clean.defs.tags | frozenset('article section header footer hgroup nav aside figure'.split())
safe_attrs = clean.defs.safe_attrs | frozenset(['style'])
allowed_tags = clean.defs.tags | frozenset('article section header footer hgroup nav aside figure main'.split())
safe_attrs = clean.defs.safe_attrs | frozenset(
['style',
'data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-type', 'data-oe-expression', 'data-oe-translate', 'data-oe-nodeid',
'data-snippet-id', 'data-publish', 'data-id', 'data-res_id', 'data-member_id', 'data-view-id'
])
def html_sanitize(src, silent=True):
def html_sanitize(src, silent=True, strict=False):
if not src:
return src
src = ustr(src, errors='replace')
@ -75,22 +80,31 @@ def html_sanitize(src, silent=True):
else:
kwargs['remove_tags'] = tags_to_kill + tags_to_remove
if etree.LXML_VERSION >= (3, 1, 0):
kwargs.update({
'safe_attrs_only': True,
'safe_attrs': safe_attrs,
})
if strict:
if etree.LXML_VERSION >= (3, 1, 0):
# lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
kwargs.update({
'safe_attrs_only': True,
'safe_attrs': safe_attrs,
})
else:
# lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
kwargs['safe_attrs_only'] = False
kwargs['safe_attrs_only'] = False # keep oe-data attributes + style
kwargs['frames'] = False, # do not remove frames (embbed video in CMS blogs)
try:
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
cleaner = clean.Cleaner(**kwargs)
cleaned = cleaner.clean_html(src)
# MAKO compatibility: $, { and } inside quotes are escaped, preventing correct mako execution
cleaned = cleaned.replace('%24', '$')
cleaned = cleaned.replace('%7B', '{')
cleaned = cleaned.replace('%7D', '}')
cleaned = cleaned.replace('%20', ' ')
cleaned = cleaned.replace('%5B', '[')
cleaned = cleaned.replace('%5D', ']')
except etree.ParserError, e:
if 'empty' in str(e):
return ""
if 'empty' in str(e):
return ""
if not silent:
raise
logger.warning('ParserError obtained when sanitizing %r', src, exc_info=True)
@ -100,6 +114,11 @@ def html_sanitize(src, silent=True):
raise
logger.warning('unknown error obtained when sanitizing %r', src, exc_info=True)
cleaned = '<p>Unknown error when sanitizing</p>'
# this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
if cleaned.startswith('<div>') and cleaned.endswith('</div>'):
cleaned = cleaned[5:-6]
return cleaned
@ -107,7 +126,8 @@ def html_sanitize(src, silent=True):
# HTML Cleaner
#----------------------------------------------------------
def html_email_clean(html, remove=False, shorten=False, max_length=300):
def html_email_clean(html, remove=False, shorten=False, max_length=300, expand_options=None,
protect_sections=False):
""" html_email_clean: clean the html by doing the following steps:
- try to strip email quotes, by removing blockquotes or having some client-
@ -132,6 +152,32 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
be flagged as to remove
:param int max_length: if shortening, maximum number of characters before
shortening
:param dict expand_options: options for the read more link when shortening
the content.The used keys are the following:
- oe_expand_container_tag: class applied to the
container of the whole read more link
- oe_expand_container_class: class applied to the
link container (default: oe_mail_expand)
- oe_expand_container_content: content of the
container (default: ...)
- oe_expand_separator_node: optional separator, like
adding ... <br /><br /> <a ...>read more</a> (default: void)
- oe_expand_a_href: href of the read more link itself
(default: #)
- oe_expand_a_class: class applied to the <a> containing
the link itself (default: oe_mail_expand)
- oe_expand_a_content: content of the <a> (default: read more)
The formatted read more link is the following:
<cont_tag class="oe_expand_container_class">
oe_expand_container_content
if expand_options.get('oe_expand_separator_node'):
<oe_expand_separator_node/>
<a href="oe_expand_a_href" class="oe_expand_a_class">
oe_expand_a_content
</a>
</span>
"""
def _replace_matching_regex(regex, source, replace=''):
""" Replace all matching expressions in source by replace """
@ -212,8 +258,29 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
node.text = innertext
# create <span> ... <a href="#">read more</a></span> node
read_more_node = _create_node('span', ' ... ', None, {'class': 'oe_mail_expand'})
read_more_link_node = _create_node('a', 'read more', None, {'href': '#', 'class': 'oe_mail_expand'})
read_more_node = _create_node(
expand_options.get('oe_expand_container_tag', 'span'),
expand_options.get('oe_expand_container_content', ' ... '),
None,
{'class': expand_options.get('oe_expand_container_class', 'oe_mail_expand')}
)
if expand_options.get('oe_expand_separator_node'):
read_more_separator_node = _create_node(
expand_options.get('oe_expand_separator_node'),
'',
None,
{}
)
read_more_node.append(read_more_separator_node)
read_more_link_node = _create_node(
'a',
expand_options.get('oe_expand_a_content', 'read more'),
None,
{
'href': expand_options.get('oe_expand_a_href', '#'),
'class': expand_options.get('oe_expand_a_class', 'oe_mail_expand'),
}
)
read_more_node.append(read_more_link_node)
# create outertext node
overtext_node = _create_node('span', outertext)
@ -223,6 +290,9 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
node.append(read_more_node)
node.append(overtext_node)
if expand_options is None:
expand_options = {}
if not html or not isinstance(html, basestring):
return html
html = ustr(html)
@ -265,6 +335,8 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
# signature_begin = False # try dynamic signature recognition
quote_begin = False
overlength = False
overlength_section_id = None
overlength_section_count = 0
cur_char_nbr = 0
for node in root.iter():
# do not take into account multiple spaces that are displayed as max 1 space in html
@ -276,14 +348,22 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
if 'SkyDrivePlaceholder' in node.get('class', '') or 'SkyDrivePlaceholder' in node.get('id', ''):
root.set('hotmail', '1')
# protect sections by tagging section limits and blocks contained inside sections, using an increasing id to re-find them later
if node.tag == 'section':
overlength_section_count += 1
node.set('section_closure', str(overlength_section_count))
if node.getparent() is not None and (node.getparent().get('section_closure') or node.getparent().get('section_inner')):
node.set('section_inner', str(overlength_section_count))
# state of the parsing: flag quotes and tails to remove
if quote_begin:
node.set('in_quote', '1')
node.set('tail_remove', '1')
# state of the parsing: flag when being in over-length content
# state of the parsing: flag when being in over-length content, depending on section content if defined (only when having protect_sections)
if overlength:
node.set('in_overlength', '1')
node.set('tail_remove', '1')
if not overlength_section_id or int(node.get('section_inner', overlength_section_count + 1)) > overlength_section_count:
node.set('in_overlength', '1')
node.set('tail_remove', '1')
# find quote in msoffice / hotmail / blockquote / text quote and signatures
if root.get('msoffice') and node.tag == 'div' and 'border-top:solid' in node.get('style', ''):
@ -298,13 +378,25 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
node.set('in_quote', '1')
# shorten:
# 1/ truncate the text at the next available space
# 2/ create a 'read more' node, next to current node
# 3/ add the truncated text in a new node, next to 'read more' node
# if protect section:
# 1/ find the first parent not being inside a section
# 2/ add the read more link
# else:
# 1/ truncate the text at the next available space
# 2/ create a 'read more' node, next to current node
# 3/ add the truncated text in a new node, next to 'read more' node
node_text = (node.text or '').strip().strip('\n').strip()
if shorten and not overlength and cur_char_nbr + len(node_text) > max_length:
node_to_truncate = node
while node_to_truncate.get('in_quote') and node_to_truncate.getparent() is not None:
node_to_truncate = node_to_truncate.getparent()
while node_to_truncate.getparent() is not None:
if node_to_truncate.get('in_quote'):
node_to_truncate = node_to_truncate.getparent()
elif protect_sections and (node_to_truncate.getparent().get('section_inner') or node_to_truncate.getparent().get('section_closure')):
node_to_truncate = node_to_truncate.getparent()
overlength_section_id = node_to_truncate.get('section_closure')
else:
break
overlength = True
node_to_truncate.set('truncate', '1')
if node_to_truncate == node:
@ -340,7 +432,7 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
if remove:
node.getparent().remove(node)
else:
if not 'oe_mail_expand' in node.get('class', ''): # trick: read more link should be displayed even if it's in overlength
if not expand_options.get('oe_expand_a_class', 'oe_mail_expand') in node.get('class', ''): # trick: read more link should be displayed even if it's in overlength
node_class = node.get('class', '') + ' oe_mail_cleaned'
node.set('class', node_class)

View File

@ -35,7 +35,7 @@ import sys
import threading
import time
import zipfile
from collections import defaultdict
from collections import defaultdict, Mapping
from datetime import datetime
from itertools import islice, izip, groupby
from lxml import etree
@ -822,6 +822,76 @@ DATETIME_FORMATS_MAP = {
'%Z': '',
}
POSIX_TO_LDML = {
'a': 'E',
'A': 'EEEE',
'b': 'MMM',
'B': 'MMMM',
#'c': '',
'd': 'dd',
'H': 'HH',
'I': 'hh',
'j': 'DDD',
'm': 'MM',
'M': 'mm',
'p': 'a',
'S': 'ss',
'U': 'w',
'w': 'e',
'W': 'w',
'y': 'yy',
'Y': 'yyyy',
# see comments above, and babel's format_datetime assumes an UTC timezone
# for naive datetime objects
#'z': 'Z',
#'Z': 'z',
}
def posix_to_ldml(fmt, locale):
""" Converts a posix/strftime pattern into an LDML date format pattern.
:param fmt: non-extended C89/C90 strftime pattern
:param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
:return: unicode
"""
buf = []
pc = False
quoted = []
for c in fmt:
# LDML date format patterns uses letters, so letters must be quoted
if not pc and c.isalpha():
quoted.append(c if c != "'" else "''")
continue
if quoted:
buf.append("'")
buf.append(''.join(quoted))
buf.append("'")
quoted = []
if pc:
if c == '%': # escaped percent
buf.append('%')
elif c == 'x': # date format, short seems to match
buf.append(locale.date_formats['short'].pattern)
elif c == 'X': # time format, seems to include seconds. short does not
buf.append(locale.time_formats['medium'].pattern)
else: # look up format char in static mapping
buf.append(POSIX_TO_LDML[c])
pc = False
elif c == '%':
pc = True
else:
buf.append(c)
# flush anything remaining in quoted buffer
if quoted:
buf.append("'")
buf.append(''.join(quoted))
buf.append("'")
return ''.join(buf)
def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
tz_offset=True, ignore_unparsable_time=True):
"""
@ -1005,6 +1075,8 @@ class mute_logger(object):
def __enter__(self):
for logger in self.loggers:
assert isinstance(logger, basestring),\
"A logger name must be a string, got %s" % type(logger)
logging.getLogger(logger).addFilter(self)
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
@ -1070,6 +1142,34 @@ def stripped_sys_argv(*strip_args):
return [x for i, x in enumerate(args) if not strip(args, i)]
class ConstantMapping(Mapping):
"""
An immutable mapping returning the provided value for every single key.
Useful for default value to methods
"""
__slots__ = ['_value']
def __init__(self, val):
self._value = val
def __len__(self):
"""
defaultdict updates its length for each individually requested key, is
that really useful?
"""
return 0
def __iter__(self):
"""
same as len, defaultdict udpates its iterable keyset with each key
requested, is there a point for this?
"""
return iter([])
def __getitem__(self, item):
return self._value
def dumpstacks(sig, frame):
""" Signal handler: dump a stack trace for each existing thread."""
code = []
@ -1107,4 +1207,5 @@ def dumpstacks(sig, frame):
_logger.info("\n".join(code))
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -174,7 +174,7 @@ def _import(name, globals=None, locals=None, fromlist=None, level=-1):
return __import__(name, globals, locals, level)
raise ImportError(name)
def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False, locals_builtins=False):
"""safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
System-restricted Python expression evaluation
@ -218,29 +218,37 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
locals_dict = dict(locals_dict)
globals_dict.update(
__builtins__ = {
'__import__': _import,
'True': True,
'False': False,
'None': None,
'str': str,
'globals': locals,
'locals': locals,
'bool': bool,
'dict': dict,
'list': list,
'tuple': tuple,
'map': map,
'abs': abs,
'min': min,
'max': max,
'reduce': reduce,
'filter': filter,
'round': round,
'len': len,
'set' : set
}
__builtins__={
'__import__': _import,
'True': True,
'False': False,
'None': None,
'str': str,
'globals': locals,
'locals': locals,
'bool': bool,
'dict': dict,
'list': list,
'tuple': tuple,
'map': map,
'abs': abs,
'min': min,
'max': max,
'reduce': reduce,
'filter': filter,
'round': round,
'len': len,
'set': set,
'repr': repr,
'int': int,
'float': float,
'range': range,
}
)
if locals_builtins:
if locals_dict is None:
locals_dict = {}
locals_dict.update(globals_dict.get('__builtins__'))
c = test_expr(expr, _SAFE_OPCODES, mode=mode)
try:
return eval(c, globals_dict, locals_dict)

View File

@ -41,8 +41,7 @@ def valid_att_in_label(arch):
def valid_att_in_form(arch):
"""A `string` attribute must be on a `form` node."""
return not arch.xpath('//form[not (@string)]')
return True
def valid_type_in_colspan(arch):
@ -67,8 +66,8 @@ def valid_type_in_col(arch):
def valid_view(arch):
if arch.tag == 'form':
for pred in [valid_page_in_book, valid_att_in_form, valid_type_in_colspan,\
valid_type_in_col, valid_att_in_field, valid_att_in_label]:
for pred in [valid_page_in_book, valid_att_in_form, valid_type_in_colspan,
valid_type_in_col, valid_att_in_field, valid_att_in_label]:
if not pred(arch):
_logger.error('Invalid XML: %s', pred.__doc__)
return False

View File

@ -328,7 +328,7 @@ class YamlInterpreter(object):
if config.get('import_partial'):
self.cr.commit()
def _create_record(self, model, fields, view_info=False, parent={}, default=True):
def _create_record(self, model, fields, view_info=None, parent={}, default=True):
"""This function processes the !record tag in yalm files. It simulates the record creation through an xml
view (either specified on the !record tag or the default one for this object), including the calls to
on_change() functions, and sending only values for fields that aren't set as readonly.
@ -447,7 +447,13 @@ class YamlInterpreter(object):
args = map(lambda x: eval(x, ctx), match.group(2).split(','))
result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
for key, val in (result or {}).get('value', {}).items():
assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist either on the object '%s', either in the view '%s' used for the creation" % (key, match.group(1), model._name, view_info['name'])
assert key in fg, (
"The field %r returned from the onchange call %r "
"does not exist in the source view %r (of object "
"%r). This field will be ignored (and thus not "
"populated) when clients saves the new record" % (
key, match.group(1), view_info.get('name', '?'), model._name
))
if key not in fields:
# do not shadow values explicitly set in yaml.
record_dict[key] = process_val(key, val)

View File

@ -106,7 +106,7 @@ class WorkflowInstance(object):
for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall():
cur_record = Record(cur_model_name, cur_record_id)
for act_name in act_names:
WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.{}'.format(act_name[0]))
WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.%s' % act_name[0])
return ok

View File

@ -57,11 +57,11 @@ def required_or_default(name, h):
a mandatory argument.
"""
if os.environ.get('OPENERP_' + name.upper()):
d = {'default': os.environ['OPENERP_' + name.upper()]}
d = {'default': os.environ['OPENERP_' + name.upper()]}
else:
d = {'required': True}
d = {'required': True}
d['help'] = h + '. The environment variable OPENERP_' + \
name.upper() + ' can be used instead.'
name.upper() + ' can be used instead.'
return d
class Command(object):
@ -77,7 +77,7 @@ class Command(object):
self.parser = parser = subparsers.add_parser(self.command_name,
description=self.__class__.__doc__)
else:
self.parser = parser = argparse.ArgumentParser(
self.parser = parser = argparse.ArgumentParser(
description=self.__class__.__doc__)
parser.add_argument('-d', '--database', metavar='DATABASE',

View File

@ -2,9 +2,9 @@
Execute the unittest2 tests available in OpenERP addons.
"""
import os
import sys
import types
import argparse
import common
@ -56,31 +56,33 @@ def get_test_modules(module, submodule, explode):
print ' ', x
sys.exit(1)
fast_suite = getattr(m, 'fast_suite', [])
checks = getattr(m, 'checks', [])
if submodule is None:
# Use auto-discovered sub-modules.
ms = submodules
elif submodule == '__fast_suite__':
# Obtain the explicit test sub-modules list.
ms = getattr(sys.modules[module], 'fast_suite', None)
# `suite` was used before the 6.1 release instead of `fast_suite`.
ms = ms if ms else getattr(sys.modules[module], 'suite', None)
if ms is None:
ms = fast_suite if hasattr(m, 'fast_suite') else getattr(m, 'suite', None)
if not ms:
if explode:
print 'The module `%s` has no defined test suite.' % (module,)
show_submodules_and_exit()
else:
ms = []
elif submodule == '__sanity_checks__':
ms = getattr(sys.modules[module], 'checks', None)
if ms is None:
ms = checks
if not ms:
if explode:
print 'The module `%s` has no defined sanity checks.' % (module,)
show_submodules_and_exit()
else:
ms = []
elif submodule == '__slow_suite__':
ms = list(set(submodules).difference(fast_suite, checks))
else:
# Pick the command-line-specified test sub-module.
m = getattr(sys.modules[module], submodule, None)
m = getattr(m, submodule, None)
ms = [m]
if m is None:
@ -104,15 +106,15 @@ def run(args):
config['xmlrpc_port'] = int(args.port)
config['admin_passwd'] = 'admin'
config['db_password'] = 'a2aevl8w' # TODO from .openerpserverrc
config['addons_path'] = args.addons.replace(':',',')
if args.addons:
args.addons = args.addons.split(':')
args.addons = args.addons.replace(':',',').split(',')
else:
args.addons = []
if args.sanity_checks and args.fast_suite:
print 'Only at most one of `--sanity-checks` and `--fast-suite` ' \
'can be specified.'
sys.exit(1)
# ensure no duplication in addons paths
args.addons = list(set(args.addons))
config['addons_path'] = ','.join(args.addons)
import logging
openerp.netsvc.init_alternative_logger()
@ -121,45 +123,26 @@ def run(args):
# Install the import hook, to import openerp.addons.<module>.
openerp.modules.module.initialize_sys_path()
# Extract module, submodule from the command-line args.
if args.module is None:
module, submodule = None, None
else:
splitted = args.module.split('.')
if len(splitted) == 1:
module, submodule = splitted[0], None
elif len(splitted) == 2:
module, submodule = splitted
else:
print 'The `module` argument must have the form ' \
'`module[.submodule]`.'
sys.exit(1)
module = args.module
submodule = args.submodule
# Import the necessary modules and get the corresponding suite.
if module is None:
# TODO
modules = common.get_addons_from_paths(args.addons, []) # TODO openerp.addons.base is not included ?
test_modules = []
test_modules = []
for module in ['openerp'] + modules:
if args.fast_suite:
submodule = '__fast_suite__'
if args.sanity_checks:
submodule = '__sanity_checks__'
test_modules.extend(get_test_modules(module,
submodule, explode=False))
test_modules.extend(
get_test_modules(module, submodule, explode=False))
else:
if submodule and args.fast_suite:
print "Submodule name `%s` given, ignoring `--fast-suite`." % (submodule,)
if submodule and args.sanity_checks:
print "Submodule name `%s` given, ignoring `--sanity-checks`." % (submodule,)
if not submodule and args.fast_suite:
submodule = '__fast_suite__'
if not submodule and args.sanity_checks:
submodule = '__sanity_checks__'
test_modules = get_test_modules(module,
submodule, explode=True)
test_modules = get_test_modules(module, submodule, explode=True)
print 'Test modules:'
for test_module in test_modules:
print ' ', test_module.__name__
print
sys.stdout.flush()
# Run the test suite.
if not args.dry_run:
suite = unittest2.TestSuite()
for test_module in test_modules:
@ -167,10 +150,6 @@ def run(args):
r = unittest2.TextTestRunner(verbosity=2).run(suite)
if r.errors or r.failures:
sys.exit(1)
else:
print 'Test modules:'
for test_module in test_modules:
print ' ', test_module.__name__
def add_parser(subparsers):
parser = subparsers.add_parser('run-tests',
@ -181,21 +160,55 @@ def add_parser(subparsers):
parser.add_argument('-p', '--port', metavar='PORT',
help='the port used for WML-RPC tests')
common.add_addons_argument(parser)
parser.add_argument('-m', '--module', metavar='MODULE',
default=None,
help='the module to test in `module[.submodule]` notation. '
'Use `openerp` for the core OpenERP tests. '
'Leave empty to run every declared tests. '
'Give a module but no submodule to run all the module\'s declared '
'tests. If both the module and the submodule are given, '
'the sub-module can be run even if it is not declared in the module.')
parser.add_argument('--fast-suite', action='store_true',
help='run only the tests explicitely declared in the fast suite (this '
parser.add_argument(
'-m', '--module', metavar='MODULE', action=ModuleAction, default=None,
help="the module to test in `module[.submodule]` notation. "
"Use `openerp` for the core OpenERP tests. "
"Leave empty to run every declared tests. "
"Give a module but no submodule to run all the module's declared "
"tests. If both the module and the submodule are given, "
"the sub-module can be run even if it is not declared in the module.")
group = parser.add_mutually_exclusive_group()
group.add_argument(
'--fast-suite',
dest='submodule', action=GuardAction, nargs=0, const='__fast_suite__',
help='run only the tests explicitly declared in the fast suite (this '
'makes sense only with the bare `module` notation or no module at '
'all).')
parser.add_argument('--sanity-checks', action='store_true',
group.add_argument(
'--sanity-checks',
dest='submodule', action=GuardAction, nargs=0, const='__sanity_checks__',
help='run only the sanity check tests')
group.add_argument(
'--slow-suite',
dest='submodule', action=GuardAction, nargs=0, const='__slow_suite__',
help="Only run slow tests (tests which are neither in the fast nor in"
" the sanity suite)")
parser.add_argument('--dry-run', action='store_true',
help='do not run the tests')
parser.set_defaults(run=run)
parser.set_defaults(run=run, submodule=None)
class ModuleAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
split = values.split('.')
if len(split) == 1:
module, submodule = values, None
elif len(split) == 2:
module, submodule = split
else:
raise argparse.ArgumentError(
option_string,
"must have the form 'module[.submodule]', got '%s'" % values)
setattr(namespace, self.dest, module)
if submodule is not None:
setattr(namespace, 'submodule', submodule)
class GuardAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
if getattr(namespace, self.dest, None):
print "%s value provided, ignoring '%s'" % (self.dest, option_string)
return
setattr(namespace, self.dest, self.const)