diff --git a/openerp/addons/base/base_data.xml b/openerp/addons/base/base_data.xml
index e01bc497b70..2df3979c0de 100644
--- a/openerp/addons/base/base_data.xml
+++ b/openerp/addons/base/base_data.xml
@@ -1054,9 +1054,8 @@
zw
-
+
Your Company
-
diff --git a/openerp/addons/base/base_demo.xml b/openerp/addons/base/base_demo.xml
index 34feb3af990..064b1f49e69 100644
--- a/openerp/addons/base/base_demo.xml
+++ b/openerp/addons/base/base_demo.xml
@@ -1,10 +1,15 @@
+
+ Demo User
+
+
+
+
demo
demo
- Demo User
Mr Demo
diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml
index dcbbe28b96f..29bb681981a 100644
--- a/openerp/addons/base/ir/ir.xml
+++ b/openerp/addons/base/ir/ir.xml
@@ -556,29 +556,6 @@
-
-
- ir.needaction_users_rel.tree
- ir.needaction_users_rel
-
-
-
-
-
-
-
-
-
-
- Need action relationships
- ir.needaction_users_rel
- form
- tree,form
-
-
-
-
-
ir.ui.view
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index 019ef62a6bd..383f78181e9 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -44,7 +44,7 @@ class actions(osv.osv):
_table = 'ir_actions'
_order = 'name'
_columns = {
- 'name': fields.char('Action Name', required=True, size=64),
+ 'name': fields.char('Name', size=64, required=True),
'type': fields.char('Action Type', required=True, size=32,readonly=True),
'usage': fields.char('Action Usage', size=32),
}
@@ -861,11 +861,10 @@ class act_client(osv.osv):
_order = 'name'
def _get_params(self, cr, uid, ids, field_name, arg, context):
- return dict([
- ((record.id, ast.literal_eval(record.params_store))
- if record.params_store else (record.id, False))
- for record in self.browse(cr, uid, ids, context=context)
- ])
+ result = {}
+ for record in self.browse(cr, uid, ids, context=context):
+ result[record.id] = record.params_store and eval(record.params_store, {'uid': uid}) or False
+ return result
def _set_params(self, cr, uid, id, field_name, field_value, arg, context):
if isinstance(field_value, dict):
@@ -874,10 +873,15 @@ class act_client(osv.osv):
self.write(cr, uid, id, {'params_store': field_value}, context=context)
_columns = {
+ 'name': fields.char('Action Name', required=True, size=64, translate=True),
'tag': fields.char('Client action tag', size=64, required=True,
help="An arbitrary string, interpreted by the client"
" according to its own needs and wishes. There "
"is no central tag repository across clients."),
+ 'res_model': fields.char('Destination Model', size=64,
+ help="Optional model, mostly used for needactions."),
+ 'context': fields.char('Context Value', size=250, required=True,
+ help="Context dictionary as Python expression, empty by default (Default: {})"),
'params': fields.function(_get_params, fnct_inv=_set_params,
type='binary',
string="Supplementary arguments",
@@ -887,6 +891,7 @@ class act_client(osv.osv):
}
_defaults = {
'type': 'ir.actions.client',
+ 'context': '{}',
}
act_client()
diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py
index 0f28c33ded9..db133ad9904 100644
--- a/openerp/addons/base/ir/ir_needaction.py
+++ b/openerp/addons/base/ir/ir_needaction.py
@@ -19,180 +19,45 @@
#
##############################################################################
-import openerp.pooler as pooler
-from operator import itemgetter
-from osv import osv, fields
-from tools.translate import _
+from osv import osv
-class ir_needaction_users_rel(osv.Model):
- ''' ir_needaction_users_rel holds data related to the needaction
- mechanism inside OpenERP. A row in this model is characterized
- by:
- - res_model: model of the record requiring an action
- - res_id: ID of the record requiring an action
- - user_id: foreign key to the res.users table, to the user that has to
- perform the action
- This model can be seen as a many2many, linking (res_model, res_id) to users
- (those whose attention is required on the record). '''
-
- _name = 'ir.needaction_users_rel'
- _description = 'Needaction relationship table'
- _rec_name = 'id'
- _order = 'id desc'
- _columns = {
- 'res_model': fields.char('Related Document Model', size=128,
- select=1, required=True),
- 'res_id': fields.integer('Related Document ID',
- select=1, required=True),
- 'user_id': fields.many2one('res.users', string='Related User',
- ondelete='cascade', select=1, required=True),
- }
-
- def _get_users(self, cr, uid, res_ids, res_model, context=None):
- """Given res_ids of res_model, get user_ids present in table"""
- rel_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context)
- return list(set(map(itemgetter('user_id'), self.read(cr, uid, rel_ids, ['user_id'], context=context))))
-
- def create_users(self, cr, uid, res_ids, res_model, user_ids, context=None):
- """Given res_ids of res_model, add user_ids to the relationship table"""
- for res_id in res_ids:
- for user_id in user_ids:
- self.create(cr, uid, {'res_model': res_model, 'res_id': res_id, 'user_id': user_id}, context=context)
- return True
-
- def unlink_users(self, cr, uid, res_ids, res_model, context=None):
- """Given res_ids of res_model, delete all entries in the relationship table"""
- to_del_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context)
- return self.unlink(cr, uid, to_del_ids, context=context)
-
- def update_users(self, cr, uid, res_ids, res_model, user_ids, context=None):
- """Given res_ids of res_model, update their entries in the relationship table to user_ids"""
- # read current records
- cur_users = self._get_users(cr, uid, res_ids, res_model, context=context)
- if len(cur_users) == len(user_ids) and all(cur_user in user_ids for cur_user in cur_users):
- return True
- # unlink old records
- self.unlink_users(cr, uid, res_ids, res_model, context=context)
- # link new records
- self.create_users(cr, uid, res_ids, res_model, user_ids, context=context)
- return True
-
-
-class ir_needaction_mixin(osv.Model):
+class ir_needaction_mixin(osv.AbstractModel):
'''Mixin class for objects using the need action feature.
-
- Need action feature can be used by objects having to be able to
- signal that an action is required on a particular record. If in
- the business logic an action must be performed by somebody, for
- instance validation by a manager, this mechanism allows to set a
+
+ Need action feature can be used by models that have to be able to
+ signal that an action is required on a particular record. If in
+ the business logic an action must be performed by somebody, for
+ instance validation by a manager, this mechanism allows to set a
list of users asked to perform an action.
-
- This class wraps a class (ir.ir_needaction_users_rel) that
- behaves like a many2many field. This class handles the low-level
- considerations of updating relationships. Every change made on
- the record calls a method that updates the relationships.
-
- Objects using the 'need_action' feature should override the
- ``get_needaction_user_ids`` method. This methods returns a
- dictionary whose keys are record ids, and values a list of user
- ids, like in a many2many relationship. Therefore by defining
- only one method, you can specify if an action is required by
- defining the users that have to do it, in every possible
- situation.
-
- This class also offers several global services:
- - ``needaction_get_record_ids``: for the current model and uid, get
- all record ids that ask this user to perform an action. This
- mechanism is used for instance to display the number of pending
- actions in menus, such as Leads (12)
- - ``needaction_get_action_count``: as ``needaction_get_record_ids``
- but returns only the number of action, not the ids (performs a
- search with count=True)
- The ``ir_needaction_mixin`` class adds a calculated field
- ``needaction_pending``. This function field allows to state
- whether a given record has a needaction for the current user.
- This is usefull if you want to customize views according to the
- needaction feature. For example, you may want to set records in
- bold in a list view if the current user has an action to perform
- on the record. '''
-
+ Models using the 'need_action' feature should override the
+ ``_needaction_domain_get`` method. This method returns a
+ domain to filter records requiring an action for a specific user.
+
+ This class also offers several global services:
+ - ``_needaction_count``: returns the number of actions uid has to perform
+ '''
+
_name = 'ir.needaction_mixin'
- _description = 'Need action mixin'
-
- def get_needaction_pending(self, cr, uid, ids, name, arg, context=None):
- res = {}
- needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context)
- for id in ids:
- res[id] = uid in needaction_user_ids[id]
- return res
+ _needaction = True
- def search_needaction_pending(self, cr, uid, self_again, field_name, criterion, context=None):
- ids = self.needaction_get_record_ids(
- cr, uid, uid, limit=1024, context=context)
- return [('id', 'in', ids)]
-
- _columns = {
- 'needaction_pending': fields.function(
- get_needaction_pending, type='boolean',
- fnct_search=search_needaction_pending,
- string='Need action pending',
- help="If True, this field states that users have to perform an " \
- "action This field comes from the ir.needaction_mixin class."),
- }
-
#------------------------------------------------------
- # Addon API
+ # Addons API
#------------------------------------------------------
-
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- """ Returns the user_ids that have to perform an action
- :return: dict { record_id: [user_ids], }
+
+ def _needaction_domain_get(self, cr, uid, context=None):
+ """ Returns the domain to filter records that require an action
+ :return: domain or False is no action
"""
- return dict((id,list()) for id in ids)
-
- def create(self, cr, uid, values, context=None):
- rel_obj = self.pool.get('ir.needaction_users_rel')
- # perform create
- obj_id = super(ir_needaction_mixin, self).create(cr, uid, values, context=context)
- # link user_ids
- needaction_user_ids = self.get_needaction_user_ids(cr, uid, [obj_id], context=context)
- rel_obj.create_users(cr, uid, [obj_id], self._name, needaction_user_ids[obj_id], context=context)
- return obj_id
-
- def write(self, cr, uid, ids, values, context=None):
- rel_obj = self.pool.get('ir.needaction_users_rel')
- # perform write
- write_res = super(ir_needaction_mixin, self).write(cr, uid, ids, values, context=context)
- # get and update user_ids
- needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context)
- for id in ids:
- rel_obj.update_users(cr, uid, [id], self._name, needaction_user_ids[id], context=context)
- return write_res
-
- def unlink(self, cr, uid, ids, context=None):
- # unlink user_ids
- rel_obj = self.pool.get('ir.needaction_users_rel')
- rel_obj.unlink_users(cr, uid, ids, self._name, context=context)
- # perform unlink
- return super(ir_needaction_mixin, self).unlink(cr, uid, ids, context=context)
-
+ return False
+
#------------------------------------------------------
# "Need action" API
#------------------------------------------------------
-
- def needaction_get_record_ids(self, cr, uid, user_id, limit=80, context=None):
- """Given the current model and a user_id
- return the record ids that require the user to perform an
- action"""
- rel_obj = self.pool.get('ir.needaction_users_rel')
- rel_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context)
- return map(itemgetter('res_id'), rel_obj.read(cr, uid, rel_ids, ['res_id'], context=context))
-
- def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None):
- """Given the current model and a user_id
- get the number of actions it has to perform"""
- rel_obj = self.pool.get('ir.needaction_users_rel')
- return rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context)
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+ def _needaction_count(self, cr, uid, domain=[], context=None):
+ """ Get the number of actions uid has to perform. """
+ dom = self._needaction_domain_get(cr, uid, context=context)
+ if not dom:
+ return 0
+ return self.search(cr, uid, (domain or []) +dom, context=context, count=True)
diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py
index e3136b92b6e..8b03cd3f3b3 100644
--- a/openerp/addons/base/ir/ir_sequence.py
+++ b/openerp/addons/base/ir/ir_sequence.py
@@ -210,7 +210,7 @@ class ir_sequence(openerp.osv.osv.osv):
def next_by_id(self, cr, uid, sequence_id, context=None):
""" Draw an interpolated string using the specified sequence."""
- self.check_read(cr, uid)
+ self.check_access_rights(cr, uid, 'read')
company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False]
ids = self.search(cr, uid, ['&',('id','=', sequence_id),('company_id','in',company_ids)])
return self._next(cr, uid, ids, context)
@@ -227,7 +227,7 @@ class ir_sequence(openerp.osv.osv.osv):
sequence selection. A matching sequence for that
specific company will get higher priority.
"""
- self.check_read(cr, uid)
+ self.check_access_rights(cr, uid, 'read')
company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False]
ids = self.search(cr, uid, ['&',('code','=', sequence_code),('company_id','in',company_ids)])
return self._next(cr, uid, ids, context)
diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py
index add4a39a182..51aee886390 100644
--- a/openerp/addons/base/ir/ir_ui_menu.py
+++ b/openerp/addons/base/ir/ir_ui_menu.py
@@ -23,7 +23,7 @@
import base64
import re
import threading
-
+from tools.safe_eval import safe_eval as eval
import tools
import openerp.modules
from osv import fields, osv
@@ -256,23 +256,24 @@ class ir_ui_menu(osv.osv):
return res
- def _get_needaction_info(self, cr, uid, id, domain=[], context={}):
- return [False, 0]
-
def _get_needaction(self, cr, uid, ids, field_names, args, context=None):
- if context is None:
- context = {}
res = {}
for menu in self.browse(cr, uid, ids, context=context):
- res[menu.id] = {}
- if menu.action and menu.action.type == 'ir.actions.act_window' and menu.action.res_model:
- menu_needaction_res = self.pool.get(menu.action.res_model)._get_needaction_info(cr, uid, uid, domain=menu.action.domain, context=context)
- else:
- menu_needaction_res = [False, 0]
- res[menu.id]['needaction_enabled'] = menu_needaction_res[0]
- res[menu.id]['needaction_counter'] = menu_needaction_res[1]
+ res[menu.id] = {
+ 'needaction_enabled': False,
+ 'needaction_counter': False,
+ }
+ if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model:
+ obj = self.pool.get(menu.action.res_model)
+ if obj._needaction:
+ if menu.action.type=='ir.actions.act_window':
+ dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or []
+ else:
+ dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain')
+ res[menu.id]['needaction_enabled'] = obj._needaction
+ res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, dom, context=context)
return res
-
+
_columns = {
'name': fields.char('Menu', size=64, required=True, translate=True),
'sequence': fields.integer('Sequence'),
diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py
index 56cb08fa41d..1609fa2a16f 100644
--- a/openerp/addons/base/res/res_partner.py
+++ b/openerp/addons/base/res/res_partner.py
@@ -215,6 +215,7 @@ class res_partner(osv.osv):
"Use this field anywhere a small image is required."),
'company_id': fields.many2one('res.company', 'Company', select=1),
'color': fields.integer('Color Index'),
+ 'user_ids': fields.one2many('res.users', 'partner_id', 'Users'),
'contact_address': fields.function(_address_display, type='char', string='Complete Address'),
}
@@ -357,35 +358,33 @@ class res_partner(osv.osv):
res.append((record.id, name))
return res
+ def _parse_partner_name(self, text, context=None):
+ """ Supported syntax:
+ - 'Raoul ': will find name and email address
+ - otherwise: default, everything is set as the name """
+ match = re.search(r'([^\s,<@]+@[^>\s,]+)', text)
+ if match:
+ email = match.group(1)
+ name = text[:text.index(email)].replace('"','').replace('<','').strip()
+ else:
+ name, email = text, ''
+ return name, email
+
def name_create(self, cr, uid, name, context=None):
""" Override of orm's name_create method for partners. The purpose is
to handle some basic formats to create partners using the
name_create.
- Supported syntax:
- - 'raoul@grosbedon.fr': create a partner with name raoul@grosbedon.fr
- and sets its email to raoul@grosbedon.fr
- - 'Raoul Grosbedon ': create a partner with name
- Raoul Grosbedon, and set its email to raoul@grosbedon.fr
- - anything else: fall back on the default name_create
- Regex :
- - ([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8}): raoul@grosbedon.fr
- - ([\w\s.\\-]+)[\<]([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8})[\>]:
- Raoul Grosbedon, raoul@grosbedon.fr
- """
- contact_regex = re.compile('([\w\s.\\-]+)[\<]([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8})[\>]')
- email_regex = re.compile('([a-zA-Z0-9._%-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9._]{1,8})')
- contact_regex_res = contact_regex.findall(name)
- email_regex_res = email_regex.findall(name)
- email = False
- if contact_regex_res:
- name = contact_regex_res[0][0].rstrip(' ') # remove extra spaces on the right
- email = contact_regex_res[0][1]
- elif email_regex_res:
- email = '%s' % (email_regex_res[0])
- else:
- pass
- data = {self._rec_name: name, 'email': email}
- rec_id = self.create(cr, uid, data, context);
+ If only an email address is received and that the regex cannot find
+ a name, the name will have the email value.
+ If 'force_email' key in context: must find the email address. """
+ if context is None:
+ context = {}
+ name, email = self._parse_partner_name(name, context=context)
+ if context.get('force_email') and not email:
+ raise osv.except_osv(_('Warning'), _("Couldn't create contact without email address !"))
+ if not name and email:
+ name = email
+ rec_id = self.create(cr, uid, {self._rec_name: name or email, 'email': email or False}, context=context)
return self.name_get(cr, uid, [rec_id], context)[0]
def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
@@ -410,6 +409,21 @@ class res_partner(osv.osv):
return self.name_get(cr, uid, ids, context)
return super(res_partner,self).name_search(cr, uid, name, args, operator=operator, context=context, limit=limit)
+ def find_or_create(self, cr, uid, email, context=None):
+ """ Find a partner with the given ``email`` or use :py:method:`~.name_create`
+ to create one
+
+ :param str email: email-like string, which should contain at least one email,
+ e.g. ``"Raoul Grosbedon "``"""
+ assert email, 'an email is required for find_or_create to work'
+ emails = tools.email_split(email)
+ if emails:
+ email = emails[0]
+ ids = self.search(cr, uid, [('email','ilike',email)], context=context)
+ if not ids:
+ return self.name_create(cr, uid, email, context=context)[0]
+ return ids[0]
+
def _email_send(self, cr, uid, ids, email_from, subject, body, on_error=None):
partners = self.browse(cr, uid, ids)
for partner in partners:
diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml
index 7a7e6f155e3..a79610b7a18 100644
--- a/openerp/addons/base/res/res_partner_view.xml
+++ b/openerp/addons/base/res/res_partner_view.xml
@@ -186,13 +186,10 @@
-
- |
-
-
-
-
-
+
+
|
diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv
index 2d7b751860a..c340e922396 100644
--- a/openerp/addons/base/security/ir.model.access.csv
+++ b/openerp/addons/base/security/ir.model.access.csv
@@ -120,6 +120,5 @@
"access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
"access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
-"access_ir_needaction_users_rel","ir_needaction_users_rel","model_ir_needaction_users_rel",,1,1,1,1
"access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1
diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py
index 5ea7654c008..42fef2eb39d 100644
--- a/openerp/addons/base/tests/__init__.py
+++ b/openerp/addons/base/tests/__init__.py
@@ -1,5 +1,5 @@
-import test_ir_values
+import test_ir_values, test_base
checks = [
- test_ir_values,
+ test_ir_values, test_base
]
diff --git a/openerp/addons/base/tests/test_base.py b/openerp/addons/base/tests/test_base.py
new file mode 100644
index 00000000000..285af8fbf6e
--- /dev/null
+++ b/openerp/addons/base/tests/test_base.py
@@ -0,0 +1,43 @@
+import unittest2
+
+import openerp.tests.common as common
+
+class test_base(common.TransactionCase):
+
+ def setUp(self):
+ super(test_base,self).setUp()
+ self.res_partner = self.registry('res.partner')
+
+ # samples use effective TLDs from the Mozilla public suffix
+ # list at http://publicsuffix.org
+ self.samples = [
+ ('"Raoul Grosbedon" ', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
+ ('ryu+giga-Sushi@aizubange.fukushima.jp', '', 'ryu+giga-Sushi@aizubange.fukushima.jp'),
+ ('Raoul chirurgiens-dentistes.fr', 'Raoul chirurgiens-dentistes.fr', ''),
+ (" Raoul O'hara ", "Raoul O'hara", '!@historicalsociety.museum')
+ ]
+
+ def test_00_res_partner_name_create(self):
+ cr, uid = self.cr, self.uid
+ parse = self.res_partner._parse_partner_name
+ for text, name, mail in self.samples:
+ self.assertEqual((name,mail), parse(text), 'Partner name parsing failed')
+ partner_id, dummy = self.res_partner.name_create(cr, uid, text)
+ partner = self.res_partner.browse(cr, uid, partner_id)
+ self.assertEqual(name or mail, partner.name, 'Partner name incorrect')
+ self.assertEqual(mail or False, partner.email, 'Partner email incorrect')
+
+ def test_10_res_partner_find_or_create(self):
+ cr,uid = self.cr, self.uid
+ email = self.samples[0][0]
+ partner_id, dummy = self.res_partner.name_create(cr, uid, email)
+ found_id = self.res_partner.find_or_create(cr, uid, email)
+ self.assertEqual(partner_id, found_id, 'find_or_create failed')
+ new_id = self.res_partner.find_or_create(cr, uid, self.samples[1][0])
+ self.assertTrue(new_id > partner_id, 'find_or_create failed - should have created new one')
+ new_id2 = self.res_partner.find_or_create(cr, uid, self.samples[2][0])
+ self.assertTrue(new_id2 > new_id, 'find_or_create failed - should have created new one again')
+
+
+if __name__ == '__main__':
+ unittest2.main()
\ No newline at end of file
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index b6b43f3f4c9..037d2a90986 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -515,7 +515,8 @@ class one2many(_column):
for id in ids:
res[id] = []
- ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
+ domain = self._domain(obj) if callable(self._domain) else self._domain
+ ids2 = obj.pool.get(self._obj).search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
for r in obj.pool.get(self._obj)._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'])
@@ -557,7 +558,8 @@ class one2many(_column):
reverse_rel = obj._all_columns.get(self._fields_id)
assert reverse_rel, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o'
# if the o2m has a static domain we must respect it when unlinking
- extra_domain = self._domain if isinstance(getattr(self, '_domain', None), list) else []
+ domain = self._domain(obj) if callable(self._domain) else self._domain
+ extra_domain = domain or []
ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context)
# If the model has cascade deletion, we delete the rows because it is the intended behavior,
# otherwise we only nullify the reverse foreign key column.
@@ -575,7 +577,8 @@ class one2many(_column):
return result
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
- return obj.pool.get(self._obj).name_search(cr, uid, value, self._domain, operator, context=context,limit=limit)
+ domain = self._domain(obj) if callable(self._domain) else self._domain
+ return obj.pool.get(self._obj).name_search(cr, uid, value, domain, operator, context=context,limit=limit)
@classmethod
@@ -1540,9 +1543,7 @@ def field_to_dict(model, cr, user, field, context=None):
"""
res = {'type': field._type}
- # This additional attributes for M2M and function field is added
- # because we need to display tooltip with this additional information
- # when client is started in debug mode.
+ # some attributes for m2m/function field are added as debug info only
if isinstance(field, function):
res['function'] = field._fnct and field._fnct.func_name or False
res['store'] = field.store
@@ -1577,7 +1578,7 @@ def field_to_dict(model, cr, user, field, context=None):
res['selection'] = field.selection(model, cr, user, context)
if res['type'] in ('one2many', 'many2many', 'many2one'):
res['relation'] = field._obj
- res['domain'] = field._domain
+ res['domain'] = field._domain(model) if callable(field._domain) else field._domain
res['context'] = field._context
if isinstance(field, one2many):
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index b5e78718553..2ba8afd8e72 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -654,6 +654,7 @@ class BaseModel(object):
may be set to False.
"""
__metaclass__ = MetaModel
+ _auto = True # create database backend
_register = False # Set to false if the model shouldn't be automatically discovered.
_name = None
_columns = {}
@@ -667,6 +668,7 @@ class BaseModel(object):
_order = 'id'
_sequence = None
_description = None
+ _needaction = False
# dict of {field:method}, with method returning the name_get of records
# to include in the _read_group, if grouped on this field
@@ -1823,12 +1825,11 @@ class BaseModel(object):
fields = {}
if node.tag == 'diagram':
if node.getchildren()[0].tag == 'node':
- node_fields = self.pool.get(node.getchildren()[0].get('object')).fields_get(cr, user, None, context)
+ node_model = self.pool.get(node.getchildren()[0].get('object'))
+ node_fields = node_model.fields_get(cr, user, None, context)
fields.update(node_fields)
- if not node.get("create"):
- fn = getattr(self.pool.get(node.getchildren()[1].get('object')), 'check_create')
- if not fn(cr, user, raise_exception=False):
- node.set("create", 'false')
+ 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.get(node.getchildren()[1].get('object')).fields_get(cr, user, None, context)
fields.update(arrow_fields)
@@ -1837,8 +1838,8 @@ class BaseModel(object):
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, fn in (('create', 'check_create'), ('delete', 'check_unlink'), ('edit', 'check_write')):
- if not node.get(action) and not getattr(self, fn)(cr, user, raise_exception=False):
+ 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():
@@ -2354,7 +2355,7 @@ class BaseModel(object):
def read_string(self, cr, uid, id, langs, fields=None, context=None):
res = {}
res2 = {}
- self.pool.get('ir.translation').check_read(cr, uid)
+ self.pool.get('ir.translation').check_access_rights(cr, uid, 'read')
if not fields:
fields = self._columns.keys() + self._inherit_fields.keys()
#FIXME: collect all calls to _get_source into one SQL call.
@@ -2378,7 +2379,7 @@ class BaseModel(object):
return res
def write_string(self, cr, uid, id, langs, vals, context=None):
- self.pool.get('ir.translation').check_write(cr, uid)
+ self.pool.get('ir.translation').check_access_rights(cr, uid, 'write')
#FIXME: try to only call the translation in one SQL
for lang in langs:
for field in vals:
@@ -2526,7 +2527,7 @@ class BaseModel(object):
"""
context = context or {}
- self.check_read(cr, uid)
+ self.check_access_rights(cr, uid, 'read')
if not fields:
fields = self._columns.keys()
@@ -3407,8 +3408,7 @@ class BaseModel(object):
if context is None:
context = {}
- write_access = self.check_write(cr, user, False) or \
- self.check_create(cr, user, False)
+ write_access = self.check_access_rights(cr, user, 'write') or self.check_access_rights(cr, user, 'create')
res = {}
@@ -3472,7 +3472,7 @@ class BaseModel(object):
if not context:
context = {}
- self.check_read(cr, user)
+ self.check_access_rights(cr, user, 'read')
if not fields:
fields = list(set(self._columns.keys() + self._inherit_fields.keys()))
if isinstance(ids, (int, long)):
@@ -3746,18 +3746,6 @@ class BaseModel(object):
according to the access rights."""
return self.pool.get('ir.model.access').check(cr, uid, self._name, operation, raise_exception)
- def check_create(self, cr, uid, raise_exception=True):
- return self.check_access_rights(cr, uid, 'create', raise_exception)
-
- def check_read(self, cr, uid, raise_exception=True):
- return self.check_access_rights(cr, uid, 'read', raise_exception)
-
- def check_unlink(self, cr, uid, raise_exception=True):
- return self.check_access_rights(cr, uid, 'unlink', raise_exception)
-
- def check_write(self, cr, uid, raise_exception=True):
- return self.check_access_rights(cr, uid, 'write', raise_exception)
-
def check_access_rule(self, cr, uid, ids, operation, context=None):
"""Verifies that the operation given by ``operation`` is allowed for the user
according to ir.rules.
@@ -3821,7 +3809,7 @@ class BaseModel(object):
self._check_concurrency(cr, ids, context)
- self.check_unlink(cr, uid)
+ self.check_access_rights(cr, uid, 'unlink')
ir_property = self.pool.get('ir.property')
@@ -3957,7 +3945,7 @@ class BaseModel(object):
ids = [ids]
self._check_concurrency(cr, ids, context)
- self.check_write(cr, user)
+ self.check_access_rights(cr, user, 'write')
result = self._store_get_values(cr, user, ids, vals.keys(), context) or []
@@ -4177,7 +4165,7 @@ class BaseModel(object):
if self.is_transient():
self._transient_vacuum(cr, user)
- self.check_create(cr, user)
+ self.check_access_rights(cr, user, 'create')
if self._log_access:
for f in LOG_ACCESS_COLUMNS:
@@ -4669,7 +4657,7 @@ class BaseModel(object):
"""
if context is None:
context = {}
- self.check_read(cr, access_rights_uid or user)
+ self.check_access_rights(cr, access_rights_uid or user, 'read')
# For transient models, restrict acces to the current user, except for the super-user
if self.is_transient() and self._log_access and user != SUPERUSER_ID:
@@ -4969,37 +4957,7 @@ class BaseModel(object):
# backwards compatibility
get_xml_id = get_external_id
_get_xml_ids = _get_external_ids
-
- def _get_needaction_info(self, cr, uid, user_id, limit=None, order=None, domain=False, context=None):
- """Base method for needaction mechanism
- - see ir.needaction for actual implementation
- - if the model uses the need action mechanism
- (hasattr(model_obj, 'needaction_get_record_ids')):
- - get the record ids on which the user has actions to perform
- - evaluate the menu domain
- - compose a new domain: menu domain, limited to ids of
- records requesting an action
- - count the number of records maching that domain, that
- is the number of actions the user has to perform
- - this method returns default values
- :param: model_name: the name of the model (ex: hr.holidays)
- :param: user_id: the id of user
- :return: [uses_needaction=True/False, needaction_uid_ctr=%d]
- """
- if hasattr(self, 'needaction_get_record_ids'):
- # Arbitrary limit, but still much lower thant infinity, to avoid
- # getting too much data.
- ids = self.needaction_get_record_ids(cr, uid, user_id, limit=8192, context=context)
- if not ids:
- return [True, 0]
- if domain:
- new_domain = eval(domain, locals_dict={'uid': user_id}) + [('id', 'in', ids)]
- else:
- new_domain = [('id', 'in', ids)]
- return [True, self.search(cr, uid, new_domain, limit=limit, order=order, count=True, context=context)]
- else:
- return [False, 0]
-
+
# Transience
def is_transient(self):
""" Return whether the model is transient.
@@ -5126,6 +5084,7 @@ class Model(BaseModel):
The system will later instantiate the class once per database (on
which the class' module is installed).
"""
+ _auto = True
_register = False # not visible in ORM registry, meant to be python-inherited only
_transient = False # True in a TransientModel
@@ -5138,6 +5097,7 @@ class TransientModel(BaseModel):
records they created. The super-user has unrestricted access
to all TransientModel records.
"""
+ _auto = True
_register = False # not visible in ORM registry, meant to be python-inherited only
_transient = True
diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py
index d3ab62ebc1c..aa3055f471c 100644
--- a/openerp/tests/__init__.py
+++ b/openerp/tests/__init__.py
@@ -9,7 +9,7 @@ See the :ref:`test-framework` section in the :ref:`features` list.
"""
from . import test_expression, test_html_sanitize, test_ir_sequence, test_orm,\
- test_view_validation, test_uninstall
+ test_view_validation, test_uninstall, test_misc
fast_suite = [
test_ir_sequence,
@@ -20,6 +20,7 @@ checks = [
test_html_sanitize,
test_orm,
test_view_validation,
+ test_misc,
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/test_html_sanitize.py b/openerp/tests/test_html_sanitize.py
index 90ccc88cc1d..cb46b325144 100755
--- a/openerp/tests/test_html_sanitize.py
+++ b/openerp/tests/test_html_sanitize.py
@@ -26,11 +26,15 @@ class TestSanitizer(unittest.TestCase):
def test_simple(self):
x = "yop"
self.assertEqual(x, html_sanitize(x))
+
+ def test_trailing_text(self):
+ x = 'lalayop
xxx'
+ self.assertEqual(x, html_sanitize(x))
- def test_test_case(self):
+ def test_no_exception(self):
html_sanitize(test_case)
- def test_crm(self):
+ def test_unicode(self):
html_sanitize("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci")
if __name__ == '__main__':
diff --git a/openerp/tests/test_misc.py b/openerp/tests/test_misc.py
new file mode 100644
index 00000000000..7661f253b17
--- /dev/null
+++ b/openerp/tests/test_misc.py
@@ -0,0 +1,21 @@
+# This test can be run stand-alone with something like:
+# > PYTHONPATH=. python2 openerp/tests/test_misc.py
+
+import unittest2
+
+class test_misc(unittest2.TestCase):
+ """ Test some of our generic utility functions """
+
+ def test_append_to_html(self):
+ from openerp.tools import append_content_to_html
+ test_samples = [
+ ('some content', '--\nYours truly', True,
+ 'some content\n--\nYours truly
\n'),
+ ('some content', '\n\n--
\nYours truly
\n\n', False,
+ 'some content\n\n\n--
\nYours truly
\n\n\n'),
+ ]
+ for html, content, flag, expected in test_samples:
+ self.assertEqual(append_content_to_html(html,content,flag), expected, 'append_content_to_html is broken')
+
+if __name__ == '__main__':
+ unittest2.main()
\ No newline at end of file
diff --git a/openerp/tools/html_sanitize.py b/openerp/tools/html_sanitize.py
index 6763b464ab1..6ea7b90e2ba 100644
--- a/openerp/tools/html_sanitize.py
+++ b/openerp/tools/html_sanitize.py
@@ -1,62 +1,92 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (C) 2012 OpenERP S.A. ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+import lxml.html
+import operator
import re
-def html_sanitize(x):
- return x # It seems that our test suite doesn't care.
- if not x:
- return x
- root = pq("")
- if type(x) == str:
- x = unicode(x, "utf8", "replace")
- root.html(x)
- result = handle_element(root[0])
- new = pq(result)
- return new.html()
+from openerp.loglevels import ustr
+def html_sanitize(src):
+ if not src:
+ return src
+ src = ustr(src, errors='replace')
+ root = lxml.html.fromstring(u"%s
" % src)
+ result = handle_element(root)
+ res = []
+ for element in children(result[0]):
+ if isinstance(element, basestring):
+ res.append(element)
+ else:
+ element.tail = ""
+ res.append(lxml.html.tostring(element))
+ return ''.join(res)
+
+# FIXME: shouldn't this be a whitelist rather than a blacklist?!
to_remove = set(["script", "head", "meta", "title", "link", "img"])
to_unwrap = set(["html", "body"])
-javascript_regex = re.compile("""^\s*javascript\s*\:.*$""")
+javascript_regex = re.compile(r"^\s*javascript\s*:.*$", re.IGNORECASE)
+
def handle_a(el, new):
href = el.get("href", "#")
if javascript_regex.search(href):
href = "#"
new.set("href", href)
+
special = {
"a": handle_a,
}
-def handle_element(el):
- if type(el) == str or type(el) == unicode:
- return [el]
- if el.tag in to_remove:
+def handle_element(element):
+ if isinstance(element, basestring):
+ return [element]
+ if element.tag in to_remove:
return []
- if el.tag in to_unwrap:
- return reduce(lambda x,y: x+y, [handle_element(x) for x in children(el)])
- new = pq("<%s />" % el.tag)[0]
- for i in children(el):
- append_to(handle_element(i), new)
- if el.tag in special:
- special[el.tag](el, new)
- return [new]
-
-def children(el):
+ if element.tag in to_unwrap:
+ return reduce(operator.add, [handle_element(x) for x in children(element)])
+ result = lxml.html.fromstring("<%s />" % element.tag)
+ for c in children(element):
+ append_to(handle_element(c), result)
+ if element.tag in special:
+ special[element.tag](element, result)
+ return [result]
+
+def children(node):
res = []
- if el.text is not None:
- res.append(el.text)
- for i in el.getchildren():
- res.append(i)
- if i.tail is not None:
- res.append(i.tail)
+ if node.text is not None:
+ res.append(node.text)
+ for child_node in node.getchildren():
+ res.append(child_node)
+ if child_node.tail is not None:
+ res.append(child_node.tail)
return res
-def append_to(new_ones, el):
- for i in new_ones:
- if type(i) == str or type(i) == unicode:
- children = el.getchildren()
+def append_to(elements, dest_node):
+ for element in elements:
+ if isinstance(element, basestring):
+ children = dest_node.getchildren()
if len(children) == 0:
- el.text = i
+ dest_node.text = element
else:
- children[-1].tail = i
+ children[-1].tail = element
else:
- el.append(i)
+ dest_node.append(element)
diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py
index 5d49e380243..9a7b3499425 100644
--- a/openerp/tools/misc.py
+++ b/openerp/tools/misc.py
@@ -406,6 +406,42 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
cr.close()
return res
+def email_split(text):
+ """ Return a list of the email addresses found in ``text`` """
+ if not text: return []
+ return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
+
+def append_content_to_html(html, content, plaintext=True):
+ """Append extra content at the end of an HTML snippet, trying
+ to locate the end of the HTML document (