From 75e0749542e51ce76fa29a9d2f4b6a02cb4067d1 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 1 Sep 2011 20:12:54 +0200 Subject: [PATCH 01/91] [IMP] ir.values: separated defaults and actions API, added different views This should prepare for a future split of the two parts currently composing ir.values entries: user defaults and action bindings to a model's UI. bzr revid: odo@openerp.com-20110901181254-g14lyeogs0wv23bz --- openerp/addons/base/ir/ir.xml | 130 +++-- openerp/addons/base/ir/ir_values.py | 511 ++++++++++++++------ openerp/addons/base/test/test_ir_values.yml | 3 + 3 files changed, 415 insertions(+), 229 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 011ad437cca..eda044336df 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -7,44 +7,50 @@ ir.values.form.action ir.values form - 20 -
- + + - + + - + - + + + + + + - - + + ir.values.form.defaults + ir.values + form + +
+ + + + + + + - - - - + + + - - - - - - - - - - - +
@@ -53,11 +59,11 @@ ir.values tree - + - + @@ -73,7 +79,7 @@ - + @@ -81,23 +87,21 @@ - Client Events + Action Bindings ir.actions.act_window ir.values form tree,form [('key','=','action')] - {'read':'default','default_object':1} + {'default_object':1,'default_key':'action'} - tree - form @@ -105,54 +109,27 @@ - - - - - - - ir.values.form - ir.values - form - -
- - - - - - - - - - - - -
- - - ir.values.tree - ir.values - tree - - - - - - - - - - - - - - Client Actions Connections + + User-defined Defaults ir.actions.act_window ir.values form - - {'read':'default'} + tree,form + + [('key','=','default')] + {'default_object':0,'default_key':'default','default_key2':''} + + + + tree + + + + + + form + + @@ -342,6 +319,7 @@ + diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index c2a59ada55e..c75831f9c2f 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -28,7 +28,76 @@ EXCLUDED_FIELDS = set(( 'report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml', 'report_sxw_content_data', 'report_rml_content_data', 'search_view', )) +#: Possible slots to bind an action to with :meth:`~.set_action` +ACTION_SLOTS = [ + "client_action_multi", # sidebar wizard action + "client_print_multi", # sidebar report printing button + "client_action_relate", # sidebar related link + "tree_but_open", # double-click on item in tree view + "tree_but_action", # deprecated: same as tree_but_open + ] + + class ir_values(osv.osv): + """Holds internal model-specific action bindings and user-defined default + field values. definitions. This is a legacy internal model, mixing + two different concepts, and will likely be updated or replaced in a + future version by cleaner, separate models. You should not depend + explicitly on it. + + The purpose of each ``ir.values`` entry depends on its type, defined + by the ``key`` column: + + * 'default': user-defined default values, used when creating new + records of this model: + * 'action': binding of an action to a particular *action slot* of + this model, making the action easily available in the user + interface for this model. + + The ``key2`` column acts as a qualifier, further refining the type + of the entry. The possible values are: + + * for 'default' entries: an optional condition restricting the + cases where this particular default value will be applicable, + or ``False`` for no condition + * for 'action' entries: the ``key2`` qualifier is one of the available + action slots, defining how this action can be invoked: + + * ``'client_print_multi'`` for report printing actions that will + be available on views displaying items from this model + * ``'client_action_multi'`` for assistants (wizards) actions + that will be available in views displaying objects of this model + * ``'client_action_relate'`` for links towards related documents + that should be available in views displaying objects of this model + * ``'tree_but_open'`` for actions that will be triggered when + double-clicking an item from this model in a hierarchical tree view + + Each entry is specific to a model (``model`` column), and for ``'actions'`` + type, may even be made specific to a given record of that model when the + ``res_id`` column contains a record ID (``False`` means it's global for + all records). + + The content of the entry is defined by the ``value`` column, which may either + contain an arbitrary value (for default values - when the ``object`` column + is False), or a reference string defining the action that should be executed. + + .. rubric:: Usage: default values + + The ``'default'`` entries are usually defined manually by the + users, and set by their UI clients calling :meth:`~.set_default`. + These default values are then automatically used by the + ORM every time a new record is about to be created, i.e. when + :meth:`~openerp.osv.osv.osv.default_get` + or :meth:`~openerp.osv.osv.osv.create` are called. + + .. rubric:: Usage: action bindings + + Business applications will usually bind their actions during + installation, and OpenERP UI clients will apply them as defined, + based on the list of actions included in the result of + :meth:`~openerp.osv.osv.osv.fields_view_get`, + or directly returned by explicit calls to :meth:`~.get_actions`. + """ _name = 'ir.values' def _value_unpickle(self, cursor, user, ids, name, arg, context=None): @@ -38,7 +107,7 @@ class ir_values(osv.osv): if not report.object and value: try: value = str(pickle.loads(value)) - except: + except Exception: pass res[report.id] = value return res @@ -53,44 +122,69 @@ class ir_values(osv.osv): value = pickle.dumps(value) self.write(cursor, user, id, {name[:-9]: value}, context=ctx) - def onchange_object_id(self, cr, uid, ids, object_id, context={}): + def onchange_object_id(self, cr, uid, ids, object_id, context=None): if not object_id: return {} act = self.pool.get('ir.model').browse(cr, uid, object_id, context=context) return { 'value': {'model': act.model} } - def onchange_action_id(self, cr, uid, ids, action_id, context={}): + def onchange_action_id(self, cr, uid, ids, action_id, context=None): if not action_id: return {} act = self.pool.get('ir.actions.actions').browse(cr, uid, action_id, context=context) return { 'value': {'value_unpickle': act.type+','+str(act.id)} } + def onchange_key(self, cr, uid, ids, key, context=None): + return { + 'value': {'object': key == 'action'} + } + _columns = { - 'name': fields.char('Name', size=128), - 'model_id': fields.many2one('ir.model', 'Object', size=128, - help="This field is not used, it only helps you to select a good model."), - 'model': fields.char('Object Name', size=128, select=True), - 'action_id': fields.many2one('ir.actions.actions', 'Action', - help="This field is not used, it only helps you to select the right action."), - 'value': fields.text('Value'), + 'name': fields.char('Name', size=128, required=True), + 'model': fields.char('Model Name', size=128, select=True, required=True, + help="Model to which this entry applies"), + + # TODO: model_id and action_id should be read-write function fields + 'model_id': fields.many2one('ir.model', 'Model (change only)', size=128, + help="Model to which this entry applies - " + "helper field for setting a model, will " + "automatically set the correct model name"), + 'action_id': fields.many2one('ir.actions.actions', 'Action (change only)', + help="Action bound to this entry - " + "helper field for binding an action, will " + "automatically set the correct reference"), + + 'value': fields.text('Value', help="Default value (pickled) or reference to an action"), 'value_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle, - method=True, type='text', string='Value'), - 'object': fields.boolean('Is Object'), - 'key': fields.selection([('action','Action'),('default','Default')], 'Type', size=128, select=True), - 'key2' : fields.char('Event Type',help="The kind of action or button in the client side that will trigger the action.", size=128, select=True), - 'meta': fields.text('Meta Datas'), - 'meta_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle, - method=True, type='text', string='Metadata'), - 'res_id': fields.integer('Object ID', help="Keep 0 if the action must appear on all resources.", select=True), - 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True), - 'company_id': fields.many2one('res.company', 'Company', select=True) + type='text', + string='Default value or action reference'), + 'object': fields.boolean('No value serialization', help="Should be enabled when Type is Action"), + 'key': fields.selection([('action','Action'),('default','Default')], + 'Action Type', size=128, select=True, required=True, + help="- Action: an action attached to one slot of the given model\n" + "- Default: a default value for a model field"), + 'key2' : fields.char('Action Qualifier', size=128, select=True, + help="For actions, one of the possible action slots: \n" + " - client_action_multi\n" + " - client_print_multi\n" + " - client_action_relate\n" + " - tree_but_open\n" + "For defaults, an optional condition" + ,), + 'res_id': fields.integer('Record ID', select=True, + help="Database identifier of the record to which this applies." + "0 = for all records"), + 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True, + help="If set, action binding only applies for this user."), + 'company_id': fields.many2one('res.company', 'Company', ondelete='cascade', select=True, + help="If set, action binding only applies for this company") } _defaults = { - 'key': lambda *a: 'action', - 'key2': lambda *a: 'tree_but_open', - 'company_id': lambda *a: False + 'key': 'action', + 'key2': 'tree_but_open', + 'object': True, } def _auto_init(self, cr, context=None): @@ -99,140 +193,251 @@ class ir_values(osv.osv): if not cr.fetchone(): cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)') - def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False): + def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False): + """Defines a default value for the given model and field_name. Any previous + default will be replaced and lost in the process. + + :param string model: model name + :param string field_name: field name to which the default applies + :param value: the default field value to set + :type value: any serializable Python value + :param bool for_all_users: whether the default should apply to everybody or only + the user calling the method + :param int company_id: optional ID of the company to which the default should + apply. If omitted, the default will be global. If True + is passed, the current user's company will be used. + :param string condition: optional condition specification that can be used to + restrict the applicability of the default values + (e.g. based on another field's value) + :return: id of the newly created ir.values entry + """ if isinstance(value, unicode): value = value.encode('utf8') - if not isobject: - value = pickle.dumps(value) - if meta: - meta = pickle.dumps(meta) - assert isinstance(models, (list, tuple)), models - ids_res = [] - for model in models: - if isinstance(model, (list, tuple)): - model,res_id = model - else: - res_id = False - if replace: - search_criteria = [ - ('key', '=', key), - ('key2', '=', key2), - ('model', '=', model), - ('res_id', '=', res_id), - ('user_id', '=', preserve_user and uid) - ] - if key in ('meta', 'default'): - search_criteria.append(('name', '=', name)) - else: - search_criteria.append(('value', '=', value)) + if company_id is True: + # should be company-specific, need to get company id + user = self.pool.get('res.users').browse(cr, uid, uid) + company_id = user.company_id.id - self.unlink(cr, uid, self.search(cr, uid, search_criteria)) - vals = { - 'name': name, - 'value': value, - 'model': model, - 'object': isobject, - 'key': key, - 'key2': key2 and key2[:200], - 'meta': meta, - 'user_id': preserve_user and uid, - } - if company: - cid = self.pool.get('res.users').browse(cr, uid, uid, context={}).company_id.id - vals['company_id']=cid - if res_id: - vals['res_id']= res_id - ids_res.append(self.create(cr, uid, vals)) - return ids_res + # remove existing defaults for the same scope + search_criteria = [ + ('key', '=', 'default'), + ('key2', '=', condition), + ('model', '=', model), + ('name', '=', field_name), + ('user_id', '=', False if for_all_users else uid), + ('company_id','=', company_id) + ] + self.unlink(cr, uid, self.search(cr, uid, search_criteria)) + + return self.create(cr, uid, { + 'name': field_name, + 'value': pickle.dumps(value), + 'model': model, + 'object': False, + 'key': 'default', + 'key2': condition and condition[:200], + 'user_id': False if for_all_users else uid, + 'company_id': company_id, + }) + + def get_defaults(self, cr, uid, model, condition=False): + """Returns any user-defined values registered for the given model. + This is not field-specific, but an optional ``condition`` can be + provided to query for default values that should be applied only + when the given condition is met (usually another field's value). + Defaults that have been set for the user herself will have higher + priority that those that have been set for everyone + (see :meth:`~.set_default`). + + :param string model: model name + :param string condition: optional condition specification that can be used to + restrict the applicability of the default values + (e.g. based on another field's value) + :return: list of default values tuples of the form ``(id, field_name, value)`` + (``id`` is the ID of the default entry, usually irrelevant) + """ + # use a direct SQL query for performance reasons, + # this is called very often + query = """SELECT v.id, v.name, v.value FROM ir_values v + LEFT JOIN res_users u ON (v.user_id = u.id) + WHERE v.key = %%s AND v.model = %%s + AND (v.user_id = %%s OR v.user_id IS NULL) + AND (u.company_id IS NULL OR + u.company_id = + (SELECT company_id from res_users where id = %%s) + ) + %s + ORDER BY v.user_id, u.company_id""" + query = query % ('AND v.key2 = %s' if condition else '') + params = ('default', model, uid, uid) + if condition: + params += (condition,) + cr.execute(query, params) + + # keep only the highest priority default for each field + defaults = {} + for row in cr.dictfetchall(): + defaults.setdefault(row['name'], + (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8')))) + return defaults.values() + + def set_action(self, cr, uid, name, action_slot, model, action, res_id=False): + """Binds an the given action to the given model's action slot - for later + retrieval via :meth:`~.get_actions`. Any existing binding of the same action + to the same slot is first removed, allowing an update of the action's name. + See the class description for more details about the various action + slots: :class:`~ir_values`. + + :param string name: action label, usually displayed by UI client + :param string action_slot: the action slot to which the action should be + bound to - one of ``client_action_multi``, + ``client_print_multi``, ``client_action_relate``, + ``tree_but_open``. + :param string model: model name + :param string action: action reference, in the form ``'model,id'`` + :param int res_id: optional record id - will bind the action only to a + specific record of the model, not all records. + :return: id of the newly created ir.values entry + """ + assert isinstance(action, basestring) and ',' in action, \ + 'Action definition must be an action reference, e.g. "ir.actions.act_window,42"' + assert action_slot in ACTION_SLOTS, \ + 'Action slot (%s) must be one of: %r' % (action_slot, ACTION_SLOTS) + + # remove existing action definition of same slot and value + search_criteria = [ + ('key', '=', 'action'), + ('key2', '=', action_slot), + ('model', '=', model), + ('res_id', '=', res_id), + ('value', '=', action), + ] + self.unlink(cr, uid, self.search(cr, uid, search_criteria)) + + return self.create(cr, uid, { + 'key': 'action', + 'key2': action_slot, + 'object': True, + 'model': model, + 'res_id': res_id or False, + 'name': name, + 'value': action, + }) + + def get_actions(self, cr, uid, action_slot, model, res_id=False, context=None): + """Retrieves the list of actions bound to the given model's action slot. + See the class description for more details about the various action + slots: :class:`~.ir_values`. + + :param string action_slot: the action slot to which the actions should be + bound to - one of ``client_action_multi``, + ``client_print_multi``, ``client_action_relate``, + ``tree_but_open``. + :param string model: model name + :param int res_id: optional record id - will bind the action only to a + specific record of the model, not all records. + :return: list of action tuples of the form ``(id, name, action_def)``, + where ``id`` is the ID of the default entry, ``name`` is the + action label, and ``action_def`` is a dict containing the + action definition as obtained by calling + :meth:`~openerp.osv.osv.osv.read` on the action record. + """ + assert action_slot in ACTION_SLOTS, 'Illegal action slot value: %s' % action_slot + # use a direct SQL query for performance reasons, + # this is called very often + query = """SELECT v.id, v.name, v.value FROM ir_values v + WHERE v.key = %s AND v.key2 = %s + AND v.model = %s + AND (v.res_id = %s + OR v.res_id IS NULL + OR v.res_id = 0) + ORDER BY v.id""" + cr.execute(query, ('action', action_slot, model, res_id or None)) + results = {} + for action in cr.dictfetchall(): + action_model,id = action['value'].split(',') + fields = [ + field + for field in self.pool.get(action_model)._all_columns + if field not in EXCLUDED_FIELDS] + # FIXME: needs cleanup + try: + action_def = self.pool.get(action_model).read(cr, uid, int(id), fields, context) + if action_def: + if action_model in ('ir.actions.report.xml','ir.actions.act_window', + 'ir.actions.wizard'): + groups = action_def.get('groups_id') + if groups: + cr.execute('SELECT 1 FROM res_groups_users_rel WHERE gid IN %s AND uid=%s', + (tuple(groups), uid)) + if not cr.fetchone(): + if action['name'] == 'Menuitem': + raise osv.except_osv('Error !', + 'You do not have the permission to perform this operation !!!') + continue + # keep only the first action registered for each action name + results[action['name']] = (action['id'], action['name'], action_def) + except except_orm, e: + continue + return results.values() + + def _map_legacy_model_list(self, model_list, map_fn): + """Apply map_fn to the various models passed, according to + legacy way to specify models/records. + + :param model_list: list of models/records reference in the form ``[model,..]`` + or ``[(model,res_id), ...]`` + :param map_fn: the map function to apply - should expect 2 arguments: + ``model`` and ``res_id``. + """ + assert isinstance(model_list, (list, tuple)), \ + "model_list should be in the form [model,..] or [(model,res_id), ..]" + results = [] + for model in model_list: + res_id = False + if isinstance(model, (list, tuple)): + model, res_id = model + result = map_fn(model, res_id) + # some of the functions return one result at a time (tuple or id) + # and some return a list of many of them - care for both + if isinstance(result,list): + results.extend(result) + else: + results.append(result) + return results + + # Backards-compatibility adapter layer to retrofit into split API + def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False): + """Deprecated legacy method to set default values and bind actions to models' action slots. + Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.set_default` + (``key=='default'``) or :meth:`~.set_action` (``key == 'action'``). + + :deprecated: As of v6.1, ``set_default()`` or ``set_action()`` should be used directly. + """ + assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']" + if key == 'default': + def do_set(model,res_id): + return self.set_default(cr, uid, model, field_name=name, value=value, + for_all_users=(not preserve_user), company_id=company, + condition=key2) + elif key == 'action': + def do_set(model,res_id): + return self.set_action(cr, uid, name, action_slot=key2, model=model, action=value, res_id=res_id) + return self._map_legacy_model_list(models, do_set) def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True): - if context is None: - context = {} - result = [] - assert isinstance(models, (list, tuple)), models + """Deprecated legacy method to get the list of default values or actions bound to models' action slots. + Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.get_defaults` + (``key=='default'``) or :meth:`~.get_actions` (``key == 'action'``) - for m in models: - if isinstance(m, (list, tuple)): - m, res_id = m - else: - res_id = False + :deprecated: As of v6.1, ``get_defaults()`` or ``get_actions()`` should be used directly. - where = ['key=%s','model=%s'] - params = [key, str(m)] - if key2: - where.append('key2=%s') - params.append(key2[:200]) - elif key2_req and not meta: - where.append('key2 is null') - if res_id_req and (models[-1][0] == m): - if res_id: - where.append('res_id=%s') - params.append(res_id) - else: - where.append('(res_id is NULL)') - elif res_id: - if (models[-1][0]==m): - where.append('(res_id=%s or (res_id is null))') - params.append(res_id) - else: - where.append('res_id=%s') - params.append(res_id) - order = 'id' - if key == 'default': - # Make sure we get first the values for specific users, then - # the global values. The map/filter below will retain the first - # value for any given name. The 'order by' will put the null - # values last; this may be postgres specific (it is the - # behavior in postgres at least since 8.2). - order = 'user_id' - where.append('(user_id=%s or (user_id IS NULL)) order by '+ order) - params.append(uid) - clause = ' and '.join(where) - cr.execute('select id,name,value,object,meta, key from ir_values where ' + clause, params) - result = cr.fetchall() - if result: - break - - if not result: - return [] - - def _result_get(x, keys): - if x[1] in keys: - return False - keys.append(x[1]) - if x[3]: - model,id = x[2].split(',') - # FIXME: It might be a good idea to opt-in that kind of stuff - # FIXME: instead of arbitrarily removing random fields - fields = [ - field - for field in self.pool.get(model).fields_get_keys(cr, uid) - if field not in EXCLUDED_FIELDS] - - try: - datas = self.pool.get(model).read(cr, uid, [int(id)], fields, context) - except except_orm, e: - return False - datas = datas and datas[0] - if not datas: - return False - else: - datas = pickle.loads(x[2].encode('utf-8')) - if meta: - return (x[0], x[1], datas, pickle.loads(x[4])) - return (x[0], x[1], datas) - keys = [] - res = filter(None, map(lambda x: _result_get(x, keys), result)) - res2 = res[:] - for r in res: - if isinstance(r[2], dict) and r[2].get('type') in ('ir.actions.report.xml','ir.actions.act_window','ir.actions.wizard'): - groups = r[2].get('groups_id') - if groups: - cr.execute('SELECT COUNT(1) FROM res_groups_users_rel WHERE gid IN %s AND uid=%s',(tuple(groups), uid)) - cnt = cr.fetchone()[0] - if not cnt: - res2.remove(r) - if r[1] == 'Menuitem' and not res2: - raise osv.except_osv('Error !','You do not have the permission to perform this operation !!!') - return res2 -ir_values() + """ + assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']" + if key == 'default': + def do_get(model,res_id): + return self.get_defaults(cr, uid, model, condition=key2) + elif key == 'action': + def do_get(model,res_id): + return self.get_actions(cr, uid, action_slot=key2, model=model, res_id=res_id, context=context) + return self._map_legacy_model_list(models, do_get) diff --git a/openerp/addons/base/test/test_ir_values.yml b/openerp/addons/base/test/test_ir_values.yml index b4c08db31ff..da26b9837b5 100644 --- a/openerp/addons/base/test/test_ir_values.yml +++ b/openerp/addons/base/test/test_ir_values.yml @@ -21,5 +21,8 @@ - !python {model: ir.values }: | d = self.get(cr, uid, 'default', False, ['unexisting_model']) + assert len(d) == 1, "Only one default must be returned per field" assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value." assert d[0][2] == 'specific value', "Can't retrieve the created default value." + +# TODO: ADD tests for action bindings too, the other half of ir_values. \ No newline at end of file From 38ddb76960c21af0bf097112abd1eca78316bea1 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 12:15:30 +0200 Subject: [PATCH 02/91] [IMP] ir.values: minor cleanup/view improvements bzr revid: odo@openerp.com-20110902101530-13r328uwhwdpudn5 --- openerp/addons/base/ir/ir.xml | 3 +-- openerp/addons/base/ir/ir_values.py | 17 ++++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index eda044336df..3f9c48b467f 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -42,7 +42,7 @@ - + @@ -63,7 +63,6 @@ - diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index c75831f9c2f..f3faacfd4d4 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -162,10 +162,10 @@ class ir_values(osv.osv): string='Default value or action reference'), 'object': fields.boolean('No value serialization', help="Should be enabled when Type is Action"), 'key': fields.selection([('action','Action'),('default','Default')], - 'Action Type', size=128, select=True, required=True, + 'Type', size=128, select=True, required=True, help="- Action: an action attached to one slot of the given model\n" "- Default: a default value for a model field"), - 'key2' : fields.char('Action Qualifier', size=128, select=True, + 'key2' : fields.char('Qualifier', size=128, select=True, help="For actions, one of the possible action slots: \n" " - client_action_multi\n" " - client_print_multi\n" @@ -174,7 +174,7 @@ class ir_values(osv.osv): "For defaults, an optional condition" ,), 'res_id': fields.integer('Record ID', select=True, - help="Database identifier of the record to which this applies." + help="Database identifier of the record to which this applies. " "0 = for all records"), 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True, help="If set, action binding only applies for this user."), @@ -381,14 +381,9 @@ class ir_values(osv.osv): continue return results.values() - def _map_legacy_model_list(self, model_list, map_fn): + def _map_legacy_model_list(self, model_list, map_fn, merge_results=False): """Apply map_fn to the various models passed, according to legacy way to specify models/records. - - :param model_list: list of models/records reference in the form ``[model,..]`` - or ``[(model,res_id), ...]`` - :param map_fn: the map function to apply - should expect 2 arguments: - ``model`` and ``res_id``. """ assert isinstance(model_list, (list, tuple)), \ "model_list should be in the form [model,..] or [(model,res_id), ..]" @@ -400,7 +395,7 @@ class ir_values(osv.osv): result = map_fn(model, res_id) # some of the functions return one result at a time (tuple or id) # and some return a list of many of them - care for both - if isinstance(result,list): + if merge_results: results.extend(result) else: results.append(result) @@ -440,4 +435,4 @@ class ir_values(osv.osv): elif key == 'action': def do_get(model,res_id): return self.get_actions(cr, uid, action_slot=key2, model=model, res_id=res_id, context=context) - return self._map_legacy_model_list(models, do_get) + return self._map_legacy_model_list(models, do_get, merge_results=True) From e0624cbc954b946bb962fc8da252eb1ffed80cc8 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 14:40:38 +0200 Subject: [PATCH 03/91] [IMP] ir.values: smoke test for action bindings bzr revid: odo@openerp.com-20110902124038-36ip41ccytr2f9vt --- openerp/addons/base/test/test_ir_values.yml | 54 +++++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/test/test_ir_values.yml b/openerp/addons/base/test/test_ir_values.yml index da26b9837b5..a8912e0ad3b 100644 --- a/openerp/addons/base/test/test_ir_values.yml +++ b/openerp/addons/base/test/test_ir_values.yml @@ -2,27 +2,71 @@ Create some default value for some (non-existing) model, for all users. - !python {model: ir.values }: | - self.set(cr, uid, 'default', False, 'my_test_ir_value',['unexisting_model'], 'global value') + self.set(cr, uid, 'default', False, 'my_test_field',['unexisting_model'], 'global value') - Retrieve it. - !python {model: ir.values }: | # d is a list of triple (id, name, value) d = self.get(cr, uid, 'default', False, ['unexisting_model']) - assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value." + assert d[0][1] == 'my_test_field', "Can't retrieve the created default value." assert d[0][2] == 'global value', "Can't retrieve the created default value." - Do it again but for a specific user. - !python {model: ir.values }: | - self.set(cr, uid, 'default', False, 'my_test_ir_value',['unexisting_model'], 'specific value', preserve_user=True) + self.set(cr, uid, 'default', False, 'my_test_field',['unexisting_model'], 'specific value', preserve_user=True) - Retrieve it and check it is the one for the current user. - !python {model: ir.values }: | d = self.get(cr, uid, 'default', False, ['unexisting_model']) assert len(d) == 1, "Only one default must be returned per field" - assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value." + assert d[0][1] == 'my_test_field', "Can't retrieve the created default value." assert d[0][2] == 'specific value', "Can't retrieve the created default value." +- + Create some action bindings for a non-existing model +- + !python {model: ir.values }: | + self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True) + self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action 2', ['unexisting_model'], 'ir.actions.act_window,11', isobject=True) + self.set(cr, uid, 'action', 'client_action_multi', 'Side Wizard', ['unexisting_model'], 'ir.actions.act_window,12', isobject=True) + self.set(cr, uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,2', isobject=True) + self.set(cr, uid, 'action', 'client_action_relate', 'Related Stuff', ['unexisting_model'], 'ir.actions.act_window,14', isobject=True) +- + Replace one action binding to set a new name +- + !python {model: ir.values }: | + self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action New', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True) +- + Retrieve the action bindings and check they're correct +- + !python {model: ir.values }: | + actions = self.get(cr, uid, 'action', 'tree_but_open', ['unexisting_model']) + assert len(actions) == 2, "Mismatching number of bound actions" + #first action + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'OnDblClick Action 2', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 11, 'Bound action does not match definition' + #second action - this ones comes last because it was re-created with a different name + assert len(actions[1]) == 3, "Malformed action definition" + assert actions[1][1] == 'OnDblClick Action New', 'Re-Registering an action should replace it' + assert isinstance(actions[1][2], dict) and actions[1][2]['id'] == 10, 'Bound action does not match definition' -# TODO: ADD tests for action bindings too, the other half of ir_values. \ No newline at end of file + actions = self.get(cr, uid, 'action', 'client_action_multi', ['unexisting_model']) + assert len(actions) == 1, "Mismatching number of bound actions" + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'Side Wizard', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 12, 'Bound action does not match definition' + + actions = self.get(cr, uid, 'action', 'client_print_multi', ['unexisting_model']) + assert len(actions) == 1, "Mismatching number of bound actions" + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'Nice Report', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 2, 'Bound action does not match definition' + + actions = self.get(cr, uid, 'action', 'client_action_relate', ['unexisting_model']) + assert len(actions) == 1, "Mismatching number of bound actions" + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'Related Stuff', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 14, 'Bound action does not match definition' From 32020e13531267301abb0e58711bed70c65ff8e7 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 14:56:48 +0200 Subject: [PATCH 04/91] [FIX] ir.values: re-binding of actions should properly delete the previous one bzr revid: odo@openerp.com-20110902125648-x0cir5m7818h9ba6 --- openerp/addons/base/ir/ir_values.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index f3faacfd4d4..acb0da72159 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -309,7 +309,7 @@ class ir_values(osv.osv): ('key', '=', 'action'), ('key2', '=', action_slot), ('model', '=', model), - ('res_id', '=', res_id), + ('res_id', '=', res_id or 0), # int field -> NULL == 0 ('value', '=', action), ] self.unlink(cr, uid, self.search(cr, uid, search_criteria)) @@ -319,7 +319,7 @@ class ir_values(osv.osv): 'key2': action_slot, 'object': True, 'model': model, - 'res_id': res_id or False, + 'res_id': res_id, 'name': name, 'value': action, }) From d0af2f830c933a5dab635170d45b79a8278b99aa Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 17:14:42 +0200 Subject: [PATCH 05/91] [FIX] ir.values: get_defaults() should cut the condition at 200 chars to match the conditions that were set bzr revid: odo@openerp.com-20110902151442-rqrh62z67g8k921o --- openerp/addons/base/ir/ir_values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index acb0da72159..ca2973f948e 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -271,7 +271,7 @@ class ir_values(osv.osv): query = query % ('AND v.key2 = %s' if condition else '') params = ('default', model, uid, uid) if condition: - params += (condition,) + params += (condition[:200],) cr.execute(query, params) # keep only the highest priority default for each field From 083691b392b1f9556dd64787f46e7536e7d2f7c3 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 5 Sep 2011 14:37:56 +0200 Subject: [PATCH 06/91] [IMP] ir.values: improve security: users can only write to their personal defaults Administrator access is required to set defaults for everybody, as well as to alter the action bindings. bzr revid: odo@openerp.com-20110905123756-oqum5k2pnbyoa11r --- openerp/addons/base/security/base_security.xml | 7 +++++++ openerp/addons/base/security/ir.model.access.csv | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/security/base_security.xml b/openerp/addons/base/security/base_security.xml index 00aca6613d6..fe3a10fdaed 100644 --- a/openerp/addons/base/security/base_security.xml +++ b/openerp/addons/base/security/base_security.xml @@ -65,6 +65,13 @@ [('company_id','child_of',[user.company_id.id])] + + Defaults: alter personal values only + + [('key','=','default'),('user_id','=',user.id)] + + + diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index 8949052f89b..6460c06d1f7 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -38,8 +38,7 @@ "access_ir_ui_view_custom_group_user","ir_ui_view_custom_group_user","model_ir_ui_view_custom",,1,0,0,0 "access_ir_ui_view_custom_group_system","ir_ui_view_custom_group_system","model_ir_ui_view_custom","group_system",1,1,1,1 "access_ir_ui_view_sc_group_user","ir_ui_view_sc group_user","model_ir_ui_view_sc",,1,1,1,1 -"access_ir_values_group_erp_manager","ir_values group_erp_manager","model_ir_values","group_erp_manager",1,1,1,1 -"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,0,1,0 +"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_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0 From e333a2eb2e81763cb532b3cd514a80892f185fa1 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 5 Sep 2011 16:10:58 +0200 Subject: [PATCH 07/91] [IMP] ir.values: improved defaults doc + removed `object` column completely The `object` column actually directly depended on the value of the `key` column, so it can be totally removed with no side-effects. Docstrings updated following review comments. bzr revid: odo@openerp.com-20110905141058-xa01o77l1rto6hg9 --- openerp/addons/base/ir/ir.xml | 4 +- openerp/addons/base/ir/ir_values.py | 73 +++++++++++++++++++---------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 3f9c48b467f..3ab0bb3f167 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -93,7 +93,7 @@ tree,form [('key','=','action')] - {'default_object':1,'default_key':'action'} + {'default_key':'action'} @@ -116,7 +116,7 @@ tree,form [('key','=','default')] - {'default_object':0,'default_key':'default','default_key2':''} + {'default_key':'default','default_key2':''} diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index ca2973f948e..2eb7b97d18a 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -78,8 +78,8 @@ class ir_values(osv.osv): all records). The content of the entry is defined by the ``value`` column, which may either - contain an arbitrary value (for default values - when the ``object`` column - is False), or a reference string defining the action that should be executed. + contain an arbitrary value, or a reference string defining the action that + should be executed. .. rubric:: Usage: default values @@ -102,14 +102,15 @@ class ir_values(osv.osv): def _value_unpickle(self, cursor, user, ids, name, arg, context=None): res = {} - for report in self.browse(cursor, user, ids, context=context): - value = report[name[:-9]] - if not report.object and value: + for record in self.browse(cursor, user, ids, context=context): + value = record[name[:-9]] + if record.key == 'default' and value: + # default values are pickled on the fly try: value = str(pickle.loads(value)) except Exception: pass - res[report.id] = value + res[record.id] = value return res def _value_pickle(self, cursor, user, id, name, value, arg, context=None): @@ -118,7 +119,9 @@ class ir_values(osv.osv): ctx = context.copy() if self.CONCURRENCY_CHECK_FIELD in ctx: del ctx[self.CONCURRENCY_CHECK_FIELD] - if not self.browse(cursor, user, id, context=context).object: + record = self.browse(cursor, user, id, context=context) + if record.key == 'default': + # default values are pickled on the fly value = pickle.dumps(value) self.write(cursor, user, id, {name[:-9]: value}, context=ctx) @@ -136,11 +139,6 @@ class ir_values(osv.osv): 'value': {'value_unpickle': act.type+','+str(act.id)} } - def onchange_key(self, cr, uid, ids, key, context=None): - return { - 'value': {'object': key == 'action'} - } - _columns = { 'name': fields.char('Name', size=128, required=True), 'model': fields.char('Model Name', size=128, select=True, required=True, @@ -160,7 +158,6 @@ class ir_values(osv.osv): 'value_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle, type='text', string='Default value or action reference'), - 'object': fields.boolean('No value serialization', help="Should be enabled when Type is Action"), 'key': fields.selection([('action','Action'),('default','Default')], 'Type', size=128, select=True, required=True, help="- Action: an action attached to one slot of the given model\n" @@ -184,7 +181,6 @@ class ir_values(osv.osv): _defaults = { 'key': 'action', 'key2': 'tree_but_open', - 'object': True, } def _auto_init(self, cr, context=None): @@ -195,7 +191,17 @@ class ir_values(osv.osv): def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False): """Defines a default value for the given model and field_name. Any previous - default will be replaced and lost in the process. + default for the same scope (model, field_name, value, for_all_users, company_id, condition) + will be replaced and lost in the process. + + Defaults can be later retrieved via :meth:`~.get_defaults`, which will return + the highest priority default for any given field. Defaults that are more specific + have a higher priority, in the following order (highest to lowest): + + * specific to user and company + * specific to user only + * specific to company only + * global to everyone :param string model: model name :param string field_name: field name to which the default applies @@ -208,7 +214,13 @@ class ir_values(osv.osv): is passed, the current user's company will be used. :param string condition: optional condition specification that can be used to restrict the applicability of the default values - (e.g. based on another field's value) + (e.g. based on another field's value). This is an + opaque string as far as the API is concerned, but client + stacks typically use single-field conditions in the + form ``'key=stringified_value'``. + (Currently, the condition is trimmed to 200 characters, + so values that share the same first 200 characters always + match) :return: id of the newly created ir.values entry """ if isinstance(value, unicode): @@ -233,7 +245,6 @@ class ir_values(osv.osv): 'name': field_name, 'value': pickle.dumps(value), 'model': model, - 'object': False, 'key': 'default', 'key2': condition and condition[:200], 'user_id': False if for_all_users else uid, @@ -241,18 +252,29 @@ class ir_values(osv.osv): }) def get_defaults(self, cr, uid, model, condition=False): - """Returns any user-defined values registered for the given model. - This is not field-specific, but an optional ``condition`` can be - provided to query for default values that should be applied only - when the given condition is met (usually another field's value). - Defaults that have been set for the user herself will have higher - priority that those that have been set for everyone - (see :meth:`~.set_default`). + """Returns any default values that are defined for the current model and user, + (and match ``condition``, if specified), previously registered via + :meth:`~.set_default`. + + Defaults are global to a model, not field-specific, but an optional + ``condition`` can be provided to restrict matching default values + to those that were defined for the same condition (usually based + on another field's value). + + Default values also have priorities depending on whom they apply + to: only the highest priority value will be returned for any + field. See :meth:`~.set_default` for more details. :param string model: model name :param string condition: optional condition specification that can be used to restrict the applicability of the default values - (e.g. based on another field's value) + (e.g. based on another field's value). This is an + opaque string as far as the API is concerned, but client + stacks typically use single-field conditions in the + form ``'key=stringified_value'``. + (Currently, the condition is trimmed to 200 characters, + so values that share the same first 200 characters always + match) :return: list of default values tuples of the form ``(id, field_name, value)`` (``id`` is the ID of the default entry, usually irrelevant) """ @@ -317,7 +339,6 @@ class ir_values(osv.osv): return self.create(cr, uid, { 'key': 'action', 'key2': action_slot, - 'object': True, 'model': model, 'res_id': res_id, 'name': name, From fbb6e5e56aaf36ff652edbec0181a0fcb95a3601 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 8 Sep 2011 13:38:33 +0200 Subject: [PATCH 08/91] [FIX] ir.values.get_defaults: typo in SQL query - spotted by Naresh, thanks! bzr revid: odo@openerp.com-20110908113833-z2cgkj093940usys --- openerp/addons/base/ir/ir_values.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index 2eb7b97d18a..d908c2096b3 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -284,8 +284,8 @@ class ir_values(osv.osv): LEFT JOIN res_users u ON (v.user_id = u.id) WHERE v.key = %%s AND v.model = %%s AND (v.user_id = %%s OR v.user_id IS NULL) - AND (u.company_id IS NULL OR - u.company_id = + AND (v.company_id IS NULL OR + v.company_id = (SELECT company_id from res_users where id = %%s) ) %s From 8a9c0dbad7323509b07cda605aed84bf51584753 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 13:12:13 +0200 Subject: [PATCH 09/91] [IMP] bulk-update fields_view_get result via dict.update instead of a bunch of __setitem__ bzr revid: xmo@openerp.com-20110915111213-zigij077k9ss9p6e --- openerp/osv/orm.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 7efe2272754..315d2bce0de 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1954,14 +1954,13 @@ class orm_template(object): # if a view was found if sql_res: - result['type'] = sql_res['type'] - result['view_id'] = sql_res['id'] - source = etree.fromstring(encode(sql_res['arch'])) - result['arch'] = apply_view_inheritance(source, result['view_id']) - - result['name'] = sql_res['name'] - result['field_parent'] = sql_res['field_parent'] or False + result.update( + arch=apply_view_inheritance(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) else: # otherwise, build some kind of default view @@ -1994,10 +1993,11 @@ class orm_template(object): else: # what happens here, graph case? raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type) - result['arch'] = etree.fromstring(encode(xml)) - result['name'] = 'default' - result['field_parent'] = False - result['view_id'] = 0 + result.update( + arch=etree.fromstring(encode(xml)), + name='default', + field_parent=False, + view_id=0) if parent_view_model != self._name: ctx = context.copy() From 734c1a43a0e4a208185ab967cc1bf087a2d15e5d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 13:12:21 +0200 Subject: [PATCH 10/91] [REF] extract the two inlined default view generators left into fields_view_get into their own methods follow the preexisting __get_default_$name_view bzr revid: xmo@openerp.com-20110915111221-20ktl1bi2qne5wz7 --- openerp/osv/orm.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 315d2bce0de..960217b4de8 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1689,6 +1689,28 @@ class orm_template(object): raise except_orm('View error', msg) return arch, fields + def __get_default_form_view(self, cr, user, context=None): + # TODO it seems fields_get can be replaced by _all_columns (no need for translation) + res = self.fields_get(cr, user, context=context) + xml = ' ' \ + '
' % (self._description,) + for x in res: + if res[x]['type'] not in ('one2many', 'many2many'): + xml += '' % (x,) + if res[x]['type'] == 'text': + xml += "" + xml += "" + return xml + + def __get_default_tree_view(self, cr, user, context=None): + _rec_name = self._rec_name + if _rec_name not in self._columns: + _rec_name = self._columns.keys()[0] + xml = '' \ + '' \ + % (self._description, _rec_name) + return xml + def __get_default_calendar_view(self): """Generate a default calendar view (For internal use only). """ @@ -1965,24 +1987,10 @@ class orm_template(object): # otherwise, build some kind of default view if view_type == 'form': - # TODO it seems fields_get can be replaced by _all_columns (no need for translation) - res = self.fields_get(cr, user, context=context) - xml = ' ' \ - '
' % (self._description,) - for x in res: - if res[x]['type'] not in ('one2many', 'many2many'): - xml += '' % (x,) - if res[x]['type'] == 'text': - xml += "" - xml += "" + xml = self.__get_default_form_view(cr, user, context) elif view_type == 'tree': - _rec_name = self._rec_name - if _rec_name not in self._columns: - _rec_name = self._columns.keys()[0] - xml = '' \ - '' \ - % (self._description, _rec_name) + xml = self.__get_default_tree_view(cr, user, context) elif view_type == 'calendar': xml = self.__get_default_calendar_view() From ebb2072207c23fe1ad12310207a72ac27e2a754e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 13:12:55 +0200 Subject: [PATCH 11/91] [IMP] regroup all __get_default_$name_view calls into a single parametric getattr Also rename __get_default_ to _get_default_ so it can be found by getattr bzr revid: xmo@openerp.com-20110915111255-1i0y555er3nwtkzg --- openerp/osv/orm.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 960217b4de8..773feb5808d 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1689,7 +1689,7 @@ class orm_template(object): raise except_orm('View error', msg) return arch, fields - def __get_default_form_view(self, cr, user, context=None): + def _get_default_form_view(self, cr, user, context=None): # TODO it seems fields_get can be replaced by _all_columns (no need for translation) res = self.fields_get(cr, user, context=context) xml = ' ' \ @@ -1702,7 +1702,7 @@ class orm_template(object): xml += "" return xml - def __get_default_tree_view(self, cr, user, context=None): + def _get_default_tree_view(self, cr, user, context=None): _rec_name = self._rec_name if _rec_name not in self._columns: _rec_name = self._columns.keys()[0] @@ -1711,7 +1711,7 @@ class orm_template(object): % (self._description, _rec_name) return xml - def __get_default_calendar_view(self): + def _get_default_calendar_view(self, cr, user, context=None): """Generate a default calendar view (For internal use only). """ # TODO could return an etree instead of a string @@ -1758,7 +1758,7 @@ class orm_template(object): return arch - def __get_default_search_view(self, cr, uid, context=None): + def _get_default_search_view(self, cr, uid, context=None): form_view = self.fields_view_get(cr, uid, False, 'form', context=context) tree_view = self.fields_view_get(cr, uid, False, 'tree', context=context) @@ -1984,23 +1984,14 @@ class orm_template(object): name=sql_res['name'], field_parent=sql_res['field_parent'] or False) else: - # otherwise, build some kind of default view - if view_type == 'form': - xml = self.__get_default_form_view(cr, user, context) - - elif view_type == 'tree': - xml = self.__get_default_tree_view(cr, user, context) - - elif view_type == 'calendar': - xml = self.__get_default_calendar_view() - - elif view_type == 'search': - xml = self.__get_default_search_view(cr, user, context) - - else: + try: + xml = getattr(self, '_get_default_%s_view' % view_type)( + cr, user, context) + 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) + result.update( arch=etree.fromstring(encode(xml)), name='default', From 005e2b87a57bba82cec4b73b26ce8af6392ffc4d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:04:29 +0200 Subject: [PATCH 12/91] [IMP] etree-ify default view generations * Search already used etree internally, remove serialization to string * form and tree were easy to convert * pull down fvg's parsing (etree.fromstring) into calendar generator before converting it bzr revid: xmo@openerp.com-20110915120429-syz190w61iq52rel --- openerp/osv/orm.py | 70 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 773feb5808d..c60c440af24 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1690,32 +1690,53 @@ class orm_template(object): 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. + + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: a form view as an lxml document + :rtype: etree._Element + """ + view = etree.Element('form', string=self._description) # TODO it seems fields_get can be replaced by _all_columns (no need for translation) - res = self.fields_get(cr, user, context=context) - xml = ' ' \ - '
' % (self._description,) - for x in res: - if res[x]['type'] not in ('one2many', 'many2many'): - xml += '' % (x,) - if res[x]['type'] == 'text': - xml += "" - xml += "" - return xml + for field, descriptor in self.fields_get(cr, user, context=context).iteritems(): + if descriptor['type'] in ('one2many', 'many2many'): + continue + etree.SubElement(view, 'field', name=field) + if descriptor['type'] == 'text': + etree.SubElement(view, 'newline') + return view def _get_default_tree_view(self, cr, user, context=None): + """ Generates a single-field tree view, using _rec_name if + it's one of the columns or the first column it finds otherwise + + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: a tree view as an lxml document + :rtype: etree._Element + """ _rec_name = self._rec_name if _rec_name not in self._columns: _rec_name = self._columns.keys()[0] - xml = '' \ - '' \ - % (self._description, _rec_name) - return xml + + view = etree.Element('tree', string=self._description) + etree.SubElement(view, 'field', name=_rec_name) + return view def _get_default_calendar_view(self, cr, user, context=None): - """Generate a default calendar view (For internal use only). + """ Generates a default calendar view by trying to infer + calendar fields from a number of pre-set attribute names + + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: a calendar view + :rtype: etree._Element """ - # TODO could return an etree instead of a string - arch = ('\n' '\n' '') % (self._rec_name) - return arch + return etree.fromstring(arch.encode('utf-8')) def _get_default_search_view(self, cr, uid, context=None): + """ + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: an lxml document of the view + :rtype: etree._Element + """ form_view = self.fields_view_get(cr, uid, False, 'form', context=context) tree_view = self.fields_view_get(cr, uid, False, 'tree', context=context) @@ -1783,7 +1811,7 @@ class orm_template(object): field_group.append(etree.Element("field", attrib={'name': field_name})) #TODO tostring can be removed as fromstring is call directly after... - return etree.tostring(search_view, encoding="utf-8").replace('\t', '') + return search_view # # if view_id, view_type is not required @@ -1986,14 +2014,14 @@ class orm_template(object): else: # otherwise, build some kind of default view try: - xml = getattr(self, '_get_default_%s_view' % view_type)( + view = getattr(self, '_get_default_%s_view' % view_type)( cr, user, context) 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) result.update( - arch=etree.fromstring(encode(xml)), + arch=view, name='default', field_parent=False, view_id=0) From 4cdda26b6c959d2c6c7323dd9e173f352e83c788 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:11:29 +0200 Subject: [PATCH 13/91] [IMP] convert default calendar view generation (from fields_view_get) to lxml.etree bzr revid: xmo@openerp.com-20110915121129-fcdq8e0rix2ocpwt --- openerp/osv/orm.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index c60c440af24..e8556dbc9d4 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1737,8 +1737,8 @@ class orm_template(object): :returns: a calendar view :rtype: etree._Element """ - arch = ('\n' - '\n' - ' \n' - '') % (self._rec_name) - - return etree.fromstring(arch.encode('utf-8')) + return view def _get_default_search_view(self, cr, uid, context=None): """ From 74e98f422043bec3b2b10067b1e5665eeb9f9aff Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:15:44 +0200 Subject: [PATCH 14/91] [ADD] error when neither date_stop nor date_delay can be generated for a default calendar view bzr revid: xmo@openerp.com-20110915121544-eq3mumulb0qp9yfa --- openerp/osv/orm.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index e8556dbc9d4..1d9fb9ccfa0 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1761,12 +1761,15 @@ class orm_template(object): if dt_stop in self._columns: view.set('date_stop', dt_stop) break - - if not view.get('date_stop'): + if 'date_stop' not in view.attrib: for dt_delay in ["date_delay", "planned_hours", "x_date_delay", "x_planned_hours"]: if dt_delay in self._columns: view.set('date_delay', dt_delay) break + if 'date_delay' not in view.attrib: + raise except_orm( + _('Invalid Object Architecture!'), + _("Insufficient fields to generate a Calendar View for %s, missing a date_stop or a date_delay" % (self._name))) return view From e878560c7450f4f3bbf7b43d93aa04214218a07a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:24:43 +0200 Subject: [PATCH 15/91] [ADD] small helper to calendar generation code, in order to make flow clearer bzr revid: xmo@openerp.com-20110915122443-hvk1u2372ppd77m1 --- openerp/osv/orm.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 1d9fb9ccfa0..c0ca7e9d696 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1737,6 +1737,19 @@ class orm_template(object): :returns: a calendar view :rtype: etree._Element """ + def set_first_of(seq, in_, to): + """Sets the first value of ``set`` also found in ``in`` to + the ``to`` attribute of the view being closed over. + + Returns whether it's found a suitable value (and set it on + the attribute) or not + """ + for item in seq: + if item in in_: + view.set(key, item) + return True + return False + view = etree.Element('calendar', string=self._description) etree.SubElement(view, 'field', name=self._rec_name) @@ -1752,21 +1765,13 @@ class orm_template(object): raise except_orm(_('Invalid Object Architecture!'), _("Insufficient fields for Calendar View!")) view.set('date_start', self._date_name) - for color in ["user_id", "partner_id", "x_user_id", "x_partner_id"]: - if color in self._columns: - view.set('color', color) - break + set_first_of(["user_id", "partner_id", "x_user_id", "x_partner_id"], + self._columns, 'color') - for dt_stop in ["date_stop", "date_end", "x_date_stop", "x_date_end"]: - if dt_stop in self._columns: - view.set('date_stop', dt_stop) - break - if 'date_stop' not in view.attrib: - for dt_delay in ["date_delay", "planned_hours", "x_date_delay", "x_planned_hours"]: - if dt_delay in self._columns: - view.set('date_delay', dt_delay) - break - if 'date_delay' not in view.attrib: + if not set_first_of(["date_stop", "date_end", "x_date_stop", "x_date_end"], + self._columns, 'date_stop'): + if not set_first_of(["date_delay", "planned_hours", "x_date_delay", "x_planned_hours"], + self._columns, 'date_delay'): raise except_orm( _('Invalid Object Architecture!'), _("Insufficient fields to generate a Calendar View for %s, missing a date_stop or a date_delay" % (self._name))) From eeb31efa882408be3bbdb98afb928bd8a297edb7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:28:34 +0200 Subject: [PATCH 16/91] [IMP] automatic search view generation: comprehensions are awesome bzr revid: xmo@openerp.com-20110915122834-0ec4dkaznbhtc0kc --- openerp/osv/orm.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index c0ca7e9d696..48b607273de 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1789,12 +1789,11 @@ class orm_template(object): form_view = self.fields_view_get(cr, uid, False, 'form', context=context) tree_view = self.fields_view_get(cr, uid, False, 'tree', context=context) - fields_to_search = set() # TODO it seems _all_columns could be used instead of fields_get (no need for translated fields info) fields = self.fields_get(cr, uid, context=context) - for field in fields: - if fields[field].get('select'): - fields_to_search.add(field) + fields_to_search = set( + field for field, descriptor in fields.iteritems() + if descriptor.get('select')) for view in (form_view, tree_view): view_root = etree.fromstring(view['arch']) # Only care about select=1 in xpath below, because select=2 is covered From 7bb856b834b6ba30f406459d0b36b34dcd351973 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:31:55 +0200 Subject: [PATCH 17/91] [IMP] search view generation: update fields set in place instead of creating a new set via union bzr revid: xmo@openerp.com-20110915123155-a5ht8nhfp9kq0kst --- openerp/osv/orm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 48b607273de..625247d464f 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1794,11 +1794,12 @@ class orm_template(object): fields_to_search = set( field for field, descriptor in fields.iteritems() if descriptor.get('select')) + for view in (form_view, tree_view): view_root = etree.fromstring(view['arch']) # Only care about select=1 in xpath below, because select=2 is covered # by the custom advanced search in clients - fields_to_search = fields_to_search.union(view_root.xpath("//field[@select=1]/@name")) + fields_to_search.update(view_root.xpath("//field[@select=1]/@name")) tree_view_root = view_root # as provided by loop above search_view = etree.Element("search", attrib={'string': tree_view_root.get("string", "")}) From a09be0e9b94626d45db20272ba9ecab469aa8c66 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:37:07 +0200 Subject: [PATCH 18/91] [IMP] simplify search view generation: rely on etree.SubElement to graft new nodes to the search view bzr revid: xmo@openerp.com-20110915123707-901mkfkz8e6ikre4 --- openerp/osv/orm.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 625247d464f..8070d56d9e4 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1802,14 +1802,12 @@ class orm_template(object): fields_to_search.update(view_root.xpath("//field[@select=1]/@name")) tree_view_root = view_root # as provided by loop above - search_view = etree.Element("search", attrib={'string': tree_view_root.get("string", "")}) - field_group = etree.Element("group") - search_view.append(field_group) + search_view = etree.Element("search", string=tree_view_root.get("string", "")) + field_group = etree.SubElement(search_view, "group") for field_name in fields_to_search: - field_group.append(etree.Element("field", attrib={'name': field_name})) + etree.SubElement(field_group, "field", name=field_name)) - #TODO tostring can be removed as fromstring is call directly after... return search_view # From 662c8842f4eef498feb6711c87a3fdd2c75a3611 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:47:41 +0200 Subject: [PATCH 19/91] [IMP] fields_view_get: listcomps are still awesome also use itertools.chain to iterate over multiple lists in sequence, instead of concatenating them bzr revid: xmo@openerp.com-20110915124741-v5uneqmt2c3v6cmp --- openerp/osv/orm.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 8070d56d9e4..c08fe1e2357 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -44,6 +44,7 @@ import calendar import copy import datetime +import itertools import logging import warnings import operator @@ -2052,13 +2053,11 @@ class orm_template(object): resrelate = ir_values_obj.get(cr, user, 'action', 'client_action_relate', [(self._name, False)], False, context) - resprint = map(clean, resprint) - resaction = map(clean, resaction) - resaction = filter(lambda x: not x.get('multi', False), resaction) - resprint = filter(lambda x: not x.get('multi', False), resprint) + resaction = [clean(action) for action in resaction if not action.get('multi')] + resprint = [clean(print_) for print_ in resprint if not print_.get('multi')] resrelate = map(lambda x: x[2], resrelate) - for x in resprint + resaction + resrelate: + for x in itertools.chain(resprint, resaction, resrelate): x['string'] = x['name'] result['toolbar'] = { From d57135eaefa4ff9bc0e9c80838a2a3b163e92f4b Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Fri, 16 Sep 2011 16:02:52 +0200 Subject: [PATCH 20/91] [IMP] fetchmail: Add a new type for the incoming server and generate a zip file with the script of the mailgateway and the right configuration bzr revid: stw@openerp.com-20110916140252-movu1cxjubt0bc44 --- addons/fetchmail/fetchmail.py | 63 +++++++++++++++++++++++++++-- addons/fetchmail/fetchmail_view.xml | 16 ++++++-- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/addons/fetchmail/fetchmail.py b/addons/fetchmail/fetchmail.py index b7460965c3b..1df9ba998b1 100644 --- a/addons/fetchmail/fetchmail.py +++ b/addons/fetchmail/fetchmail.py @@ -25,6 +25,14 @@ from imaplib import IMAP4 from imaplib import IMAP4_SSL from poplib import POP3 from poplib import POP3_SSL +try: + import cStringIO as StringIO +except ImportError: + import StringIO + +import zipfile +import base64 +import addons import netsvc from osv import osv, fields @@ -46,11 +54,12 @@ class fetchmail_server(osv.osv): ('draft', 'Not Confirmed'), ('done', 'Confirmed'), ], 'State', select=True, readonly=True), - 'server' : fields.char('Server Name', size=256, required=True, readonly=True, help="Hostname or IP of the mail server", states={'draft':[('readonly', False)]}), - 'port' : fields.integer('Port', required=True, readonly=True, states={'draft':[('readonly', False)]}), + 'server' : fields.char('Server Name', size=256, readonly=True, help="Hostname or IP of the mail server", states={'draft':[('readonly', False)]}), + 'port' : fields.integer('Port', readonly=True, states={'draft':[('readonly', False)]}), 'type':fields.selection([ ('pop', 'POP Server'), ('imap', 'IMAP Server'), + ('private_mta', 'Private Mail Server'), ], 'Server Type', select=True, required=True, readonly=False), 'is_ssl':fields.boolean('SSL/TLS', help="Connections are encrypted with SSL/TLS through a dedicated port (default: IMAPS=993, POP3S=995)"), 'attach':fields.boolean('Keep Attachments', help="Whether attachments should be downloaded. " @@ -58,7 +67,8 @@ class fetchmail_server(osv.osv): 'original':fields.boolean('Keep Original', help="Whether a full original copy of each email should be kept for reference" "and attached to each processed message. This will usually double the size of your message database."), 'date': fields.datetime('Last Fetch Date', readonly=True), - 'user' : fields.char('Username', size=256, required=True, readonly=True, states={'draft':[('readonly', False)]}), + 'user' : fields.char('Username', size=256, readonly=True, states={'draft':[('readonly', False)]}), + 'user_id' : fields.many2one('res.users', 'User', states={'draft' : [('readonly', False)]}), 'password' : fields.char('Password', size=1024, required=True, readonly=True, states={'draft':[('readonly', False)]}), 'note': fields.text('Description'), 'action_id':fields.many2one('ir.actions.server', 'Server Action', help="Optional custom server action to trigger for each incoming mail, " @@ -70,6 +80,9 @@ class fetchmail_server(osv.osv): 'priority': fields.integer('Server Priority', readonly=True, states={'draft':[('readonly', False)]}, help="Defines the order of processing, " "lower values mean higher priority"), 'message_ids': fields.one2many('mail.message', 'fetchmail_server_id', 'Messages', readonly=True), + 'configuration' : fields.text('Configuration', readonly=True), + 'archive' : fields.binary('Archive', readonly=True), + 'archive_filename' : fields.char('Filename', size=128, readonly=True), } _defaults = { 'state': "draft", @@ -78,13 +91,55 @@ class fetchmail_server(osv.osv): 'attach': True, } + def generate_configuration(self, cr, uid, ids, context=None): + this = self.browse(cr, uid, ids[0], context=context) + + configuration = """You should use this command line with your Mail Transport Agent (MTA) +openerp_mailgate.py -u %(user_id)d -p PASSWORD -o %(model)s -d %(database)s --host=HOSTNAME --port=PORT + """ % {'user_id' : this.user_id.id, + 'model' : this.object_id.model, + 'database' : cr.dbname, + } + + values = dict( + configuration=configuration, + ) + + try: + archive_io = StringIO.StringIO() + archive = zipfile.ZipFile(archive_io, 'w') + content = open( + addons.get_module_resource('mail', 'scripts', 'openerp_mailgate', 'openerp_mailgate.py'), + 'rb' + ).read() + archive.writestr('openerp_mailgate.py', content) + archive.writestr('README', configuration) + archive_io.seek(0) + + values.update( + archive_filename = 'openerp-mailgate.zip', + archive = base64.encodestring(archive_io.getvalue()) + ) + + finally: + archive.close() + archive_io.close() + + return this.write(values) + def onchange_server_type(self, cr, uid, ids, server_type=False, ssl=False): port = 0 + values = {} if server_type == 'pop': port = ssl and 995 or 110 elif server_type == 'imap': port = ssl and 993 or 143 - return {'value':{'port':port}} + else: + values['server'] = '' + + values['port'] = port + + return {'value':values} def set_draft(self, cr, uid, ids, context=None): self.write(cr, uid, ids , {'state':'draft'}) diff --git a/addons/fetchmail/fetchmail_view.xml b/addons/fetchmail/fetchmail_view.xml index 05f62f4b9fb..ceb28284a6c 100644 --- a/addons/fetchmail/fetchmail_view.xml +++ b/addons/fetchmail/fetchmail_view.xml @@ -32,15 +32,16 @@ - + - - + + - + + @@ -50,6 +51,13 @@ + + + +