Files
smoothschedule/smoothschedule/tickets/signals.py
poduck ed11b9c634 fix: Prevent notification errors from rolling back ticket creation
Add availability check for notifications app to avoid database errors
when the notifications table doesn't exist.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 05:33:56 -05:00

289 lines
11 KiB
Python

import logging
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from .models import Ticket, TicketComment
from smoothschedule.users.models import User
logger = logging.getLogger(__name__)
# Flag to check if notifications app is available
_notifications_available = None
def is_notifications_available():
"""Check if the notifications app is installed and migrated."""
global _notifications_available
if _notifications_available is None:
try:
from notifications.models import Notification
# Check if the table exists by doing a simple query
Notification.objects.exists()
_notifications_available = True
except Exception:
_notifications_available = False
return _notifications_available
def send_websocket_notification(group_name, message_data):
"""Safely send a WebSocket notification, handling errors gracefully."""
try:
channel_layer = get_channel_layer()
if channel_layer is None:
logger.warning("Channel layer not configured, skipping WebSocket notification")
return
async_to_sync(channel_layer.group_send)(
group_name,
{
"type": "notification_message",
"message": message_data
}
)
except Exception as e:
logger.error(f"Failed to send WebSocket notification to {group_name}: {e}")
def create_notification(recipient, actor, verb, action_object, target, data):
"""Safely create a notification, handling import and creation errors."""
# Skip notification creation if the notifications app is not available
if not is_notifications_available():
logger.debug("notifications app not available, skipping notification creation")
return
try:
from notifications.models import Notification
Notification.objects.create(
recipient=recipient,
actor=actor,
verb=verb,
action_object=action_object,
target=target,
data=data
)
except ImportError:
logger.warning("notifications app not installed, skipping notification creation")
except Exception as e:
logger.error(f"Failed to create notification for {recipient}: {e}")
def get_platform_support_team():
"""Get all platform support team members."""
try:
return User.objects.filter(
role__in=[User.Role.PLATFORM_SUPPORT, User.Role.PLATFORM_MANAGER, User.Role.SUPERUSER],
is_active=True
)
except Exception as e:
logger.error(f"Failed to fetch platform support team: {e}")
return User.objects.none()
def get_tenant_managers(tenant):
"""Get all owners and managers for a tenant."""
try:
if not tenant:
return User.objects.none()
return User.objects.filter(
tenant=tenant,
role__in=[User.Role.TENANT_OWNER, User.Role.TENANT_MANAGER],
is_active=True
)
except Exception as e:
logger.error(f"Failed to fetch tenant managers for {tenant}: {e}")
return User.objects.none()
@receiver(post_save, sender=Ticket)
def ticket_notification_handler(sender, instance, created, **kwargs):
"""Handle ticket save events and send notifications."""
try:
if created:
# Handle ticket creation notifications
_handle_ticket_creation(instance)
else:
# Handle ticket update notifications
_handle_ticket_update(instance)
except Exception as e:
logger.error(f"Error in ticket_notification_handler for ticket {instance.id}: {e}")
def _handle_ticket_creation(ticket):
"""Send notifications when a ticket is created."""
try:
creator_name = ticket.creator.full_name if ticket.creator else "Someone"
if ticket.ticket_type == Ticket.TicketType.PLATFORM:
# PLATFORM tickets: Notify platform support team
platform_team = get_platform_support_team()
for member in platform_team:
create_notification(
recipient=member,
actor=ticket.creator,
verb=f"New platform support ticket #{ticket.id}: '{ticket.subject}'",
action_object=ticket,
target=ticket,
data={
'ticket_id': ticket.id,
'subject': ticket.subject,
'priority': ticket.priority,
'category': ticket.category
}
)
send_websocket_notification(
f"user_{member.id}",
{
"type": "new_ticket",
"ticket_id": ticket.id,
"subject": ticket.subject,
"ticket_type": ticket.ticket_type,
"priority": ticket.priority,
"creator_name": creator_name,
"message": f"New platform support ticket from {creator_name}: {ticket.subject}"
}
)
elif ticket.ticket_type in [
Ticket.TicketType.CUSTOMER,
Ticket.TicketType.STAFF_REQUEST,
Ticket.TicketType.INTERNAL
]:
# CUSTOMER, STAFF_REQUEST, INTERNAL tickets: Notify tenant owner/managers
tenant_managers = get_tenant_managers(ticket.tenant)
ticket_type_display = ticket.get_ticket_type_display()
for manager in tenant_managers:
create_notification(
recipient=manager,
actor=ticket.creator,
verb=f"New {ticket_type_display.lower()} ticket #{ticket.id}: '{ticket.subject}'",
action_object=ticket,
target=ticket,
data={
'ticket_id': ticket.id,
'subject': ticket.subject,
'priority': ticket.priority,
'category': ticket.category,
'ticket_type': ticket.ticket_type
}
)
send_websocket_notification(
f"user_{manager.id}",
{
"type": "new_ticket",
"ticket_id": ticket.id,
"subject": ticket.subject,
"ticket_type": ticket.ticket_type,
"priority": ticket.priority,
"creator_name": creator_name,
"message": f"New {ticket_type_display.lower()} from {creator_name}: {ticket.subject}"
}
)
except Exception as e:
logger.error(f"Error handling ticket creation for ticket {ticket.id}: {e}")
def _handle_ticket_update(ticket):
"""Send notifications when a ticket is updated."""
try:
# Notify assignee if one exists
if not ticket.assignee:
return
# Create Notification object for the assignee
create_notification(
recipient=ticket.assignee,
actor=ticket.creator,
verb=f"Ticket #{ticket.id} '{ticket.subject}' was updated.",
action_object=ticket,
target=ticket,
data={'ticket_id': ticket.id, 'subject': ticket.subject, 'status': ticket.status}
)
# Send WebSocket message to assignee's personal channel
send_websocket_notification(
f"user_{ticket.assignee.id}",
{
"type": "ticket_update",
"ticket_id": ticket.id,
"subject": ticket.subject,
"status": ticket.status,
"assignee_id": str(ticket.assignee.id),
"message": f"Ticket #{ticket.id} '{ticket.subject}' updated. Status: {ticket.status}"
}
)
except Exception as e:
logger.error(f"Error handling ticket update for ticket {ticket.id}: {e}")
@receiver(post_save, sender=TicketComment)
def comment_notification_handler(sender, instance, created, **kwargs):
"""Handle comment creation and send notifications to relevant parties."""
if not created:
return
try:
ticket = instance.ticket
author_name = instance.author.full_name if instance.author else "Someone"
# Track first_response_at: when a comment is added by someone other than the ticket creator
if not ticket.first_response_at and instance.author and instance.author != ticket.creator:
try:
ticket.first_response_at = timezone.now()
ticket.save(update_fields=['first_response_at'])
logger.info(f"Set first_response_at for ticket {ticket.id}")
except Exception as e:
logger.error(f"Failed to set first_response_at for ticket {ticket.id}: {e}")
# Notify creator of the ticket (if not the commenter)
if ticket.creator and ticket.creator != instance.author:
create_notification(
recipient=ticket.creator,
actor=instance.author,
verb=f"New comment on your ticket #{ticket.id} '{ticket.subject}'.",
action_object=instance,
target=ticket,
data={'ticket_id': ticket.id, 'subject': ticket.subject, 'comment_id': instance.id}
)
send_websocket_notification(
f"user_{ticket.creator.id}",
{
"type": "new_comment",
"ticket_id": ticket.id,
"subject": ticket.subject,
"comment_id": instance.id,
"author_name": author_name,
"message": f"New comment on your ticket #{ticket.id} from {author_name}."
}
)
# Notify assignee of the ticket (if not the commenter and not the creator)
if ticket.assignee and ticket.assignee != instance.author and ticket.assignee != ticket.creator:
create_notification(
recipient=ticket.assignee,
actor=instance.author,
verb=f"New comment on ticket #{ticket.id} '{ticket.subject}' you are assigned to.",
action_object=instance,
target=ticket,
data={'ticket_id': ticket.id, 'subject': ticket.subject, 'comment_id': instance.id}
)
send_websocket_notification(
f"user_{ticket.assignee.id}",
{
"type": "new_comment",
"ticket_id": ticket.id,
"subject": ticket.subject,
"comment_id": instance.id,
"author_name": author_name,
"message": f"New comment on ticket #{ticket.id} you are assigned to from {author_name}."
}
)
except Exception as e:
logger.error(f"Error in comment_notification_handler for comment {instance.id}: {e}")