tcms.testcases.models.TestCase.list()   F
last analyzed

Complexity

Conditions 17

Size

Total Lines 80
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 56
dl 0
loc 80
rs 1.8
c 0
b 0
f 0
cc 17
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

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