mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 07:45:18 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user