Passed
Push — master ( 62d7d9...8230b1 )
by Alexander
03:29
created

tcms.testcases.models   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 432
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 51
eloc 273
dl 0
loc 432
rs 7.92
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A TestCaseStatus.__str__() 0 2 1
F TestCase.list() 0 80 17
A TestCase.__str__() 0 2 1
A TestCase.add_component() 0 2 1
A TestCaseStatus.get_confirmed() 0 3 1
A TestCaseEmailSettings.add_cc() 0 18 4
A TestCaseStatus.string_to_instance() 0 3 1
A TestCase._get_absolute_url() 0 2 1
A TestCaseStatus.get_proposed() 0 3 1
A TestCase.create() 0 27 2
A TestCase.add_tag() 0 2 1
A TestCase._get_email_conf() 0 5 2
A TestCase.get_text_with_version() 0 8 3
A Category.__str__() 0 2 1
A TestCase.to_xmlrpc() 0 9 1
A TestCaseEmailSettings.remove_cc() 0 17 4
A TestCase.remove_tag() 0 2 1
A TestCaseEmailSettings.get_cc_list() 0 5 2
A TestCaseStatus.is_confirmed() 0 3 2
A TestCase.get_previous_and_next() 0 9 2
A BugSystem.__str__() 0 2 1
A TestCase.remove_component() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like tcms.testcases.models often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
from django.conf import settings
3
from django.urls import reverse
4
from django.db import models
5
from django.utils.translation import override
6
from django.db.models import ObjectDoesNotExist
7
8
import vinaigrette
9
10
from tcms.core.models import TCMSActionModel
11
from tcms.core.history import KiwiHistoricalRecords
12
from tcms.testcases.fields import MultipleEmailField
13
14
15
class TestCaseStatus(TCMSActionModel):
16
    id = models.AutoField(
17
        db_column='case_status_id', max_length=6, primary_key=True
18
    )
19
    # FIXME: if name has unique value for each status, give unique constraint
20
    # to this field. Otherwise, all SQL queries filtering upon this
21
    #        field will cost much time in the database side.
22
    name = models.CharField(max_length=255)
23
    description = models.TextField(null=True, blank=True)
24
25
    class Meta:
26
        verbose_name = "Test case status"
27
        verbose_name_plural = "Test case statuses"
28
29
    def __str__(self):
30
        return self.name
31
32
    @classmethod
33
    def get_proposed(cls):
34
        return cls.objects.get(name='PROPOSED')
35
36
    @classmethod
37
    def get_confirmed(cls):
38
        return cls.objects.get(name='CONFIRMED')
39
40
    @classmethod
41
    def string_to_instance(cls, name):
42
        return cls.objects.get(name=name)
43
44
    def is_confirmed(self):
45
        with override('en'):
46
            return self.name == 'CONFIRMED'
47
48
49
# register model for DB translations
50
vinaigrette.register(TestCaseStatus, ['name'])
51
52
53
class Category(TCMSActionModel):
54
    id = models.AutoField(db_column='category_id', primary_key=True)
55
    name = models.CharField(max_length=255)
56
    product = models.ForeignKey('management.Product', related_name="category",
57
                                on_delete=models.CASCADE)
58
    description = models.TextField(blank=True)
59
60
    class Meta:
61
        verbose_name_plural = u'test case categories'
62
        unique_together = ('product', 'name')
63
64
    def __str__(self):
65
        return self.name
66
67
68
class TestCase(TCMSActionModel):
69
    history = KiwiHistoricalRecords()
70
71
    case_id = models.AutoField(primary_key=True)
72
    create_date = models.DateTimeField(db_column='creation_date', auto_now_add=True)
73
    is_automated = models.BooleanField(default=False)
74
    script = models.TextField(blank=True, null=True)
75
    arguments = models.TextField(blank=True, null=True)
76
    extra_link = models.CharField(max_length=1024, default=None, blank=True, null=True)
77
    summary = models.CharField(max_length=255)
78
    requirement = models.CharField(max_length=255, blank=True, null=True)
79
    notes = models.TextField(blank=True, null=True)
80
    text = models.TextField(blank=True)
81
82
    case_status = models.ForeignKey(TestCaseStatus, on_delete=models.CASCADE)
