Based on the git diff provided, here's a concise and descriptive commit message:

feat: add passkey authentication and enhance user preferences

- Add passkey login security event type with fingerprint icon
- Include request and site context in email confirmation for backend
- Add user_id exact match filter to prevent incorrect user lookups
- Enable PATCH method for updating user preferences via API
- Add moderation_preferences support to user settings
- Optimize ticket queries with select_related and prefetch_related

This commit introduces passkey authentication tracking, improves user
profile filtering accuracy, and extends the preferences API to support
updates. Query optimizations reduce database hits for ticket listings.
This commit is contained in:
pacnpal
2026-01-12 19:13:05 -05:00
parent 2b66814d82
commit d631f3183c
56 changed files with 5860 additions and 264 deletions

View File

@@ -2131,12 +2131,14 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing photo submissions.
Now queries EditSubmission with submission_type="PHOTO" for unified model.
Includes claim/unclaim endpoints with concurrency protection using
database row locking (select_for_update) to prevent race conditions.
"""
queryset = PhotoSubmission.objects.all()
serializer_class = PhotoSubmissionSerializer
# Use EditSubmission filtered by PHOTO type instead of separate PhotoSubmission model
queryset = EditSubmission.objects.filter(submission_type="PHOTO")
serializer_class = EditSubmissionSerializer # Use unified serializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
search_fields = ["caption", "notes"]
ordering_fields = ["created_at", "status"]
@@ -2144,10 +2146,10 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
permission_classes = [CanViewModerationData]
def get_queryset(self):
queryset = super().get_queryset()
status = self.request.query_params.get("status")
if status:
queryset = queryset.filter(status=status)
queryset = EditSubmission.objects.filter(submission_type="PHOTO")
status_param = self.request.query_params.get("status")
if status_param:
queryset = queryset.filter(status=status_param)
# User filter
user_id = self.request.query_params.get("user")
@@ -2168,8 +2170,9 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
with transaction.atomic():
try:
submission = PhotoSubmission.objects.select_for_update(nowait=True).get(pk=pk)
except PhotoSubmission.DoesNotExist:
# Use EditSubmission filtered by PHOTO type
submission = EditSubmission.objects.filter(submission_type="PHOTO").select_for_update(nowait=True).get(pk=pk)
except EditSubmission.DoesNotExist:
return Response({"error": "Submission not found"}, status=status.HTTP_404_NOT_FOUND)
except DatabaseError:
return Response(
@@ -2198,9 +2201,10 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
log_business_event(
logger,
event_type="submission_claimed",
message=f"PhotoSubmission {submission.id} claimed by {request.user.username}",
message=f"Photo EditSubmission {submission.id} claimed by {request.user.username}",
context={
"model": "PhotoSubmission",
"model": "EditSubmission",
"submission_type": "PHOTO",
"object_id": submission.id,
"claimed_by": request.user.username,
},
@@ -2767,3 +2771,55 @@ class ModerationStatsView(APIView):
}
return Response(stats_data)
# ============================================================================
# Moderation Audit Log ViewSet
# ============================================================================
class ModerationAuditLogViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet for viewing moderation audit logs.
Provides read-only access to moderation action history for auditing
and accountability purposes.
"""
from .models import ModerationAuditLog
from .serializers import ModerationAuditLogSerializer
queryset = ModerationAuditLog.objects.select_related(
"submission", "submission__content_type", "moderator"
).all()
serializer_class = ModerationAuditLogSerializer
permission_classes = [IsAdminOrSuperuser]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["action", "is_system_action", "is_test_data"]
search_fields = ["notes"]
ordering_fields = ["created_at", "action"]
ordering = ["-created_at"]
def get_queryset(self):
queryset = super().get_queryset()
# Filter by submission ID
submission_id = self.request.query_params.get("submission_id")
if submission_id:
queryset = queryset.filter(submission_id=submission_id)
# Filter by moderator ID
moderator_id = self.request.query_params.get("moderator_id")
if moderator_id:
queryset = queryset.filter(moderator_id=moderator_id)
# Date range filtering
start_date = self.request.query_params.get("start_date")
end_date = self.request.query_params.get("end_date")
if start_date:
queryset = queryset.filter(created_at__gte=start_date)
if end_date:
queryset = queryset.filter(created_at__lte=end_date)
return queryset