Passed
Push — master ( cddcf6...1d3855 )
by Alexander
01:59
created

tcms/xmlrpc/serializer.py (1 issue)

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
The variable field_name does not seem to be defined.
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