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