feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -86,9 +86,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
filtering, search, and permission controls.
"""
queryset = ModerationReport.objects.select_related(
"reported_by", "assigned_moderator", "content_type"
).all()
queryset = ModerationReport.objects.select_related("reported_by", "assigned_moderator", "content_type").all()
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = ModerationReportFilter
@@ -207,9 +205,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
return Response(serializer.data)
except User.DoesNotExist:
return Response(
{"error": "Moderator not found"}, status=status.HTTP_404_NOT_FOUND
)
return Response({"error": "Moderator not found"}, status=status.HTTP_404_NOT_FOUND)
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
def resolve(self, request, pk=None):
@@ -313,17 +309,11 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
overdue_reports += 1
# Reports by priority and type
reports_by_priority = dict(
queryset.values_list("priority").annotate(count=Count("id"))
)
reports_by_type = dict(
queryset.values_list("report_type").annotate(count=Count("id"))
)
reports_by_priority = dict(queryset.values_list("priority").annotate(count=Count("id")))
reports_by_type = dict(queryset.values_list("report_type").annotate(count=Count("id")))
# Average resolution time
resolved_queryset = queryset.filter(
status="RESOLVED", resolved_at__isnull=False
)
resolved_queryset = queryset.filter(status="RESOLVED", resolved_at__isnull=False)
avg_resolution_time = 0
if resolved_queryset.exists():
@@ -430,9 +420,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
"log": None,
},
)
return Response(
{"error": "Log not found"}, status=status.HTTP_404_NOT_FOUND
)
return Response({"error": "Log not found"}, status=status.HTTP_404_NOT_FOUND)
# Filter by model type with app_label support for correct ContentType resolution
model_type = request.query_params.get("model_type")
@@ -441,9 +429,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
try:
if app_label:
# Use both app_label and model for precise matching
content_type = ContentType.objects.get_by_natural_key(
app_label, model_type
)
content_type = ContentType.objects.get_by_natural_key(app_label, model_type)
else:
# Map common model names to their app_labels for correct resolution
model_app_mapping = {
@@ -457,9 +443,7 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
}
mapped_app_label = model_app_mapping.get(model_type.lower())
if mapped_app_label:
content_type = ContentType.objects.get_by_natural_key(
mapped_app_label, model_type.lower()
)
content_type = ContentType.objects.get_by_natural_key(mapped_app_label, model_type.lower())
else:
# Fallback to model-only lookup
content_type = ContentType.objects.get(model=model_type)
@@ -576,9 +560,7 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
completion, and progress tracking.
"""
queryset = ModerationQueue.objects.select_related(
"assigned_to", "related_report", "content_type"
).all()
queryset = ModerationQueue.objects.select_related("assigned_to", "related_report", "content_type").all()
serializer_class = ModerationQueueSerializer
permission_classes = [CanViewModerationData]
@@ -871,9 +853,7 @@ class ModerationActionViewSet(viewsets.ModelViewSet):
and status management.
"""
queryset = ModerationAction.objects.select_related(
"moderator", "target_user", "related_report"
).all()
queryset = ModerationAction.objects.select_related("moderator", "target_user", "related_report").all()
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = ModerationActionFilter
@@ -907,9 +887,7 @@ class ModerationActionViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=["get"], permission_classes=[CanViewModerationData])
def active(self, request):
"""Get all active moderation actions."""
queryset = self.get_queryset().filter(
is_active=True, expires_at__gt=timezone.now()
)
queryset = self.get_queryset().filter(is_active=True, expires_at__gt=timezone.now())
page = self.paginate_queryset(queryset)
if page is not None:
@@ -922,9 +900,7 @@ class ModerationActionViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=["get"], permission_classes=[CanViewModerationData])
def expired(self, request):
"""Get all expired moderation actions."""
queryset = self.get_queryset().filter(
expires_at__lte=timezone.now(), is_active=True
)
queryset = self.get_queryset().filter(expires_at__lte=timezone.now(), is_active=True)
page = self.paginate_queryset(queryset)
if page is not None:
@@ -1173,9 +1149,7 @@ class UserModerationViewSet(viewsets.ViewSet):
if not query:
return Response([])
queryset = User.objects.filter(
Q(username__icontains=query) | Q(email__icontains=query)
)[:20]
queryset = User.objects.filter(Q(username__icontains=query) | Q(email__icontains=query))[:20]
users_data = [
{
@@ -1194,9 +1168,7 @@ class UserModerationViewSet(viewsets.ViewSet):
try:
user = User.objects.get(pk=pk)
except User.DoesNotExist:
return Response(
{"error": "User not found"}, status=status.HTTP_404_NOT_FOUND
)
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)
# Gather user moderation data
reports_made = ModerationReport.objects.filter(reported_by=user).count()
@@ -1206,12 +1178,8 @@ class UserModerationViewSet(viewsets.ViewSet):
actions_against = ModerationAction.objects.filter(target_user=user)
warnings_received = actions_against.filter(action_type="WARNING").count()
suspensions_received = actions_against.filter(
action_type="USER_SUSPENSION"
).count()
active_restrictions = actions_against.filter(
is_active=True, expires_at__gt=timezone.now()
).count()
suspensions_received = actions_against.filter(action_type="USER_SUSPENSION").count()
active_restrictions = actions_against.filter(is_active=True, expires_at__gt=timezone.now()).count()
# Risk assessment (simplified)
risk_factors = []
@@ -1230,9 +1198,7 @@ class UserModerationViewSet(viewsets.ViewSet):
risk_level = "HIGH"
# Recent activity
recent_reports = ModerationReport.objects.filter(reported_by=user).order_by(
"-created_at"
)[:5]
recent_reports = ModerationReport.objects.filter(reported_by=user).order_by("-created_at")[:5]
recent_actions = actions_against.order_by("-created_at")[:5]
@@ -1244,9 +1210,7 @@ class UserModerationViewSet(viewsets.ViewSet):
account_status = "RESTRICTED"
last_violation = (
actions_against.filter(
action_type__in=["WARNING", "USER_SUSPENSION", "USER_BAN"]
)
actions_against.filter(action_type__in=["WARNING", "USER_SUSPENSION", "USER_BAN"])
.order_by("-created_at")
.first()
)
@@ -1266,16 +1230,10 @@ class UserModerationViewSet(viewsets.ViewSet):
"active_restrictions": active_restrictions,
"risk_level": risk_level,
"risk_factors": risk_factors,
"recent_reports": ModerationReportSerializer(
recent_reports, many=True
).data,
"recent_actions": ModerationActionSerializer(
recent_actions, many=True
).data,
"recent_reports": ModerationReportSerializer(recent_reports, many=True).data,
"recent_actions": ModerationActionSerializer(recent_actions, many=True).data,
"account_status": account_status,
"last_violation_date": (
last_violation.created_at if last_violation else None
),
"last_violation_date": (last_violation.created_at if last_violation else None),
"next_review_date": None, # Would be calculated based on business rules
}
@@ -1287,13 +1245,9 @@ class UserModerationViewSet(viewsets.ViewSet):
try:
user = User.objects.get(pk=pk)
except User.DoesNotExist:
return Response(
{"error": "User not found"}, status=status.HTTP_404_NOT_FOUND
)
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)
serializer = CreateModerationActionSerializer(
data=request.data, context={"request": request}
)
serializer = CreateModerationActionSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
# Override target_user_id with the user from URL
@@ -1331,9 +1285,7 @@ class UserModerationViewSet(viewsets.ViewSet):
queryset = User.objects.all()
if query:
queryset = queryset.filter(
Q(username__icontains=query) | Q(email__icontains=query)
)
queryset = queryset.filter(Q(username__icontains=query) | Q(email__icontains=query))
if role:
queryset = queryset.filter(role=role)
@@ -1376,12 +1328,8 @@ class UserModerationViewSet(viewsets.ViewSet):
def stats(self, request):
"""Get overall user moderation statistics."""
total_actions = ModerationAction.objects.count()
active_actions = ModerationAction.objects.filter(
is_active=True, expires_at__gt=timezone.now()
).count()
expired_actions = ModerationAction.objects.filter(
expires_at__lte=timezone.now()
).count()
active_actions = ModerationAction.objects.filter(is_active=True, expires_at__gt=timezone.now()).count()
expired_actions = ModerationAction.objects.filter(expires_at__lte=timezone.now()).count()
stats_data = {
"total_actions": total_actions,
@@ -1404,6 +1352,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
Includes claim/unclaim endpoints with concurrency protection using
database row locking (select_for_update) to prevent race conditions.
"""
queryset = EditSubmission.objects.all()
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
search_fields = ["reason", "changes"]
@@ -1425,7 +1374,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
# User filter
user_id = self.request.query_params.get("user")
if user_id:
queryset = queryset.filter(user_id=user_id)
queryset = queryset.filter(user_id=user_id)
return queryset
@@ -1452,15 +1401,12 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
# Lock the row for update - other transactions will fail immediately
submission = EditSubmission.objects.select_for_update(nowait=True).get(pk=pk)
except EditSubmission.DoesNotExist:
return Response(
{"error": "Submission not found"},
status=status.HTTP_404_NOT_FOUND
)
return Response({"error": "Submission not found"}, status=status.HTTP_404_NOT_FOUND)
except DatabaseError:
# Row is already locked by another transaction
return Response(
{"error": "Submission is being claimed by another moderator. Please try again."},
status=status.HTTP_409_CONFLICT
status=status.HTTP_409_CONFLICT,
)
# Check if already claimed
@@ -1471,14 +1417,14 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
"claimed_by": submission.claimed_by.username if submission.claimed_by else None,
"claimed_at": submission.claimed_at.isoformat() if submission.claimed_at else None,
},
status=status.HTTP_409_CONFLICT
status=status.HTTP_409_CONFLICT,
)
# Check if in valid state for claiming
if submission.status != "PENDING":
return Response(
{"error": f"Cannot claim submission in {submission.status} state"},
status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
)
try:
@@ -1512,15 +1458,11 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
# Only the claiming user or an admin can unclaim
if submission.claimed_by != request.user and not request.user.is_staff:
return Response(
{"error": "Only the claiming moderator or an admin can unclaim"},
status=status.HTTP_403_FORBIDDEN
{"error": "Only the claiming moderator or an admin can unclaim"}, status=status.HTTP_403_FORBIDDEN
)
if submission.status != "CLAIMED":
return Response(
{"error": "Submission is not claimed"},
status=status.HTTP_400_BAD_REQUEST
)
return Response({"error": "Submission is not claimed"}, status=status.HTTP_400_BAD_REQUEST)
try:
submission.unclaim(user=request.user)
@@ -1557,8 +1499,8 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
reason = request.data.get("reason", "")
try:
submission.reject(moderator=user, reason=reason)
return Response(self.get_serializer(submission).data)
submission.reject(moderator=user, reason=reason)
return Response(self.get_serializer(submission).data)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -1569,8 +1511,8 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
reason = request.data.get("reason", "")
try:
submission.escalate(moderator=user, reason=reason)
return Response(self.get_serializer(submission).data)
submission.escalate(moderator=user, reason=reason)
return Response(self.get_serializer(submission).data)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -1582,6 +1524,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
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
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
@@ -1599,7 +1542,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
# User filter
user_id = self.request.query_params.get("user")
if user_id:
queryset = queryset.filter(user_id=user_id)
queryset = queryset.filter(user_id=user_id)
return queryset
@@ -1617,14 +1560,11 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
try:
submission = PhotoSubmission.objects.select_for_update(nowait=True).get(pk=pk)
except PhotoSubmission.DoesNotExist:
return Response(
{"error": "Submission not found"},
status=status.HTTP_404_NOT_FOUND
)
return Response({"error": "Submission not found"}, status=status.HTTP_404_NOT_FOUND)
except DatabaseError:
return Response(
{"error": "Submission is being claimed by another moderator. Please try again."},
status=status.HTTP_409_CONFLICT
status=status.HTTP_409_CONFLICT,
)
if submission.status == "CLAIMED":
@@ -1634,13 +1574,13 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
"claimed_by": submission.claimed_by.username if submission.claimed_by else None,
"claimed_at": submission.claimed_at.isoformat() if submission.claimed_at else None,
},
status=status.HTTP_409_CONFLICT
status=status.HTTP_409_CONFLICT,
)
if submission.status != "PENDING":
return Response(
{"error": f"Cannot claim submission in {submission.status} state"},
status=status.HTTP_400_BAD_REQUEST
status=status.HTTP_400_BAD_REQUEST,
)
try:
@@ -1669,15 +1609,11 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
if submission.claimed_by != request.user and not request.user.is_staff:
return Response(
{"error": "Only the claiming moderator or an admin can unclaim"},
status=status.HTTP_403_FORBIDDEN
{"error": "Only the claiming moderator or an admin can unclaim"}, status=status.HTTP_403_FORBIDDEN
)
if submission.status != "CLAIMED":
return Response(
{"error": "Submission is not claimed"},
status=status.HTTP_400_BAD_REQUEST
)
return Response({"error": "Submission is not claimed"}, status=status.HTTP_400_BAD_REQUEST)
try:
submission.unclaim(user=request.user)
@@ -1731,4 +1667,3 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
return Response(self.get_serializer(submission).data)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)