django-orchestra/orchestra/contrib/issues/models.py

203 lines
7.2 KiB
Python

from django.conf import settings as djsettings
from django.db import models
from django.db.models import query, Q
from django.utils.translation import gettext_lazy as _
from orchestra.contrib.contacts import settings as contacts_settings
from orchestra.contrib.contacts.models import Contact
from orchestra.models.fields import MultiSelectField
from orchestra.utils.mail import send_email_template
from . import settings
class Queue(models.Model):
name = models.CharField(_("name"), max_length=128, unique=True)
verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True)
default = models.BooleanField(_("default"), default=False)
notify = MultiSelectField(_("notify"), max_length=256, blank=True,
choices=Contact.EMAIL_USAGES,
default=contacts_settings.CONTACTS_DEFAULT_EMAIL_USAGES,
help_text=_("Contacts to notify by email"))
def __str__(self):
return self.verbose_name or self.name
def save(self, *args, **kwargs):
""" mark as default queue if needed """
existing_default = Queue.objects.filter(default=True)
if self.default:
existing_default.update(default=False)
elif not existing_default:
self.default = True
super(Queue, self).save(*args, **kwargs)
class TicketQuerySet(query.QuerySet):
def involved_by(self, user, *args, **kwargs):
qset = Q(creator=user) | Q(owner=user) | Q(messages__author=user)
return self.filter(qset, *args, **kwargs).distinct()
class Ticket(models.Model):
HIGH = 'HIGH'
MEDIUM = 'MEDIUM'
LOW = 'LOW'
PRIORITIES = (
(HIGH, 'High'),
(MEDIUM, 'Medium'),
(LOW, 'Low'),
)
NEW = 'NEW'
IN_PROGRESS = 'IN_PROGRESS'
RESOLVED = 'RESOLVED'
FEEDBACK = 'FEEDBACK'
REJECTED = 'REJECTED'
CLOSED = 'CLOSED'
STATES = (
(NEW, 'New'),
(IN_PROGRESS, 'In Progress'),
(RESOLVED, 'Resolved'),
(FEEDBACK, 'Feedback'),
(REJECTED, 'Rejected'),
(CLOSED, 'Closed'),
)
creator = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("created by"),
related_name='tickets_created', null=True, on_delete=models.SET_NULL)
creator_name = models.CharField(_("creator name"), max_length=256, blank=True)
owner = models.ForeignKey(djsettings.AUTH_USER_MODEL, null=True, blank=True,
on_delete=models.SET_NULL,
related_name='tickets_owned', verbose_name=_("assigned to"))
queue = models.ForeignKey(Queue, related_name='tickets', null=True, blank=True,
on_delete=models.SET_NULL)
subject = models.CharField(_("subject"), max_length=256)
description = models.TextField(_("description"))
priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES, default=MEDIUM)
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(_("modified"), auto_now=True)
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), blank=True)
objects = TicketQuerySet.as_manager()
class Meta:
ordering = ['-updated_at']
def __str__(self):
return str(self.pk)
def get_notification_emails(self):
""" Get emails of the users related to the ticket """
emails = list(settings.ISSUES_SUPPORT_EMAILS)
emails.append(self.creator.email)
if self.owner:
emails.append(self.owner.email)
for contact in self.creator.contacts.all():
if self.queue and set(contact.email_usage).union(set(self.queue.notify)):
emails.append(contact.email)
for message in self.messages.distinct('author'):
emails.append(message.author.email)
return set(emails + self.get_cc_emails())
def notify(self, message=None, content=None):
""" Send an email to ticket stakeholders notifying an state update """
emails = self.get_notification_emails()
template = 'issues/ticket_notification.mail'
html_template = 'issues/ticket_notification_html.mail'
context = {
'ticket': self,
'ticket_message': message
}
send_email_template(template, context, emails, html=html_template)
def save(self, *args, **kwargs):
""" notify stakeholders of new ticket """
new_issue = not self.pk
if not self.creator_name and self.creator:
self.creator_name = self.creator.get_full_name()
super(Ticket, self).save(*args, **kwargs)
if new_issue:
# PK should be available for rendering the template
self.notify()
def is_involved_by(self, user):
""" returns whether user has participated or is referenced on the ticket
as owner or member of the group
"""
return Ticket.objects.filter(pk=self.pk).involved_by(user).exists()
def get_cc_emails(self):
return self.cc.split(',') if self.cc else []
def mark_as_read_by(self, user):
self.trackers.get_or_create(user=user)
def mark_as_unread_by(self, user):
self.trackers.filter(user=user).delete()
def mark_as_unread(self):
self.trackers.all().delete()
def is_read_by(self, user):
return self.trackers.filter(user=user).exists()
def reject(self):
self.state = Ticket.REJECTED
self.save(update_fields=('state', 'updated_at'))
def resolve(self):
self.state = Ticket.RESOLVED
self.save(update_fields=('state', 'updated_at'))
def close(self):
self.state = Ticket.CLOSED
self.save(update_fields=('state', 'updated_at'))
def take(self, user):
self.owner = user
self.save(update_fields=('state', 'updated_at'))
class Message(models.Model):
ticket = models.ForeignKey('issues.Ticket', on_delete=models.CASCADE,
verbose_name=_("ticket"), related_name='messages')
author = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
verbose_name=_("author"), related_name='ticket_messages')
author_name = models.CharField(_("author name"), max_length=256, blank=True)
content = models.TextField(_("content"))
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
class Meta:
get_latest_by = 'id'
def __str__(self):
return "#%i" % self.id
def save(self, *args, **kwargs):
""" notify stakeholders of ticket update """
if not self.pk:
self.ticket.mark_as_unread()
self.ticket.mark_as_read_by(self.author)
self.ticket.notify(message=self)
self.author_name = self.author.get_full_name()
super(Message, self).save(*args, **kwargs)
@property
def number(self):
return self.ticket.messages.filter(id__lte=self.id).count()
class TicketTracker(models.Model):
""" Keeps track of user read tickets """
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE,
verbose_name=_("ticket"), related_name='trackers')
user = models.ForeignKey(djsettings.AUTH_USER_MODEL, on_delete=models.CASCADE,
verbose_name=_("user"), related_name='ticket_trackers')
class Meta:
unique_together = (
('ticket', 'user'),
)