This commit is contained in:
pacnpal
2026-01-08 13:44:37 -05:00
parent 40cba5bdb2
commit fe960e8b62
21 changed files with 2008 additions and 59 deletions

View File

@@ -864,12 +864,13 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
self.save()
def auto_approve(self) -> None:
"""Auto - approve submissions from moderators"""
"""Auto-approve submissions from moderators."""
# Get user role safely
user_role = getattr(self.user, "role", None)
# If user is moderator or above, auto-approve
# If user is moderator or above, claim then approve
if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]:
self.claim(user=self.user)
self.approve(self.user)
def escalate(self, moderator: UserType = None, notes: str = "", user=None) -> None:

View File

@@ -1718,6 +1718,148 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=["post"], permission_classes=[IsModeratorOrAdmin], url_path="release-expired")
def release_expired_locks(self, request):
"""
Release all expired claim locks.
This is typically handled by a Celery task, but can be triggered manually.
Claims are expired after 30 minutes by default.
"""
from datetime import timedelta
expiry_threshold = timezone.now() - timedelta(minutes=30)
expired_claims = EditSubmission.objects.filter(
status="CLAIMED",
claimed_at__lt=expiry_threshold
)
released_count = 0
for submission in expired_claims:
submission.status = "PENDING"
submission.claimed_by = None
submission.claimed_at = None
submission.save(update_fields=["status", "claimed_by", "claimed_at"])
released_count += 1
return Response({
"released_count": released_count,
"message": f"Released {released_count} expired lock(s)"
})
@action(detail=True, methods=["post"], permission_classes=[IsAdminOrSuperuser], url_path="admin-release")
def admin_release(self, request, pk=None):
"""
Admin/superuser force release of a specific claim.
"""
submission = self.get_object()
if submission.status != "CLAIMED":
return Response(
{"error": "Submission is not claimed"},
status=status.HTTP_400_BAD_REQUEST
)
submission.status = "PENDING"
submission.claimed_by = None
submission.claimed_at = None
submission.save(update_fields=["status", "claimed_by", "claimed_at"])
return Response({
"success": True,
"message": f"Lock released on submission {submission.id}"
})
@action(detail=False, methods=["post"], permission_classes=[IsAdminOrSuperuser], url_path="admin-release-all")
def admin_release_all(self, request):
"""
Admin/superuser force release of all active claims.
"""
claimed_submissions = EditSubmission.objects.filter(status="CLAIMED")
released_count = 0
for submission in claimed_submissions:
submission.status = "PENDING"
submission.claimed_by = None
submission.claimed_at = None
submission.save(update_fields=["status", "claimed_by", "claimed_at"])
released_count += 1
return Response({
"released_count": released_count,
"message": f"Released all {released_count} active lock(s)"
})
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin], url_path="reassign")
def reassign(self, request, pk=None):
"""
Reassign a submission to a different moderator.
Only admins can reassign submissions claimed by other moderators.
The submission must be in CLAIMED status.
"""
submission = self.get_object()
new_moderator_id = request.data.get("new_moderator_id")
if not new_moderator_id:
return Response(
{"error": "new_moderator_id is required"},
status=status.HTTP_400_BAD_REQUEST
)
try:
new_moderator = User.objects.get(pk=new_moderator_id)
except User.DoesNotExist:
return Response(
{"error": "Moderator not found"},
status=status.HTTP_404_NOT_FOUND
)
# Check moderator permissions
if new_moderator.role not in ["MODERATOR", "ADMIN", "SUPERUSER"]:
return Response(
{"error": "User is not a moderator"},
status=status.HTTP_400_BAD_REQUEST
)
# Update the claim
submission.claimed_by = new_moderator
submission.claimed_at = timezone.now()
submission.save(update_fields=["claimed_by", "claimed_at"])
return Response({
"success": True,
"message": f"Submission reassigned to {new_moderator.username}"
})
@action(detail=False, methods=["post"], permission_classes=[IsModeratorOrAdmin], url_path="audit-log")
def log_admin_action(self, request):
"""
Log an admin action for audit trail.
This creates an audit log entry for moderator actions.
"""
action_type = request.data.get("action_type", "")
action_details = request.data.get("action_details", {})
target_entity = request.data.get("target_entity", {})
# Create audit log entry
logger.info(
f"[AdminAction] User {request.user.username} - {action_type}",
extra={
"user_id": request.user.id,
"action_type": action_type,
"action_details": action_details,
"target_entity": target_entity,
}
)
return Response({
"success": True,
"message": "Action logged successfully"
})
@action(detail=False, methods=["get"], permission_classes=[IsModeratorOrAdmin], url_path="my-active-claim")
def my_active_claim(self, request):
"""