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

Complexity

Conditions 16

Size

Total Lines 78
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

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