Passed
Push — master ( 2b1785...27363e )
by Alexander
04:03 queued 01:20
created

tcms.testcases.models.TestCase.expected_duration()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
# -*- coding: utf-8 -*-
2
from datetime import timedelta
3
4
import vinaigrette
5
from django.conf import settings
6
from django.db import models
7
from django.db.models import ObjectDoesNotExist
8
from django.urls import reverse
9
from django.utils.translation import gettext_lazy as _
10
11
from tcms.core.history import KiwiHistoricalRecords
12
from tcms.core.models.base import UrlMixin
13
from tcms.testcases.fields import MultipleEmailField
14
15
16
class TestCaseStatus(models.Model, UrlMixin):
17
    name = models.CharField(max_length=255)
18
    description = models.TextField(null=True, blank=True)
19
    is_confirmed = models.BooleanField(db_index=True, default=False)
20
21
    class Meta:
22
        verbose_name = _("Test case status")
23
        verbose_name_plural = _("Test case statuses")
24
25
    def __str__(self):
26
        return self.name
27
28
29
# register model for DB translations
30
vinaigrette.register(TestCaseStatus, ["name"])
31
32
33
class Category(models.Model, UrlMixin):
34
    name = models.CharField(max_length=255)
35
    product = models.ForeignKey(
36
        "management.Product", related_name="category", on_delete=models.CASCADE
37
    )
38
    description = models.TextField(blank=True)
39
40
    class Meta:
41
        verbose_name_plural = u"test case categories"
42
        unique_together = ("product", "name")
43
44
    def __str__(self):
45
        return self.name
46
47
48
class TestCase(models.Model, UrlMixin):
49
    history = KiwiHistoricalRecords()
50
51
    create_date = models.DateTimeField(auto_now_add=True)
52
    is_automated = models.BooleanField(default=False)
53
    script = models.TextField(blank=True, null=True)
54
    arguments = models.TextField(blank=True, null=True)
55
    extra_link = models.CharField(max_length=1024, default=None, blank=True, null=True)
56
    summary = models.CharField(max_length=255, db_index=True)
57
    requirement = models.CharField(max_length=255, blank=True, null=True)
58
    notes = models.TextField(blank=True, null=True)
59
    text = models.TextField(blank=True)
60
    setup_duration = models.DurationField(db_index=True, null=True, blank=True)
61
    testing_duration = models.DurationField(db_index=True, null=True, blank=True)
62
63
    case_status = models.ForeignKey(TestCaseStatus, on_delete=models.CASCADE)
64
    category = models.ForeignKey(
65
        Category, related_name="category_case", on_delete=models.CASCADE
66
    )
67
    priority = models.ForeignKey(
68
        "management.Priority", related_name="priority_case", on_delete=models.CASCADE
69
    )
70
    author = models.ForeignKey(
71
        settings.AUTH_USER_MODEL,
72
        related_name="cases_as_author",
73
        on_delete=models.CASCADE,
74
    )
75
    default_tester = models.ForeignKey(
76
        settings.AUTH_USER_MODEL,
77
        related_name="cases_as_default_tester",
78
        blank=True,
79
        null=True,
80
        on_delete=models.CASCADE,
81
    )
82
    reviewer = models.ForeignKey(
83
        settings.AUTH_USER_MODEL,
84
        related_name="cases_as_reviewer",
85
        null=True,
86
        on_delete=models.CASCADE,
87
    )
88
89
    plan = models.ManyToManyField(
90
        "testplans.TestPlan", related_name="cases", through="testcases.TestCasePlan"
91
    )
92
93
    component = models.ManyToManyField(
94
        "management.Component",
95
        related_name="cases",
96
        through="testcases.TestCaseComponent",
97
    )
98
99
    tag = models.ManyToManyField(
100
        "management.Tag", related_name="case", through="testcases.TestCaseTag"
101
    )
102
103
    @property
104
    def expected_duration(self):
105
        result = timedelta(0)
106
        result += self.setup_duration or timedelta(0)
107
        result += self.testing_duration or timedelta(0)
108
        return result
109
110
    def __str__(self):
111
        return self.summary
112
113
    def add_component(self, component):
114
        return TestCaseComponent.objects.get_or_create(case=self, component=component)
115
116
    def add_tag(self, tag):