83
    category = models.ForeignKey(Category, related_name='category_case',
84
                                 on_delete=models.CASCADE)
85
    priority = models.ForeignKey('management.Priority', related_name='priority_case',
86
                                 on_delete=models.CASCADE)
87
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='cases_as_author',
88
                               on_delete=models.CASCADE)
89
    default_tester = models.ForeignKey(settings.AUTH_USER_MODEL,
90
                                       related_name='cases_as_default_tester',
91
                                       blank=True,
92
                                       null=True,
93
                                       on_delete=models.CASCADE)
94
    reviewer = models.ForeignKey(settings.AUTH_USER_MODEL,
95
                                 related_name='cases_as_reviewer',
96
                                 null=True,
97
                                 on_delete=models.CASCADE)
98
99
    # FIXME: related_name should be cases instead of case. But now keep it
100
    # named case due to historical reason.
101
    plan = models.ManyToManyField('testplans.TestPlan', related_name='case',
102
                                  through='testcases.TestCasePlan')
103
104
    component = models.ManyToManyField('management.Component', related_name='cases',
105
                                       through='testcases.TestCaseComponent')
106
107
    tag = models.ManyToManyField('management.Tag', related_name='case',
108
                                 through='testcases.TestCaseTag')
109
110
    def __str__(self):
111
        return self.summary
112
113
    @classmethod
114
    def to_xmlrpc(cls, query=None):
115
        from tcms.xmlrpc.serializer import TestCaseXMLRPCSerializer
116
        from tcms.xmlrpc.utils import distinct_filter
117
118
        _query = query or {}
119
        qs = distinct_filter(TestCase, _query).order_by('pk')
120
        serializer = TestCaseXMLRPCSerializer(model_class=cls, queryset=qs)
121
        return serializer.serialize_queryset()
122
123
    # todo: does this check permissions ???
124
    @classmethod
125
    def create(cls, author, values):
126
        """
127
        Create the case element based on models/forms.
128
        """
129
        case = cls.objects.create(
130
            author=author,
131
            is_automated=values['is_automated'],
132
            # sortkey = values['sortkey'],
133
            script=values['script'],
134
            arguments=values['arguments'],
135
            extra_link=values['extra_link'],
136
            summary=values['summary'],
137
            requirement=values['requirement'],
138
            case_status=values['case_status'],
139
            category=values['category'],
140
            priority=values['priority'],
141
            default_tester=values['default_tester'],
142
            notes=values['notes'],
143
            text=values['text'],
144
        )
145
146
        # todo: should use add_tag
147
        tags = values.get('tag')
148
        if tags:
149
            map(case.add_tag, tags)
150
        return case
151
152
    @classmethod
153
    def list(cls, query, plan=None):
154
        """List the cases with request"""
155
        from django.db.models import Q
156
157
        if not plan:
158
            queryset = cls.objects
159
        else:
160
            queryset = cls.objects.filter(plan=plan)
161
162
        if query.get('case_id_set'):
163
            queryset = queryset.filter(pk__in=query['case_id_set'])
164
165
        if query.get('search'):
166
            queryset = queryset.filter(
167
                Q(pk__icontains=query['search']) |
168
                Q(summary__icontains=query['search']) |
169
                Q(author__email__startswith=query['search'])
170
            )
171
172
        if query.get('summary'):
173
            queryset = queryset.filter(Q(summary__icontains=query['summary']))
174
175
        if query.get('author'):
176
            queryset = queryset.filter(
177
                Q(author__first_name__startswith=query['author']) |
178
                Q(author__last_name__startswith=query['author']) |
179
                Q(author__username__icontains=query['author']) |
180
                Q(author__email__startswith=query['author'])
181
            )
182
183
        if query.get('default_tester'):
184
            queryset = queryset.filter(
185
                Q(default_tester__first_name__startswith=query[
186
                    'default_tester']) |
187
                Q(default_tester__last_name__startswith=query[
188
                    'default_tester']) |
189
                Q(default_tester__username__icontains=query[
190
                    'default_tester']) |
191
                Q(default_tester__email__startswith=query[
192
                    'default_tester'])
193
            )
194
195
        if query.get('tag__name__in'):
196
            queryset = queryset.filter(tag__name__in=query['tag__name__in'])
