Passed
Push — master ( e32157...9a4845 )
by Alexander
04:03
created

tcms/rpc/serializer.py (2 issues)

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
The variable field_name does not seem to be defined.
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
The variable m2m_fields_query does not seem to be defined for all execution paths.
Loading history...
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