1 | # -*- coding: utf-8 -*- |
||
2 | |||
3 | from datetime import datetime, timedelta |
||
4 | from itertools import groupby |
||
5 | |||
6 | from django.db.models import ObjectDoesNotExist |
||
7 | from django.db.models.fields.related import ForeignKey |
||
8 | from django.utils.translation import gettext_lazy as _ |
||
9 | |||
10 | SECONDS_PER_MIN = 60 |
||
11 | SECONDS_PER_HOUR = 3600 |
||
12 | SECONDS_PER_DAY = 86400 |
||
13 | |||
14 | # ## Data format conversion functions ### |
||
15 | |||
16 | |||
17 | def do_nothing(value): |
||
18 | return value |
||
19 | |||
20 | |||
21 | def to_str(value): |
||
22 | return value if value is None else str(value) |
||
23 | |||
24 | |||
25 | def datetime_to_str(value): |
||
26 | if value is None: |
||
27 | return value |
||
28 | return datetime.strftime(value, "%Y-%m-%d %H:%M:%S") |
||
29 | |||
30 | |||
31 | def timedelta_to_str(value): |
||
32 | if value is None: |
||
33 | return value |
||
34 | |||
35 | total_seconds = value.seconds + (value.days * SECONDS_PER_DAY) |
||
36 | hours = total_seconds / SECONDS_PER_HOUR |
||
37 | # minutes - Total seconds subtract the used hours |
||
38 | minutes = total_seconds / SECONDS_PER_MIN - \ |
||
39 | total_seconds / SECONDS_PER_HOUR * 60 |
||
40 | seconds = total_seconds % SECONDS_PER_MIN |
||
41 | return '%02i:%02i:%02i' % (hours, minutes, seconds) |
||
42 | |||
43 | |||
44 | # ## End of functions ### |
||
45 | |||
46 | |||
47 | class Serializer: |
||
48 | """ |
||
49 | Django XMLRPC Serializer |
||
50 | The goal is to process the datetime and timedelta data structure |
||
51 | that python xmlrpc.client can not handle. |
||
52 | |||
53 | How to use it: |
||
54 | # Model |
||
55 | m = Model.objects.get(pk = 1) |
||
56 | s = Serializer(model = m) |
||
57 | s.serialize() |
||
58 | |||
59 | Or |
||
60 | # QuerySet |
||
61 | q = Model.objects.all() |
||
62 | s = Serializer(queryset = q) |
||
63 | s.serialize() |
||
64 | """ |
||
65 | |||
66 | def __init__(self, queryset=None, model=None): |
||
67 | """Initial the class""" |
||
68 | if hasattr(queryset, '__iter__'): |
||
69 | self.queryset = queryset |
||
70 | return |
||
71 | if hasattr(model, '__dict__'): |
||
72 | self.model = model |
||
73 | return |
||
74 | |||
75 | raise TypeError("QuerySet(list) or Models(dictionary) is required") |
||
76 | |||
77 | def serialize_model(self): |
||
78 | """ |
||
79 | Check the fields of models and convert the data |
||
80 | |||
81 | Returns: Dictionary |
||
82 | """ |
||
83 | if not hasattr(self.model, '__dict__'): |
||
84 | raise TypeError("Models or Dictionary is required") |
||
85 | response = {} |
||
86 | opts = self.model._meta |
||
87 | for field in opts.local_fields: |
||
88 | # for a django model, retrieving a foreignkey field |
||
89 | # will fail when the field value isn't set |
||
90 | try: |
||
91 | value = getattr(self.model, field.name) |
||
92 | except ObjectDoesNotExist: |
||
93 | value = None |
||
94 | if isinstance(value, datetime): |
||
95 | value = datetime_to_str(value) |
||
96 | if isinstance(value, timedelta): |
||
97 | value = timedelta_to_str(value) |
||
98 | if isinstance(field, ForeignKey): |
||
99 | fk_id = "%s_id" % field.name |
||
100 | if value is None: |
||
101 | response[fk_id] = None |
||
102 | else: |
||
103 | response[fk_id] = getattr(self.model, fk_id) |
||
104 | value = str(value) |
||
105 | response[field.name] = value |
||
106 | for field in opts.local_many_to_many: |
||
107 | value = getattr(self.model, field.name) |
||
108 | value = value.values_list('pk', flat=True) |
||
109 | response[field.name] = list(value) |
||
110 | return response |
||
111 | |||
112 | def serialize_queryset(self): |
||
113 | """ |
||
114 | Check the fields of QuerySet and convert the data |
||
115 | |||
116 | Returns: List |
||
117 | """ |
||
118 | response = [] |
||
119 | for model in self.queryset: |
||
120 | self.model = model |
||
121 | model = self.serialize_model() |
||
122 | response.append(model) |
||
123 | |||
124 | del self.queryset |
||
125 | return response |
||
126 | |||
127 | |||
128 | def _get_single_field_related_object_pks(m2m_field_query, model_pk, field_name): |
||
129 | field_names = [] |
||
130 | for item in m2m_field_query[model_pk]: |
||
131 | if item[field_name]: |
||
132 | field_names.append(item[field_name]) |
||
133 | return field_names |
||
134 | |||
135 | |||
136 | def _get_related_object_pks(m2m_fields_query, model_pk, field_name): |
||
137 | """Return related object pks from query result via ManyToManyFields |
||
138 | |||
139 | Any object pk with value 0 or None values will be excluded in the final |
||
140 | list. |
||
141 | |||
142 | :param m2m_fields_query: the result returned from _query_m2m_fields |
||
143 | :type m2m_fields_query: dict |
||
144 | :param model_pk: whose object's related object pks will be retrieved |
||
145 | :type model_pk: int or long |
||
146 | :param field_name: field name of the related object |
||
147 | :type field_name: str |
||
148 | :return: list of related objects' pks |
||
149 | :rtype: list |
||
150 | """ |
||
151 | data = m2m_fields_query[field_name] |
||
152 | return _get_single_field_related_object_pks(data, model_pk, field_name) |
||
153 | |||
154 | |||
155 | def _serialize_names(row, values_fields_mapping): |
||
156 | """Replace name from ORM side to the serialization side as expected""" |
||
157 | new_serialized_data = {} |
||
158 | |||
159 | if not values_fields_mapping: |
||
160 | # If no fields mapping, just use the original row as the |
||
161 | # serialization result, and no data format conversion is |
||
162 | # required obviously |
||
163 | new_serialized_data.update(row) # pylint: disable=objects-update-used |
||
164 | return new_serialized_data |
||
165 | |||
166 | for orm_name, serialize_info in values_fields_mapping.items(): |
||
167 | serialize_name, conv_func = serialize_info |
||
168 | value = conv_func(row[orm_name]) |
||
169 | new_serialized_data[serialize_name] = value |
||
170 | |||
171 | return new_serialized_data |
||
172 | |||
173 | |||
174 | class QuerySetBasedRPCSerializer(Serializer): |
||
175 | """Serializer for TestPlan |
||
176 | |||
177 | To configure the serialization, developer can specify following class |
||
178 | attribute, values_fields_mapping, m2m_fields, and primary_key. |
||
179 | |||
180 | An unknown issue is that the primary key must appear in the |
||
181 | values_fields_mapping. If doesn't, error would happen. |
||
182 | """ |
||
183 | |||
184 | # Define the mapping relationship of names from ORM side to XMLRPC output |
||
185 | # side. |
||
186 | # Key is the name from ORM side. |
||
187 | # Value is the name from the the XMLRPC output side |
||
188 | values_fields_mapping = {} |
||
189 | |||
190 | # Define extra fields to allow provide extra fields in the serialization |
||
191 | # result beside valid fields in database. |
||
192 | extra_fields = {} |
||
193 | |||
194 | m2m_fields = () |
||
195 | |||
196 | def __init__(self, model_class, queryset): |
||
197 | super().__init__(model_class, queryset) |
||
198 | if model_class is None: |
||
199 | raise ValueError('model_class should not be None') |
||
200 | if queryset is None: |
||
201 | raise ValueError('queryset should not be None') |
||
202 | |||
203 | self.model_class = model_class |
||
204 | self.queryset = queryset |
||
205 | |||
206 | def get_extra_fields(self): |
||
207 | """Get definition of extra fields mappings |
||
208 | |||
209 | By default, user defined extra fields will be used. If not exist, an |
||
210 | empty extra fields mapping is returned as to do nothing. |
||
211 | |||
212 | This method can also be override in subclass to provide the extra |
||
213 | fields programatically. |
||
214 | """ |
||
215 | fields = getattr(self, 'extra_fields', None) |
||
216 | if fields is None: |
||
217 | fields = {} |
||
218 | return fields |
||
219 | |||
220 | def _get_values_fields_mapping(self): |
||
221 | """Return values fields mapping definition |
||
222 | |||
223 | Values fields mapping can be also provided by overriding this method in |
||
224 | subclass. |
||
225 | |||
226 | :return: the mapping defined in class if presents, otherwise an empty |
||
227 | dictionary object. |
||
228 | :rtype: dict |
||
229 | """ |
||
230 | return getattr(self.__class__, 'values_fields_mapping', {}) |
||
231 | |||
232 | def _get_values_fields(self): |
||
233 | """Return ORM side field names defined in the values fields mapping |
||
234 | |||
235 | :return: list of fields in the ORM side. If `values_fields_mapping` is |
||
236 | not defined in class, fields will be retrieved from coresponding Model |
||
237 | class. |
||
238 | :rtype: list |
||
239 | |||
240 | """ |
||
241 | values_fields_mapping = self._get_values_fields_mapping() |
||
242 | if values_fields_mapping: |
||
243 | return values_fields_mapping.keys() |
||
244 | |||
245 | field_names = [] |
||
246 | |||
247 | for field in self.model_class._meta.fields: |
||
248 | field_names.append(field.name) |
||
249 | |||
250 | return field_names |
||
251 | |||
252 | def _get_m2m_fields(self): |
||
253 | """Return names of fields with type ManyToManyField in ORM side |
||
254 | |||
255 | By default, field names will be retreived from `m2m_fields` defined in |
||
256 | class. If it does not present there, all fields with type |
||
257 | ManyToManyField will be inspected and return names of all of them. |
||
258 | |||
259 | Customized field names can be returned by overriding this method in |
||
260 | subclass. |
||
261 | |||
262 | :return: names of fields with type ManyToManyField |
||
263 | :rtype: list |
||
264 | """ |
||
265 | if self.m2m_fields: |
||
266 | return self.m2m_fields |
||
267 | |||
268 | return tuple(field.name for field in |
||
269 | self.model_class._meta.many_to_many) |
||
270 | |||
271 | def _get_primary_key_field(self): |
||
272 | """ |
||
273 | Return the primary key field name by inspecting Model's fields. |
||
274 | |||
275 | This method can be overrided in subclass to provide custom primary key. |
||
276 | |||
277 | :return: the name of primary key field |
||
278 | :rtype: str |
||
279 | :raises ValueError: if model does not have a primary key field during |
||
280 | the process of inspecting primary key from model's field. |
||
281 | """ |
||
282 | for field in self.model_class._meta.fields: |
||
283 | if field.primary_key: |
||
284 | return field.name |
||
285 | |||
286 | raise ValueError( |
||
287 | _('Model %s has no primary key. You have to specify such ' |
||
288 | 'field manually.') % self.model_class.__name__) |
||
289 | |||
290 | def _query_m2m_field(self, field_name): |
||
291 | """Query ManyToManyField order by model's pk |
||
292 | |||
293 | Return value's format: |
||
294 | { |
||
295 | object_pk1: ({'pk': object_pk1, 'field_name': related_object_pk1}, |
||
296 | {'pk': object_pk1, 'field_name': related_object_pk2}, |
||
297 | ), |
||
298 | object_pk2: ({'pk': object_pk2, 'field_name': related_object_pk3}, |
||
299 | {'pk': object_pk2, 'field_name': related_object_pk4}, |
||
300 | {'pk': object_pk3, 'field_name': related_object_pk5}, |
||
301 | ), |
||
302 | ... |
||
303 | } |
||
304 | |||
305 | :param str field_name: field name of a ManyToManyField |
||
306 | :return: dictionary mapping between model's pk and related object's pk |
||
307 | :rtype: dict |
||
308 | """ |
||
309 | qs = self.queryset.values('pk', field_name).order_by('pk') |
||
310 | return dict((pk, tuple(values)) for pk, values in |
||
311 | groupby(qs.iterator(), lambda item: item['pk'])) |
||
312 | |||
313 | def _query_m2m_fields(self): |
||
314 | m2m_fields = self._get_m2m_fields() |
||
315 | result = ((field_name, self._query_m2m_field(field_name)) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
316 | for field_name in m2m_fields) |
||
317 | return dict(result) |
||
318 | |||
319 | def _handle_extra_fields(self, data): |
||
320 | """Add extra fields |
||
321 | |||
322 | Currently, alias is supported. |
||
323 | |||
324 | - alias: add alias for any other serialized field name. If the |
||
325 | specified field name does not exist in serialization result, it |
||
326 | will be ignored. |
||
327 | """ |
||
328 | extra_fields = self.get_extra_fields() |
||
329 | |||
330 | for handle_name, value in extra_fields.items(): |
||
331 | if handle_name == 'alias': |
||
332 | for original_name, alias in value.items(): |
||
333 | if original_name in data: |
||
334 | data[alias] = data[original_name] |
||
335 | |||
336 | def serialize_queryset(self): |
||
337 | """Core of QuerySet based serialization |
||
338 | |||
339 | The process of serialization has following steps |
||
340 | |||
341 | - Get data from database using QuerySet.values method |
||
342 | - Transfer data to the output destiation according to serialization |
||
343 | standard, where two things must be done: |
||
344 | |||
345 | - field name must be replaced with right name rather than the |
||
346 | internal name used for SQL query |
||
347 | - some data must be converted in proper type. Currently, data with |
||
348 | type datetime.datetime and datetime.timedelta must be converted to |
||
349 | str (not UNICODE). |
||
350 | - During the process of the above transfer, data associated with |
||
351 | ManyToManyField should be retrieved from database and attached to |
||
352 | each serialized data object. |
||
353 | """ |
||
354 | queryset = self.queryset.values(*self._get_values_fields()) |
||
355 | primary_key_field = self._get_primary_key_field() |
||
356 | values_fields_mapping = self._get_values_fields_mapping() |
||
357 | m2m_fields = self._get_m2m_fields() |
||
358 | m2m_not_queried = True |
||
359 | serialize_result = [] |
||
360 | |||
361 | # Handle ManyToManyFields, add such fields' values to final |
||
362 | # serialization |
||
363 | for row in queryset.iterator(): |
||
364 | new_serialized_data = _serialize_names(row, values_fields_mapping) |
||
365 | |||
366 | # Attach values of each ManyToManyField field |
||
367 | # Lazy ManyToManyField query, to avoid query on ManyToManyFields if |
||
368 | # serialization data is empty from database. |
||
369 | if m2m_not_queried: |
||
370 | m2m_fields_query = self._query_m2m_fields() |
||
371 | m2m_not_queried = False |
||
372 | model_pk = row[primary_key_field] |
||
373 | for field_name in m2m_fields: |
||
374 | related_object_pks = _get_related_object_pks( |
||
375 | m2m_fields_query, model_pk, field_name) |
||
0 ignored issues
–
show
|
|||
376 | new_serialized_data[field_name] = related_object_pks |
||
377 | |||
378 | # Finally, there might be some extra fields to added to final JSON |
||
379 | # result to provide more custom information besides those data from |
||
380 | # database. Add such extra fields in various ways that developers |
||
381 | # define. This should be determined during the development |
||
382 | # according to requirement. |
||
383 | self._handle_extra_fields(new_serialized_data) |
||
384 | |||
385 | serialize_result.append(new_serialized_data) |
||
386 | |||
387 | return serialize_result |
||
388 | |||
389 | |||
390 | class TestPlanRPCSerializer(QuerySetBasedRPCSerializer): |
||
391 | """Serializer for TestPlan""" |
||
392 | |||
393 | values_fields_mapping = { |
||
394 | 'id': ('id', do_nothing), |
||
395 | 'create_date': ('create_date', datetime_to_str), |
||
396 | 'extra_link': ('extra_link', do_nothing), |
||
397 | 'is_active': ('is_active', do_nothing), |
||
398 | 'name': ('name', do_nothing), |
||
399 | 'text': ('text', do_nothing), |
||
400 | 'author': ('author_id', do_nothing), |
||
401 | 'author__username': ('author', to_str), |
||
402 | 'parent': ('parent_id', do_nothing), |
||
403 | 'parent__name': ('parent', do_nothing), |
||
404 | 'product': ('product_id', do_nothing), |
||
405 | 'product__name': ('product', do_nothing), |
||
406 | 'product_version': ('product_version_id', do_nothing), |
||
407 | 'product_version__value': ('product_version', do_nothing), |
||
408 | 'type': ('type_id', do_nothing), |
||
409 | 'type__name': ('type', do_nothing), |
||
410 | } |
||
411 | |||
412 | extra_fields = { |
||
413 | 'alias': {'product_version': 'default_product_version'}, |
||
414 | } |
||
415 | |||
416 | m2m_fields = ('case', 'tag') |
||
417 | |||
418 | |||
419 | class TestExecutionRPCSerializer(QuerySetBasedRPCSerializer): |
||
420 | """Serializer for TestExecution""" |
||
421 | |||
422 | values_fields_mapping = { |
||
423 | 'id': ('id', do_nothing), |
||
424 | 'case_text_version': ('case_text_version', do_nothing), |
||
425 | 'close_date': ('close_date', datetime_to_str), |
||
426 | 'sortkey': ('sortkey', do_nothing), |
||
427 | |||
428 | 'assignee': ('assignee_id', do_nothing), |
||
429 | 'assignee__username': ('assignee', to_str), |
||
430 | 'build': ('build_id', do_nothing), |
||
431 | 'build__name': ('build', do_nothing), |
||
432 | 'case': ('case_id', do_nothing), |
||
433 | 'case__summary': ('case', do_nothing), |
||
434 | 'status': ('status_id', do_nothing), |
||
435 | 'status__name': ('status', do_nothing), |
||
436 | 'run': ('run_id', do_nothing), |
||
437 | 'run__summary': ('run', do_nothing), |
||
438 | 'tested_by': ('tested_by_id', do_nothing), |
||
439 | 'tested_by__username': ('tested_by', to_str), |
||
440 | } |
||
441 | |||
442 | |||
443 | class TestRunRPCSerializer(QuerySetBasedRPCSerializer): |
||
444 | """Serializer for TestRun""" |
||
445 | |||
446 | values_fields_mapping = { |
||
447 | 'notes': ('notes', do_nothing), |
||
448 | 'id': ('id', do_nothing), |
||
449 | 'start_date': ('start_date', datetime_to_str), |
||
450 | 'stop_date': ('stop_date', datetime_to_str), |
||
451 | 'summary': ('summary', do_nothing), |
||
452 | |||
453 | 'build': ('build_id', do_nothing), |
||
454 | 'build__name': ('build', do_nothing), |
||
455 | 'default_tester': ('default_tester_id', do_nothing), |
||
456 | 'default_tester__username': ('default_tester', to_str), |
||
457 | 'manager': ('manager_id', do_nothing), |
||
458 | 'manager__username': ('manager', to_str), |
||
459 | 'plan': ('plan_id', do_nothing), |
||
460 | 'plan__name': ('plan', do_nothing), |
||
461 | 'product_version': ('product_version_id', do_nothing), |
||
462 | 'product_version__value': ('product_version', do_nothing), |
||
463 | } |
||
464 | |||
465 | |||
466 | class TestCaseRPCSerializer(QuerySetBasedRPCSerializer): |
||
467 | """Serializer for TestCase""" |
||
468 | |||
469 | values_fields_mapping = { |
||
470 | 'arguments': ('arguments', do_nothing), |
||
471 | 'id': ('id', do_nothing), |
||
472 | 'create_date': ('create_date', datetime_to_str), |
||
473 | 'extra_link': ('extra_link', do_nothing), |
||
474 | 'is_automated': ('is_automated', do_nothing), |
||
475 | 'notes': ('notes', do_nothing), |
||
476 | 'text': ('text', do_nothing), |
||
477 | 'requirement': ('requirement', do_nothing), |
||
478 | 'script': ('script', do_nothing), |
||
479 | 'summary': ('summary', do_nothing), |
||
480 | |||
481 | 'author': ('author_id', do_nothing), |
||
482 | 'author__username': ('author', to_str), |
||
483 | 'case_status': ('case_status_id', do_nothing), |
||
484 | 'case_status__name': ('case_status', do_nothing), |
||
485 | 'category': ('category_id', do_nothing), |
||
486 | 'category__name': ('category', do_nothing), |
||
487 | 'default_tester': ('default_tester_id', do_nothing), |
||
488 | 'default_tester__username': ('default_tester', to_str), |
||
489 | 'priority': ('priority_id', do_nothing), |
||
490 | 'priority__value': ('priority', do_nothing), |
||
491 | 'reviewer': ('reviewer_id', do_nothing), |
||
492 | 'reviewer__username': ('reviewer', to_str), |
||
493 | } |
||
494 | |||
495 | |||
496 | class ProductRPCSerializer(QuerySetBasedRPCSerializer): |
||
497 | """Serializer for Product""" |
||
498 | |||
499 | values_fields_mapping = { |
||
500 | 'id': ('id', do_nothing), |
||
501 | 'name': ('name', do_nothing), |
||
502 | 'description': ('description', do_nothing), |
||
503 | 'classification': ('classification_id', do_nothing), |
||
504 | 'classification__name': ('classification', do_nothing), |
||
505 | } |
||
506 | |||
507 | |||
508 | class BuildRPCSerializer(QuerySetBasedRPCSerializer): |
||
509 | """Serializer for Build""" |
||
510 | |||
511 | values_fields_mapping = { |
||
512 | 'id': ('id', do_nothing), |
||
513 | 'is_active': ('is_active', do_nothing), |
||
514 | 'name': ('name', do_nothing), |
||
515 | 'product': ('product_id', do_nothing), |
||
516 | 'product__name': ('product', do_nothing), |
||
517 | } |
||
518 |