Merge lp:~openerp-dev/openobject-server/trunk-import-fixes-xmo into lp:openobject-server

Proposed by Xavier (Open ERP)
Status: Rejected
Rejected by: Antony Lesuisse (OpenERP)
Proposed branch: lp:~openerp-dev/openobject-server/trunk-import-fixes-xmo
Merge into: lp:openobject-server
Diff against target: 389 lines (+269/-38)
2 files modified
openerp/osv/orm.py (+62/-38)
openerp/tests/test_import.py (+207/-0)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-import-fixes-xmo
Reviewer Review Type Date Requested Status
Aline (OpenERP) Pending
OpenERP Core Team Pending
Review via email: mp+84447@code.launchpad.net

Description of the change

Fixes to import_data:
* Added some tests to check behavior
* Improved doc and naming
* Fixed handling of non-id o2m subfields not preceded by an id-type field for the same o2m (would get treated as "names" and blow up if "name_search" call failed)

To post a comment you must log in.
3853. By Xavier (Open ERP)

[MERGE] from trunk

Unmerged revisions

3853. By Xavier (Open ERP)

[MERGE] from trunk

3852. By Xavier (Open ERP)

[ADD] documentary note about one of the probably numerous UBs in import_data

3851. By Xavier (Open ERP)

[FIX] special case for handling o2m fields without sub-fields

3850. By Xavier (Open ERP)

[ADD] test of updating an existing o2m (linked by db id)

3849. By Xavier (Open ERP)

[IMP] clarify process_liness code for o2m

3848. By Xavier (Open ERP)

[ADD] o2m import testing, from empty db, fix code which blows up in that case

3847. By Xavier (Open ERP)

[ADD] basics for correctly testing import

3846. By Xavier (Open ERP)

[MERGE] trunk improvements

3845. By Xavier (Open ERP)

[REM] redundant check (that line[i] be non-empty is checked at the start of the iteration)

also moved res assignment (to a falsy value) closer to where it's actually needed

3844. By Xavier (Open ERP)

[FIX] variable naming

