kiwitcms/Kiwi

View on GitHub
tcms/testcases/models.py

Summary

Maintainability
B
4 hrs
Test Coverage
# -*- coding: utf-8 -*-
from datetime import timedelta

import vinaigrette
from django.conf import settings
from django.db import models
from django.db.models import ObjectDoesNotExist
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from tcms.core.history import KiwiHistoricalRecords
from tcms.core.models import abstract
from tcms.core.models.base import UrlMixin
from tcms.testcases.fields import MultipleEmailField


class TestCaseStatus(models.Model, UrlMixin):
    name = models.CharField(max_length=255)
    description = models.TextField(null=True, blank=True)
    is_confirmed = models.BooleanField(db_index=True, default=False)

    class Meta:
        verbose_name = _("Test case status")
        verbose_name_plural = _("Test case statuses")

    def __str__(self):
        return self.name


# register model for DB translations
vinaigrette.register(TestCaseStatus, ["name"])


class Category(models.Model, UrlMixin):
    name = models.CharField(max_length=255)
    product = models.ForeignKey(
        "management.Product", related_name="category", on_delete=models.CASCADE
    )
    description = models.TextField(blank=True)

    class Meta:
        verbose_name_plural = _("Categories")
        unique_together = ("product", "name")

    def __str__(self):
        return self.name


class TestCase(models.Model, UrlMixin):
    history = KiwiHistoricalRecords()

    create_date = models.DateTimeField(auto_now_add=True)
    is_automated = models.BooleanField(default=False)
    script = models.TextField(blank=True, null=True)
    arguments = models.TextField(blank=True, null=True)
    extra_link = models.CharField(max_length=1024, default=None, blank=True, null=True)
    summary = models.CharField(max_length=255, db_index=True)
    requirement = models.CharField(max_length=255, blank=True, null=True)
    notes = models.TextField(blank=True, null=True)
    text = models.TextField(blank=True)
    setup_duration = models.DurationField(db_index=True, null=True, blank=True)
    testing_duration = models.DurationField(db_index=True, null=True, blank=True)

    case_status = models.ForeignKey(TestCaseStatus, on_delete=models.CASCADE)
    category = models.ForeignKey(
        Category, related_name="category_case", on_delete=models.CASCADE
    )
    priority = models.ForeignKey(
        "management.Priority", related_name="priority_case", on_delete=models.CASCADE
    )
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="cases_as_author",
        on_delete=models.CASCADE,
    )
    default_tester = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="cases_as_default_tester",
        blank=True,
        null=True,
        on_delete=models.CASCADE,
    )
    reviewer = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="cases_as_reviewer",
        null=True,
        on_delete=models.CASCADE,
    )

    plan = models.ManyToManyField(
        "testplans.TestPlan", related_name="cases", through="testcases.TestCasePlan"
    )

    component = models.ManyToManyField(
        "management.Component",
        related_name="cases",
        through="testcases.TestCaseComponent",
    )

    tag = models.ManyToManyField(
        "management.Tag", related_name="case", through="testcases.TestCaseTag"
    )

    @property
    def expected_duration(self):
        result = timedelta(0)
        result += self.setup_duration or timedelta(0)
        result += self.testing_duration or timedelta(0)
        return result

    def __str__(self):
        return self.summary

    def add_component(self, component):
        return TestCaseComponent.objects.get_or_create(case=self, component=component)

    def add_tag(self, tag):
        return TestCaseTag.objects.get_or_create(case=self, tag=tag)

    def get_text_with_version(self, case_text_version=None):
        if case_text_version:
            try:
                return self.history.get(  # pylint: disable=no-member
                    history_id=case_text_version
                ).text
            except ObjectDoesNotExist:
                return self.text

        return self.text

    def remove_component(self, component):
        # note: cannot use self.component.remove(component) on a ManyToManyField
        # which specifies an intermediary model so we use the model manager!
        self.component.through.objects.filter(
            case=self.pk, component=component.pk
        ).delete()

    def remove_tag(self, tag):
        self.tag.through.objects.filter(case=self.pk, tag=tag.pk).delete()

    def _get_absolute_url(self, request=None):
        return reverse(
            "testcases-get",
            args=[
                self.pk,
            ],
        )

    def get_absolute_url(self):
        return self._get_absolute_url()

    def _get_email_conf(self):
        try:
            # note: this is the reverse_name of a 1-to-1 field
            return self.email_settings  # pylint: disable=no-member
        except ObjectDoesNotExist:
            return TestCaseEmailSettings.objects.create(case=self)

    emailing = property(_get_email_conf)

    def clone(self, new_author, test_plans):
        values = self.__dict__.copy()
        del values["_state"]
        del values["id"]
        if "sortkey" in values:
            sortkey = values.pop("sortkey")
        else:
            sortkey = None

        values["case_status_id"] = (
            TestCaseStatus.objects.filter(is_confirmed=False).first().pk
        )
        values["author_id"] = new_author.pk

        new_tc = self.__class__.objects.create(**values)

        # apply tags as well
        for tag in self.tag.all():
            new_tc.add_tag(tag)

        for plan in test_plans:
            plan.add_case(new_tc, sortkey)

            # clone TC category b/c we may be cloning a 'linked'
            # TC which has a different Product that doesn't have the
            # same categories yet
            try:
                tc_category = plan.product.category.get(name=self.category.name)
            except ObjectDoesNotExist:
                tc_category = plan.product.category.create(
                    name=self.category.name,
                    description=self.category.description,
                )
            new_tc.category = tc_category
            new_tc.save()

            # clone TC components b/c we may be cloning a 'linked'
            # TC which has a different Product that doesn't have the
            # same components yet
            for component in self.component.all():
                try:
                    new_component = plan.product.component.get(name=component.name)
                except ObjectDoesNotExist:
                    new_component = plan.product.component.create(
                        name=component.name,
                        initial_owner=new_author,
                        description=component.description,
                    )
                new_tc.add_component(new_component)

        return new_tc


