Free and open source ticket system written in python
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253from django.db import modelsfrom core.models import PawUserfrom django.utils import timezonefrom django.utils.translation import gettext_lazy as _from django.db.models.signals import post_save, pre_savefrom django.dispatch import receiverfrom uuid import uuid4from core.models import MailTemplatefrom .storage import SecureFileStoragefrom django.conf import settings
def ticket_directory_path(instance, filename): """ file will be uploaded to (SECURE_)MEDIA_ROOT/attachments/ticket_<id>/<filename> """ ext = filename.split('.')[-1] return "attachments/ticket_{0}/{1}.{2}".format(instance.ticket.id, uuid4(), ext)
class Team(models.Model): name = models.CharField(max_length=200) description = models.TextField(blank=True) members = models.ManyToManyField(PawUser) access_non_category_tickets = models.BooleanField(default=False) readonly_access = models.BooleanField(default=False)
def __str__(self): return self.name
class Category(models.Model): name = models.CharField(max_length=200) team = models.ForeignKey( Team, on_delete=models.CASCADE, null=True, blank=True, help_text=_("If a team is selected, new tickets will automatically assigned to this team."))
def __str__(self): return self.name
class Ticket(models.Model):
class Status(models.TextChoices): OPEN = 'open', _('Open') IN_PROGRESS = 'in_progress', _('In Progress') CLOSED = 'closed', _('Closed')
class Priority(models.IntegerChoices): LOW = 3, _("Low") MEDIUM = 2, _("Medium") HIGH = 1, _("High")
title = models.CharField(max_length=200) user = models.ForeignKey(PawUser, on_delete=models.CASCADE) description = models.TextField() category = models.ForeignKey( Category, on_delete=models.CASCADE, null=True, blank=True) status = models.CharField( max_length=20, choices=Status.choices, default=Status.OPEN) priority = models.PositiveSmallIntegerField( choices=Priority.choices, db_index=True, default=Priority.MEDIUM) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) assigned_to = models.ForeignKey( PawUser, on_delete=models.CASCADE, related_name='assigned_to_user', null=True, blank=True) assigned_team = models.ForeignKey( Team, on_delete=models.CASCADE, related_name='assigned_to_team', null=True, blank=True) follow_up_to = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True, related_name='follow_ups')
class Meta: indexes = [ models.Index(fields=["priority", "title"]), ]
@classmethod def _get_tickets(cls, user) -> models.QuerySet: """ For regular users with no team: return all open tickets that are created by the user """ if user.is_superuser: return cls.objects.all() user_teams = user.team_set.all() if not user_teams: return cls.objects.filter(user=user) q = cls.objects.filter( models.Q(user=user) | # tickets created by user (models.Q(assigned_team__in=user_teams) | models.Q(assigned_to=user)) | # tickets assigned to user or user's team (models.Q(assigned_team=None) & models.Q(category=None)) # tickets that are not assigned and have no category (general), needs to be excluded with filter )
if not any([team.access_non_category_tickets for team in user_teams]): return q.exclude(models.Q(assigned_team=None) & models.Q(category=None) & ~models.Q(user=user)) return q
@classmethod def get_open_tickets(cls, user) -> models.QuerySet: """ For regular users with no team: return all open tickets that are created by the user """ return cls._get_tickets(user).exclude(status=cls.Status.CLOSED)
@classmethod def get_closed_tickets(cls, user) -> models.QuerySet: """ For regular users with no team: return all closed tickets that are created by the user """ return cls._get_tickets(user).filter(status=cls.Status.CLOSED) def can_open(self, user): if user.is_superuser: return True return self.user == user or self.assigned_to == user or self.assigned_team in user.team_set.all() or self.assigned_team is None and user.team_set.filter(access_non_category_tickets=True).exists() def can_edit(self, user): if user.is_superuser: return True assigned_and_write_access = self.assigned_team in user.team_set.filter(readonly_access=False) or self.assigned_to == user unassigned_and_write_access = self.assigned_team is None and user.team_set.filter(access_non_category_tickets=True, readonly_access=False).exists() return self.can_open(user) and (assigned_and_write_access or unassigned_and_write_access)
def close_ticket(self): self.status = self.Status.CLOSED self.save()
def assign_to_team(self, team): if self.assigned_team != team and team is not None: self.assigned_to = None
self.assigned_team = team self.save() def followed_up_by(self): return Ticket.objects.filter(follow_up_to=self)
def get_priority(self): return self.Priority(self.priority).label def get_status(self): return self.Status(self.status).label
def __str__(self): return self.title
@receiver(post_save, sender=Ticket, dispatch_uid="team_auto_assignment")def update_team_assignment(sender, instance, created, **kwargs): if not created: return None if not instance.category or not instance.category.team: team_addresses = list(Team.objects.filter(access_non_category_tickets=True).values_list('members__email', flat=True))
else: # assign team to ticket instance.assigned_team = instance.category.team instance.save() team_addresses = list(instance.category.team.members.values_list('email', flat=True))
mail_template = MailTemplate.get_template('ticket_assigned') if not mail_template: return None mail_template.send_mail(team_addresses, { 'ticket_id': instance.id, 'ticket_title': instance.title, 'ticket_description': instance.description, 'ticket_priority': instance.get_priority(), 'ticket_category': instance.category.name if instance.category else _('General'), 'ticket_creator_username': instance.user.username})
@receiver(post_save, sender=Ticket, dispatch_uid="mail_notification")def send_mail_notification(sender, instance, created, **kwargs): if created and instance.user.receive_email_notifications: mail_template = MailTemplate.get_template('new_ticket', instance.user.language) if not mail_template: return None mail_template.send_mail([instance.user.email], { 'ticket_id': instance.id, 'ticket_creator_username': instance.user.username, 'ticket_title': instance.title, 'ticket_description': instance.description, 'ticket_category': instance.category.name if instance.category else _('General')})
@receiver(pre_save, sender=Ticket, dispatch_uid="mail_change_notification")def send_mail_change_notification(sender, instance: Ticket, update_fields=None, **kwargs): if not instance.user.receive_email_notifications: return None try: old_instance = Ticket.objects.get(id=instance.id) except Ticket.DoesNotExist: return None if old_instance.status != instance.status: mail_template = MailTemplate.get_template('ticket_status_change', instance.user.language) if not mail_template: return None mail_template.send_mail([instance.user.email], { 'ticket_id': instance.id, 'ticket_creator_username': instance.user.username, 'ticket_status': instance.get_status(), 'ticket_status_old': old_instance.get_status(), 'ticket_title': instance.title })
class Comment(models.Model): ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) user = models.ForeignKey(PawUser, on_delete=models.CASCADE) text = models.TextField() is_only_for_staff = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs): self.ticket.status = Ticket.Status.IN_PROGRESS self.ticket.updated_at = timezone.now() self.ticket.save()
super().save(*args, **kwargs)
def __str__(self): return f'Comment by {self.user.username} on {self.ticket.title}'
@receiver(post_save, sender=Comment, dispatch_uid="mail_comment_notification")def send_mail_comment_notification(sender, instance, created, **kwargs): if created and instance.ticket.user.receive_email_notifications and instance.user != instance.ticket.user and not instance.is_only_for_staff: mail_template = MailTemplate.get_template('new_comment', instance.user.language) if not mail_template: return None mail_template.send_mail([instance.ticket.user.email], { 'ticket_id': instance.ticket.id, 'ticket_title': instance.ticket.title, 'ticket_creator_username': instance.user.username, 'comment_text': instance.text})
class FileAttachment(models.Model): ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) file = models.FileField( upload_to=ticket_directory_path, max_length=255, storage=SecureFileStorage() if settings.SECURE_MEDIA_ENABLED else None) uploaded_at = models.DateTimeField(auto_now_add=True)
def __str__(self): return f'{_('Attachment for')} {self.ticket.title}' def get_attachment_url(self): """ Returns the URL for downloading the attachment. """ from django.urls import reverse return reverse('download_attachment', args=[self.id])
class Template(models.Model): name = models.CharField(max_length=200) content = models.TextField() category = models.ForeignKey( Category, on_delete=models.CASCADE, null=True, blank=True)
def __str__(self): return self.name