197
198
        if query.get('category'):
199
            queryset = queryset.filter(category__name=query['category'].name)
200
201
        if query.get('priority'):
202
            queryset = queryset.filter(priority__in=query['priority'])
203
204
        if query.get('case_status'):
205
            queryset = queryset.filter(case_status__in=query['case_status'])
206
207
        # If plan exists, remove leading and trailing whitespace from it.
208
        plan_str = query.get('plan', '').strip()
209
        if plan_str:
210
            try:
211
                # Is it an integer?  If so treat as a plan_id:
212
                plan_id = int(plan_str)
213
                queryset = queryset.filter(plan__plan_id=plan_id)
214
            except ValueError:
215
                # Not an integer - treat plan_str as a plan name:
216
                queryset = queryset.filter(plan__name__icontains=plan_str)
217
        del plan_str
218
219
        if query.get('product'):
220
            queryset = queryset.filter(category__product=query['product'])
221
222
        if query.get('component'):
223
            queryset = queryset.filter(component=query['component'])
224
225
        if query.get('bug_id'):
226
            queryset = queryset.filter(case_bug__bug_id__in=query['bug_id'])
227
228
        if query.get('is_automated'):
229
            queryset = queryset.filter(is_automated=query['is_automated'])
230
231
        return queryset.distinct()
232
233
    def add_component(self, component):
234
        return TestCaseComponent.objects.get_or_create(case=self, component=component)
235
236
    def add_tag(self, tag):
237
        return TestCaseTag.objects.get_or_create(case=self, tag=tag)
238
239
    def get_previous_and_next(self, pk_list):
240
        current_idx = pk_list.index(self.pk)
241
        prev = TestCase.objects.get(pk=pk_list[current_idx - 1])
242
        try:
243
            _next = TestCase.objects.get(pk=pk_list[current_idx + 1])
244
        except IndexError:
245
            _next = TestCase.objects.get(pk=pk_list[0])
246
247
        return (prev, _next)
248
249
    def get_text_with_version(self, case_text_version=None):
250
        if case_text_version:
251
            try:
252
                return self.history.get(history_id=case_text_version).text
253
            except ObjectDoesNotExist:
254
                return self.text
255
256
        return self.text
257
258
    def remove_component(self, component):
259
        # note: cannot use self.component.remove(component) on a ManyToManyField
260
        # which specifies an intermediary model so we use the model manager!
261
        self.component.through.objects.filter(case=self.pk, component=component.pk).delete()
262
263
    def remove_tag(self, tag):
264
        self.tag.through.objects.filter(case=self.pk, tag=tag.pk).delete()
265
266
    def _get_absolute_url(self, request=None):
267
        return reverse('testcases-get', args=[self.pk, ])
268
269
    def _get_email_conf(self):
270
        try:
271
            return self.email_settings
272
        except ObjectDoesNotExist:
273
            return TestCaseEmailSettings.objects.create(case=self)
274
275
    emailing = property(_get_email_conf)
276
277
278
class TestCasePlan(models.Model):
279
    plan = models.ForeignKey('testplans.TestPlan', on_delete=models.CASCADE)
280
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
281
    sortkey = models.IntegerField(null=True, blank=True)
282
283
    # TODO: create FOREIGN KEY constraint on plan_id and case_id individually
284
    # in database.
285
286
    class Meta:
287
        unique_together = ('plan', 'case')
288
289
290
class TestCaseComponent(models.Model):
291
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)  # case_id
292
    component = models.ForeignKey('management.Component', on_delete=models.CASCADE)  # component_id
293
294
295
class TestCaseTag(models.Model):
296
    tag = models.ForeignKey('management.Tag', on_delete=models.CASCADE)
