Merge lp:~openerp-connector-core-editors/openerp-connector/7.0-connector-mapper-refactor into lp:~openerp-connector-core-editors/openerp-connector/7.0
- 7.0-connector-mapper-refactor
- Merge into 7.0
Status: | Superseded |
---|---|
Proposed branch: | lp:~openerp-connector-core-editors/openerp-connector/7.0-connector-mapper-refactor |
Merge into: | lp:~openerp-connector-core-editors/openerp-connector/7.0 |
Diff against target: |
1344 lines (+902/-187) 8 files modified
connector/CHANGES.rst (+3/-0) connector/backend.py (+7/-4) connector/connector.py (+5/-1) connector/doc/guides/concepts.rst (+4/-8) connector/exception.py (+4/-0) connector/tests/test_backend.py (+3/-2) connector/tests/test_mapper.py (+333/-40) connector/unit/mapper.py (+543/-132) |
To merge this branch: | bzr merge lp:~openerp-connector-core-editors/openerp-connector/7.0-connector-mapper-refactor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenERP Connector Core Editors | Pending | ||
Review via email: mp+194484@code.launchpad.net |
This proposal has been superseded by a proposal from 2013-11-08.
Commit message
Description of the change
Refactoring of the Mappers.
=======
Work in progress.
- 584. By Guewen Baconnier @ Camptocamp
-
[CHG] rename map_record.
values( only_create= True) to map_record. values( for_create= True) -> less ambiguous - 585. By Guewen Baconnier @ Camptocamp
-
[CHG] move deprecated methods, set some private methods to public (the ones that are really intended to be inherited)
- 586. By Guewen Baconnier @ Camptocamp
-
[CHG] updated the docstring according to the last developments
- 587. By Guewen Baconnier @ Camptocamp
-
[FIX] simplify the MapRecord.update() method
- 588. By Guewen Baconnier @ Camptocamp
-
[IMP] docstring
- 589. By Guewen Baconnier @ Camptocamp
-
[FIX] mapping options: use an object whose items can be accessed like attributes.
It offers a far better interface as well as for the the call to MapRecord.values() as for the usage in mapping methods
- 590. By Guewen Baconnier @ Camptocamp
-
[FIX] the backward compatibility code was outdated: binding is the name of the binding to use, to provide only when the relation is not a binding
- 591. By Guewen Baconnier @ Camptocamp
-
[CHG] MapOptions now returns None when a key is undefined, allowing a better handling in mapping methods when an option is not used
- 592. By Guewen Baconnier @ Camptocamp
-
[MRG] sync with master branch
- 593. By Guewen Baconnier @ Camptocamp
-
[MRG] sync with master branch
- 594. By Guewen Baconnier @ Camptocamp
-
[IMP] extract the build of the MapChild unit instance to allow better reusability
- 595. By Guewen Baconnier @ Camptocamp
-
[MRG] from lp:openerp-connector
Unmerged revisions
Preview Diff
1 | === modified file 'connector/CHANGES.rst' | |||
2 | --- connector/CHANGES.rst 2013-10-09 18:52:36 +0000 | |||
3 | +++ connector/CHANGES.rst 2013-11-08 11:46:46 +0000 | |||
4 | @@ -4,6 +4,9 @@ | |||
5 | 4 | 2.0.1.dev0 | 4 | 2.0.1.dev0 |
6 | 5 | ~~~~~~~~~~ | 5 | ~~~~~~~~~~ |
7 | 6 | 6 | ||
8 | 7 | * Add the possibility to use 'Modifiers' functions in the 'direct | ||
9 | 8 | mappings' (details in the documentation of the Mapper class) | ||
10 | 9 | |||
11 | 7 | 2.0.1 (2013-09-12) | 10 | 2.0.1 (2013-09-12) |
12 | 8 | ~~~~~~~~~~~~~~~~~~ | 11 | ~~~~~~~~~~~~~~~~~~ |
13 | 9 | 12 | ||
14 | 10 | 13 | ||
15 | === modified file 'connector/backend.py' | |||
16 | --- connector/backend.py 2013-08-26 20:05:18 +0000 | |||
17 | +++ connector/backend.py 2013-11-08 11:46:46 +0000 | |||
18 | @@ -20,6 +20,7 @@ | |||
19 | 20 | ############################################################################## | 20 | ############################################################################## |
20 | 21 | from functools import partial | 21 | from functools import partial |
21 | 22 | from collections import namedtuple | 22 | from collections import namedtuple |
22 | 23 | from .exception import NoConnectorUnitError | ||
23 | 23 | 24 | ||
24 | 24 | __all__ = ['Backend'] | 25 | __all__ = ['Backend'] |
25 | 25 | 26 | ||
26 | @@ -257,10 +258,12 @@ | |||
27 | 257 | """ | 258 | """ |
28 | 258 | matching_classes = self._get_classes(base_class, session, | 259 | matching_classes = self._get_classes(base_class, session, |
29 | 259 | model_name) | 260 | model_name) |
34 | 260 | assert matching_classes, ('No matching class found for %s ' | 261 | if not matching_classes: |
35 | 261 | 'with session: %s, ' | 262 | raise NoConnectorUnitError('No matching class found for %s ' |
36 | 262 | 'model name: %s' % | 263 | 'with session: %s, ' |
37 | 263 | (base_class, session, model_name)) | 264 | 'model name: %s' % |
38 | 265 | (base_class, session, model_name)) | ||
39 | 266 | |||
40 | 264 | assert len(matching_classes) == 1, ( | 267 | assert len(matching_classes) == 1, ( |
41 | 265 | 'Several classes found for %s ' | 268 | 'Several classes found for %s ' |
42 | 266 | 'with session %s, model name: %s. Found: %s' % | 269 | 'with session %s, model name: %s. Found: %s' % |
43 | 267 | 270 | ||
44 | === modified file 'connector/connector.py' | |||
45 | --- connector/connector.py 2013-09-13 13:22:49 +0000 | |||
46 | +++ connector/connector.py 2013-11-08 11:46:46 +0000 | |||
47 | @@ -283,11 +283,15 @@ | |||
48 | 283 | """ | 283 | """ |
49 | 284 | raise NotImplementedError | 284 | raise NotImplementedError |
50 | 285 | 285 | ||
52 | 286 | def to_backend(self, binding_id): | 286 | def to_backend(self, binding_id, wrap=False): |
53 | 287 | """ Give the external ID for an OpenERP binding ID | 287 | """ Give the external ID for an OpenERP binding ID |
54 | 288 | (ID in a model magento.*) | 288 | (ID in a model magento.*) |
55 | 289 | 289 | ||
56 | 290 | :param binding_id: OpenERP binding ID for which we want the backend id | 290 | :param binding_id: OpenERP binding ID for which we want the backend id |
57 | 291 | :param wrap: if False, binding_id is the ID of the binding, | ||
58 | 292 | if True, binding_id is the ID of the normal record, the | ||
59 | 293 | method will search the corresponding binding and returns | ||
60 | 294 | the backend id of the binding | ||
61 | 291 | :return: external ID of the record | 295 | :return: external ID of the record |
62 | 292 | """ | 296 | """ |
63 | 293 | raise NotImplementedError | 297 | raise NotImplementedError |
64 | 294 | 298 | ||
65 | === modified file 'connector/doc/guides/concepts.rst' | |||
66 | --- connector/doc/guides/concepts.rst 2013-11-01 09:10:27 +0000 | |||
67 | +++ connector/doc/guides/concepts.rst 2013-11-08 11:46:46 +0000 | |||
68 | @@ -146,8 +146,7 @@ | |||
69 | 146 | systems. | 146 | systems. |
70 | 147 | 147 | ||
71 | 148 | The connector defines some base classes, which you can find below. | 148 | The connector defines some base classes, which you can find below. |
74 | 149 | Note that you can define your own ConnectorUnits as well without | 149 | Note that you can define your own ConnectorUnits as well. |
73 | 150 | reusing them. | ||
75 | 151 | 150 | ||
76 | 152 | Mappings | 151 | Mappings |
77 | 153 | ======== | 152 | ======== |
78 | @@ -159,19 +158,16 @@ | |||
79 | 159 | 158 | ||
80 | 160 | It supports: | 159 | It supports: |
81 | 161 | 160 | ||
84 | 162 | * direct mappings | 161 | direct mappings |
83 | 163 | |||
85 | 164 | Fields *a* is written in field *b*. | 162 | Fields *a* is written in field *b*. |
86 | 165 | 163 | ||
89 | 166 | * method mappings | 164 | method mappings |
88 | 167 | |||
90 | 168 | A method is used to convert one or many fields to one or many | 165 | A method is used to convert one or many fields to one or many |
91 | 169 | fields, with transformation. | 166 | fields, with transformation. |
92 | 170 | It can be filtered, for example only applied when the record is | 167 | It can be filtered, for example only applied when the record is |
93 | 171 | created or when the source fields are modified. | 168 | created or when the source fields are modified. |
94 | 172 | 169 | ||
97 | 173 | * submapping | 170 | submapping |
96 | 174 | |||
98 | 175 | a sub-record (lines of a sale order) is converted using another | 171 | a sub-record (lines of a sale order) is converted using another |
99 | 176 | Mapper | 172 | Mapper |
100 | 177 | 173 | ||
101 | 178 | 174 | ||
102 | === modified file 'connector/exception.py' | |||
103 | --- connector/exception.py 2013-06-19 14:47:17 +0000 | |||
104 | +++ connector/exception.py 2013-11-08 11:46:46 +0000 | |||
105 | @@ -24,6 +24,10 @@ | |||
106 | 24 | """ Base Exception for the connectors """ | 24 | """ Base Exception for the connectors """ |
107 | 25 | 25 | ||
108 | 26 | 26 | ||
109 | 27 | class NoConnectorUnitError(ConnectorException): | ||
110 | 28 | """ No ConnectorUnit has been found """ | ||
111 | 29 | |||
112 | 30 | |||
113 | 27 | class InvalidDataError(ConnectorException): | 31 | class InvalidDataError(ConnectorException): |
114 | 28 | """ Data Invalid """ | 32 | """ Data Invalid """ |
115 | 29 | 33 | ||
116 | 30 | 34 | ||
117 | === modified file 'connector/tests/test_backend.py' | |||
118 | --- connector/tests/test_backend.py 2013-08-23 20:34:05 +0000 | |||
119 | +++ connector/tests/test_backend.py 2013-11-08 11:46:46 +0000 | |||
120 | @@ -6,6 +6,7 @@ | |||
121 | 6 | from openerp.addons.connector.backend import (Backend, | 6 | from openerp.addons.connector.backend import (Backend, |
122 | 7 | get_backend, | 7 | get_backend, |
123 | 8 | BACKENDS) | 8 | BACKENDS) |
124 | 9 | from openerp.addons.connector.exception import NoConnectorUnitError | ||
125 | 9 | from openerp.addons.connector.connector import (Binder, | 10 | from openerp.addons.connector.connector import (Binder, |
126 | 10 | ConnectorUnit) | 11 | ConnectorUnit) |
127 | 11 | from openerp.addons.connector.unit.mapper import (Mapper, | 12 | from openerp.addons.connector.unit.mapper import (Mapper, |
128 | @@ -73,7 +74,7 @@ | |||
129 | 73 | self.uid) | 74 | self.uid) |
130 | 74 | 75 | ||
131 | 75 | def tearDown(self): | 76 | def tearDown(self): |
133 | 76 | super(test_backend_register, self).setUp() | 77 | super(test_backend_register, self).tearDown() |
134 | 77 | BACKENDS.backends.clear() | 78 | BACKENDS.backends.clear() |
135 | 78 | del self.backend._class_entries[:] | 79 | del self.backend._class_entries[:] |
136 | 79 | 80 | ||
137 | @@ -110,7 +111,7 @@ | |||
138 | 110 | 111 | ||
139 | 111 | def test_no_register_error(self): | 112 | def test_no_register_error(self): |
140 | 112 | """ Error when asking for a class and none is found""" | 113 | """ Error when asking for a class and none is found""" |
142 | 113 | with self.assertRaises(AssertionError): | 114 | with self.assertRaises(NoConnectorUnitError): |
143 | 114 | ref = self.backend.get_class(BackendAdapter, | 115 | ref = self.backend.get_class(BackendAdapter, |
144 | 115 | self.session, | 116 | self.session, |
145 | 116 | 'res.users') | 117 | 'res.users') |
146 | 117 | 118 | ||
147 | === modified file 'connector/tests/test_mapper.py' | |||
148 | --- connector/tests/test_mapper.py 2013-04-08 11:48:59 +0000 | |||
149 | +++ connector/tests/test_mapper.py 2013-11-08 11:46:46 +0000 | |||
150 | @@ -2,15 +2,26 @@ | |||
151 | 2 | 2 | ||
152 | 3 | import unittest2 | 3 | import unittest2 |
153 | 4 | import mock | 4 | import mock |
154 | 5 | import openerp.tests.common as common | ||
155 | 5 | 6 | ||
156 | 6 | from openerp.addons.connector.unit.mapper import ( | 7 | from openerp.addons.connector.unit.mapper import ( |
157 | 7 | Mapper, | 8 | Mapper, |
158 | 8 | ImportMapper, | 9 | ImportMapper, |
159 | 10 | ImportMapChild, | ||
160 | 9 | MappingDefinition, | 11 | MappingDefinition, |
161 | 10 | changed_by, | 12 | changed_by, |
162 | 11 | only_create, | 13 | only_create, |
163 | 14 | convert, | ||
164 | 15 | m2o_to_backend, | ||
165 | 16 | backend_to_m2o, | ||
166 | 17 | none, | ||
167 | 12 | mapping) | 18 | mapping) |
168 | 13 | 19 | ||
169 | 20 | from openerp.addons.connector.exception import NoConnectorUnitError | ||
170 | 21 | from openerp.addons.connector.backend import Backend | ||
171 | 22 | from openerp.addons.connector.connector import Environment | ||
172 | 23 | from openerp.addons.connector.session import ConnectorSession | ||
173 | 24 | |||
174 | 14 | 25 | ||
175 | 15 | class test_mapper(unittest2.TestCase): | 26 | class test_mapper(unittest2.TestCase): |
176 | 16 | """ Test Mapper """ | 27 | """ Test Mapper """ |
177 | @@ -151,11 +162,11 @@ | |||
178 | 151 | record = {'name': 'Guewen', | 162 | record = {'name': 'Guewen', |
179 | 152 | 'street': 'street'} | 163 | 'street': 'street'} |
180 | 153 | mapper = MyMapper(env) | 164 | mapper = MyMapper(env) |
182 | 154 | mapper.convert(record) | 165 | map_record = mapper.map_record(record) |
183 | 155 | expected = {'out_name': 'Guewen', | 166 | expected = {'out_name': 'Guewen', |
184 | 156 | 'out_street': 'STREET'} | 167 | 'out_street': 'STREET'} |
187 | 157 | self.assertEqual(mapper.data, expected) | 168 | self.assertEqual(map_record.values(), expected) |
188 | 158 | self.assertEqual(mapper.data_for_create, expected) | 169 | self.assertEqual(map_record.values(only_create=True), expected) |
189 | 159 | 170 | ||
190 | 160 | def test_mapping_record_on_create(self): | 171 | def test_mapping_record_on_create(self): |
191 | 161 | """ Map a record and check the result for creation of record """ | 172 | """ Map a record and check the result for creation of record """ |
192 | @@ -176,61 +187,343 @@ | |||
193 | 176 | record = {'name': 'Guewen', | 187 | record = {'name': 'Guewen', |
194 | 177 | 'street': 'street'} | 188 | 'street': 'street'} |
195 | 178 | mapper = MyMapper(env) | 189 | mapper = MyMapper(env) |
197 | 179 | mapper.convert(record) | 190 | map_record = mapper.map_record(record) |
198 | 180 | expected = {'out_name': 'Guewen', | 191 | expected = {'out_name': 'Guewen', |
199 | 181 | 'out_street': 'STREET'} | 192 | 'out_street': 'STREET'} |
201 | 182 | self.assertEqual(mapper.data, expected) | 193 | self.assertEqual(map_record.values(), expected) |
202 | 183 | expected = {'out_name': 'Guewen', | 194 | expected = {'out_name': 'Guewen', |
203 | 184 | 'out_street': 'STREET', | 195 | 'out_street': 'STREET', |
204 | 185 | 'out_city': 'city'} | 196 | 'out_city': 'city'} |
206 | 186 | self.assertEqual(mapper.data_for_create, expected) | 197 | self.assertEqual(map_record.values(only_create=True), expected) |
207 | 198 | |||
208 | 199 | def test_mapping_update(self): | ||
209 | 200 | """ Force values on a map record """ | ||
210 | 201 | class MyMapper(ImportMapper): | ||
211 | 202 | |||
212 | 203 | direct = [('name', 'out_name')] | ||
213 | 204 | |||
214 | 205 | @mapping | ||
215 | 206 | def street(self, record): | ||
216 | 207 | return {'out_street': record['street'].upper()} | ||
217 | 208 | |||
218 | 209 | @only_create | ||
219 | 210 | @mapping | ||
220 | 211 | def city(self, record): | ||
221 | 212 | return {'out_city': 'city'} | ||
222 | 213 | |||
223 | 214 | env = mock.MagicMock() | ||
224 | 215 | record = {'name': 'Guewen', | ||
225 | 216 | 'street': 'street'} | ||
226 | 217 | mapper = MyMapper(env) | ||
227 | 218 | map_record = mapper.map_record(record) | ||
228 | 219 | map_record.update(out_city='forced') | ||
229 | 220 | map_record.update({'test': 1}) | ||
230 | 221 | expected = {'out_name': 'Guewen', | ||
231 | 222 | 'out_street': 'STREET', | ||
232 | 223 | 'out_city': 'forced', | ||
233 | 224 | 'test': 1} | ||
234 | 225 | self.assertEqual(map_record.values(), expected) | ||
235 | 226 | expected = {'out_name': 'Guewen', | ||
236 | 227 | 'out_street': 'STREET', | ||
237 | 228 | 'out_city': 'forced', | ||
238 | 229 | 'test': 1} | ||
239 | 230 | self.assertEqual(map_record.values(only_create=True), expected) | ||
240 | 231 | |||
241 | 232 | def test_finalize(self): | ||
242 | 233 | """ Inherit _finalize to modify values """ | ||
243 | 234 | class MyMapper(ImportMapper): | ||
244 | 235 | |||
245 | 236 | direct = [('name', 'out_name')] | ||
246 | 237 | |||
247 | 238 | def _finalize(self, record, values): | ||
248 | 239 | result = super(MyMapper, self)._finalize(record, values) | ||
249 | 240 | result['test'] = 'abc' | ||
250 | 241 | return result | ||
251 | 242 | |||
252 | 243 | env = mock.MagicMock() | ||
253 | 244 | record = {'name': 'Guewen', | ||
254 | 245 | 'street': 'street'} | ||
255 | 246 | mapper = MyMapper(env) | ||
256 | 247 | map_record = mapper.map_record(record) | ||
257 | 248 | expected = {'out_name': 'Guewen', | ||
258 | 249 | 'test': 'abc'} | ||
259 | 250 | self.assertEqual(map_record.values(), expected) | ||
260 | 251 | expected = {'out_name': 'Guewen', | ||
261 | 252 | 'test': 'abc'} | ||
262 | 253 | self.assertEqual(map_record.values(only_create=True), expected) | ||
263 | 254 | |||
264 | 255 | def test_some_fields(self): | ||
265 | 256 | """ Map only a selection of fields """ | ||
266 | 257 | class MyMapper(ImportMapper): | ||
267 | 258 | |||
268 | 259 | direct = [('name', 'out_name'), | ||
269 | 260 | ('street', 'out_street'), | ||
270 | 261 | ] | ||
271 | 262 | |||
272 | 263 | @changed_by('country') | ||
273 | 264 | @mapping | ||
274 | 265 | def country(self, record): | ||
275 | 266 | return {'country': 'country'} | ||
276 | 267 | |||
277 | 268 | env = mock.MagicMock() | ||
278 | 269 | record = {'name': 'Guewen', | ||
279 | 270 | 'street': 'street', | ||
280 | 271 | 'country': 'country'} | ||
281 | 272 | mapper = MyMapper(env) | ||
282 | 273 | map_record = mapper.map_record(record) | ||
283 | 274 | expected = {'out_name': 'Guewen', | ||
284 | 275 | 'country': 'country'} | ||
285 | 276 | self.assertEqual(map_record.values(fields=['name', 'country']), | ||
286 | 277 | expected) | ||
287 | 278 | expected = {'out_name': 'Guewen', | ||
288 | 279 | 'country': 'country'} | ||
289 | 280 | self.assertEqual(map_record.values(only_create=True, | ||
290 | 281 | fields=['name', 'country']), | ||
291 | 282 | expected) | ||
292 | 283 | |||
293 | 284 | def test_mapping_modifier(self): | ||
294 | 285 | """ Map a direct record with a modifier function """ | ||
295 | 286 | |||
296 | 287 | def do_nothing(field): | ||
297 | 288 | def transform(self, record, to_attr): | ||
298 | 289 | return record[field] | ||
299 | 290 | return transform | ||
300 | 291 | |||
301 | 292 | class MyMapper(ImportMapper): | ||
302 | 293 | direct = [(do_nothing('name'), 'out_name')] | ||
303 | 294 | |||
304 | 295 | env = mock.MagicMock() | ||
305 | 296 | record = {'name': 'Guewen'} | ||
306 | 297 | mapper = MyMapper(env) | ||
307 | 298 | map_record = mapper.map_record(record) | ||
308 | 299 | expected = {'out_name': 'Guewen'} | ||
309 | 300 | self.assertEqual(map_record.values(), expected) | ||
310 | 301 | self.assertEqual(map_record.values(only_create=True), expected) | ||
311 | 302 | |||
312 | 303 | def test_mapping_convert(self): | ||
313 | 304 | """ Map a direct record with the convert modifier function """ | ||
314 | 305 | class MyMapper(ImportMapper): | ||
315 | 306 | direct = [(convert('name', int), 'out_name')] | ||
316 | 307 | |||
317 | 308 | env = mock.MagicMock() | ||
318 | 309 | record = {'name': '300'} | ||
319 | 310 | mapper = MyMapper(env) | ||
320 | 311 | map_record = mapper.map_record(record) | ||
321 | 312 | expected = {'out_name': 300} | ||
322 | 313 | self.assertEqual(map_record.values(), expected) | ||
323 | 314 | self.assertEqual(map_record.values(only_create=True), expected) | ||
324 | 315 | |||
325 | 316 | def test_mapping_modifier_none(self): | ||
326 | 317 | """ Pipeline of modifiers """ | ||
327 | 318 | class MyMapper(ImportMapper): | ||
328 | 319 | direct = [(none('in_f'), 'out_f'), | ||
329 | 320 | (none('in_t'), 'out_t')] | ||
330 | 321 | |||
331 | 322 | env = mock.MagicMock() | ||
332 | 323 | record = {'in_f': False, 'in_t': True} | ||
333 | 324 | mapper = MyMapper(env) | ||
334 | 325 | map_record = mapper.map_record(record) | ||
335 | 326 | expected = {'out_f': None, 'out_t': True} | ||
336 | 327 | self.assertEqual(map_record.values(), expected) | ||
337 | 328 | self.assertEqual(map_record.values(only_create=True), expected) | ||
338 | 329 | |||
339 | 330 | def test_mapping_modifier_pipeline(self): | ||
340 | 331 | """ Pipeline of modifiers """ | ||
341 | 332 | class MyMapper(ImportMapper): | ||
342 | 333 | direct = [(none(convert('in_f', bool)), 'out_f'), | ||
343 | 334 | (none(convert('in_t', bool)), 'out_t')] | ||
344 | 335 | |||
345 | 336 | env = mock.MagicMock() | ||
346 | 337 | record = {'in_f': 0, 'in_t': 1} | ||
347 | 338 | mapper = MyMapper(env) | ||
348 | 339 | map_record = mapper.map_record(record) | ||
349 | 340 | expected = {'out_f': None, 'out_t': True} | ||
350 | 341 | self.assertEqual(map_record.values(), expected) | ||
351 | 342 | self.assertEqual(map_record.values(only_create=True), expected) | ||
352 | 343 | |||
353 | 344 | def test_mapping_custom_option(self): | ||
354 | 345 | """ Usage of custom options in mappings """ | ||
355 | 346 | class MyMapper(ImportMapper): | ||
356 | 347 | @mapping | ||
357 | 348 | def any(self, record): | ||
358 | 349 | if self.options.get('custom'): | ||
359 | 350 | res = True | ||
360 | 351 | else: | ||
361 | 352 | res = False | ||
362 | 353 | return {'res': res} | ||
363 | 354 | |||
364 | 355 | env = mock.MagicMock() | ||
365 | 356 | record = {} | ||
366 | 357 | mapper = MyMapper(env) | ||
367 | 358 | map_record = mapper.map_record(record) | ||
368 | 359 | expected = {'res': True} | ||
369 | 360 | self.assertEqual(map_record.values(options=dict(custom=True)), | ||
370 | 361 | expected) | ||
371 | 362 | |||
372 | 363 | |||
373 | 364 | class test_mapper_binding(common.TransactionCase): | ||
374 | 365 | """ Test Mapper with Bindings""" | ||
375 | 366 | |||
376 | 367 | def setUp(self): | ||
377 | 368 | super(test_mapper_binding, self).setUp() | ||
378 | 369 | self.session = ConnectorSession(self.cr, self.uid) | ||
379 | 370 | self.Partner = self.registry('res.partner') | ||
380 | 371 | self.backend = mock.Mock(wraps=Backend('x', version='y'), | ||
381 | 372 | name='backend') | ||
382 | 373 | backend_record = mock.Mock() | ||
383 | 374 | backend_record.get_backend.return_value = self.backend | ||
384 | 375 | self.env = Environment(backend_record, self.session, 'res.partner') | ||
385 | 376 | self.country_binder = mock.Mock(name='country_binder') | ||
386 | 377 | self.country_binder.return_value = self.country_binder | ||
387 | 378 | self.backend.get_class.return_value = self.country_binder | ||
388 | 379 | |||
389 | 380 | def test_mapping_m2o_to_backend(self): | ||
390 | 381 | """ Map a direct record with the m2o_to_backend modifier function """ | ||
391 | 382 | class MyMapper(ImportMapper): | ||
392 | 383 | _model_name = 'res.partner' | ||
393 | 384 | direct = [(m2o_to_backend('country_id'), 'country')] | ||
394 | 385 | |||
395 | 386 | partner_id = self.ref('base.main_partner') | ||
396 | 387 | self.Partner.write(self.cr, self.uid, partner_id, | ||
397 | 388 | {'country_id': self.ref('base.ch')}) | ||
398 | 389 | partner = self.Partner.browse(self.cr, self.uid, partner_id) | ||
399 | 390 | self.country_binder.to_backend.return_value = 10 | ||
400 | 391 | |||
401 | 392 | mapper = MyMapper(self.env) | ||
402 | 393 | map_record = mapper.map_record(partner) | ||
403 | 394 | self.assertEqual(map_record.values(), {'country': 10}) | ||
404 | 395 | self.country_binder.to_backend.assert_called_once_with( | ||
405 | 396 | partner.country_id.id, wrap=False) | ||
406 | 397 | |||
407 | 398 | def test_mapping_backend_to_m2o(self): | ||
408 | 399 | """ Map a direct record with the backend_to_m2o modifier function """ | ||
409 | 400 | class MyMapper(ImportMapper): | ||
410 | 401 | _model_name = 'res.partner' | ||
411 | 402 | direct = [(backend_to_m2o('country'), 'country_id')] | ||
412 | 403 | |||
413 | 404 | record = {'country': 10} | ||
414 | 405 | self.country_binder.to_openerp.return_value = 44 | ||
415 | 406 | mapper = MyMapper(self.env) | ||
416 | 407 | map_record = mapper.map_record(record) | ||
417 | 408 | self.assertEqual(map_record.values(), {'country_id': 44}) | ||
418 | 409 | self.country_binder.to_openerp.assert_called_once_with( | ||
419 | 410 | 10, unwrap=False) | ||
420 | 411 | |||
421 | 412 | def test_mapping_record_children_no_map_child(self): | ||
422 | 413 | """ Map a record with children, using default MapChild """ | ||
423 | 414 | |||
424 | 415 | backend = Backend('backend', '42') | ||
425 | 416 | |||
426 | 417 | @backend | ||
427 | 418 | class LineMapper(ImportMapper): | ||
428 | 419 | _model_name = 'res.currency.rate' | ||
429 | 420 | direct = [('name', 'name')] | ||
430 | 421 | |||
431 | 422 | @mapping | ||
432 | 423 | def price(self, record): | ||
433 | 424 | return {'rate': record['rate'] * 2} | ||
434 | 425 | |||
435 | 426 | @only_create | ||
436 | 427 | @mapping | ||
437 | 428 | def discount(self, record): | ||
438 | 429 | return {'test': .5} | ||
439 | 430 | |||
440 | 431 | @backend | ||
441 | 432 | class ObjectMapper(ImportMapper): | ||
442 | 433 | _model_name = 'res.currency' | ||
443 | 434 | |||
444 | 435 | direct = [('name', 'name')] | ||
445 | 436 | |||
446 | 437 | children = [('lines', 'line_ids', 'res.currency.rate')] | ||
447 | 438 | |||
448 | 439 | |||
449 | 440 | backend_record = mock.Mock() | ||
450 | 441 | backend_record.get_backend.side_effect = lambda *a: backend | ||
451 | 442 | env = Environment(backend_record, self.session, 'res.currency') | ||
452 | 443 | |||
453 | 444 | record = {'name': 'SO1', | ||
454 | 445 | 'lines': [{'name': '2013-11-07', | ||
455 | 446 | 'rate': 10}, | ||
456 | 447 | {'name': '2013-11-08', | ||
457 | 448 | 'rate': 20}]} | ||
458 | 449 | mapper = ObjectMapper(env) | ||
459 | 450 | map_record = mapper.map_record(record) | ||
460 | 451 | expected = {'name': 'SO1', | ||
461 | 452 | 'line_ids': [(0, 0, {'name': '2013-11-07', | ||
462 | 453 | 'rate': 20}), | ||
463 | 454 | (0, 0, {'name': '2013-11-08', | ||
464 | 455 | 'rate': 40})] | ||
465 | 456 | } | ||
466 | 457 | self.assertEqual(map_record.values(), expected) | ||
467 | 458 | expected = {'name': 'SO1', | ||
468 | 459 | 'line_ids': [(0, 0, {'name': '2013-11-07', | ||
469 | 460 | 'rate': 20, | ||
470 | 461 | 'test': .5}), | ||
471 | 462 | (0, 0, {'name': '2013-11-08', | ||
472 | 463 | 'rate': 40, | ||
473 | 464 | 'test': .5})] | ||
474 | 465 | } | ||
475 | 466 | self.assertEqual(map_record.values(only_create=True), expected) | ||
476 | 187 | 467 | ||
477 | 188 | def test_mapping_record_children(self): | 468 | def test_mapping_record_children(self): |
480 | 189 | """ Map a record with children and check the result """ | 469 | """ Map a record with children, using defined MapChild """ |
481 | 190 | 470 | ||
482 | 471 | backend = Backend('backend', '42') | ||
483 | 472 | |||
484 | 473 | @backend | ||
485 | 191 | class LineMapper(ImportMapper): | 474 | class LineMapper(ImportMapper): |
486 | 475 | _model_name = 'res.currency.rate' | ||
487 | 192 | direct = [('name', 'name')] | 476 | direct = [('name', 'name')] |
488 | 193 | 477 | ||
489 | 194 | @mapping | 478 | @mapping |
490 | 195 | def price(self, record): | 479 | def price(self, record): |
492 | 196 | return {'price': record['price'] * 2} | 480 | return {'rate': record['rate'] * 2} |
493 | 197 | 481 | ||
494 | 198 | @only_create | 482 | @only_create |
495 | 199 | @mapping | 483 | @mapping |
496 | 200 | def discount(self, record): | 484 | def discount(self, record): |
500 | 201 | return {'discount': .5} | 485 | return {'test': .5} |
501 | 202 | 486 | ||
502 | 203 | class SaleMapper(ImportMapper): | 487 | @backend |
503 | 488 | class SaleLineImportMapChild(ImportMapChild): | ||
504 | 489 | _model_name = 'res.currency.rate' | ||
505 | 490 | def _format_items(self, items_values): | ||
506 | 491 | return [('ABC', values) for values in items_values] | ||
507 | 492 | |||
508 | 493 | |||
509 | 494 | @backend | ||
510 | 495 | class ObjectMapper(ImportMapper): | ||
511 | 496 | _model_name = 'res.currency' | ||
512 | 204 | 497 | ||
513 | 205 | direct = [('name', 'name')] | 498 | direct = [('name', 'name')] |
514 | 206 | 499 | ||
521 | 207 | children = [('lines', 'line_ids', 'sale.order.line')] | 500 | children = [('lines', 'line_ids', 'res.currency.rate')] |
522 | 208 | 501 | ||
523 | 209 | def _init_child_mapper(self, model_name): | 502 | |
524 | 210 | return LineMapper(self.environment) | 503 | backend_record = mock.Mock() |
525 | 211 | 504 | backend_record.get_backend.side_effect = lambda *a: backend | |
526 | 212 | env = mock.MagicMock() | 505 | env = Environment(backend_record, self.session, 'res.currency') |
527 | 213 | 506 | ||
528 | 214 | record = {'name': 'SO1', | 507 | record = {'name': 'SO1', |
551 | 215 | 'lines': [{'name': 'Mango', | 508 | 'lines': [{'name': '2013-11-07', |
552 | 216 | 'price': 10}, | 509 | 'rate': 10}, |
553 | 217 | {'name': 'Pumpkin', | 510 | {'name': '2013-11-08', |
554 | 218 | 'price': 20}]} | 511 | 'rate': 20}]} |
555 | 219 | mapper = SaleMapper(env) | 512 | mapper = ObjectMapper(env) |
556 | 220 | mapper.convert(record) | 513 | map_record = mapper.map_record(record) |
557 | 221 | expected = {'name': 'SO1', | 514 | expected = {'name': 'SO1', |
558 | 222 | 'line_ids': [(0, 0, {'name': 'Mango', | 515 | 'line_ids': [('ABC', {'name': '2013-11-07', |
559 | 223 | 'price': 20}), | 516 | 'rate': 20}), |
560 | 224 | (0, 0, {'name': 'Pumpkin', | 517 | ('ABC', {'name': '2013-11-08', |
561 | 225 | 'price': 40})] | 518 | 'rate': 40})] |
562 | 226 | } | 519 | } |
563 | 227 | self.assertEqual(mapper.data, expected) | 520 | self.assertEqual(map_record.values(), expected) |
564 | 228 | expected = {'name': 'SO1', | 521 | expected = {'name': 'SO1', |
565 | 229 | 'line_ids': [(0, 0, {'name': 'Mango', | 522 | 'line_ids': [('ABC', {'name': '2013-11-07', |
566 | 230 | 'price': 20, | 523 | 'rate': 20, |
567 | 231 | 'discount': .5}), | 524 | 'test': .5}), |
568 | 232 | (0, 0, {'name': 'Pumpkin', | 525 | ('ABC', {'name': '2013-11-08', |
569 | 233 | 'price': 40, | 526 | 'rate': 40, |
570 | 234 | 'discount': .5})] | 527 | 'test': .5})] |
571 | 235 | } | 528 | } |
572 | 236 | self.assertEqual(mapper.data_for_create, expected) | 529 | self.assertEqual(map_record.values(only_create=True), expected) |
573 | 237 | 530 | ||
574 | === modified file 'connector/unit/mapper.py' | |||
575 | --- connector/unit/mapper.py 2013-10-28 15:17:17 +0000 | |||
576 | +++ connector/unit/mapper.py 2013-11-08 11:46:46 +0000 | |||
577 | @@ -21,9 +21,10 @@ | |||
578 | 21 | 21 | ||
579 | 22 | import logging | 22 | import logging |
580 | 23 | from collections import namedtuple | 23 | from collections import namedtuple |
581 | 24 | from contextlib import contextmanager | ||
582 | 24 | 25 | ||
583 | 25 | from ..connector import ConnectorUnit, MetaConnectorUnit, Environment | 26 | from ..connector import ConnectorUnit, MetaConnectorUnit, Environment |
585 | 26 | from ..exception import MappingError | 27 | from ..exception import MappingError, NoConnectorUnitError |
586 | 27 | 28 | ||
587 | 28 | _logger = logging.getLogger(__name__) | 29 | _logger = logging.getLogger(__name__) |
588 | 29 | 30 | ||
589 | @@ -60,6 +61,142 @@ | |||
590 | 60 | return func | 61 | return func |
591 | 61 | 62 | ||
592 | 62 | 63 | ||
593 | 64 | def none(field): | ||
594 | 65 | """ A modifier intended to be used on the ``direct`` mappings. | ||
595 | 66 | |||
596 | 67 | Replace the False-ish values by None. | ||
597 | 68 | It can be used in a pipeline of modifiers when . | ||
598 | 69 | |||
599 | 70 | Example:: | ||
600 | 71 | |||
601 | 72 | direct = [(none('source'), 'target'), | ||
602 | 73 | (none(m2o_to_backend('rel_id'), 'rel_id')] | ||
603 | 74 | |||
604 | 75 | :param field: name of the source field in the record | ||
605 | 76 | :param binding: True if the relation is a binding record | ||
606 | 77 | """ | ||
607 | 78 | def modifier(self, record, to_attr): | ||
608 | 79 | if callable(field): | ||
609 | 80 | result = field(self, record, to_attr) | ||
610 | 81 | else: | ||
611 | 82 | result = record[field] | ||
612 | 83 | if not result: | ||
613 | 84 | return None | ||
614 | 85 | return result | ||
615 | 86 | return modifier | ||
616 | 87 | |||
617 | 88 | |||
618 | 89 | def convert(field, conv_type): | ||
619 | 90 | """ A modifier intended to be used on the ``direct`` mappings. | ||
620 | 91 | |||
621 | 92 | Convert a field's value to a given type. | ||
622 | 93 | |||
623 | 94 | Example:: | ||
624 | 95 | |||
625 | 96 | direct = [(convert('source', str), 'target')] | ||
626 | 97 | |||
627 | 98 | :param field: name of the source field in the record | ||
628 | 99 | :param binding: True if the relation is a binding record | ||
629 | 100 | """ | ||
630 | 101 | def modifier(self, record, to_attr): | ||
631 | 102 | value = record[field] | ||
632 | 103 | if not value: | ||
633 | 104 | return False | ||
634 | 105 | return conv_type(value) | ||
635 | 106 | return modifier | ||
636 | 107 | |||
637 | 108 | |||
638 | 109 | def m2o_to_backend(field, binding=None): | ||
639 | 110 | """ A modifier intended to be used on the ``direct`` mappings. | ||
640 | 111 | |||
641 | 112 | For a many2one, get the ID on the backend and returns it. | ||
642 | 113 | |||
643 | 114 | When the field's relation is not a binding (i.e. it does not point to | ||
644 | 115 | something like ``magento.*``), the binding model needs to be provided | ||
645 | 116 | in the ``binding`` keyword argument. | ||
646 | 117 | |||
647 | 118 | Example:: | ||
648 | 119 | |||
649 | 120 | direct = [(m2o_to_backend('country_id', binding='magento.res.country'), | ||
650 | 121 | 'country'), | ||
651 | 122 | (m2o_to_backend('magento_country_id'), 'country')] | ||
652 | 123 | |||
653 | 124 | :param field: name of the source field in the record | ||
654 | 125 | :param binding: name of the binding model is the relation is not a binding | ||
655 | 126 | """ | ||
656 | 127 | def modifier(self, record, to_attr): | ||
657 | 128 | if not record[field]: | ||
658 | 129 | return False | ||
659 | 130 | column = self.model._all_columns[field].column | ||
660 | 131 | if column._type != 'many2one': | ||
661 | 132 | raise ValueError('The column %s should be a many2one, got %s' % | ||
662 | 133 | field, column._type) | ||
663 | 134 | rel_id = record[field].id | ||
664 | 135 | if binding is None: | ||
665 | 136 | binding_model = column._obj | ||
666 | 137 | else: | ||
667 | 138 | binding_model = binding | ||
668 | 139 | binder = self.get_binder_for_model(binding_model) | ||
669 | 140 | # if a relation is not a binding, we wrap the record in the | ||
670 | 141 | # binding, we'll return the id of the binding | ||
671 | 142 | wrap = bool(binding) | ||
672 | 143 | value = binder.to_backend(rel_id, wrap=wrap) | ||
673 | 144 | if not value: | ||
674 | 145 | raise MappingError("Can not find an external id for record " | ||
675 | 146 | "%s in model %s %s wrapping" % | ||
676 | 147 | (rel_id, binding_model, | ||
677 | 148 | 'with' if wrap else 'without')) | ||
678 | 149 | return value | ||
679 | 150 | return modifier | ||
680 | 151 | |||
681 | 152 | |||
682 | 153 | def backend_to_m2o(field, binding=None, with_inactive=False): | ||
683 | 154 | """ A modifier intended to be used on the ``direct`` mappings. | ||
684 | 155 | |||
685 | 156 | For a field from a backend which is an ID, search the corresponding | ||
686 | 157 | binding in OpenERP and returns its ID. | ||
687 | 158 | |||
688 | 159 | When the field's relation is not a binding (i.e. it does not point to | ||
689 | 160 | something like ``magento.*``), the binding model needs to be provided | ||
690 | 161 | in the ``binding`` keyword argument. | ||
691 | 162 | |||
692 | 163 | Example:: | ||
693 | 164 | |||
694 | 165 | direct = [(backend_to_m2o('country', binding='magento.res.country'), | ||
695 | 166 | 'country_id'), | ||
696 | 167 | (backend_to_m2o('country'), 'magento_country_id')] | ||
697 | 168 | |||
698 | 169 | :param field: name of the source field in the record | ||
699 | 170 | :param binding: name of the binding model is the relation is not a binding | ||
700 | 171 | :param with_inactive: include the inactive records in OpenERP in the search | ||
701 | 172 | """ | ||
702 | 173 | def modifier(self, record, to_attr): | ||
703 | 174 | if not record[field]: | ||
704 | 175 | return False | ||
705 | 176 | column = self.model._all_columns[to_attr].column | ||
706 | 177 | if column._type != 'many2one': | ||
707 | 178 | raise ValueError('The column %s should be a many2one, got %s' % | ||
708 | 179 | to_attr, column._type) | ||
709 | 180 | rel_id = record[field] | ||
710 | 181 | if binding is None: | ||
711 | 182 | binding_model = column._obj | ||
712 | 183 | else: | ||
713 | 184 | binding_model = binding | ||
714 | 185 | binder = self.get_binder_for_model(binding_model) | ||
715 | 186 | # if we want the ID of a normal record, not a binding, | ||
716 | 187 | # we ask the unwrapped id to the binder | ||
717 | 188 | unwrap = bool(binding) | ||
718 | 189 | with self.session.change_context({'active_test': False}): | ||
719 | 190 | value = binder.to_openerp(rel_id, unwrap=unwrap) | ||
720 | 191 | if not value: | ||
721 | 192 | raise MappingError("Can not find an existing %s for external " | ||
722 | 193 | "record %s %s unwrapping" % | ||
723 | 194 | (binding_model, rel_id, | ||
724 | 195 | 'with' if unwrap else 'without')) | ||
725 | 196 | return value | ||
726 | 197 | return modifier | ||
727 | 198 | |||
728 | 199 | |||
729 | 63 | MappingDefinition = namedtuple('MappingDefinition', | 200 | MappingDefinition = namedtuple('MappingDefinition', |
730 | 64 | ['changed_by', | 201 | ['changed_by', |
731 | 65 | 'only_create']) | 202 | 'only_create']) |
732 | @@ -105,8 +242,189 @@ | |||
733 | 105 | return cls | 242 | return cls |
734 | 106 | 243 | ||
735 | 107 | 244 | ||
736 | 245 | class MapChild(ConnectorUnit): | ||
737 | 246 | """ | ||
738 | 247 | |||
739 | 248 | """ | ||
740 | 249 | _model_name = None | ||
741 | 250 | |||
742 | 251 | def _child_mapper(self): | ||
743 | 252 | raise NotImplementedError | ||
744 | 253 | |||
745 | 254 | def _skip_convert_child(self, map_record): | ||
746 | 255 | """ Hook to implement in sub-classes when some child | ||
747 | 256 | records should be skipped. | ||
748 | 257 | |||
749 | 258 | The parent record is accessible in ``map_record``. | ||
750 | 259 | If it returns True, the current child record is skipped. | ||
751 | 260 | |||
752 | 261 | :param map_record: record that we are converting | ||
753 | 262 | :type map_record: :py:class:`MapRecord` | ||
754 | 263 | """ | ||
755 | 264 | return False | ||
756 | 265 | |||
757 | 266 | def get_items(self, items, parent, to_attr, options): | ||
758 | 267 | mapper = self._child_mapper() | ||
759 | 268 | mapped = [] | ||
760 | 269 | for item in items: | ||
761 | 270 | map_record = mapper.map_record(item, parent=parent) | ||
762 | 271 | if self._skip_convert_child(map_record): | ||
763 | 272 | continue | ||
764 | 273 | mapped.append(self._get_item_values(map_record, to_attr, options)) | ||
765 | 274 | return self._format_items(mapped) | ||
766 | 275 | |||
767 | 276 | def _get_item_values(self, map_record, to_attr, options): | ||
768 | 277 | """ Get the values from the child Mappers for the items. | ||
769 | 278 | |||
770 | 279 | It can be overridden for instance to: | ||
771 | 280 | |||
772 | 281 | * Change options | ||
773 | 282 | * Use a Binder to know if an item already exists to modify an | ||
774 | 283 | existing item, rather than to add it | ||
775 | 284 | |||
776 | 285 | :param map_record: record that we are converting | ||
777 | 286 | :type map_record: :py:class:`MapRecord` | ||
778 | 287 | :param to_attr: destination field (can be used for introspecting | ||
779 | 288 | the relation) | ||
780 | 289 | :type to_attr: str | ||
781 | 290 | :param options: dict of options, herited from the main mapper | ||
782 | 291 | """ | ||
783 | 292 | return map_record.values(options=options) | ||
784 | 293 | |||
785 | 294 | def _format_items(self, items_values): | ||
786 | 295 | """ Format the values of the items mapped from the child Mappers. | ||
787 | 296 | |||
788 | 297 | It can be overridden for instance to add the OpenERP | ||
789 | 298 | relationships commands ``(6, 0, [IDs])``, ... | ||
790 | 299 | |||
791 | 300 | As instance, it can be modified to handle update of existing | ||
792 | 301 | items: check if 'id' existings in values, use the ``(1, ID, | ||
793 | 302 | {values}``) command | ||
794 | 303 | |||
795 | 304 | :param items_values: list of values for the items to create | ||
796 | 305 | """ | ||
797 | 306 | return items_values | ||
798 | 307 | |||
799 | 308 | |||
800 | 309 | class ImportMapChild(MapChild): | ||
801 | 310 | |||
802 | 311 | def _child_mapper(self): | ||
803 | 312 | return self.get_connector_unit_for_model(ImportMapper, self.model._name) | ||
804 | 313 | |||
805 | 314 | def _format_items(self, items_values): | ||
806 | 315 | """ Format the values of the items mapped from the child Mappers. | ||
807 | 316 | |||
808 | 317 | It can be overridden for instance to add the OpenERP | ||
809 | 318 | relationships commands ``(6, 0, [IDs])``, ... | ||
810 | 319 | |||
811 | 320 | As instance, it can be modified to handle update of existing | ||
812 | 321 | items: check if 'id' existings in values, use the ``(1, ID, | ||
813 | 322 | {values}``) command | ||
814 | 323 | |||
815 | 324 | :param items_values: list of values for the items to create | ||
816 | 325 | """ | ||
817 | 326 | return [(0, 0, values) for values in items_values] | ||
818 | 327 | |||
819 | 328 | |||
820 | 329 | class ExportMapChild(MapChild): | ||
821 | 330 | |||
822 | 331 | def _child_mapper(self): | ||
823 | 332 | return self.get_connector_unit_for_model(ExportMapper, self.model._name) | ||
824 | 333 | |||
825 | 334 | |||
826 | 108 | class Mapper(ConnectorUnit): | 335 | class Mapper(ConnectorUnit): |
828 | 109 | """ Transform a record to a defined output """ | 336 | """ A Mapper translates an external record to an OpenERP record and |
829 | 337 | conversely. The output of a Mapper is a ``dict``. | ||
830 | 338 | |||
831 | 339 | 3 types of mappings are supported: | ||
832 | 340 | |||
833 | 341 | Direct Mappings | ||
834 | 342 | Example:: | ||
835 | 343 | |||
836 | 344 | direct = [('source', 'target')] | ||
837 | 345 | |||
838 | 346 | Here, the ``source`` field will be copied in the ``target`` field. | ||
839 | 347 | |||
840 | 348 | A modifier can be used in the source item. | ||
841 | 349 | The modifier will be applied to the source field before being | ||
842 | 350 | copied in the target field. | ||
843 | 351 | It should be a closure function respecting this idiom:: | ||
844 | 352 | |||
845 | 353 | def a_function(field): | ||
846 | 354 | ''' ``field`` is the name of the source field ''' | ||
847 | 355 | def modifier(self, record, to_attr): | ||
848 | 356 | ''' self is the current Mapper, | ||
849 | 357 | record is the current record to map, | ||
850 | 358 | to_attr is the target field''' | ||
851 | 359 | return record[field] | ||
852 | 360 | return modifier | ||
853 | 361 | |||
854 | 362 | And used like that:: | ||
855 | 363 | |||
856 | 364 | direct = [ | ||
857 | 365 | (a_function('source'), 'target'), | ||
858 | 366 | ] | ||
859 | 367 | |||
860 | 368 | A more concrete example of modifier:: | ||
861 | 369 | |||
862 | 370 | def convert(field, conv_type): | ||
863 | 371 | ''' Convert the source field to a defined ``conv_type`` | ||
864 | 372 | (ex. str) before returning it''' | ||
865 | 373 | def modifier(self, record, to_attr): | ||
866 | 374 | value = record[field] | ||
867 | 375 | if not value: | ||
868 | 376 | return None | ||
869 | 377 | return conv_type(value) | ||
870 | 378 | return modifier | ||
871 | 379 | |||
872 | 380 | And used like that:: | ||
873 | 381 | |||
874 | 382 | direct = [ | ||
875 | 383 | (convert('myfield', float), 'target_field'), | ||
876 | 384 | ] | ||
877 | 385 | |||
878 | 386 | More examples of modifiers: | ||
879 | 387 | |||
880 | 388 | * :py:func:`convert` | ||
881 | 389 | * :py:func:`m2o_to_backend` | ||
882 | 390 | * :py:func:`backend_to_m2o` | ||
883 | 391 | |||
884 | 392 | Method Mappings | ||
885 | 393 | A mapping method allows to execute arbitrary code and return one | ||
886 | 394 | or many fields:: | ||
887 | 395 | |||
888 | 396 | @mapping | ||
889 | 397 | def compute_state(self, record): | ||
890 | 398 | # compute some state, using the ``record`` or not | ||
891 | 399 | state = 'pending' | ||
892 | 400 | return {'state': state} | ||
893 | 401 | |||
894 | 402 | We can also specify that a mapping methods should be applied | ||
895 | 403 | only when an object is created, and never applied on further | ||
896 | 404 | updates:: | ||
897 | 405 | |||
898 | 406 | @only_create | ||
899 | 407 | @mapping | ||
900 | 408 | def default_warehouse(self, record): | ||
901 | 409 | # get default warehouse | ||
902 | 410 | warehouse_id = ... | ||
903 | 411 | return {'warehouse_id': warehouse_id} | ||
904 | 412 | |||
905 | 413 | Submappings | ||
906 | 414 | When a record contains sub-items, like the lines of a sales order, | ||
907 | 415 | we can convert the children using another Mapper:: | ||
908 | 416 | |||
909 | 417 | children = [('items', 'line_ids', LineMapper)] | ||
910 | 418 | |||
911 | 419 | Usage of a Mapper:: | ||
912 | 420 | |||
913 | 421 | mapper = Mapper(env) | ||
914 | 422 | map_record = mapper.map_record() | ||
915 | 423 | values = map_record.values() | ||
916 | 424 | values = map_record.values(only_create=True) | ||
917 | 425 | values = map_record.values(fields=['name', 'street']) | ||
918 | 426 | |||
919 | 427 | """ | ||
920 | 110 | 428 | ||
921 | 111 | __metaclass__ = MetaMapper | 429 | __metaclass__ = MetaMapper |
922 | 112 | 430 | ||
923 | @@ -118,6 +436,8 @@ | |||
924 | 118 | 436 | ||
925 | 119 | _map_methods = None | 437 | _map_methods = None |
926 | 120 | 438 | ||
927 | 439 | _map_child_class = None | ||
928 | 440 | |||
929 | 121 | def __init__(self, environment): | 441 | def __init__(self, environment): |
930 | 122 | """ | 442 | """ |
931 | 123 | 443 | ||
932 | @@ -125,191 +445,282 @@ | |||
933 | 125 | :type environment: :py:class:`connector.connector.Environment` | 445 | :type environment: :py:class:`connector.connector.Environment` |
934 | 126 | """ | 446 | """ |
935 | 127 | super(Mapper, self).__init__(environment) | 447 | super(Mapper, self).__init__(environment) |
942 | 128 | self._data = None | 448 | self._options = None |
937 | 129 | self._data_for_create = None | ||
938 | 130 | self._data_children = None | ||
939 | 131 | |||
940 | 132 | def _init_child_mapper(self, model_name): | ||
941 | 133 | raise NotImplementedError | ||
943 | 134 | 449 | ||
944 | 135 | def _after_mapping(self, result): | 450 | def _after_mapping(self, result): |
946 | 136 | return result | 451 | """ Mapper.convert_child() has been deprecated """ |
947 | 452 | raise DeprecationWarning('Mapper._after_mapping() has been deprecated, ' | ||
948 | 453 | 'use Mapper._finalize()') | ||
949 | 137 | 454 | ||
950 | 138 | def _map_direct(self, record, from_attr, to_attr): | 455 | def _map_direct(self, record, from_attr, to_attr): |
951 | 456 | """ Apply the ``direct`` mappings. | ||
952 | 457 | |||
953 | 458 | :param record: record to convert from a source to a target | ||
954 | 459 | :param from_attr: name of the source attribute or a callable | ||
955 | 460 | :type from_attr: callable | str | ||
956 | 461 | :param to_attr: name of the target attribute | ||
957 | 462 | :type to_attr: str | ||
958 | 463 | """ | ||
959 | 139 | raise NotImplementedError | 464 | raise NotImplementedError |
960 | 140 | 465 | ||
961 | 141 | def _map_children(self, record, attr, model): | 466 | def _map_children(self, record, attr, model): |
962 | 142 | raise NotImplementedError | 467 | raise NotImplementedError |
963 | 143 | 468 | ||
964 | 469 | def skip_convert_child(self, record, parent_values=None): | ||
965 | 470 | """ Hook to implement in sub-classes when some child | ||
966 | 471 | records should be skipped. | ||
967 | 472 | |||
968 | 473 | This method is only relevable for child mappers. | ||
969 | 474 | |||
970 | 475 | If it returns True, the current child record is skipped.""" | ||
971 | 476 | return False | ||
972 | 477 | |||
973 | 144 | @property | 478 | @property |
974 | 145 | def map_methods(self): | 479 | def map_methods(self): |
975 | 146 | for meth, definition in self._map_methods.iteritems(): | 480 | for meth, definition in self._map_methods.iteritems(): |
976 | 147 | yield getattr(self, meth), definition | 481 | yield getattr(self, meth), definition |
977 | 148 | 482 | ||
987 | 149 | def _convert(self, record, fields=None, parent_values=None): | 483 | def convert_child(self, record, parent_values=None): |
988 | 150 | if fields is None: | 484 | """ Mapper.convert_child() has been deprecated """ |
989 | 151 | fields = {} | 485 | raise DeprecationWarning('Mapper.convert_child() has been deprecated, ' |
990 | 152 | 486 | 'use Mapper.map_record() then ' | |
991 | 153 | self._data = {} | 487 | 'map_record.values() ') |
992 | 154 | self._data_for_create = {} | 488 | |
993 | 155 | self._data_children = {} | 489 | def convert(self, record, fields=None): |
994 | 156 | 490 | """ Mapper.convert() has been deprecated | |
995 | 157 | _logger.debug('converting record %s to model %s', record, self._model_name) | 491 | |
996 | 492 | The usage of a Mapper is now:: | ||
997 | 493 | |||
998 | 494 | map_record = Mapper(env).map_record(record) | ||
999 | 495 | values = map_record.values() | ||
1000 | 496 | |||
1001 | 497 | See :py:meth:`Mapper.map_record`, :py:meth:`MapRecord.values` | ||
1002 | 498 | |||
1003 | 499 | """ | ||
1004 | 500 | raise DeprecationWarning('Mapper.convert() has been deprecated, ' | ||
1005 | 501 | 'use Mapper.map_record() then ' | ||
1006 | 502 | 'map_record.values() ') | ||
1007 | 503 | |||
1008 | 504 | @property | ||
1009 | 505 | def data(self): | ||
1010 | 506 | """ Mapper.data has been deprecated | ||
1011 | 507 | |||
1012 | 508 | See :py:meth:`Mapper.map_record`, :py:meth:`MapRecord.values` | ||
1013 | 509 | |||
1014 | 510 | """ | ||
1015 | 511 | raise DeprecationWarning('Mapper.data has been deprecated, ' | ||
1016 | 512 | 'use Mapper.map_record() then ' | ||
1017 | 513 | 'map_record.values() ') | ||
1018 | 514 | |||
1019 | 515 | @property | ||
1020 | 516 | def data_for_create(self): | ||
1021 | 517 | """ Mapper.data has been deprecated | ||
1022 | 518 | |||
1023 | 519 | See :py:meth:`Mapper.map_record`, :py:meth:`MapRecord.values` | ||
1024 | 520 | """ | ||
1025 | 521 | raise DeprecationWarning('Mapper.data_for_create has been deprecated, ' | ||
1026 | 522 | 'use Mapper.map_record() then ' | ||
1027 | 523 | 'map_record.values() ') | ||
1028 | 524 | |||
1029 | 525 | def _map_child(self, map_record, from_attr, to_attr, model_name): | ||
1030 | 526 | assert self._map_child_class is not None, "_map_child_class required" | ||
1031 | 527 | child_records = map_record.source[from_attr] | ||
1032 | 528 | try: | ||
1033 | 529 | mapper_child = self.get_connector_unit_for_model( | ||
1034 | 530 | self._map_child_class, model_name) | ||
1035 | 531 | except NoConnectorUnitError: | ||
1036 | 532 | # does not force developers to use a MapChild -> | ||
1037 | 533 | # will use the default one if not explicitely defined | ||
1038 | 534 | env = Environment(self.backend_record, | ||
1039 | 535 | self.session, | ||
1040 | 536 | model_name) | ||
1041 | 537 | mapper_child = self._map_child_class(env) | ||
1042 | 538 | items = mapper_child.get_items(child_records, map_record, | ||
1043 | 539 | to_attr, options=self.options) | ||
1044 | 540 | return items | ||
1045 | 541 | |||
1046 | 542 | @contextmanager | ||
1047 | 543 | def _mapping_options(self, options): | ||
1048 | 544 | """ Change the mapping options for the Mapper. | ||
1049 | 545 | |||
1050 | 546 | Context Manager to use in order to alter the behavior | ||
1051 | 547 | of the mapping, when using ``_apply`` or ``finalize``. | ||
1052 | 548 | |||
1053 | 549 | """ | ||
1054 | 550 | current = self._options | ||
1055 | 551 | self._options = options | ||
1056 | 552 | yield | ||
1057 | 553 | self._options = current | ||
1058 | 554 | |||
1059 | 555 | @property | ||
1060 | 556 | def options(self): | ||
1061 | 557 | """ Should not be modified manually, only with _mapping_options. | ||
1062 | 558 | |||
1063 | 559 | Options can be accessed in the mapping methods. | ||
1064 | 560 | |||
1065 | 561 | """ | ||
1066 | 562 | return self._options | ||
1067 | 563 | |||
1068 | 564 | def map_record(self, record, parent=None): | ||
1069 | 565 | """ Get a MapRecord with record, ready to be converted using the | ||
1070 | 566 | current Mapper. | ||
1071 | 567 | |||
1072 | 568 | :param record: record to transform | ||
1073 | 569 | """ | ||
1074 | 570 | return MapRecord(self, record, parent=parent) | ||
1075 | 571 | |||
1076 | 572 | def _apply(self, map_record, options=None): | ||
1077 | 573 | if options is None: | ||
1078 | 574 | options = {} | ||
1079 | 575 | with self._mapping_options(options): | ||
1080 | 576 | return self._apply_with_options(map_record) | ||
1081 | 577 | |||
1082 | 578 | def _apply_with_options(self, map_record): | ||
1083 | 579 | assert self.options is not None, ( | ||
1084 | 580 | "options should be defined with '_mapping_options'") | ||
1085 | 581 | _logger.debug('converting record %s to model %s', | ||
1086 | 582 | map_record.source, self.model._name) | ||
1087 | 583 | |||
1088 | 584 | fields = self.options.get('fields') | ||
1089 | 585 | only_create = self.options.get('only_create') | ||
1090 | 586 | result = {} | ||
1091 | 158 | for from_attr, to_attr in self.direct: | 587 | for from_attr, to_attr in self.direct: |
1092 | 159 | if (not fields or from_attr in fields): | 588 | if (not fields or from_attr in fields): |
1097 | 160 | # XXX not compatible with all | 589 | value = self._map_direct(map_record.source, |
1094 | 161 | # record type (wrap | ||
1095 | 162 | # records in a standard class representation?) | ||
1096 | 163 | value = self._map_direct(record, | ||
1098 | 164 | from_attr, | 590 | from_attr, |
1099 | 165 | to_attr) | 591 | to_attr) |
1101 | 166 | self._data[to_attr] = value | 592 | result[to_attr] = value |
1102 | 167 | 593 | ||
1103 | 168 | for meth, definition in self.map_methods: | 594 | for meth, definition in self.map_methods: |
1104 | 169 | changed_by = definition.changed_by | 595 | changed_by = definition.changed_by |
1105 | 170 | if (not fields or not changed_by or | 596 | if (not fields or not changed_by or |
1106 | 171 | changed_by.intersection(fields)): | 597 | changed_by.intersection(fields)): |
1108 | 172 | values = meth(record) | 598 | values = meth(map_record.source) |
1109 | 173 | if not values: | 599 | if not values: |
1110 | 174 | continue | 600 | continue |
1111 | 175 | if not isinstance(values, dict): | 601 | if not isinstance(values, dict): |
1112 | 176 | raise ValueError('%s: invalid return value for the ' | 602 | raise ValueError('%s: invalid return value for the ' |
1113 | 177 | 'mapping method %s' % (values, meth)) | 603 | 'mapping method %s' % (values, meth)) |
1118 | 178 | if definition.only_create: | 604 | if not definition.only_create or only_create: |
1119 | 179 | self._data_for_create.update(values) | 605 | result.update(values) |
1116 | 180 | else: | ||
1117 | 181 | self._data.update(values) | ||
1120 | 182 | 606 | ||
1121 | 183 | for from_attr, to_attr, model_name in self.children: | 607 | for from_attr, to_attr, model_name in self.children: |
1122 | 184 | if (not fields or from_attr in fields): | 608 | if (not fields or from_attr in fields): |
1197 | 185 | self._map_child(record, from_attr, to_attr, model_name) | 609 | result[to_attr] = self._map_child(map_record, from_attr, |
1198 | 186 | 610 | to_attr, model_name) | |
1199 | 187 | def skip_convert_child(self, record, parent_values=None): | 611 | |
1200 | 188 | """ Hook to implement in sub-classes when some child | 612 | return self._finalize(map_record, result) |
1201 | 189 | records should be skipped. | 613 | |
1202 | 190 | 614 | def _finalize(self, map_record, values): | |
1203 | 191 | This method is only relevable for child mappers. | 615 | """ Called at the end of the mapping. Can be used to |
1204 | 192 | 616 | modify the values before returning them. | |
1205 | 193 | If it returns True, the current child record is skipped.""" | 617 | |
1206 | 194 | return False | 618 | :param map_record: source map_record |
1207 | 195 | 619 | :type map_record: :py:class:`MapRecord` | |
1208 | 196 | def convert_child(self, record, parent_values=None): | 620 | :param values: mapped values |
1209 | 197 | """ Transform child row contained in a main record, only | 621 | :returns: mapped values |
1210 | 198 | called from another Mapper. | 622 | :rtype: dict |
1211 | 199 | 623 | """ | |
1212 | 200 | :param parent_values: openerp record of the containing object | 624 | return values |
1139 | 201 | (e.g. sale_order for a sale_order_line) | ||
1140 | 202 | """ | ||
1141 | 203 | self._convert(record, parent_values=parent_values) | ||
1142 | 204 | |||
1143 | 205 | def convert(self, record, fields=None): | ||
1144 | 206 | """ Transform an external record to an OpenERP record or the opposite | ||
1145 | 207 | |||
1146 | 208 | Sometimes we want to map values only when we create the records. | ||
1147 | 209 | The mapping methods have to be decorated with ``only_create`` to | ||
1148 | 210 | map values only for creation of records. | ||
1149 | 211 | |||
1150 | 212 | :param record: record to transform | ||
1151 | 213 | :param fields: list of fields to convert, if empty, all fields | ||
1152 | 214 | are converted | ||
1153 | 215 | """ | ||
1154 | 216 | self._convert(record, fields=fields) | ||
1155 | 217 | |||
1156 | 218 | @property | ||
1157 | 219 | def data(self): | ||
1158 | 220 | """ Returns a dict for a record processed by | ||
1159 | 221 | :py:meth:`~_convert` """ | ||
1160 | 222 | if self._data is None: | ||
1161 | 223 | raise ValueError('Mapper.convert should be called before ' | ||
1162 | 224 | 'accessing the data') | ||
1163 | 225 | result = self._data.copy() | ||
1164 | 226 | for attr, mappers in self._data_children.iteritems(): | ||
1165 | 227 | child_data = [mapper.data for mapper in mappers] | ||
1166 | 228 | if child_data: | ||
1167 | 229 | result[attr] = self._format_child_rows(child_data) | ||
1168 | 230 | return self._after_mapping(result) | ||
1169 | 231 | |||
1170 | 232 | @property | ||
1171 | 233 | def data_for_create(self): | ||
1172 | 234 | """ Returns a dict for a record processed by | ||
1173 | 235 | :py:meth:`~_convert` to use only for creation of the record. """ | ||
1174 | 236 | if self._data is None: | ||
1175 | 237 | raise ValueError('Mapper.convert should be called before ' | ||
1176 | 238 | 'accessing the data') | ||
1177 | 239 | result = self._data.copy() | ||
1178 | 240 | result.update(self._data_for_create) | ||
1179 | 241 | for attr, mappers in self._data_children.iteritems(): | ||
1180 | 242 | child_data = [mapper.data_for_create for mapper in mappers] | ||
1181 | 243 | if child_data: | ||
1182 | 244 | result[attr] = self._format_child_rows(child_data) | ||
1183 | 245 | return self._after_mapping(result) | ||
1184 | 246 | |||
1185 | 247 | def _format_child_rows(self, child_records): | ||
1186 | 248 | return child_records | ||
1187 | 249 | |||
1188 | 250 | def _map_child(self, record, from_attr, to_attr, model_name): | ||
1189 | 251 | child_records = record[from_attr] | ||
1190 | 252 | self._data_children[to_attr] = [] | ||
1191 | 253 | for child_record in child_records: | ||
1192 | 254 | mapper = self._init_child_mapper(model_name) | ||
1193 | 255 | if mapper.skip_convert_child(child_record, parent_values=record): | ||
1194 | 256 | continue | ||
1195 | 257 | mapper.convert_child(child_record, parent_values=record) | ||
1196 | 258 | self._data_children[to_attr].append(mapper) | ||
1213 | 259 | 625 | ||
1214 | 260 | 626 | ||
1215 | 261 | class ImportMapper(Mapper): | 627 | class ImportMapper(Mapper): |
1216 | 262 | """ Transform a record from a backend to an OpenERP record """ | 628 | """ Transform a record from a backend to an OpenERP record """ |
1217 | 263 | 629 | ||
1218 | 630 | _map_child_class = ImportMapChild | ||
1219 | 631 | |||
1220 | 264 | def _map_direct(self, record, from_attr, to_attr): | 632 | def _map_direct(self, record, from_attr, to_attr): |
1221 | 633 | """ Apply the ``direct`` mappings. | ||
1222 | 634 | |||
1223 | 635 | :param record: record to convert from a source to a target | ||
1224 | 636 | :param from_attr: name of the source attribute or a callable | ||
1225 | 637 | :type from_attr: callable | str | ||
1226 | 638 | :param to_attr: name of the target attribute | ||
1227 | 639 | :type to_attr: str | ||
1228 | 640 | """ | ||
1229 | 641 | if callable(from_attr): | ||
1230 | 642 | return from_attr(self, record, to_attr) | ||
1231 | 643 | |||
1232 | 265 | value = record.get(from_attr) | 644 | value = record.get(from_attr) |
1233 | 266 | if not value: | 645 | if not value: |
1234 | 267 | return False | 646 | return False |
1235 | 268 | 647 | ||
1236 | 648 | # may be replaced by the explicit backend_to_m2o | ||
1237 | 269 | column = self.model._all_columns[to_attr].column | 649 | column = self.model._all_columns[to_attr].column |
1238 | 270 | if column._type == 'many2one': | 650 | if column._type == 'many2one': |
1247 | 271 | rel_id = record[from_attr] | 651 | mapping = backend_to_m2o(from_attr, binding=True) |
1248 | 272 | model_name = column._obj | 652 | value = mapping(self, record, to_attr) |
1241 | 273 | binder = self.get_binder_for_model(model_name) | ||
1242 | 274 | value = binder.to_openerp(rel_id) | ||
1243 | 275 | |||
1244 | 276 | if not value: | ||
1245 | 277 | raise MappingError("Can not find an existing %s for external " | ||
1246 | 278 | "record %s" % (model_name, rel_id)) | ||
1249 | 279 | return value | 653 | return value |
1250 | 280 | 654 | ||
1251 | 281 | def _init_child_mapper(self, model_name): | ||
1252 | 282 | env = Environment(self.backend_record, | ||
1253 | 283 | self.session, | ||
1254 | 284 | model_name) | ||
1255 | 285 | return env.get_connector_unit(ImportMapper) | ||
1256 | 286 | |||
1257 | 287 | def _format_child_rows(self, child_records): | ||
1258 | 288 | return [(0, 0, data) for data in child_records] | ||
1259 | 289 | |||
1260 | 290 | 655 | ||
1261 | 291 | class ExportMapper(Mapper): | 656 | class ExportMapper(Mapper): |
1262 | 292 | """ Transform a record from OpenERP to a backend record """ | 657 | """ Transform a record from OpenERP to a backend record """ |
1263 | 293 | 658 | ||
1264 | 659 | _map_child_class = ExportMapChild | ||
1265 | 660 | |||
1266 | 294 | def _map_direct(self, record, from_attr, to_attr): | 661 | def _map_direct(self, record, from_attr, to_attr): |
1267 | 662 | """ Apply the ``direct`` mappings. | ||
1268 | 663 | |||
1269 | 664 | :param record: record to convert from a source to a target | ||
1270 | 665 | :param from_attr: name of the source attribute or a callable | ||
1271 | 666 | :type from_attr: callable | str | ||
1272 | 667 | :param to_attr: name of the target attribute | ||
1273 | 668 | :type to_attr: str | ||
1274 | 669 | """ | ||
1275 | 670 | if callable(from_attr): | ||
1276 | 671 | return from_attr(self, record, to_attr) | ||
1277 | 672 | |||
1278 | 295 | value = record[from_attr] | 673 | value = record[from_attr] |
1279 | 296 | if not value: | 674 | if not value: |
1280 | 297 | return False | 675 | return False |
1281 | 298 | 676 | ||
1282 | 677 | # may be replaced by the explicit m2o_to_backend | ||
1283 | 299 | column = self.model._all_columns[from_attr].column | 678 | column = self.model._all_columns[from_attr].column |
1284 | 300 | if column._type == 'many2one': | 679 | if column._type == 'many2one': |
1293 | 301 | rel_id = record[from_attr].id | 680 | mapping = m2o_to_backend(from_attr, binding=True) |
1294 | 302 | model_name = column._obj | 681 | value = mapping(self, record, to_attr) |
1287 | 303 | binder = self.get_binder_for_model(model_name) | ||
1288 | 304 | value = binder.to_backend(rel_id) | ||
1289 | 305 | |||
1290 | 306 | if not value: | ||
1291 | 307 | raise MappingError("Can not find an external id for record " | ||
1292 | 308 | "%s in model %s" % (rel_id, model_name)) | ||
1295 | 309 | return value | 682 | return value |
1296 | 310 | 683 | ||
1302 | 311 | def _init_child_mapper(self, model_name): | 684 | |
1303 | 312 | env = Environment(self.backend_record, | 685 | class MapRecord(object): |
1304 | 313 | self.session, | 686 | |
1305 | 314 | model_name) | 687 | def __init__(self, mapper, source, parent=None): |
1306 | 315 | return env.get_connector_unit(ExportMapper) | 688 | self._source = source |
1307 | 689 | self._mapper = mapper | ||
1308 | 690 | self._parent = parent | ||
1309 | 691 | self._forced_values = {} | ||
1310 | 692 | |||
1311 | 693 | @property | ||
1312 | 694 | def source(self): | ||
1313 | 695 | return self._source | ||
1314 | 696 | |||
1315 | 697 | @property | ||
1316 | 698 | def parent(self): | ||
1317 | 699 | return self._parent | ||
1318 | 700 | |||
1319 | 701 | def values(self, only_create=None, fields=None, options=None): | ||
1320 | 702 | """ | ||
1321 | 703 | |||
1322 | 704 | Sometimes we want to map values only for creation the records. | ||
1323 | 705 | The mapping methods have to be decorated with ``only_create`` to | ||
1324 | 706 | map values only for creation of records. Then, we must give the | ||
1325 | 707 | ``only_create=True`` argument to ``values()``. | ||
1326 | 708 | |||
1327 | 709 | """ | ||
1328 | 710 | if options is None: | ||
1329 | 711 | options = {} | ||
1330 | 712 | if only_create is not None: | ||
1331 | 713 | options['only_create'] = only_create | ||
1332 | 714 | if fields is not None: | ||
1333 | 715 | options['fields'] = fields | ||
1334 | 716 | values = self._mapper._apply(self, options=options) | ||
1335 | 717 | values.update(self._forced_values) | ||
1336 | 718 | return values | ||
1337 | 719 | |||
1338 | 720 | def update(self, *args, **kwargs): | ||
1339 | 721 | if args: | ||
1340 | 722 | assert len(args) == 1, 'dict expected, got: %s' % args | ||
1341 | 723 | assert isinstance(args[0], dict), 'dict expected, got %s' % args | ||
1342 | 724 | self._forced_values.update(args[0]) | ||
1343 | 725 | if kwargs: | ||
1344 | 726 | self._forced_values.update(kwargs) |