""" Contact submission API endpoints. Handles user contact form submissions and admin management. """ from typing import Optional from uuid import UUID from ninja import Router from django.db.models import Q, Count, Avg from django.utils import timezone from datetime import timedelta from apps.contact.models import ContactSubmission from apps.contact.tasks import send_contact_confirmation_email, notify_admins_new_contact, send_contact_resolution_email from apps.users.permissions import require_role from api.v1.schemas import ( ContactSubmissionCreate, ContactSubmissionUpdate, ContactSubmissionOut, ContactSubmissionListOut, ContactSubmissionStatsOut, MessageSchema, ErrorSchema, ) router = Router(tags=["Contact"]) # ============================================================================ # Public Endpoints # ============================================================================ @router.post("/submit", response={200: ContactSubmissionOut, 400: ErrorSchema}) def submit_contact_form(request, data: ContactSubmissionCreate): """ Submit a contact form. Available to both authenticated and anonymous users. """ try: # Create the contact submission contact = ContactSubmission.objects.create( name=data.name, email=data.email, subject=data.subject, message=data.message, category=data.category, user=request.auth if request.auth else None ) # Send confirmation email to user send_contact_confirmation_email.delay(str(contact.id)) # Notify admins notify_admins_new_contact.delay(str(contact.id)) # Prepare response response = ContactSubmissionOut.from_orm(contact) response.user_email = contact.user.email if contact.user else None return 200, response except Exception as e: return 400, {"error": "Failed to submit contact form", "detail": str(e)} # ============================================================================ # Admin/Moderator Endpoints # ============================================================================ @router.get("/", response={200: ContactSubmissionListOut, 403: ErrorSchema}) @require_role(['moderator', 'admin']) def list_contact_submissions( request, page: int = 1, page_size: int = 20, status: Optional[str] = None, category: Optional[str] = None, assigned_to_me: bool = False, search: Optional[str] = None ): """ List contact submissions (moderators/admins only). Supports filtering and pagination. """ queryset = ContactSubmission.objects.all() # Apply filters if status: queryset = queryset.filter(status=status) if category: queryset = queryset.filter(category=category) if assigned_to_me and request.auth: queryset = queryset.filter(assigned_to=request.auth) if search: queryset = queryset.filter( Q(ticket_number__icontains=search) | Q(name__icontains=search) | Q(email__icontains=search) | Q(subject__icontains=search) | Q(message__icontains=search) ) # Get total count total = queryset.count() # Apply pagination start = (page - 1) * page_size end = start + page_size contacts = queryset[start:end] # Prepare response items = [] for contact in contacts: item = ContactSubmissionOut.from_orm(contact) item.user_email = contact.user.email if contact.user else None item.assigned_to_email = contact.assigned_to.email if contact.assigned_to else None item.resolved_by_email = contact.resolved_by.email if contact.resolved_by else None items.append(item) return { "items": items, "total": total, "page": page, "page_size": page_size, "total_pages": (total + page_size - 1) // page_size } @router.get("/{contact_id}", response={200: ContactSubmissionOut, 404: ErrorSchema, 403: ErrorSchema}) @require_role(['moderator', 'admin']) def get_contact_submission(request, contact_id: UUID): """ Get a specific contact submission by ID (moderators/admins only). """ try: contact = ContactSubmission.objects.get(id=contact_id) response = ContactSubmissionOut.from_orm(contact) response.user_email = contact.user.email if contact.user else None response.assigned_to_email = contact.assigned_to.email if contact.assigned_to else None response.resolved_by_email = contact.resolved_by.email if contact.resolved_by else None return 200, response except ContactSubmission.DoesNotExist: return 404, {"error": "Contact submission not found"} @router.patch("/{contact_id}", response={200: ContactSubmissionOut, 404: ErrorSchema, 400: ErrorSchema, 403: ErrorSchema}) @require_role(['moderator', 'admin']) def update_contact_submission(request, contact_id: UUID, data: ContactSubmissionUpdate): """ Update a contact submission (moderators/admins only). Used to change status, assign, or add notes. """ try: contact = ContactSubmission.objects.get(id=contact_id) # Track if status changed to resolved status_changed_to_resolved = False old_status = contact.status # Update fields if data.status is not None: contact.status = data.status if data.status == 'resolved' and old_status != 'resolved': status_changed_to_resolved = True contact.resolved_by = request.auth contact.resolved_at = timezone.now() if data.assigned_to_id is not None: from apps.users.models import User try: contact.assigned_to = User.objects.get(id=data.assigned_to_id) except User.DoesNotExist: return 400, {"error": "Invalid user ID for assignment"} if data.admin_notes is not None: contact.admin_notes = data.admin_notes contact.save() # Send resolution email if status changed to resolved if status_changed_to_resolved: send_contact_resolution_email.delay(str(contact.id)) # Prepare response response = ContactSubmissionOut.from_orm(contact) response.user_email = contact.user.email if contact.user else None response.assigned_to_email = contact.assigned_to.email if contact.assigned_to else None response.resolved_by_email = contact.resolved_by.email if contact.resolved_by else None return 200, response except ContactSubmission.DoesNotExist: return 404, {"error": "Contact submission not found"} except Exception as e: return 400, {"error": "Failed to update contact submission", "detail": str(e)} @router.post("/{contact_id}/assign-to-me", response={200: MessageSchema, 404: ErrorSchema, 403: ErrorSchema}) @require_role(['moderator', 'admin']) def assign_to_me(request, contact_id: UUID): """ Assign a contact submission to the current user (moderators/admins only). """ try: contact = ContactSubmission.objects.get(id=contact_id) contact.assigned_to = request.auth contact.save() return 200, { "message": f"Contact submission {contact.ticket_number} assigned to you", "success": True } except ContactSubmission.DoesNotExist: return 404, {"error": "Contact submission not found"} @router.post("/{contact_id}/mark-resolved", response={200: MessageSchema, 404: ErrorSchema, 403: ErrorSchema}) @require_role(['moderator', 'admin']) def mark_resolved(request, contact_id: UUID): """ Mark a contact submission as resolved (moderators/admins only). """ try: contact = ContactSubmission.objects.get(id=contact_id) if contact.status == 'resolved': return 200, { "message": f"Contact submission {contact.ticket_number} is already resolved", "success": True } contact.status = 'resolved' contact.resolved_by = request.auth contact.resolved_at = timezone.now() contact.save() # Send resolution email send_contact_resolution_email.delay(str(contact.id)) return 200, { "message": f"Contact submission {contact.ticket_number} marked as resolved", "success": True } except ContactSubmission.DoesNotExist: return 404, {"error": "Contact submission not found"} @router.get("/stats/overview", response={200: ContactSubmissionStatsOut, 403: ErrorSchema}) @require_role(['moderator', 'admin']) def get_contact_stats(request): """ Get contact submission statistics (moderators/admins only). """ # Get counts by status total_submissions = ContactSubmission.objects.count() pending_submissions = ContactSubmission.objects.filter(status='pending').count() in_progress_submissions = ContactSubmission.objects.filter(status='in_progress').count() resolved_submissions = ContactSubmission.objects.filter(status='resolved').count() archived_submissions = ContactSubmission.objects.filter(status='archived').count() # Get counts by category submissions_by_category = dict( ContactSubmission.objects.values('category').annotate( count=Count('id') ).values_list('category', 'count') ) # Calculate average resolution time resolved_contacts = ContactSubmission.objects.filter( status='resolved', resolved_at__isnull=False ).exclude(created_at=None) avg_resolution_time = None if resolved_contacts.exists(): total_time = sum([ (contact.resolved_at - contact.created_at).total_seconds() / 3600 for contact in resolved_contacts ]) avg_resolution_time = total_time / resolved_contacts.count() # Get recent submissions recent = ContactSubmission.objects.order_by('-created_at')[:5] recent_submissions = [] for contact in recent: item = ContactSubmissionOut.from_orm(contact) item.user_email = contact.user.email if contact.user else None item.assigned_to_email = contact.assigned_to.email if contact.assigned_to else None item.resolved_by_email = contact.resolved_by.email if contact.resolved_by else None recent_submissions.append(item) return { "total_submissions": total_submissions, "pending_submissions": pending_submissions, "in_progress_submissions": in_progress_submissions, "resolved_submissions": resolved_submissions, "archived_submissions": archived_submissions, "submissions_by_category": submissions_by_category, "average_resolution_time_hours": avg_resolution_time, "recent_submissions": recent_submissions } @router.delete("/{contact_id}", response={200: MessageSchema, 404: ErrorSchema, 403: ErrorSchema}) @require_role(['admin']) def delete_contact_submission(request, contact_id: UUID): """ Delete a contact submission (admins only). Use with caution - typically should archive instead. """ try: contact = ContactSubmission.objects.get(id=contact_id) ticket_number = contact.ticket_number contact.delete() return 200, { "message": f"Contact submission {ticket_number} deleted", "success": True } except ContactSubmission.DoesNotExist: return 404, {"error": "Contact submission not found"}