digitalfabrik/integreat-cms

View on GitHub
integreat_cms/cms/models/feedback/feedback.py

Summary

Maintainability
A
0 mins
Test Coverage
A
93%
from __future__ import annotations

from typing import TYPE_CHECKING

from django.conf import settings
from django.db import models
from django.utils.functional import cached_property
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
from polymorphic.managers import PolymorphicManager, PolymorphicQuerySet
from polymorphic.models import PolymorphicModel

from ...constants import feedback_ratings
from ...utils.translation_utils import gettext_many_lazy as __
from ..abstract_base_model import AbstractBaseModel
from ..languages.language import Language
from ..regions.region import Region

if TYPE_CHECKING:
    from django.db.models.query import QuerySet


class CascadeDeletePolymorphicQuerySet(PolymorphicQuerySet):
    """
    Patch the QuerySet to call delete on the non_polymorphic QuerySet, avoiding models.deletion.Collector typing problem

    Based on workarounds proposed in: https://github.com/django-polymorphic/django-polymorphic/issues/229
    See also: https://github.com/django-polymorphic/django-polymorphic/issues/34, https://github.com/django-polymorphic/django-polymorphic/issues/84
    Related Django ticket: https://code.djangoproject.com/ticket/23076
    """

    def delete(self) -> tuple[int, dict[str, int]]:
        """
        This method deletes all objects in this QuerySet.

        :return: A tuple of the number of objects delete and the delete objects grouped by their type
        """
        if not self.polymorphic_disabled:
            return self.non_polymorphic().delete()

        return super().delete()


class CascadeDeletePolymorphicManager(PolymorphicManager):
    """
    This class is used as a workaround for a bug in django-polymorphic.
    For more information, see :class:`~integreat_cms.cms.models.feedback.feedback.CascadeDeletePolymorphicQuerySet`.
    """

    queryset_class = CascadeDeletePolymorphicQuerySet


class Feedback(PolymorphicModel, AbstractBaseModel):
    """
    Database model representing feedback from app-users.
    Do not directly create instances of this base model, but of the submodels (e.g. PageFeedback) instead.
    """

    objects = CascadeDeletePolymorphicManager()

    region = models.ForeignKey(
        Region,
        on_delete=models.CASCADE,
        related_name="feedback",
        verbose_name=_("region"),
    )
    language = models.ForeignKey(
        Language,
        on_delete=models.CASCADE,
        related_name="feedback",
        verbose_name=_("language"),
    )
    #: Manage choices in :mod:`~integreat_cms.cms.constants.feedback_ratings`
    rating = models.BooleanField(
        null=True,
        blank=True,
        default=feedback_ratings.NOT_STATED,
        choices=feedback_ratings.CHOICES,
        verbose_name=_("rating"),
        help_text=_("Whether the feedback is positive or negative"),
    )
    comment = models.TextField(blank=True, verbose_name=_("comment"))
    is_technical = models.BooleanField(
        verbose_name=_("technical"),
        help_text=_("Whether or not the feedback is targeted at the developers"),
    )
    archived = models.BooleanField(
        default=False,
        verbose_name=_("archived"),
        help_text=_("Whether or not the feedback is archived"),
    )
    read_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="feedback",
        verbose_name=_("marked as read by"),
        help_text=__(
            _("The account that marked this feedback as read."),
            _("If the feedback is unread, this field is empty."),
        ),
    )
    created_date = models.DateTimeField(
        auto_now_add=True,
        verbose_name=_("creation date"),
    )

    @property
    def category(self) -> str:
        """
        This property returns the category (verbose name of the submodel) of this feedback object.

        :return: capitalized category
        """
        return capfirst(self._meta.verbose_name)

    @cached_property
    def rating_sum_positive(self) -> int:
        """
        This property returns the sum of the up-ratings of this object.

        :return: The number of positive ratings on this feedback object
        """
        return self.related_feedback.filter(rating=feedback_ratings.POSITIVE).count()

    @cached_property
    def rating_sum_negative(self) -> int:
        """
        This property returns the sum of the down-ratings of this object.

        :return: The number of negative ratings on this feedback object
        """
        return self.related_feedback.filter(rating=feedback_ratings.NEGATIVE).count()

    @property
    def read(self) -> bool:
        """
        This property returns whether or not the feedback is marked as read or not.
        It is ``True`` if :attr:`~integreat_cms.cms.models.feedback.feedback.Feedback.read_by` is set and ``False``
        otherwise.

        :return: Whether the feedback is marked as read
        """
        return bool(self.read_by)

    @classmethod
    def search(cls, region: Region | None, query: str) -> QuerySet:
        """
        Searches for all feedbacks which match the given `query` in their comment.
        :param region: The current region or None for non-regional feedback
        :param query: The query string used for filtering the events
        :return: A query for all matching objects
        """
        kwargs: dict[str, str | bool | Region | None] = {"comment__icontains": query}
        if region is None:
            kwargs["is_technical"] = True
        else:
            kwargs["is_technical"] = False
            kwargs["region"] = region

        return cls.objects.filter(**kwargs)

    def __str__(self) -> str:
        """
        This overwrites the default Django :meth:`~django.db.models.Model.__str__` method which would return ``Feedback object (id)``.
        It is used in the Django admin backend and as label for ModelChoiceFields.

        :return: A readable string representation of the feedback
        """
        return self.comment

    def get_repr(self) -> str:
        """
        This overwrites the default Django ``__repr__()`` method which would return ``<Feedback: Feedback object (id)>``.
        It is used for logging.

        :return: The canonical string representation of the feedback
        """
        class_name = type(self).__name__
        return f"<{class_name} (id: {self.id})>"

    class Meta:
        #: The verbose name of the model
        verbose_name = _("feedback")
        #: The plural verbose name of the model
        verbose_name_plural = _("feedback")
        #: The fields which are used to sort the returned objects of a QuerySet
        ordering = ["-created_date"]
        #: The default permissions for this model
        default_permissions = ("change", "delete", "view")