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 2f7f6624885..aa3055f471c 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -8,8 +8,8 @@ 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. """ -from . import test_expression, test_ir_sequence, test_orm,\ - test_view_validation, test_uninstall +from . import test_expression, test_html_sanitize, test_ir_sequence, test_orm,\ + test_view_validation, test_uninstall, test_misc fast_suite = [ test_ir_sequence, @@ -17,8 +17,10 @@ fast_suite = [ checks = [ test_expression, + 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_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

--

\n

Yours truly

\n\n', False, + 'some content\n\n\n

--

\n

Yours 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 65f17be074f..6ea7b90e2ba 100644 --- a/openerp/tools/html_sanitize.py +++ b/openerp/tools/html_sanitize.py @@ -1,67 +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): - if not x: - return x - if type(x) == str: - x = unicode(x, "utf8", "replace") - root = lxml.html.fromstring("
%s
" % x) - result = handle_element(root) - res = "" - for el in children(result[0]): - if type(el) == str or type(el) == unicode: - res += el - else: - el.tail = "" - res += lxml.html.tostring(el) - return res +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 = lxml.html.fromstring("<%s />" % el.tag) - 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 (, , or + EOF), and wrapping the provided content in a
 block
+       unless ``plaintext`` is False. A side-effect of this
+       method is to coerce all HTML tags to lowercase in ``html``,
+       and strip enclosing  or  tags in content if
+       ``plaintext`` is False.
+       
+       :param str html: html tagsoup (doesn't have to be XHTML)
+       :param str content: extra content to append
+       :param bool plaintext: whether content is plaintext and should
+           be wrapped in a 
 tag.
+    """
+    html = ustr(html)
+    if plaintext:
+        content = u'\n
%s
\n' % ustr(content) + else: + content = re.sub(r'(?i)(||)', '', content) + content = u'\n%s\n'% ustr(content) + # Force all tags to lowercase + html = re.sub(r'(])', + lambda m: '%s%s%s' % (m.group(1),m.group(2).lower(),m.group(3)), html) + insert_location = html.find('') + if insert_location == -1: + insert_location = html.find('') + if insert_location == -1: + return '%s%s' % (html, content) + return '%s%s%s' % (html[:insert_location], content, html[insert_location:]) + + #---------------------------------------------------------- # SMS #----------------------------------------------------------