feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -8,14 +8,11 @@ import json
import logging
import queue
import threading
import time
from typing import Generator
from collections.abc import Generator
from django.http import StreamingHttpResponse, JsonResponse
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from rest_framework.views import APIView
from django.http import JsonResponse, StreamingHttpResponse
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from apps.moderation.permissions import CanViewModerationData
from apps.moderation.signals import submission_status_changed
@@ -27,15 +24,15 @@ logger = logging.getLogger(__name__)
class SSEBroadcaster:
"""
Manages SSE connections and broadcasts events to all clients.
Uses a simple subscriber pattern where each connected client
gets its own queue of events to consume.
"""
def __init__(self):
self._subscribers: list[queue.Queue] = []
self._lock = threading.Lock()
def subscribe(self) -> queue.Queue:
"""Create a new subscriber queue and register it."""
client_queue = queue.Queue()
@@ -43,14 +40,14 @@ class SSEBroadcaster:
self._subscribers.append(client_queue)
logger.debug(f"SSE client subscribed. Total clients: {len(self._subscribers)}")
return client_queue
def unsubscribe(self, client_queue: queue.Queue):
"""Remove a subscriber queue."""
with self._lock:
if client_queue in self._subscribers:
self._subscribers.remove(client_queue)
logger.debug(f"SSE client unsubscribed. Total clients: {len(self._subscribers)}")
def broadcast(self, event_data: dict):
"""Send an event to all connected clients."""
with self._lock:
@@ -68,7 +65,7 @@ sse_broadcaster = SSEBroadcaster()
def handle_submission_status_changed(sender, payload, **kwargs):
"""
Signal handler that broadcasts submission status changes to SSE clients.
Connected to the submission_status_changed signal from signals.py.
"""
sse_broadcaster.broadcast(payload)
@@ -82,14 +79,14 @@ submission_status_changed.connect(handle_submission_status_changed)
class ModerationSSEView(APIView):
"""
Server-Sent Events endpoint for real-time moderation updates.
Provides a streaming response that sends submission status changes
as they occur. Clients should connect to this endpoint and keep
the connection open to receive real-time updates.
Response format (SSE):
data: {"submission_id": 1, "new_status": "CLAIMED", ...}
Usage:
const eventSource = new EventSource('/api/moderation/sse/')
eventSource.onmessage = (event) => {
@@ -97,22 +94,22 @@ class ModerationSSEView(APIView):
// Handle update
}
"""
permission_classes = [IsAuthenticated, CanViewModerationData]
def get(self, request):
"""
Establish SSE connection and stream events.
Sends a heartbeat every 30 seconds to keep the connection alive.
"""
def event_stream() -> Generator[str, None, None]:
def event_stream() -> Generator[str]:
client_queue = sse_broadcaster.subscribe()
try:
# Send initial connection event
yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established'})}\n\n"
while True:
try:
# Wait for event with timeout for heartbeat
@@ -120,13 +117,13 @@ class ModerationSSEView(APIView):
yield f"data: {json.dumps(event)}\n\n"
except queue.Empty:
# Send heartbeat to keep connection alive
yield f": heartbeat\n\n"
yield ": heartbeat\n\n"
except GeneratorExit:
# Client disconnected
sse_broadcaster.unsubscribe(client_queue)
finally:
sse_broadcaster.unsubscribe(client_queue)
response = StreamingHttpResponse(
event_stream(),
content_type='text/event-stream'
@@ -134,17 +131,17 @@ class ModerationSSEView(APIView):
response['Cache-Control'] = 'no-cache'
response['X-Accel-Buffering'] = 'no' # Disable nginx buffering
response['Connection'] = 'keep-alive'
return response
class ModerationSSETestView(APIView):
"""
Test endpoint to manually trigger an SSE event.
This is useful for testing the SSE connection without making
actual state transitions.
POST /api/moderation/sse/test/
{
"submission_id": 1,
@@ -153,9 +150,9 @@ class ModerationSSETestView(APIView):
"previous_status": "PENDING"
}
"""
permission_classes = [IsAuthenticated, CanViewModerationData]
def post(self, request):
"""Broadcast a test event."""
test_payload = {
@@ -168,9 +165,9 @@ class ModerationSSETestView(APIView):
"changed_by": request.user.username,
"test": True,
}
sse_broadcaster.broadcast(test_payload)
return JsonResponse({
"status": "ok",
"message": f"Test event broadcast to {len(sse_broadcaster._subscribers)} clients",