117
        return TestCaseTag.objects.get_or_create(case=self, tag=tag)
118
119
    def get_text_with_version(self, case_text_version=None):
120
        if case_text_version:
121
            try:
122
                return self.history.get(  # pylint: disable=no-member
123
                    history_id=case_text_version
124
                ).text
125
            except ObjectDoesNotExist:
126
                return self.text
127
128
        return self.text
129
130
    def remove_component(self, component):
131
        # note: cannot use self.component.remove(component) on a ManyToManyField
132
        # which specifies an intermediary model so we use the model manager!
133
        self.component.through.objects.filter(
134
            case=self.pk, component=component.pk
135
        ).delete()
136
137
    def remove_tag(self, tag):
138
        self.tag.through.objects.filter(case=self.pk, tag=tag.pk).delete()
139
140
    def _get_absolute_url(self, request=None):
141
        return reverse(
142
            "testcases-get",
143
            args=[
144
                self.pk,
145
            ],
146
        )
147
148
    def get_absolute_url(self):
149
        return self._get_absolute_url()
150
151
    def _get_email_conf(self):
152
        try:
153
            # note: this is the reverse_name of a 1-to-1 field
154
            return self.email_settings  # pylint: disable=no-member
155
        except ObjectDoesNotExist:
156
            return TestCaseEmailSettings.objects.create(case=self)
157
158
    emailing = property(_get_email_conf)
159
160
    def clone(self, new_author, test_plans):
161
        new_tc = self.__class__.objects.create(
162
            is_automated=self.is_automated,
163
            script=self.script,
164
            arguments=self.arguments,
165
            extra_link=self.extra_link,
166
            summary=self.summary,
167
            requirement=self.requirement,
168
            case_status=TestCaseStatus.objects.filter(is_confirmed=False).first(),
169
            category=self.category,
170
            priority=self.priority,
171
            notes=self.notes,
172
            text=self.text,
173
            author=new_author,
174
            default_tester=self.default_tester,
175
        )
176
177
        # apply tags as well
178
        for tag in self.tag.all():
179
            new_tc.add_tag(tag)
180
181
        for plan in test_plans:
182
            plan.add_case(new_tc)
183
184
            # clone TC category b/c we may be cloning a 'linked'
185
            # TC which has a different Product that doesn't have the
186
            # same categories yet
187
            try:
188
                tc_category = plan.product.category.get(name=self.category.name)
189
            except ObjectDoesNotExist:
190
                tc_category = plan.product.category.create(
191
                    name=self.category.name,
192
                    description=self.category.description,
193
                )
194
            new_tc.category = tc_category
195
            new_tc.save()
196
197
            # clone TC components b/c we may be cloning a 'linked'
198
            # TC which has a different Product that doesn't have the
199
            # same components yet
200
            for component in self.component.all():
201
                try:
202
                    new_component = plan.product.component.get(name=component.name)
203
                except ObjectDoesNotExist:
204
                    new_component = plan.product.component.create(
205
                        name=component.name,
206
                        initial_owner=new_author,
207
                        description=component.description,
208
                    )
209
                new_tc.add_component(new_component)
210
211
        return new_tc
212
213
214
class TestCasePlan(models.Model):
215
    plan = models.ForeignKey("testplans.TestPlan", on_delete=models.CASCADE)
216
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
217
    sortkey = models.IntegerField(null=True, blank=True)
218
219
    class Meta:
220
        unique_together = ("plan", "case")
221
222
223
class TestCaseComponent(models.Model):
224
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
225
    component = models.ForeignKey("management.Component", on_delete=models.CASCADE)
226
227
228
class TestCaseTag(models.Model):
229
    tag = models.ForeignKey("management.Tag", on_delete=models.CASCADE)