class Property(abstract.Property):
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)


class TestCasePlan(models.Model):
    plan = models.ForeignKey("testplans.TestPlan", on_delete=models.CASCADE)
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
    sortkey = models.IntegerField(null=True, blank=True)

    class Meta:
        unique_together = ("plan", "case")


class TestCaseComponent(models.Model):
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
    component = models.ForeignKey("management.Component", on_delete=models.CASCADE)


class TestCaseTag(models.Model):
    tag = models.ForeignKey("management.Tag", on_delete=models.CASCADE)
    case = models.ForeignKey(TestCase, on_delete=models.CASCADE)


class BugSystem(models.Model, UrlMixin):
    """
    This model describes a bug tracking system used in
    Kiwi TCMS. Fields below can be configured via
    the admin interface and their meaning is:

    #. **name:** a visual name for this bug tracker, e.g. `Kiwi TCMS GitHub`;
    #. **tracker_type:** a select menu to specify what kind of external
       system we interface with, e.g. Bugzilla, JIRA, others;
       The available options for this field are automatically populated
       by Kiwi TCMS;

       .. warning::

            Once this field is set it can't be reset to ``NULL``. Although
            Kiwi TCMS takes care to handle misconfigurations we advise you to
            configure your API credentials properly!

    #. **base_url:** base URL of this bug tracker.

       .. warning::

            If this field is left empty funtionality that depends on it will be disabled!

    #. **api_url, api_username, api_password:** configuration for an internal RPC object
       that communicate to the issue tracking system when necessary. Depending on the
       actual type of IT we're interfacing with some of these values may not be necessary.
       Refer to :mod:`tcms.issuetracker.types` for more information!

       .. warning::

            This is saved as plain-text in the database because it needs to be passed
            to the internal RPC object!
    """

    name = models.CharField(max_length=255, unique=True)
    tracker_type = models.CharField(  # pylint:disable=form-field-help-text-used
        max_length=128,
        verbose_name="Type",
        help_text="This determines how Kiwi TCMS integrates with the IT system",
        default="IssueTrackerType",
    )

    base_url = models.CharField(  # pylint:disable=form-field-help-text-used
        max_length=1024,
        null=True,
        blank=True,
        verbose_name="Base URL",
        help_text="""Base URL, for example <strong>https://bugzilla.example.com</strong>!
Leave empty to disable!
""",
    )

    api_url = models.CharField(  # pylint:disable=form-field-help-text-used
        max_length=1024,
        null=True,
        blank=True,
        verbose_name="API URL",
        help_text="This is the URL to which API requests will be sent. Leave empty to disable!",
    )

    api_username = models.CharField(
        max_length=256, null=True, blank=True, verbose_name="API username"
    )

    api_password = models.CharField(
        max_length=256, null=True, blank=True, verbose_name="API password or token"
    )

    class Meta:
        verbose_name = "Bug tracker"
        verbose_name_plural = "Bug trackers"

    def __str__(self):
        return self.name


class TestCaseEmailSettings(models.Model):
    case = models.OneToOneField(
        TestCase, related_name="email_settings", on_delete=models.CASCADE
    )
    notify_on_case_update = models.BooleanField(default=True)
    notify_on_case_delete = models.BooleanField(default=True)
    auto_to_case_author = models.BooleanField(default=True)
    auto_to_case_tester = models.BooleanField(default=True)
    auto_to_run_manager = models.BooleanField(default=True)
    auto_to_run_tester = models.BooleanField(default=True)
    auto_to_execution_assignee = models.BooleanField(default=True)

    cc_list = models.TextField(default="")

    def add_cc(self, email_addrs):
        """Add email addresses to CC list

        Arguments:
        - email_addrs: str or list, holding one or more email addresses
        """

        emailaddr_list = self.get_cc_list()
        if not isinstance(email_addrs, list):
            email_addrs = [email_addrs]

        # skip addresses already in the list
        for address in email_addrs:
            if address not in emailaddr_list:
                emailaddr_list.append(address)

        self.cc_list = MultipleEmailField.delimiter.join(emailaddr_list)
        self.save()

    def get_cc_list(self):
        """Return the whole CC list"""
        if not self.cc_list:
            return []
        return self.cc_list.split(MultipleEmailField.delimiter)

    def remove_cc(self, email_addrs):
        """Remove one or more email addresses from EmailSettings' CC list

        If any email_addr is unknown, remove_cc will keep quiet.

        Arguments:
        - email_addrs: str or list, holding one or more email addresses
        """
        emailaddr_list = self.get_cc_list()
        if not isinstance(email_addrs, list):
            email_addrs = [email_addrs]
        for address in email_addrs:
            if address in emailaddr_list:
                emailaddr_list.remove(address)

        self.cc_list = MultipleEmailField.delimiter.join(emailaddr_list)
        self.save()


class Template(models.Model):
    history = KiwiHistoricalRecords()

    name = models.CharField(max_length=255)
    text = models.TextField(blank=False)

    class Meta:
        verbose_name = _("Template")
        verbose_name_plural = _("Templates")

    def __str__(self):
        return self.name