tcms.testcases.models.TestCase.remove_component()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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