195 lines
5.6 KiB
Python
195 lines
5.6 KiB
Python
# -*- 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
|