Free and open source ticket system written in python

📦 NEW: Add mail templates and db seeding

+223 -9
+10 -1
core/admin.py
··· 1 1 from django.contrib import admin 2 2 from django.contrib.auth.admin import UserAdmin 3 - from .models import PawUser 3 + from .models import PawUser, MailTemplate, GoogleSSOUser 4 4 5 5 6 6 @admin.register(PawUser) ··· 9 9 ('Extended Data', { 10 10 'fields': ('profile_picture', 'language', 'telegram_username')}), 11 11 ) 12 + 13 + @admin.register(MailTemplate) 14 + class MailTemplateAdmin(admin.ModelAdmin): 15 + list_display = ('name', 'event', 'language') 16 + list_filter = ('event', 'language') 17 + 18 + @admin.register(GoogleSSOUser) 19 + class GoogleSSOUserAdmin(admin.ModelAdmin): 20 + list_display = ('paw_user', 'google_id')
+39
core/migrations/0006_mailtemplate.py
··· 1 + # Generated by Django 5.0.3 on 2024-03-23 14:42 2 + 3 + from django.db import migrations, models 4 + 5 + 6 + class Migration(migrations.Migration): 7 + 8 + dependencies = [ 9 + ("core", "0005_remove_googlessouser_id_alter_googlessouser_paw_user"), 10 + ] 11 + 12 + operations = [ 13 + migrations.CreateModel( 14 + name="MailTemplate", 15 + fields=[ 16 + ( 17 + "id", 18 + models.BigAutoField( 19 + auto_created=True, 20 + primary_key=True, 21 + serialize=False, 22 + verbose_name="ID", 23 + ), 24 + ), 25 + ("event", models.CharField(max_length=100)), 26 + ( 27 + "language", 28 + models.CharField( 29 + choices=[("en", "English"), ("fr", "French"), ("de", "German")], 30 + default="en", 31 + max_length=2, 32 + ), 33 + ), 34 + ("name", models.CharField(max_length=200)), 35 + ("subject", models.CharField(max_length=200)), 36 + ("content", models.TextField()), 37 + ], 38 + ), 39 + ]
+82
core/migrations/0007_seed_mailtemplates.py
··· 1 + from django.db import migrations 2 + from core.models import MailTemplate 3 + 4 + def add_initial_templates(apps, schema_editor): 5 + MailTemplate.objects.create( 6 + event='ticket_status_change', 7 + name='Ticket Status Change', 8 + language='en', 9 + subject='Your ticket status has changed', 10 + content='''\ 11 + Hello {ticket_creator_username}, 12 + 13 + This is to inform you that the status of your ticket #{ticket_id} {ticket_title} has been changed from "{ticket_status_old} to "{ticket_status}". 14 + 15 + Thank you for your patience. 16 + 17 + Best regards, 18 + [Your Organization Name] 19 + ''' 20 + ) 21 + 22 + MailTemplate.objects.create( 23 + event='new_ticket', 24 + name='New Ticket', 25 + language='en', 26 + subject='Your ticket has been created successfully', 27 + content='''\ 28 + Dear {ticket_creator_username}, 29 + 30 + We're writing to inform you that your ticket #{ticket_id} has been created successfully. 31 + 32 + Details: 33 + Title: {ticket_title} 34 + Category: {ticket_category} 35 + Description: 36 + {ticket_description} 37 + 38 + Our team will review your ticket and respond as soon as possible. You can track the status of your ticket by logging into your account. 39 + 40 + Thank you for reaching out to us. 41 + 42 + Best regards, 43 + [Your Organization Name] 44 + ''' 45 + ) 46 + 47 + MailTemplate.objects.create( 48 + event='ticket_assigned', 49 + name='Ticket Assignment', 50 + language='en', 51 + subject='A new ticket has been assigned to your team', 52 + content='''\ 53 + Hello Team, 54 + 55 + A new ticket has been created with the following details: 56 + 57 + Ticket ID: #{ticket_id} 58 + Title: {ticket_title} 59 + Priority: {ticket_priority} 60 + Category: {ticket_category} 61 + Created By: {ticket_creator_username} 62 + Description: 63 + {ticket_description} 64 + 65 + Please review the ticket and take appropriate action. 66 + 67 + Thank you for your attention. 68 + 69 + Best regards, 70 + [Your Organization Name] 71 + ''' 72 + ) 73 + 74 + 75 + 76 + class Migration(migrations.Migration): 77 + 78 + dependencies = [ 79 + ("core", "0006_mailtemplate"), 80 + ] 81 + 82 + operations = [migrations.RunPython(add_initial_templates),]
+31
core/models.py
··· 1 1 from django.db import models 2 2 from django.contrib.auth.models import AbstractUser 3 3 from django.utils.translation import gettext_lazy as _ 4 + from django.conf import settings 5 + from django.core.mail import send_mail 4 6 5 7 6 8 class PawUser(AbstractUser): ··· 25 27 class Meta: 26 28 db_table = "google_sso_user" 27 29 verbose_name = _("Google SSO User") 30 + 31 + class MailTemplate(models.Model): 32 + event = models.CharField(max_length=100) 33 + language = models.CharField(max_length=2, default='en', choices=settings.LANGUAGES) 34 + name = models.CharField(max_length=200) 35 + subject = models.CharField(max_length=200) 36 + content = models.TextField() 37 + 38 + @classmethod 39 + def get_template(cls, event, language='en'): 40 + template = cls.objects.filter(event=event, language=language).first() 41 + if not template: 42 + template = cls.objects.filter(event=event, language='en').first() 43 + return template 44 + 45 + def send_mail(self, to, context): 46 + try: 47 + send_mail( 48 + self.subject.format(**context), 49 + self.content.format(**context), 50 + settings.DEFAULT_FROM_EMAIL, 51 + [to], 52 + fail_silently=False, 53 + ) 54 + except Exception as e: 55 + print(e) 56 + 57 + def __str__(self): 58 + return f"{self.name} - [{self.event}]"
+1 -1
paw/__init__.py
··· 1 1 from django import get_version 2 2 3 - VERSION = (0, 2, 0, "beta", 1) 3 + VERSION = (0, 3, 0, "beta", 1) 4 4 5 5 __version__ = get_version(VERSION)
+11 -2
ticketing/admin.py
··· 1 1 from django.contrib import admin 2 2 from .models import Category, Ticket, Comment, Template, Team, FileAttachment 3 3 4 - # Register your models here. 4 + 5 5 admin.site.register(Category) 6 - admin.site.register(Ticket) 6 + 7 + @admin.register(Ticket) 8 + class TicketAdmin(admin.ModelAdmin): 9 + list_display = ('title', 'status', 'priority', 'category', 'created_at', 'updated_at') 10 + list_filter = ('status', 'priority', 'category') 11 + search_fields = ('title', 'description') 12 + date_hierarchy = 'created_at' 13 + ordering = ('-created_at',) 14 + readonly_fields = ('created_at', 'updated_at') 15 + 7 16 admin.site.register(Comment) 8 17 admin.site.register(Template) 9 18 admin.site.register(Team)
+49 -5
ticketing/models.py
··· 2 2 from core.models import PawUser 3 3 from django.utils import timezone 4 4 from django.utils.translation import gettext_lazy as _ 5 - from django.db.models.signals import post_save 5 + from django.db.models.signals import post_save, pre_save 6 6 from django.dispatch import receiver 7 7 from uuid import uuid4 8 + from core.models import MailTemplate 8 9 9 10 10 11 def ticket_directory_path(instance, filename): ··· 89 90 90 91 @receiver(post_save, sender=Ticket, dispatch_uid="team_auto_assignment") 91 92 def update_team_assignment(sender, instance, created, **kwargs): 92 - if not created or not instance.category or not instance.category.team: 93 - return 94 - 93 + if not created: 94 + return None 95 + 96 + if not instance.category or not instance.category.team: 97 + mail_template = MailTemplate.get_template('ticket_assigned') 98 + if not mail_template: 99 + return None 100 + #TODO send mail to all supporters 101 + return None 102 + 103 + # assign team to ticket 95 104 instance.assigned_team = instance.category.team 96 105 instance.save() 97 106 107 + mail_template = MailTemplate.get_template('ticket_assigned') 108 + if not mail_template: 109 + return None 110 + mail_template.send_mail(instance.category.team.members.values_list('email', flat=True), { 111 + 'ticket_id': instance.id, 'ticket_title': instance.title, 'ticket_description': instance.description, 112 + 'ticket_priority': instance.get_priority(), 'ticket_category': instance.category.name if instance.category else _('General'), 113 + 'ticket_creator_username': instance.user.username}) 114 + 115 + 116 + 117 + @receiver(post_save, sender=Ticket, dispatch_uid="mail_notification") 118 + def send_mail_notification(sender, instance, created, **kwargs): 119 + if created: 120 + mail_template = MailTemplate.get_template('new_ticket', instance.user.language) 121 + if not mail_template: 122 + return None 123 + mail_template.send_mail(instance.user.email, { 124 + 'ticket_id': instance.id, 'ticket_creator_username': instance.user.username, 'ticket_title': instance.title, 125 + 'ticket_description': instance.description, 'ticket_category': instance.category.name if instance.category else _('General')}) 126 + 127 + @receiver(pre_save, sender=Ticket, dispatch_uid="mail_change_notification") 128 + def send_mail_change_notification(sender, instance, update_fields=None, **kwargs): 129 + try: 130 + old_instance = Ticket.objects.get(id=instance.id) 131 + except Ticket.DoesNotExist: 132 + return None 133 + 134 + if old_instance.status != instance.status: 135 + mail_template = MailTemplate.get_template('ticket_status_change', instance.user.language) 136 + if not mail_template: 137 + return None 138 + mail_template.send_mail(instance.user.email, { 139 + 'ticket_id': instance.id, 'ticket_creator_username': instance.user.username, 'ticket_status': instance.status, 140 + 'ticket_status_old': old_instance.status, 'ticket_title': instance.title 141 + }) 98 142 99 143 class Comment(models.Model): 100 144 ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) ··· 131 175 Category, on_delete=models.CASCADE, null=True, blank=True) 132 176 133 177 def __str__(self): 134 - return self.name 178 + return self.name