297
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
298
299
300
class BugSystem(TCMSActionModel):
301
    """
302
        This model describes a bug tracking system used in
303
        Kiwi TCMS. Fields below can be configured via
304
        the admin interface and their meaning is:
305
306
        #. **name:** a visual name for this bug tracker, e.g. `Kiwi TCMS GitHub`;
307
        #. **tracker_type:** a select menu to specify what kind of external
308
           system we interface with, e.g. Bugzilla, JIRA, others;
309
           The available options for this field are automatically populated
310
           by Kiwi TCMS;
311
312
           .. warning::
313
314
                Once this field is set it can't be reset to ``NULL``. Although
315
                Kiwi TCMS takes care to handle misconfigurations we advise you to
316
                configure your API credentials properly!
317
318
        #. **base_url:** base URL of this bug tracker.
319
320
           .. warning::
321
322
                If this field is left empty funtionality that depends on it will be disabled!
323
324
        #. **api_url, api_username, api_password:** configuration for an internal RPC object
325
           that communicate to the issue tracking system when necessary. Depending on the
326
           actual type of IT we're interfacing with some of these values may not be necessary.
327
           Refer to :mod:`tcms.issuetracker.types` for more information!
328
329
           .. warning::
330
331
                This is saved as plain-text in the database because it needs to be passed
332
                to the internal RPC object!
333
    """
334
    name = models.CharField(max_length=255, unique=True)
335
    tracker_type = models.CharField(
336
        max_length=128,
337
        verbose_name='Type',
338
        help_text='This determines how Kiwi TCMS integrates with the IT system',
339
        default='IssueTrackerType',
340
    )
341
342
    base_url = models.CharField(
343
        max_length=1024,
344
        null=True,
345
        blank=True,
346
        verbose_name='Base URL',
347
        help_text="""Base URL, for example <strong>https://bugzilla.example.com</strong>!
348
Leave empty to disable!
349
""")
350
351
    api_url = models.CharField(
352
        max_length=1024,
353
        null=True,
354
        blank=True,
355
        verbose_name='API URL',
356
        help_text='This is the URL to which API requests will be sent. Leave empty to disable!')
357
358
    api_username = models.CharField(
359
        max_length=256,
360
        null=True,
361
        blank=True,
362
        verbose_name='API username')
363
364
    api_password = models.CharField(
365
        max_length=256,
366
        null=True,
367
        blank=True,
368
        verbose_name='API password or token')
369
370
    class Meta:
371
        verbose_name = 'Bug tracker'
372
        verbose_name_plural = 'Bug trackers'
373
374
    def __str__(self):
375
        return self.name
376
377
378
class TestCaseEmailSettings(models.Model):
379
    case = models.OneToOneField(TestCase, related_name='email_settings', on_delete=models.CASCADE)
380
    notify_on_case_update = models.BooleanField(default=True)
381
    notify_on_case_delete = models.BooleanField(default=True)
382
    auto_to_case_author = models.BooleanField(default=True)
383
    auto_to_case_tester = models.BooleanField(default=True)
384
    auto_to_run_manager = models.BooleanField(default=True)
385
    auto_to_run_tester = models.BooleanField(default=True)
386
    auto_to_case_run_assignee = models.BooleanField(default=True)
387
388
    cc_list = models.TextField(default='')
389
390
    def add_cc(self, email_addrs):
391
        """Add email addresses to CC list
392
393
        Arguments:
394
        - email_addrs: str or list, holding one or more email addresses
395
        """
396
397
        emailaddr_list = self.get_cc_list()
398
        if not isinstance(email_addrs, list):
399
            email_addrs = [email_addrs]
400
401
        # skip addresses already in the list
402
        for address in email_addrs:
403
            if address not in emailaddr_list:
404
                emailaddr_list.append(address)
405
406
        self.cc_list = MultipleEmailField.delimiter.join(emailaddr_list)
407
        self.save()
408
409
    def get_cc_list(self):
410
        """ Return the whole CC list """
411
        if not self.cc_list:
412
            return []
413
        return self.cc_list.split(MultipleEmailField.delimiter)
414
415
    def remove_cc(self, email_addrs):
416
        """Remove one or more email addresses from EmailSettings' CC list
417
418
        If any email_addr is unknown, remove_cc will keep quiet.
419
420
        Arguments:
421
        - email_addrs: str or list, holding one or more email addresses
422
        """
423
        emailaddr_list = self.get_cc_list()
424
        if not isinstance(email_addrs, list):
425
            email_addrs = [email_addrs]
426
        for address in email_addrs:
427
            if address in emailaddr_list:
428
                emailaddr_list.remove(address)
429
430
        self.cc_list = MultipleEmailField.delimiter.join(emailaddr_list)
431
        self.save()
432