datas is a brazilian municipality, not an english word

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openerp/osv/orm.py'
2--- openerp/osv/orm.py 2011-12-01 12:15:03 +0000
3+++ openerp/osv/orm.py 2011-12-15 12:52:47 +0000
4@@ -1172,7 +1172,7 @@
5 datas += self.__export_row(cr, uid, row, fields_to_export, context)
6 return {'datas': datas}
7
8- def import_data(self, cr, uid, fields, datas, mode='init', current_module='', noupdate=False, context=None, filename=None):
9+ def import_data(self, cr, uid, fields, data, mode='init', current_module='', noupdate=False, context=None, filename=None):
10 """Import given data in given module
11
12 This method is used when importing data via client menu.
13@@ -1240,26 +1240,40 @@
14 id = ids[0][0]
15 return id
16
17- # IN:
18- # datas: a list of records, each record is defined by a list of values
19- # prefix: a list of prefix fields ['line_ids']
20- # position: the line to process, skip is False if it's the first line of the current record
21- # OUT:
22- # (res, position, warning, res_id) with
23- # res: the record for the next line to process (including it's one2many)
24- # position: the new position for the next line
25- # res_id: the ID of the record if it's a modification
26- def process_liness(self, datas, prefix, current_module, model_name, fields_def, position=0, skip=0):
27- line = datas[position]
28- row = {}
29+ def process_liness(self, rows, prefix, current_module, model_name, fields_def, position=0, skip=0):
30+ """ Processes a row of data to transform it into a record
31+ importable into OpenERP proper (via (ir.model.data)._update)
32+
33+ :param rows: record data to import
34+ :type rows: list[list[str]]
35+ :param list[str] prefix: list of prefix fields
36+ :param str current_module:
37+ :param str model_name:
38+ :param dict[dict] fields_def: fields definition for the current model
39+ :param int position: index of the current row to import
40+ :param int skip:
41+ :returns: 5-tuple of import information continuation:
42+ #. extracted record for db import
43+ #. the position of the next line to import
44+ #. warning messages generated while importing the row
45+ #. identifier of the record being imported, for update
46+ #. xmlid of the record being imported, for update?
47+ :rtype: (dict, int, list[str], int, str)
48+
49+ .. note::
50+ specifying a name_get (no sub-field) *and* a /.id *and* an
51+ a /id (toplevel or subfield e.g. o2m) is undefined behavior if
52+ they conflict
53+ """
54+ line = rows[position]
55+ record = {}
56 warning = []
57 data_res_id = False
58 xml_id = False
59- nbrmax = position+1
60+ next_row = position+1
61
62 done = {}
63 for i, field in enumerate(fields):
64- res = False
65 if i >= len(line):
66 raise Exception(_('Please check that all your lines have %d columns.'
67 'Stopped around line %d having %d columns.') % \
68@@ -1271,6 +1285,15 @@
69 if line[i] and skip:
70 return False
71 continue
72+
73+ if field == prefix:
74+ # o2m field by name in similarly named column
75+ try:
76+ data_res_id = _get_id(model_name, line[i], current_module, mode=None)
77+ except ValueError:
78+ pass
79+ continue
80+
81 field_name = field[len(prefix)]
82
83 #set the mode for m2o, o2m, m2m : xml_id/id/name
84@@ -1311,25 +1334,27 @@
85 newfd = relation_obj.fields_get( cr, uid, context=context )
86 pos = position
87
88- res = many_ids(line[i], relation, current_module, mode)
89+ res = [] # many_ids(line[i], relation, current_module, mode)
90
91 first = 0
92- while pos < len(datas):
93- res2 = process_liness(self, datas, prefix + [field_name], current_module, relation_obj._name, newfd, pos, first)
94+ while pos < len(rows):
95+ res2 = process_liness(self, rows, prefix + [field_name], current_module, relation_obj._name, newfd, pos, first)
96 if not res2:
97 break
98- (newrow, pos, w2, data_res_id2, xml_id2) = res2
99- nbrmax = max(nbrmax, pos)
100- warning += w2
101+ (o2m_record, pos, additional_warnings, o2m_id, o2m_external_id) = res2
102+ next_row = max(next_row, pos)
103+ warning += additional_warnings
104 first += 1
105
106- if data_res_id2:
107- res.append((4, data_res_id2))
108+ if o2m_id:
109+ res.append((4, o2m_id))
110
111- if (not newrow) or not reduce(lambda x, y: x or y, newrow.values(), 0):
112+ # Found no values to write to linked o2m, bail out
113+ if not (o2m_record and any(o2m_record.itervalues())):
114 break
115
116- res.append( (data_res_id2 and 1 or 0, data_res_id2 or 0, newrow) )
117+ # Create o2m if no matching o2m record, else update it
118+ res.append((1 if o2m_id else 0, o2m_id or False, o2m_record) )
119
120 elif field_type == 'many2one':
121 relation = fields_def[field_name]['relation']
122@@ -1346,23 +1371,22 @@
123 elif field_type == 'float':
124 res = line[i] and float(line[i]) or 0.0
125 elif field_type == 'selection':
126+ res = False
127 for key, val in fields_def[field_name]['selection']:
128 if tools.ustr(line[i]) in [tools.ustr(key), tools.ustr(val)]:
129 res = key
130 break
131- if line[i] and not res:
132+ if not res:
133+ message = _("Key/value '%s' not found in selection field '%s'")
134 logging.getLogger('orm.import').warn(
135- _("key '%s' not found in selection field '%s'"),
136- tools.ustr(line[i]), tools.ustr(field_name))
137- warning.append(_("Key/value '%s' not found in selection field '%s'") % (
138- tools.ustr(line[i]), tools.ustr(field_name)))
139-
140+ message, tools.ustr(line[i]), tools.ustr(field_name))
141+ warning.append(message % (tools.ustr(line[i]), tools.ustr(field_name)))
142 else:
143 res = line[i]
144
145- row[field_name] = res or False
146+ record[field_name] = res or False
147
148- return row, nbrmax, warning, data_res_id, xml_id
149+ return record, next_row, warning, data_res_id, xml_id
150
151 fields_def = self.fields_get(cr, uid, context=context)
152
153@@ -1372,19 +1396,19 @@
154 data = pickle.load(partial_import_file)
155 position = data.get(filename, 0)
156
157- while position<len(datas):
158+ while position < len(data):
159 (res, position, warning, res_id, xml_id) = \
160- process_liness(self, datas, [], current_module, self._name, fields_def, position=position)
161- if len(warning):
162+ process_liness(self, data, [], current_module, self._name, fields_def, position=position)
163+ if warning:
164 cr.rollback()
165- return -1, res, 'Line ' + str(position) +' : ' + '!\n'.join(warning), ''
166+ return -1, res, _("Row %d: %s") % (position, '\n'.join(warning)), ''
167
168 try:
169 ir_model_data_obj._update(cr, uid, self._name,
170 current_module, res, mode=mode, xml_id=xml_id,
171 noupdate=noupdate, res_id=res_id, context=context)
172 except Exception, e:
173- return -1, res, 'Line ' + str(position) + ' : ' + tools.ustr(e), ''
174+ return -1, res, _("Row %d: %s") % (position, tools.ustr(e)), ''
175
176 if config.get('import_partial') and filename and (not (position%100)):
177 with open(config.get('import_partial'), 'rb') as partial_import:
178
179=== added file 'openerp/tests/test_import.py'
180--- openerp/tests/test_import.py 1970-01-01 00:00:00 +0000
181+++ openerp/tests/test_import.py 2011-12-15 12:52:47 +0000
182@@ -0,0 +1,207 @@
183+# -*- coding: utf-8 -*-
184+import collections
185+import unittest2
186+
187+from openerp.osv import orm, fields
188+
189+class Myth(orm.Model):
190+ _name = 'myth'
191+ _columns = {
192+ 'foo': fields.char('Foo', 42),
193+ 'bar': fields.char('Bar', 42)
194+ }
195+
196+class RelatedMyth(orm.Model):
197+ _name = 'myth.related'
198+ _columns = {
199+ 'name': fields.char('Name', 0)
200+ }
201+ def __init__(self, *args):
202+ super(RelatedMyth, self).__init__(*args)
203+ self._records = {}
204+ self._names = {}
205+ def name_search(self, cr, user, name='', *args, **kwargs):
206+ id = self._names.get(name)
207+ if id:
208+ return [(id, name)]
209+ return []
210+ def search(self, cr, uid, predicate, *args, **kwargs):
211+ pred, = predicate
212+ field, op, id = pred
213+ r = self._records.get(id)
214+ if r:
215+ return [r]
216+ return []
217+class RelatingMyth(orm.Model):
218+ _name = 'myth.relating'
219+ _columns = {
220+ 'name': fields.char('name', 0),
221+ 'children': fields.one2many('myth.related', 'parent', 'Children')
222+ }
223+ def _name_search(self, *args, **kwargs):
224+ return []
225+
226+class FakeModel(object):
227+ _inherits = []
228+ def check(self, *args):
229+ return True
230+ def _get_source(self, *args):
231+ return None
232+# should probably be a mock.Mock instance
233+ModelRecord = collections.namedtuple('ModelRecord', 'model record xmlid dbid')
234+class FakeIrModelData(FakeModel):
235+ def record(self):
236+ self.records = []
237+ def _update(self, cr, uid, model, module, values,
238+ xml_id=False, store=True, noupdate=False, mode='init',
239+ res_id=False, context=None):
240+ self.records.append(
241+ ModelRecord(model=model, record=values, xmlid=xml_id, dbid=res_id))
242+
243+class FakePool(object):
244+ _store_function = {}
245+ models = collections.defaultdict(FakeModel)
246+ def __init__(self):
247+ self.models['ir.model.data'] = FakeIrModelData()
248+ def add(self, name, item):
249+ self.models[name] = item
250+ def get(self, name):
251+ return self.models[name]
252+class FakeCursor(object):
253+ def execute(self, stmt, params):
254+ pass
255+ def fetchone(self):
256+ pass
257+
258+class TestBasicImport(unittest2.TestCase):
259+ def setUp(self):
260+ self.pool = FakePool()
261+ self.pool.get('ir.model.data').record()
262+ self.cr = FakeCursor()
263+ self.instance = Myth.create_instance(self.pool, self.cr)
264+
265+ def test_import_nothing(self):
266+ self.assertEqual(
267+ (0, 0, 0, 0),
268+ self.instance.import_data(self.cr, 'uid', ['foo', 'bar'], []))
269+ def test_import_single(self):
270+ self.assertEqual(
271+ (1, 0, 0, 0),
272+ self.instance.import_data(
273+ self.cr, 'uid', ['foo', 'bar'], [['foo', 'bar']]))
274+ self.assertEqual(
275+ [('myth', {'foo': 'foo', 'bar': 'bar'}, False, False)],
276+ self.pool.get('ir.model.data').records)
277+ def test_import_two(self):
278+ self.assertEqual(
279+ (2, 0, 0, 0),
280+ self.instance.import_data(
281+ self.cr, 'uid', ['foo', 'bar'], [
282+ ['foo', 'bar'],
283+ ['qux', 'quux']]))
284+ self.assertEqual(
285+ [('myth', {'foo': 'foo', 'bar': 'bar'}, False, False),
286+ ('myth', {'foo': 'qux', 'bar': 'quux'}, False, False)],
287+ self.pool.get('ir.model.data').records)
288+
289+class TestO2MImportEmpty(unittest2.TestCase):
290+ def setUp(self):
291+ self.pool = FakePool()
292+ self.pool.get('ir.model.data').record()
293+ self.cr = FakeCursor()
294+ self.instance = RelatingMyth.create_instance(self.pool, self.cr)
295+ RelatedMyth.create_instance(self.pool, self.cr)
296+
297+ def test_import_one(self):
298+ self.assertEqual(
299+ (1, 0, 0, 0),
300+ self.instance.import_data(
301+ self.cr, 'uid', ['name', 'children/name'],
302+ [['parent', 'child']]))
303+ self.assertEqual(
304+ [('myth.relating',
305+ {'name': 'parent', 'children': [(0, 0, {'name': 'child'})]},
306+ False, False)],
307+ self.pool.get('ir.model.data').records)
308+
309+ def test_import_multiple_children(self):
310+ self.assertEqual(
311+ (3, 0, 0, 0),
312+ self.instance.import_data(
313+ self.cr, 'uid', ['name', 'children/name'],
314+ [['parent', 'child'],
315+ ['', 'child2'],
316+ ['', 'child3']]))
317+ self.assertEqual(
318+ [('myth.relating',
319+ {'name': 'parent', 'children': [
320+ (0, 0, {'name': 'child'}),
321+ (0, 0, {'name': 'child2'}),
322+ (0, 0, {'name': 'child3'})]},
323+ False, False)],
324+ self.pool.get('ir.model.data').records)
325+
326+ def test_import_multiple_parent(self):
327+ self.assertEqual(
328+ (3, 0, 0, 0),
329+ self.instance.import_data(
330+ self.cr, 'uid', ['name', 'children/name'],
331+ [['parent', 'child'],
332+ ['parent2', 'child2'],
333+ ['parent3', 'child3']]))
334+ self.assertEqual(
335+ [('myth.relating', {'name': 'parent', 'children': [(0, 0, {'name': 'child'})]}, False, False),
336+ ('myth.relating', {'name': 'parent2', 'children': [(0, 0, {'name': 'child2'})]}, False, False),
337+ ('myth.relating', {'name': 'parent3', 'children': [(0, 0, {'name': 'child3'})]}, False, False)
338+ ],
339+ self.pool.get('ir.model.data').records)
340+
341+ def test_import_multiple_everything(self):
342+ self.assertEqual(
343+ (4, 0, 0, 0),
344+ self.instance.import_data(
345+ self.cr, 'uid', ['name', 'children/name'],
346+ [['parent', 'child'],
347+ ['', 'child2'],
348+ ['parent2', 'child3'],
349+ ['', 'child4']]))
350+ self.assertEqual(
351+ [('myth.relating', {'name': 'parent', 'children': [
352+ (0, 0, {'name': 'child'}), (0, 0, {'name': 'child2'})]}, False, False),
353+ ('myth.relating', {'name': 'parent2', 'children': [
354+ (0, 0, {'name': 'child3'}), (0, 0, {'name': 'child4'})]}, False, False)],
355+ self.pool.get('ir.model.data').records)
356+
357+class TestO2MImportExist(unittest2.TestCase):
358+ def setUp(self):
359+ self.pool = FakePool()
360+ self.pool.get('ir.model.data').record()
361+ self.cr = FakeCursor()
362+ self.instance = RelatingMyth.create_instance(self.pool, self.cr)
363+ self.o2m = RelatedMyth.create_instance(self.pool, self.cr)
364+
365+ def test_db_id(self):
366+ self.o2m._records[42] = {'name': 'bob'}
367+ self.assertEqual(
368+ (1, 0, 0, 0),
369+ self.instance.import_data(
370+ self.cr, 'uid', ['name', 'children/name', 'children/.id'],
371+ [['parent', 'child', '42']]))
372+ self.assertEqual(
373+ [('myth.relating',
374+ {'name': 'parent', 'children': [(4, 42), (1, 42, {'name': 'child'})]},
375+ False, False)],
376+ self.pool.get('ir.model.data').records)
377+
378+ def test_db_name(self):
379+ self.o2m._names['ed'] = 42
380+ self.assertEqual(
381+ (1, 0, 0, 0),
382+ self.instance.import_data(
383+ self.cr, 'uid', ['name', 'children/name', 'children'],
384+ [['parent', 'child', 'ed']]))
385+ self.assertEqual(
386+ [('myth.relating',
387+ {'name': 'parent', 'children': [(4, 42), (1, 42, {'name': 'child'})]},
388+ False, False)],
389+ self.pool.get('ir.model.data').records)