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
=== modified file 'openerp/osv/orm.py'
--- openerp/osv/orm.py 2011-12-01 12:15:03 +0000
+++ openerp/osv/orm.py 2011-12-15 12:52:47 +0000
@@ -1172,7 +1172,7 @@
1172 datas += self.__export_row(cr, uid, row, fields_to_export, context)1172 datas += self.__export_row(cr, uid, row, fields_to_export, context)
1173 return {'datas': datas}1173 return {'datas': datas}
11741174
1175 def import_data(self, cr, uid, fields, datas, mode='init', current_module='', noupdate=False, context=None, filename=None):1175 def import_data(self, cr, uid, fields, data, mode='init', current_module='', noupdate=False, context=None, filename=None):
1176 """Import given data in given module1176 """Import given data in given module
11771177
1178 This method is used when importing data via client menu.1178 This method is used when importing data via client menu.
@@ -1240,26 +1240,40 @@
1240 id = ids[0][0]1240 id = ids[0][0]
1241 return id1241 return id
12421242
1243 # IN:1243 def process_liness(self, rows, prefix, current_module, model_name, fields_def, position=0, skip=0):
1244 # datas: a list of records, each record is defined by a list of values1244 """ Processes a row of data to transform it into a record
1245 # prefix: a list of prefix fields ['line_ids']1245 importable into OpenERP proper (via (ir.model.data)._update)
1246 # position: the line to process, skip is False if it's the first line of the current record1246
1247 # OUT:1247 :param rows: record data to import
1248 # (res, position, warning, res_id) with1248 :type rows: list[list[str]]
1249 # res: the record for the next line to process (including it's one2many)1249 :param list[str] prefix: list of prefix fields
1250 # position: the new position for the next line1250 :param str current_module:
1251 # res_id: the ID of the record if it's a modification1251 :param str model_name:
1252 def process_liness(self, datas, prefix, current_module, model_name, fields_def, position=0, skip=0):1252 :param dict[dict] fields_def: fields definition for the current model
1253 line = datas[position]1253 :param int position: index of the current row to import
1254 row = {}1254 :param int skip:
1255 :returns: 5-tuple of import information continuation:
1256 #. extracted record for db import
1257 #. the position of the next line to import
1258 #. warning messages generated while importing the row
1259 #. identifier of the record being imported, for update
1260 #. xmlid of the record being imported, for update?
1261 :rtype: (dict, int, list[str], int, str)
1262
1263 .. note::
1264 specifying a name_get (no sub-field) *and* a /.id *and* an
1265 a /id (toplevel or subfield e.g. o2m) is undefined behavior if
1266 they conflict
1267 """
1268 line = rows[position]
1269 record = {}
1255 warning = []1270 warning = []
1256 data_res_id = False1271 data_res_id = False
1257 xml_id = False1272 xml_id = False
1258 nbrmax = position+11273 next_row = position+1
12591274
1260 done = {}1275 done = {}
1261 for i, field in enumerate(fields):1276 for i, field in enumerate(fields):
1262 res = False
1263 if i >= len(line):1277 if i >= len(line):
1264 raise Exception(_('Please check that all your lines have %d columns.'1278 raise Exception(_('Please check that all your lines have %d columns.'
1265 'Stopped around line %d having %d columns.') % \1279 'Stopped around line %d having %d columns.') % \
@@ -1271,6 +1285,15 @@
1271 if line[i] and skip:1285 if line[i] and skip:
1272 return False1286 return False
1273 continue1287 continue
1288
1289 if field == prefix:
1290 # o2m field by name in similarly named column
1291 try:
1292 data_res_id = _get_id(model_name, line[i], current_module, mode=None)
1293 except ValueError:
1294 pass
1295 continue
1296
1274 field_name = field[len(prefix)]1297 field_name = field[len(prefix)]
12751298
1276 #set the mode for m2o, o2m, m2m : xml_id/id/name1299 #set the mode for m2o, o2m, m2m : xml_id/id/name
@@ -1311,25 +1334,27 @@
1311 newfd = relation_obj.fields_get( cr, uid, context=context )1334 newfd = relation_obj.fields_get( cr, uid, context=context )
1312 pos = position1335 pos = position
13131336
1314 res = many_ids(line[i], relation, current_module, mode)1337 res = [] # many_ids(line[i], relation, current_module, mode)
13151338
1316 first = 01339 first = 0
1317 while pos < len(datas):1340 while pos < len(rows):
1318 res2 = process_liness(self, datas, prefix + [field_name], current_module, relation_obj._name, newfd, pos, first)1341 res2 = process_liness(self, rows, prefix + [field_name], current_module, relation_obj._name, newfd, pos, first)
1319 if not res2:1342 if not res2:
1320 break1343 break
1321 (newrow, pos, w2, data_res_id2, xml_id2) = res21344 (o2m_record, pos, additional_warnings, o2m_id, o2m_external_id) = res2
1322 nbrmax = max(nbrmax, pos)1345 next_row = max(next_row, pos)
1323 warning += w21346 warning += additional_warnings
1324 first += 11347 first += 1
13251348
1326 if data_res_id2:1349 if o2m_id:
1327 res.append((4, data_res_id2))1350 res.append((4, o2m_id))
13281351
1329 if (not newrow) or not reduce(lambda x, y: x or y, newrow.values(), 0):1352 # Found no values to write to linked o2m, bail out
1353 if not (o2m_record and any(o2m_record.itervalues())):
1330 break1354 break
13311355
1332 res.append( (data_res_id2 and 1 or 0, data_res_id2 or 0, newrow) )1356 # Create o2m if no matching o2m record, else update it
1357 res.append((1 if o2m_id else 0, o2m_id or False, o2m_record) )
13331358
1334 elif field_type == 'many2one':1359 elif field_type == 'many2one':
1335 relation = fields_def[field_name]['relation']1360 relation = fields_def[field_name]['relation']
@@ -1346,23 +1371,22 @@
1346 elif field_type == 'float':1371 elif field_type == 'float':
1347 res = line[i] and float(line[i]) or 0.01372 res = line[i] and float(line[i]) or 0.0
1348 elif field_type == 'selection':1373 elif field_type == 'selection':
1374 res = False
1349 for key, val in fields_def[field_name]['selection']:1375 for key, val in fields_def[field_name]['selection']:
1350 if tools.ustr(line[i]) in [tools.ustr(key), tools.ustr(val)]:1376 if tools.ustr(line[i]) in [tools.ustr(key), tools.ustr(val)]:
1351 res = key1377 res = key
1352 break1378 break
1353 if line[i] and not res:1379 if not res:
1380 message = _("Key/value '%s' not found in selection field '%s'")
1354 logging.getLogger('orm.import').warn(1381 logging.getLogger('orm.import').warn(
1355 _("key '%s' not found in selection field '%s'"),1382 message, tools.ustr(line[i]), tools.ustr(field_name))
1356 tools.ustr(line[i]), tools.ustr(field_name))1383 warning.append(message % (tools.ustr(line[i]), tools.ustr(field_name)))
1357 warning.append(_("Key/value '%s' not found in selection field '%s'") % (
1358 tools.ustr(line[i]), tools.ustr(field_name)))
1359
1360 else:1384 else:
1361 res = line[i]1385 res = line[i]
13621386
1363 row[field_name] = res or False1387 record[field_name] = res or False
13641388
1365 return row, nbrmax, warning, data_res_id, xml_id1389 return record, next_row, warning, data_res_id, xml_id
13661390
1367 fields_def = self.fields_get(cr, uid, context=context)1391 fields_def = self.fields_get(cr, uid, context=context)
13681392
@@ -1372,19 +1396,19 @@
1372 data = pickle.load(partial_import_file)1396 data = pickle.load(partial_import_file)
1373 position = data.get(filename, 0)1397 position = data.get(filename, 0)
13741398
1375 while position<len(datas):1399 while position < len(data):
1376 (res, position, warning, res_id, xml_id) = \1400 (res, position, warning, res_id, xml_id) = \
1377 process_liness(self, datas, [], current_module, self._name, fields_def, position=position)1401 process_liness(self, data, [], current_module, self._name, fields_def, position=position)
1378 if len(warning):1402 if warning:
1379 cr.rollback()1403 cr.rollback()
1380 return -1, res, 'Line ' + str(position) +' : ' + '!\n'.join(warning), ''1404 return -1, res, _("Row %d: %s") % (position, '\n'.join(warning)), ''
13811405
1382 try:1406 try:
1383 ir_model_data_obj._update(cr, uid, self._name,1407 ir_model_data_obj._update(cr, uid, self._name,
1384 current_module, res, mode=mode, xml_id=xml_id,1408 current_module, res, mode=mode, xml_id=xml_id,
1385 noupdate=noupdate, res_id=res_id, context=context)1409 noupdate=noupdate, res_id=res_id, context=context)
1386 except Exception, e:1410 except Exception, e:
1387 return -1, res, 'Line ' + str(position) + ' : ' + tools.ustr(e), ''1411 return -1, res, _("Row %d: %s") % (position, tools.ustr(e)), ''
13881412
1389 if config.get('import_partial') and filename and (not (position%100)):1413 if config.get('import_partial') and filename and (not (position%100)):
1390 with open(config.get('import_partial'), 'rb') as partial_import:1414 with open(config.get('import_partial'), 'rb') as partial_import:
13911415
=== added file 'openerp/tests/test_import.py'
--- openerp/tests/test_import.py 1970-01-01 00:00:00 +0000
+++ openerp/tests/test_import.py 2011-12-15 12:52:47 +0000
@@ -0,0 +1,207 @@
1# -*- coding: utf-8 -*-
2import collections
3import unittest2
4
5from openerp.osv import orm, fields
6
7class Myth(orm.Model):
8 _name = 'myth'
9 _columns = {
10 'foo': fields.char('Foo', 42),
11 'bar': fields.char('Bar', 42)
12 }
13
14class RelatedMyth(orm.Model):
15 _name = 'myth.related'
16 _columns = {
17 'name': fields.char('Name', 0)
18 }
19 def __init__(self, *args):
20 super(RelatedMyth, self).__init__(*args)
21 self._records = {}
22 self._names = {}
23 def name_search(self, cr, user, name='', *args, **kwargs):
24 id = self._names.get(name)
25 if id:
26 return [(id, name)]
27 return []
28 def search(self, cr, uid, predicate, *args, **kwargs):
29 pred, = predicate
30 field, op, id = pred
31 r = self._records.get(id)
32 if r:
33 return [r]
34 return []
35class RelatingMyth(orm.Model):
36 _name = 'myth.relating'
37 _columns = {
38 'name': fields.char('name', 0),
39 'children': fields.one2many('myth.related', 'parent', 'Children')
40 }
41 def _name_search(self, *args, **kwargs):
42 return []
43
44class FakeModel(object):
45 _inherits = []
46 def check(self, *args):
47 return True
48 def _get_source(self, *args):
49 return None
50# should probably be a mock.Mock instance
51ModelRecord = collections.namedtuple('ModelRecord', 'model record xmlid dbid')
52class FakeIrModelData(FakeModel):
53 def record(self):
54 self.records = []
55 def _update(self, cr, uid, model, module, values,
56 xml_id=False, store=True, noupdate=False, mode='init',
57 res_id=False, context=None):
58 self.records.append(
59 ModelRecord(model=model, record=values, xmlid=xml_id, dbid=res_id))
60
61class FakePool(object):
62 _store_function = {}
63 models = collections.defaultdict(FakeModel)
64 def __init__(self):
65 self.models['ir.model.data'] = FakeIrModelData()
66 def add(self, name, item):
67 self.models[name] = item
68 def get(self, name):
69 return self.models[name]
70class FakeCursor(object):
71 def execute(self, stmt, params):
72 pass
73 def fetchone(self):
74 pass
75
76class TestBasicImport(unittest2.TestCase):
77 def setUp(self):
78 self.pool = FakePool()
79 self.pool.get('ir.model.data').record()
80 self.cr = FakeCursor()
81 self.instance = Myth.create_instance(self.pool, self.cr)
82
83 def test_import_nothing(self):
84 self.assertEqual(
85 (0, 0, 0, 0),
86 self.instance.import_data(self.cr, 'uid', ['foo', 'bar'], []))
87 def test_import_single(self):
88 self.assertEqual(
89 (1, 0, 0, 0),
90 self.instance.import_data(
91 self.cr, 'uid', ['foo', 'bar'], [['foo', 'bar']]))
92 self.assertEqual(
93 [('myth', {'foo': 'foo', 'bar': 'bar'}, False, False)],
94 self.pool.get('ir.model.data').records)
95 def test_import_two(self):
96 self.assertEqual(
97 (2, 0, 0, 0),
98 self.instance.import_data(
99 self.cr, 'uid', ['foo', 'bar'], [
100 ['foo', 'bar'],
101 ['qux', 'quux']]))
102 self.assertEqual(
103 [('myth', {'foo': 'foo', 'bar': 'bar'}, False, False),
104 ('myth', {'foo': 'qux', 'bar': 'quux'}, False, False)],
105 self.pool.get('ir.model.data').records)
106
107class TestO2MImportEmpty(unittest2.TestCase):
108 def setUp(self):
109 self.pool = FakePool()
110 self.pool.get('ir.model.data').record()
111 self.cr = FakeCursor()
112 self.instance = RelatingMyth.create_instance(self.pool, self.cr)
113 RelatedMyth.create_instance(self.pool, self.cr)
114
115 def test_import_one(self):
116 self.assertEqual(
117 (1, 0, 0, 0),
118 self.instance.import_data(
119 self.cr, 'uid', ['name', 'children/name'],
120 [['parent', 'child']]))
121 self.assertEqual(
122 [('myth.relating',
123 {'name': 'parent', 'children': [(0, 0, {'name': 'child'})]},
124 False, False)],
125 self.pool.get('ir.model.data').records)
126
127 def test_import_multiple_children(self):
128 self.assertEqual(
129 (3, 0, 0, 0),
130 self.instance.import_data(
131 self.cr, 'uid', ['name', 'children/name'],
132 [['parent', 'child'],
133 ['', 'child2'],
134 ['', 'child3']]))
135 self.assertEqual(
136 [('myth.relating',
137 {'name': 'parent', 'children': [
138 (0, 0, {'name': 'child'}),
139 (0, 0, {'name': 'child2'}),
140 (0, 0, {'name': 'child3'})]},
141 False, False)],
142 self.pool.get('ir.model.data').records)
143
144 def test_import_multiple_parent(self):
145 self.assertEqual(
146 (3, 0, 0, 0),
147 self.instance.import_data(
148 self.cr, 'uid', ['name', 'children/name'],
149 [['parent', 'child'],
150 ['parent2', 'child2'],
151 ['parent3', 'child3']]))
152 self.assertEqual(
153 [('myth.relating', {'name': 'parent', 'children': [(0, 0, {'name': 'child'})]}, False, False),
154 ('myth.relating', {'name': 'parent2', 'children': [(0, 0, {'name': 'child2'})]}, False, False),
155 ('myth.relating', {'name': 'parent3', 'children': [(0, 0, {'name': 'child3'})]}, False, False)
156 ],
157 self.pool.get('ir.model.data').records)
158
159 def test_import_multiple_everything(self):
160 self.assertEqual(
161 (4, 0, 0, 0),
162 self.instance.import_data(
163 self.cr, 'uid', ['name', 'children/name'],
164 [['parent', 'child'],
165 ['', 'child2'],
166 ['parent2', 'child3'],
167 ['', 'child4']]))
168 self.assertEqual(
169 [('myth.relating', {'name': 'parent', 'children': [
170 (0, 0, {'name': 'child'}), (0, 0, {'name': 'child2'})]}, False, False),
171 ('myth.relating', {'name': 'parent2', 'children': [
172 (0, 0, {'name': 'child3'}), (0, 0, {'name': 'child4'})]}, False, False)],
173 self.pool.get('ir.model.data').records)
174
175class TestO2MImportExist(unittest2.TestCase):
176 def setUp(self):
177 self.pool = FakePool()
178 self.pool.get('ir.model.data').record()
179 self.cr = FakeCursor()
180 self.instance = RelatingMyth.create_instance(self.pool, self.cr)
181 self.o2m = RelatedMyth.create_instance(self.pool, self.cr)
182
183 def test_db_id(self):
184 self.o2m._records[42] = {'name': 'bob'}
185 self.assertEqual(
186 (1, 0, 0, 0),
187 self.instance.import_data(
188 self.cr, 'uid', ['name', 'children/name', 'children/.id'],
189 [['parent', 'child', '42']]))
190 self.assertEqual(
191 [('myth.relating',
192 {'name': 'parent', 'children': [(4, 42), (1, 42, {'name': 'child'})]},
193 False, False)],
194 self.pool.get('ir.model.data').records)
195
196 def test_db_name(self):
197 self.o2m._names['ed'] = 42
198 self.assertEqual(
199 (1, 0, 0, 0),
200 self.instance.import_data(
201 self.cr, 'uid', ['name', 'children/name', 'children'],
202 [['parent', 'child', 'ed']]))
203 self.assertEqual(
204 [('myth.relating',
205 {'name': 'parent', 'children': [(4, 42), (1, 42, {'name': 'child'})]},
206 False, False)],
207 self.pool.get('ir.model.data').records)