Merge lp:~openerp-community/openobject-server/trunk-property-fields-using-get into lp:openobject-server

Proposed by qdp (OpenERP)
Status: Merged
Merged at revision: 3507
Proposed branch: lp:~openerp-community/openobject-server/trunk-property-fields-using-get
Merge into: lp:openobject-server
Diff against target: 162 lines (+64/-53)
2 files modified
openerp/osv/fields.py (+49/-53)
openerp/tools/misc.py (+15/-0)
To merge this branch: bzr merge lp:~openerp-community/openobject-server/trunk-property-fields-using-get
Reviewer Review Type Date Requested Status
Olivier Dony (Odoo) Approve
qdp (OpenERP) Needs Resubmitting
Review via email: mp+67223@code.launchpad.net

Description of the change

Enclosed, an improvement that targets to use the get() function of ir.property object when reading a property field (using a browse). Thanks to that, it will now be possible to force the company to use for property fields when processing flows.

While developing that feature, we had to clean and refactor the code of fnct_read and _get_defaults functions that were doing pretty nasty things to compute name_get on all the needed ids.

We also have added a function in tools (maybe that's not the perfect place?) that will factorize the code later on, on addons side. Check the example of use bellow for further information:
- .. for line in self.browse(cr, uid, ids, context=context):
+ .. set_company = tools.get_and_sort_by_field(cr, uid, obj=self, ids=ids, field='company_id', context=context)
+ .. for company_id, sol_ids in set_company.items():
+ ......ctx = context.copy()
+ ......ctx.update({'force_company': company_id})
+ ......for line in self.browse(cr, uid, sol_ids, context=ctx):

Thanks,

To post a comment you must log in.
Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :

Hello,

Thanks for the improved properties management!

Some preliminary remarks (line numbers are line numbers of the current diff):

- When adding new docstrings, we try to match the standard RST docstring format as much as possible, as we are trying to globally improve and unify our docstrings formats (therefore it is quite important in merge props). Example:

  def squares(limit):
      """Computes the square of the first positive ints, up to ``limit``.

         :param int limit: max integer to compute (excluded)
         :return: map of integers to their square
         :rtype: dict
      """
      return dict([(x, x**2) for x in xrange(limit)])

- l.40 of the diff, in _get_defaults, what is the weird loop? That's always a one-item list, so there is no reason for looping at all, nor for making the list in the first place?

- in _fnct_read(), the test `isinstance(x, orm.browse_record)` is repeated many times within the inner loops, while it should only be in the outer loop, and just once. It also seems incorrect most of the times, as it tests the default value type, but the final value is the one that is used. What if there is no default but a real value is indeed set?
Looks like this should instead be based on the property's 'type' column, shouldn't it?
The inner loops could be quite simplified too.

- l.108 in _fnct_read(): name_get() must explicitly be called as UID 1, because it is quite possible for the target record to be a record the user is not allowed to access, and seeing the name_get() is still allowed (the permission of the parent object is the one that counts). Didn't you notice the explicit comment about that in the code? ;-)

- similarly, the code had an explicit check for the existence of the target records of m2o property values, because there is not integrity management for properties done by the database. Removing that code will certainly cause regressions, unless you have taken care of it somewhere else? BTW this test must be done as UID 1 for the same reason as above. Didn't you notice the explicit comment about that stuff in the code? ;-)

- about the utility function get_and_sort_by_field(): it does not strike me as a frequent use case, so perhaps it's better to make a specific function that gives the ids of records grouped by company_id? It could be based on this generic function, but perhaps the purpose would be more clear...
  + BTW, was naming a dict value "key" a sort of joke? (l.136) ;-)
  + l.139 will not work as "set_field" is not defined, plus these 3 lines should be replaced by:
       res.setdefault(key,[]).append(item['id'])

A more complete review will probably follow with some real testing of the code.

Thanks!

-- odo, volunteer framework reviewing monkey

review: Needs Fixing
Revision history for this message
qdp (OpenERP) (qdp) wrote :

woops,

i've nothing to add nor any excuse. I'd better hide away... but i need to ask for a second review. How fool i am!

Thanks for your patience,
--qdp, "Monkey coding" addict against his will

review: Needs Resubmitting
Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :

After the last set of changes, I think can be merged :-)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openerp/osv/fields.py'
2--- openerp/osv/fields.py 2011-07-05 08:53:22 +0000
3+++ openerp/osv/fields.py 2011-07-12 08:38:26 +0000
4@@ -1212,34 +1212,30 @@
5 self._symbol_get = self._deserialize_func
6 super(serialized, self).__init__(string=string, **args)
7
8-
9 # TODO: review completly this class for speed improvement
10 class property(function):
11
12 def _get_default(self, obj, cr, uid, prop_name, context=None):
13- return self._get_defaults(obj, cr, uid, [prop_name], context=None)[0][prop_name]
14-
15- def _get_defaults(self, obj, cr, uid, prop_name, context=None):
16+ return self._get_defaults(obj, cr, uid, [prop_name], context=None)[prop_name]
17+
18+ def _get_defaults(self, obj, cr, uid, prop_names, context=None):
19+ """Get the default values for ``prop_names´´ property fields (result of ir.property.get() function for res_id = False).
20+
21+ :param list of string prop_names: list of name of property fields for those we want the default value
22+ :return: map of property field names to their default value
23+ :rtype: dict
24+ """
25 prop = obj.pool.get('ir.property')
26- domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name), ('res_id','=',False)]
27- ids = prop.search(cr, uid, domain, context=context)
28- replaces = {}
29- default_value = {}.fromkeys(prop_name, False)
30- for prop_rec in prop.browse(cr, uid, ids, context=context):
31- if default_value.get(prop_rec.fields_id.name, False):
32- continue
33- value = prop.get_by_record(cr, uid, prop_rec, context=context) or False
34- default_value[prop_rec.fields_id.name] = value
35- if value and (prop_rec.type == 'many2one'):
36- replaces.setdefault(value._name, {})
37- replaces[value._name][value.id] = True
38- return default_value, replaces
39+ res = {}
40+ for prop_name in prop_names:
41+ res[prop_name] = prop.get(cr, uid, prop_name, obj._name, context=context)
42+ return res
43
44 def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
45 prop = obj.pool.get('ir.property')
46 vids = [obj._name + ',' + str(oid) for oid in ids]
47
48- domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)]
49+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name', 'in', prop_name)]
50 #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
51 if vids:
52 domain = [('res_id', 'in', vids)] + domain
53@@ -1274,48 +1270,48 @@
54 }, context=context)
55 return False
56
57-
58- def _fnct_read(self, obj, cr, uid, ids, prop_name, obj_dest, context=None):
59- properties = obj.pool.get('ir.property')
60- domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)]
61- domain += [('res_id','in', [obj._name + ',' + str(oid) for oid in ids])]
62- nids = properties.search(cr, uid, domain, context=context)
63- default_val,replaces = self._get_defaults(obj, cr, uid, prop_name, context)
64-
65+ def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
66+ prop = obj.pool.get('ir.property')
67+ # get the default values (for res_id = False) for the property fields
68+ default_val = self._get_defaults(obj, cr, uid, prop_names, context)
69+
70+ # build the dictionary that will be returned
71 res = {}
72 for id in ids:
73 res[id] = default_val.copy()
74
75- brs = properties.browse(cr, uid, nids, context=context)
76- for prop in brs:
77- value = properties.get_by_record(cr, uid, prop, context=context)
78- res[prop.res_id.id][prop.fields_id.name] = value or False
79- if value and (prop.type == 'many2one'):
80- # check existence as root, as seeing the name of a related
81- # object depends on access right of source document,
82- # not target, so user may not have access.
83- record_exists = obj.pool.get(value._name).exists(cr, 1, value.id)
84- if record_exists:
85- replaces.setdefault(value._name, {})
86- replaces[value._name][value.id] = True
87- else:
88- res[prop.res_id.id][prop.fields_id.name] = False
89-
90- for rep in replaces:
91- # search+name_get as root, as seeing the name of a related
92- # object depends on access right of source document,
93- # not target, so user may not have access.
94- nids = obj.pool.get(rep).search(cr, 1, [('id','in',replaces[rep].keys())], context=context)
95- replaces[rep] = dict(obj.pool.get(rep).name_get(cr, 1, nids, context=context))
96-
97- for prop in prop_name:
98+ for prop_name in prop_names:
99+ property_field = obj._all_columns.get(prop_name).column
100+ property_destination_obj = property_field._obj if property_field._type == 'many2one' else False
101+ # If the property field is a m2o field, we will append the id of the value to name_get_ids
102+ # in order to make a name_get in batch for all the ids needed.
103+ name_get_ids = {}
104 for id in ids:
105- if res[id][prop] and hasattr(res[id][prop], '_name'):
106- res[id][prop] = (res[id][prop].id , replaces[res[id][prop]._name].get(res[id][prop].id, False))
107-
108+ # get the result of ir.property.get() for this res_id and save it in res if it's existing
109+ obj_reference = obj._name + ',' + str(id)
110+ value = prop.get(cr, uid, prop_name, obj._name, res_id=obj_reference, context=context)
111+ if value:
112+ res[id][prop_name] = value
113+ # Check existence as root (as seeing the name of a related
114+ # object depends on access right of source document,
115+ # not target, so user may not have access) in order to avoid
116+ # pointing on an unexisting record.
117+ if property_destination_obj:
118+ if res[id][prop_name] and obj.pool.get(property_destination_obj).exists(cr, 1, res[id][prop_name].id):
119+ name_get_ids[id] = res[id][prop_name].id
120+ else:
121+ res[id][prop_name] = False
122+ if property_destination_obj:
123+ # name_get as root (as seeing the name of a related
124+ # object depends on access right of source document,
125+ # not target, so user may not have access.)
126+ name_get_values = dict(obj.pool.get(property_destination_obj).name_get(cr, 1, name_get_ids.values(), context=context))
127+ # the property field is a m2o, we need to return a tuple with (id, name)
128+ for k, v in name_get_ids.iteritems():
129+ if res[k][prop_name]:
130+ res[k][prop_name] = (v , name_get_values.get(v))
131 return res
132
133-
134 def _field_get(self, cr, uid, model_name, prop):
135 if not self.field_id.get(cr.dbname):
136 cr.execute('SELECT id \
137
138=== modified file 'openerp/tools/misc.py'
139--- openerp/tools/misc.py 2011-06-23 09:03:57 +0000
140+++ openerp/tools/misc.py 2011-07-12 08:38:26 +0000
141@@ -1387,6 +1387,21 @@
142 a.start()
143 return True
144
145+def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
146+ """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
147+
148+ :param string field: name of the field we want to read and group by
149+ :return: mapping of field values to the list of ids that have it
150+ :rtype: dict
151+ """
152+ res = {}
153+ for record in obj.read(cr, uid, ids, [field], context=context):
154+ key = record[field]
155+ res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
156+ return res
157+
158+def get_and_group_by_company(cr, uid, obj, ids, context=None):
159+ return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
160
161 # port of python 2.6's attrgetter with support for dotted notation
162 def resolve_attr(obj, attr):