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>
289 lines
11 KiB
Python
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}")
|