""" 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 )