230
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
231
232
233
class BugSystem(models.Model, UrlMixin):
234
    """
235
    This model describes a bug tracking system used in
236
    Kiwi TCMS. Fields below can be configured via
237
    the admin interface and their meaning is:
238
239
    #. **name:** a visual name for this bug tracker, e.g. `Kiwi TCMS GitHub`;
240
    #. **tracker_type:** a select menu to specify what kind of external
241
       system we interface with, e.g. Bugzilla, JIRA, others;
242
       The available options for this field are automatically populated
243
       by Kiwi TCMS;
244
245
       .. warning::
246
247
            Once this field is set it can't be reset to ``NULL``. Although
248
            Kiwi TCMS takes care to handle misconfigurations we advise you to
249
            configure your API credentials properly!
250
251
    #. **base_url:** base URL of this bug tracker.
252
253
       .. warning::
254
255
            If this field is left empty funtionality that depends on it will be disabled!
256
257
    #. **api_url, api_username, api_password:** configuration for an internal RPC object
258
       that communicate to the issue tracking system when necessary. Depending on the
259
       actual type of IT we're interfacing with some of these values may not be necessary.
260
       Refer to :mod:`tcms.issuetracker.types` for more information!
261
262
       .. warning::
263
264
            This is saved as plain-text in the database because it needs to be passed
265
            to the internal RPC object!
266
    """
267
268
    name = models.CharField(max_length=255, unique=True)
269
    tracker_type = models.CharField(  # pylint:disable=form-field-help-text-used
270
        max_length=128,
271
        verbose_name="Type",
272
        help_text="This determines how Kiwi TCMS integrates with the IT system",
273
        default="IssueTrackerType",
274
    )
275
276
    base_url = models.CharField(  # pylint:disable=form-field-help-text-used
277
        max_length=1024,
278
        null=True,
279
        blank=True,
280
        verbose_name="Base URL",
281
        help_text="""Base URL, for example <strong>https://bugzilla.example.com</strong>!
282
Leave empty to disable!
283
""",
284
    )
285
286
    api_url = models.CharField(  # pylint:disable=form-field-help-text-used
287
        max_length=1024,
288
        null=True,
289
        blank=True,
290
        verbose_name="API URL",
291
        help_text="This is the URL to which API requests will be sent. Leave empty to disable!",
292
    )
293
294
    api_username = models.CharField(
295
        max_length=256, null=True, blank=True, verbose_name="API username"
296
    )
297
298
    api_password = models.CharField(
299
        max_length=256, null=True, blank=True, verbose_name="API password or token"
300
    )
301
302
    class Meta:
303
        verbose_name = "Bug tracker"
304
        verbose_name_plural = "Bug trackers"
305
306
    def __str__(self):
307
        return self.name
308
309
310
class TestCaseEmailSettings(models.Model):
311
    case = models.OneToOneField(
312
        TestCase, related_name="email_settings", on_delete=models.CASCADE
313
    )
314
    notify_on_case_update = models.BooleanField(default=True)
315
    notify_on_case_delete = models.BooleanField(default=True)
316
    auto_to_case_author = models.BooleanField(default=True)
317
    auto_to_case_tester = models.BooleanField(default=True)
318
    auto_to_run_manager = models.BooleanField(default=True)
319
    auto_to_run_tester = models.BooleanField(default=True)
320
    auto_to_execution_assignee = models.BooleanField(default=True)
321
322
    cc_list = models.TextField(default="")
323
324
    def add_cc(self, email_addrs):
325
        """Add email addresses to CC list
326
327
        Arguments:
328
        - email_addrs: str or list, holding one or more email addresses
329
        """
330
331
        emailaddr_list = self.get_cc_list()
332
        if not isinstance(email_addrs, list):
333
            email_addrs = [email_addrs]
334
335
        # skip addresses already in the list
336
        for address in email_addrs:
337
            if address not in emailaddr_list:
338
                emailaddr_list.append(address)
339
340
        self.cc_list = MultipleEmailField.delimiter.join(emailaddr_list)
341
        self.save()
342
343
    def get_cc_list(self):
344
        """ Return the whole CC list """
345
        if not self.cc_list:
346
            return []
347
        return self.cc_list.split(MultipleEmailField.delimiter)
348
349
    def remove_cc(self, email_addrs):
350
        """Remove one or more email addresses from EmailSettings' CC list
351
352
        If any email_addr is unknown, remove_cc will keep quiet.
353
354
        Arguments:
355
        - email_addrs: str or list, holding one or more email addresses
356
        """
357
        emailaddr_list = self.get_cc_list()
358
        if not isinstance(email_addrs, list):
359
            email_addrs = [email_addrs]
360
        for address in email_addrs:
361
            if address in emailaddr_list:
362
                emailaddr_list.remove(address)
363
364
        self.cc_list = MultipleEmailField.delimiter.join(emailaddr_list)
365
        self.save()
366