295 lines
11 KiB
Python
295 lines
11 KiB
Python
"""
|
|
Django signals for Support app
|
|
Handles automatic notifications when tickets are updated
|
|
"""
|
|
|
|
from django.db.models.signals import post_save, pre_save
|
|
from django.dispatch import receiver
|
|
from django.core.mail import EmailMultiAlternatives, get_connection
|
|
from django.template.loader import render_to_string
|
|
from django.conf import settings
|
|
import logging
|
|
import time
|
|
|
|
from .models import SupportTicket, TicketMessage, TicketActivity
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _send_email_with_retry(email_message, max_retries: int = 3, delay: float = 1.0) -> bool:
|
|
"""
|
|
Send email with retry logic for production reliability.
|
|
Uses Django settings from .env file.
|
|
|
|
Args:
|
|
email_message: EmailMultiAlternatives instance
|
|
max_retries: Maximum number of retry attempts
|
|
delay: Delay between retries in seconds
|
|
|
|
Returns:
|
|
bool: True if email was sent successfully, False otherwise
|
|
"""
|
|
for attempt in range(max_retries + 1):
|
|
try:
|
|
# Test connection before sending (uses EMAIL_BACKEND from settings)
|
|
connection = get_connection()
|
|
connection.open()
|
|
connection.close()
|
|
|
|
# Send the email (uses EMAIL_BACKEND and credentials from settings)
|
|
email_message.send()
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Email send attempt {attempt + 1} failed: {str(e)}")
|
|
|
|
if attempt < max_retries:
|
|
time.sleep(delay * (2 ** attempt)) # Exponential backoff
|
|
continue
|
|
else:
|
|
logger.error(f"Failed to send email after {max_retries + 1} attempts: {str(e)}")
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
class TicketUpdateNotifier:
|
|
"""Service for sending ticket update notifications"""
|
|
|
|
@staticmethod
|
|
def send_status_change_notification(ticket, old_status, new_status):
|
|
"""Send email when ticket status changes"""
|
|
try:
|
|
subject = f'Ticket Status Updated: {ticket.ticket_number}'
|
|
|
|
context = {
|
|
'ticket': ticket,
|
|
'ticket_number': ticket.ticket_number,
|
|
'user_name': ticket.user_name,
|
|
'title': ticket.title,
|
|
'old_status': old_status,
|
|
'new_status': new_status,
|
|
'status_color': ticket.status.color if ticket.status else '#3b82f6',
|
|
'updated_at': ticket.updated_at.strftime('%B %d, %Y at %I:%M %p'),
|
|
'support_url': f'{settings.SITE_URL}/support-center',
|
|
'logo_url': f'{settings.SITE_URL}/images/logo.png',
|
|
'site_url': settings.SITE_URL,
|
|
}
|
|
|
|
# Render HTML email
|
|
html_message = render_to_string(
|
|
'support/ticket_status_update.html',
|
|
context
|
|
)
|
|
|
|
# Create plain text version
|
|
text_message = render_to_string(
|
|
'support/ticket_status_update.txt',
|
|
context
|
|
)
|
|
|
|
# Create email (uses DEFAULT_FROM_EMAIL from settings)
|
|
email = EmailMultiAlternatives(
|
|
subject=subject,
|
|
body=text_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL, # From .env file
|
|
to=[ticket.user_email],
|
|
)
|
|
|
|
email.attach_alternative(html_message, "text/html")
|
|
|
|
# Send email with retry logic (uses EMAIL_BACKEND and credentials from settings)
|
|
success = _send_email_with_retry(email)
|
|
|
|
if success:
|
|
logger.info(f'Status change notification sent for ticket {ticket.ticket_number}')
|
|
else:
|
|
logger.error(f'Failed to send status change notification for ticket {ticket.ticket_number} after retries')
|
|
return success
|
|
|
|
except Exception as e:
|
|
logger.error(f'Failed to send status change notification: {str(e)}')
|
|
return False
|
|
|
|
@staticmethod
|
|
def send_message_notification(ticket, message):
|
|
"""Send email when a new message is added to the ticket"""
|
|
try:
|
|
# Only send if it's an agent response (not user message)
|
|
if message.message_type != 'agent_response':
|
|
return False
|
|
|
|
subject = f'New Response on Ticket: {ticket.ticket_number}'
|
|
|
|
context = {
|
|
'ticket': ticket,
|
|
'ticket_number': ticket.ticket_number,
|
|
'user_name': ticket.user_name,
|
|
'title': ticket.title,
|
|
'message': message.content,
|
|
'message_author': message.author_name or 'Support Team',
|
|
'created_at': message.created_at.strftime('%B %d, %Y at %I:%M %p'),
|
|
'support_url': f'{settings.SITE_URL}/support-center',
|
|
'logo_url': f'{settings.SITE_URL}/images/logo.png',
|
|
'site_url': settings.SITE_URL,
|
|
}
|
|
|
|
# Render HTML email
|
|
html_message = render_to_string(
|
|
'support/ticket_message_notification.html',
|
|
context
|
|
)
|
|
|
|
# Create plain text version
|
|
text_message = render_to_string(
|
|
'support/ticket_message_notification.txt',
|
|
context
|
|
)
|
|
|
|
# Create email (uses DEFAULT_FROM_EMAIL from settings)
|
|
email = EmailMultiAlternatives(
|
|
subject=subject,
|
|
body=text_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL, # From .env file
|
|
to=[ticket.user_email],
|
|
)
|
|
|
|
email.attach_alternative(html_message, "text/html")
|
|
|
|
# Send email with retry logic (uses EMAIL_BACKEND and credentials from settings)
|
|
success = _send_email_with_retry(email)
|
|
|
|
if success:
|
|
logger.info(f'Message notification sent for ticket {ticket.ticket_number}')
|
|
else:
|
|
logger.error(f'Failed to send message notification for ticket {ticket.ticket_number} after retries')
|
|
return success
|
|
|
|
except Exception as e:
|
|
logger.error(f'Failed to send message notification: {str(e)}')
|
|
return False
|
|
|
|
@staticmethod
|
|
def send_assignment_notification(ticket, assigned_to):
|
|
"""Send email when ticket is assigned"""
|
|
try:
|
|
subject = f'Ticket Assigned: {ticket.ticket_number}'
|
|
|
|
context = {
|
|
'ticket': ticket,
|
|
'ticket_number': ticket.ticket_number,
|
|
'user_name': ticket.user_name,
|
|
'title': ticket.title,
|
|
'assigned_to': assigned_to.get_full_name() or assigned_to.username,
|
|
'updated_at': ticket.updated_at.strftime('%B %d, %Y at %I:%M %p'),
|
|
'support_url': f'{settings.SITE_URL}/support-center',
|
|
'logo_url': f'{settings.SITE_URL}/images/logo.png',
|
|
'site_url': settings.SITE_URL,
|
|
}
|
|
|
|
# Render HTML email
|
|
html_message = render_to_string(
|
|
'support/ticket_assigned_notification.html',
|
|
context
|
|
)
|
|
|
|
# Create plain text version
|
|
text_message = render_to_string(
|
|
'support/ticket_assigned_notification.txt',
|
|
context
|
|
)
|
|
|
|
# Create email (uses DEFAULT_FROM_EMAIL from settings)
|
|
email = EmailMultiAlternatives(
|
|
subject=subject,
|
|
body=text_message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL, # From .env file
|
|
to=[ticket.user_email],
|
|
)
|
|
|
|
email.attach_alternative(html_message, "text/html")
|
|
|
|
# Send email with retry logic (uses EMAIL_BACKEND and credentials from settings)
|
|
success = _send_email_with_retry(email)
|
|
|
|
if success:
|
|
logger.info(f'Assignment notification sent for ticket {ticket.ticket_number}')
|
|
else:
|
|
logger.error(f'Failed to send assignment notification for ticket {ticket.ticket_number} after retries')
|
|
return success
|
|
|
|
except Exception as e:
|
|
logger.error(f'Failed to send assignment notification: {str(e)}')
|
|
return False
|
|
|
|
|
|
# Store original values before save
|
|
@receiver(pre_save, sender=SupportTicket)
|
|
def store_ticket_original_values(sender, instance, **kwargs):
|
|
"""Store original values before ticket is updated"""
|
|
if instance.pk:
|
|
try:
|
|
original = SupportTicket.objects.get(pk=instance.pk)
|
|
instance._original_status = original.status
|
|
instance._original_assigned_to = original.assigned_to
|
|
except SupportTicket.DoesNotExist:
|
|
pass
|
|
|
|
|
|
# Send notifications after ticket is saved
|
|
@receiver(post_save, sender=SupportTicket)
|
|
def send_ticket_update_notifications(sender, instance, created, **kwargs):
|
|
"""Send notifications when ticket is updated"""
|
|
if created:
|
|
# Ticket creation is handled in serializer
|
|
return
|
|
|
|
# Check for status change
|
|
if hasattr(instance, '_original_status') and instance._original_status != instance.status:
|
|
old_status = instance._original_status.name if instance._original_status else 'Unknown'
|
|
new_status = instance.status.name if instance.status else 'Unknown'
|
|
|
|
TicketUpdateNotifier.send_status_change_notification(
|
|
instance,
|
|
old_status,
|
|
new_status
|
|
)
|
|
|
|
# Create activity log
|
|
TicketActivity.objects.create(
|
|
ticket=instance,
|
|
activity_type='status_changed',
|
|
description=f'Status changed from {old_status} to {new_status}',
|
|
old_value=old_status,
|
|
new_value=new_status
|
|
)
|
|
|
|
# Check for assignment change
|
|
if hasattr(instance, '_original_assigned_to') and instance._original_assigned_to != instance.assigned_to:
|
|
if instance.assigned_to:
|
|
TicketUpdateNotifier.send_assignment_notification(
|
|
instance,
|
|
instance.assigned_to
|
|
)
|
|
|
|
# Create activity log
|
|
TicketActivity.objects.create(
|
|
ticket=instance,
|
|
activity_type='assigned',
|
|
description=f'Ticket assigned to {instance.assigned_to.get_full_name() or instance.assigned_to.username}',
|
|
new_value=str(instance.assigned_to)
|
|
)
|
|
|
|
|
|
# Send notification when message is added
|
|
@receiver(post_save, sender=TicketMessage)
|
|
def send_message_notification(sender, instance, created, **kwargs):
|
|
"""Send notification when a new message is added"""
|
|
if created and not instance.is_internal:
|
|
# Only send if it's a new message that's not internal
|
|
TicketUpdateNotifier.send_message_notification(
|
|
instance.ticket,
|
|
instance
|
|
)
|
|
|