# -*- encoding: utf-8 -*- import logging import bleach from uuid import uuid4 from datetime import timedelta, datetime from django.db import models from django.conf import settings from django.utils import timezone from django.utils.translation import gettext_lazy as _ log = logging.getLogger(__name__) class TicketPriority(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) title = models.CharField(max_length=32) colour = models.CharField(max_length=7) backgroundcolour = models.CharField(max_length=7) order = models.PositiveIntegerField(default=0, blank=False, null=False) def __str__(self): return self.title class TicketTag(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) title = models.CharField(max_length=32) colour = models.CharField(max_length=7) backgroundcolour = models.CharField(max_length=7) order = models.PositiveIntegerField(default=0, blank=False, null=False) def __str__(self): return self.title class Ticket(models.Model): """Represents a Ticket used to communicate issues or questions.""" uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) # Main Attributes title = models.CharField( verbose_name=_("title"), help_text=_("An extremely short summary of the ticket subject."), max_length=100, ) description = models.TextField( verbose_name=_("description"), help_text=_("Detailed description of the ticket subject."), max_length=650, ) author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_("author"), help_text=_("The creator of the ticket."), on_delete=models.CASCADE, ) priority = models.ForeignKey( TicketPriority, verbose_name=_("priority"), help_text=_("The importance level of this ticket."), on_delete=models.CASCADE, ) tags = models.ManyToManyField( TicketTag, verbose_name=_("tags"), help_text=_("Categories of the ticket."), blank=True, ) # Timestamps create_timestamp = models.DateTimeField( verbose_name=_("Creation Date"), help_text=_("When the user was created."), editable=True, default=timezone.now, ) edit_timestamp = models.DateTimeField( verbose_name=_("Last Edited"), help_text=_("When the user was last edited."), editable=True, default=timezone.now, ) def __str__(self): return f"#{self.uuid} • {self.title} •{f' {self.author.department.title} •' if self.author.department else ''} {self.author.formal_fullname}" def clean_description(self): """Sanitise the description as it may contain some allowed HTML tags.""" cleaned_description = bleach.clean( self.description, tags=[ 'b', 'i', 'u', 'p', 'br', 'a', 'h3', 'h4', 'h5', 'h6', 'strong', 'figure', 'table', 'tbody', 'tr', 'td' ], attributes={'a': ['href', 'title']} ) return cleaned_description def short_description(self): """Provide a snippet of the description for presentation purposes.""" short_description = bleach.clean(self.description, tags=[]) if len(short_description) > 200: return f"{short_description[:200]}..." return short_description def save(self, *args, **kwargs): """Override the save method to clean the description and apply timestamps.""" self.description = self.clean_description() now = timezone.now() self.edit_timestamp = now if self._state.adding: self.create_timestamp = now super().save(*args, **kwargs) @property def is_edited(self) -> bool: """Returns boolean if the ticket is believed to have been edited. We assume a ticket is edited if the edit_timestamp doesn't match the create_timestamp. Returns ------- bool True if `self.edit_timestamp` doesn't match `self.create_timestamp`, False otherwise. """ return self.create_timestamp != self.edit_timestamp @property def display_datetime(self) -> str: """Provides a human readable string representation of `self.timestamp` that should be displayed to represent the ticket's age. Returns ------- str The string representation of `self.timestamp`. """ difference = timezone.now() - self.timestamp days = difference.days hours = difference.total_seconds() // 3600 minutes = difference.total_seconds() // 60 seconds = difference.total_seconds() hours, minutes, seconds = map(int, (hours, minutes, seconds)) if seconds < 60: value, unit = seconds, "second" elif minutes < 60: value, unit = minutes, "minute" elif hours < 24: value, unit = hours, "hour" elif days < 7: value, unit = days, "day" else: return self.timestamp.strftime("%Y-%m-%d") if value > 1: unit += "s" return f"{value} {unit} ago" @property def timestamp(self) -> datetime: """Returns `self.edit_timestamp` if `self.is_edited` is True, otherwise returns `self.create_timestamp`. Returns ------- datetime Either `self.edit_timestamp` or `self.create_timestamp`. """ return self.edit_timestamp if self.is_edited else self.create_timestamp