[MERGE] trunk-website-al
bzr revid: al@openerp.com-20140131005207-mn7t6tar8cywe9hz
This commit is contained in:
commit
d6c1346e12
|
@ -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**
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
@ -11,3 +11,4 @@ Miscellanous
|
|||
06_misc_user_img_specs.rst
|
||||
06_misc_import.rst
|
||||
06_misc_auto_join.rst
|
||||
06_ir_qweb.rst
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
---------
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [])
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
|
@ -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()
|
||||
|
|
142
openerp/http.py
142
openerp/http.py
|
@ -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)
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
'name': 'test_convert',
|
||||
'description': "Data for xml conversion tests",
|
||||
'version': '0.0.1',
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
nothing to see here, move along
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import test_convert
|
||||
|
||||
checks = [
|
||||
test_convert
|
||||
]
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import models
|
|
@ -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:
|
|
@ -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
|
|
|
@ -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)),
|
||||
}
|
|
@ -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:
|
|
@ -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<bar>")
|
||||
|
||||
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 <b>fdslkj</b> d;lasjfa lkdja <a href=http://spam.com>lfks</a><br>
|
||||
fldkjsfhs <i style="color: red"><a href="http://spamspam.com">fldskjh</a></i><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<b>o</b>")
|
||||
|
||||
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 |
Binary file not shown.
Binary file not shown.
|
@ -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()
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
@ -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, {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
}))
|
|
@ -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:
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue