mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 05:51:08 -05:00
chore: fix pghistory migration deps and improve htmx utilities
- Update pghistory dependency from 0007 to 0006 in account migrations - Add docstrings and remove unused imports in htmx_forms.py - Add DJANGO_SETTINGS_MODULE bash commands to Claude settings - Add state transition definitions for ride statuses
This commit is contained in:
@@ -19,6 +19,13 @@ from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, Count
|
||||
from datetime import timedelta
|
||||
from django_fsm import can_proceed, TransitionNotAllowed
|
||||
|
||||
from apps.core.state_machine.exceptions import (
|
||||
TransitionPermissionDenied,
|
||||
TransitionValidationError,
|
||||
format_transition_error,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
ModerationReport,
|
||||
@@ -129,9 +136,45 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition method exists
|
||||
transition_method = getattr(report, "transition_to_under_review", None)
|
||||
if transition_method is None:
|
||||
return Response(
|
||||
{"error": "Transition method not available"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition can proceed before attempting
|
||||
if not can_proceed(transition_method, moderator):
|
||||
return Response(
|
||||
format_transition_error(
|
||||
TransitionPermissionDenied(
|
||||
message="Cannot transition to UNDER_REVIEW",
|
||||
user_message="You don't have permission to assign this report or it cannot be transitioned from the current state.",
|
||||
)
|
||||
),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
report.assigned_moderator = moderator
|
||||
report.status = "UNDER_REVIEW"
|
||||
report.save()
|
||||
try:
|
||||
transition_method(user=moderator)
|
||||
report.save()
|
||||
except TransitionPermissionDenied as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except TransitionValidationError as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except TransitionNotAllowed as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(report)
|
||||
return Response(serializer.data)
|
||||
@@ -155,7 +198,44 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
report.status = "RESOLVED"
|
||||
# Check if transition method exists
|
||||
transition_method = getattr(report, "transition_to_resolved", None)
|
||||
if transition_method is None:
|
||||
return Response(
|
||||
{"error": "Transition method not available"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition can proceed before attempting
|
||||
if not can_proceed(transition_method, request.user):
|
||||
return Response(
|
||||
format_transition_error(
|
||||
TransitionPermissionDenied(
|
||||
message="Cannot transition to RESOLVED",
|
||||
user_message="You don't have permission to resolve this report or it cannot be resolved from the current state.",
|
||||
)
|
||||
),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
try:
|
||||
transition_method(user=request.user)
|
||||
except TransitionPermissionDenied as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except TransitionValidationError as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except TransitionNotAllowed as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
report.resolution_action = resolution_action
|
||||
report.resolution_notes = resolution_notes
|
||||
report.resolved_at = timezone.now()
|
||||
@@ -224,6 +304,111 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
|
||||
|
||||
return Response(stats_data)
|
||||
|
||||
@action(detail=True, methods=['get'], permission_classes=[CanViewModerationData])
|
||||
def history(self, request, pk=None):
|
||||
"""Get transition history for this report."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
report = self.get_object()
|
||||
content_type = ContentType.objects.get_for_model(report)
|
||||
|
||||
logs = StateLog.objects.filter(
|
||||
content_type=content_type,
|
||||
object_id=report.id
|
||||
).select_related('by').order_by('-timestamp')
|
||||
|
||||
history_data = [{
|
||||
'id': log.id,
|
||||
'timestamp': log.timestamp,
|
||||
'state': log.state,
|
||||
'from_state': log.source_state,
|
||||
'to_state': log.state,
|
||||
'transition': log.transition,
|
||||
'user': log.by.username if log.by else None,
|
||||
'description': log.description,
|
||||
'reason': log.description,
|
||||
} for log in logs]
|
||||
|
||||
return Response(history_data)
|
||||
|
||||
@action(detail=False, methods=['get'], permission_classes=[CanViewModerationData])
|
||||
def all_history(self, request):
|
||||
"""Get all transition history with filtering."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
queryset = StateLog.objects.select_related('by', 'content_type').all()
|
||||
|
||||
# Filter by id (for detail view)
|
||||
log_id = request.query_params.get('id')
|
||||
if log_id:
|
||||
queryset = queryset.filter(id=log_id)
|
||||
|
||||
# Filter by model type
|
||||
model_type = request.query_params.get('model_type')
|
||||
if model_type:
|
||||
try:
|
||||
content_type = ContentType.objects.get(model=model_type)
|
||||
queryset = queryset.filter(content_type=content_type)
|
||||
except ContentType.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Filter by user
|
||||
user_id = request.query_params.get('user_id')
|
||||
if user_id:
|
||||
queryset = queryset.filter(by_id=user_id)
|
||||
|
||||
# Filter by date range
|
||||
start_date = request.query_params.get('start_date')
|
||||
end_date = request.query_params.get('end_date')
|
||||
if start_date:
|
||||
queryset = queryset.filter(timestamp__gte=start_date)
|
||||
if end_date:
|
||||
queryset = queryset.filter(timestamp__lte=end_date)
|
||||
|
||||
# Filter by state
|
||||
state = request.query_params.get('state')
|
||||
if state:
|
||||
queryset = queryset.filter(state=state)
|
||||
|
||||
# Order queryset
|
||||
queryset = queryset.order_by('-timestamp')
|
||||
|
||||
# Paginate
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
history_data = [{
|
||||
'id': log.id,
|
||||
'timestamp': log.timestamp,
|
||||
'model': log.content_type.model,
|
||||
'object_id': log.object_id,
|
||||
'state': log.state,
|
||||
'from_state': log.source_state,
|
||||
'to_state': log.state,
|
||||
'transition': log.transition,
|
||||
'user': log.by.username if log.by else None,
|
||||
'description': log.description,
|
||||
'reason': log.description,
|
||||
} for log in page]
|
||||
return self.get_paginated_response(history_data)
|
||||
|
||||
# Return all history data when pagination is not triggered
|
||||
history_data = [{
|
||||
'id': log.id,
|
||||
'timestamp': log.timestamp,
|
||||
'model': log.content_type.model,
|
||||
'object_id': log.object_id,
|
||||
'state': log.state,
|
||||
'from_state': log.source_state,
|
||||
'to_state': log.state,
|
||||
'transition': log.transition,
|
||||
'user': log.by.username if log.by else None,
|
||||
'description': log.description,
|
||||
'reason': log.description,
|
||||
} for log in queryset]
|
||||
return Response(history_data)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Moderation Queue ViewSet
|
||||
@@ -261,9 +446,46 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
|
||||
moderator_id = serializer.validated_data["moderator_id"]
|
||||
moderator = User.objects.get(id=moderator_id)
|
||||
|
||||
# Check if transition method exists
|
||||
transition_method = getattr(queue_item, "transition_to_in_progress", None)
|
||||
if transition_method is None:
|
||||
return Response(
|
||||
{"error": "Transition method not available"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition can proceed before attempting
|
||||
if not can_proceed(transition_method, moderator):
|
||||
return Response(
|
||||
format_transition_error(
|
||||
TransitionPermissionDenied(
|
||||
message="Cannot transition to IN_PROGRESS",
|
||||
user_message="You don't have permission to assign this queue item or it cannot be transitioned from the current state.",
|
||||
)
|
||||
),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
queue_item.assigned_to = moderator
|
||||
queue_item.assigned_at = timezone.now()
|
||||
queue_item.status = "IN_PROGRESS"
|
||||
try:
|
||||
transition_method(user=moderator)
|
||||
except TransitionPermissionDenied as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except TransitionValidationError as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except TransitionNotAllowed as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
queue_item.save()
|
||||
|
||||
response_serializer = self.get_serializer(queue_item)
|
||||
@@ -276,9 +498,46 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
|
||||
"""Unassign a queue item."""
|
||||
queue_item = self.get_object()
|
||||
|
||||
# Check if transition method exists
|
||||
transition_method = getattr(queue_item, "transition_to_pending", None)
|
||||
if transition_method is None:
|
||||
return Response(
|
||||
{"error": "Transition method not available"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition can proceed before attempting
|
||||
if not can_proceed(transition_method, request.user):
|
||||
return Response(
|
||||
format_transition_error(
|
||||
TransitionPermissionDenied(
|
||||
message="Cannot transition to PENDING",
|
||||
user_message="You don't have permission to unassign this queue item or it cannot be transitioned from the current state.",
|
||||
)
|
||||
),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
queue_item.assigned_to = None
|
||||
queue_item.assigned_at = None
|
||||
queue_item.status = "PENDING"
|
||||
try:
|
||||
transition_method(user=request.user)
|
||||
except TransitionPermissionDenied as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except TransitionValidationError as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except TransitionNotAllowed as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
queue_item.save()
|
||||
|
||||
serializer = self.get_serializer(queue_item)
|
||||
@@ -294,7 +553,44 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
|
||||
action_taken = serializer.validated_data["action"]
|
||||
notes = serializer.validated_data.get("notes", "")
|
||||
|
||||
queue_item.status = "COMPLETED"
|
||||
# Check if transition method exists
|
||||
transition_method = getattr(queue_item, "transition_to_completed", None)
|
||||
if transition_method is None:
|
||||
return Response(
|
||||
{"error": "Transition method not available"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition can proceed before attempting
|
||||
if not can_proceed(transition_method, request.user):
|
||||
return Response(
|
||||
format_transition_error(
|
||||
TransitionPermissionDenied(
|
||||
message="Cannot transition to COMPLETED",
|
||||
user_message="You don't have permission to complete this queue item or it cannot be transitioned from the current state.",
|
||||
)
|
||||
),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
try:
|
||||
transition_method(user=request.user)
|
||||
except TransitionPermissionDenied as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except TransitionValidationError as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except TransitionNotAllowed as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
queue_item.save()
|
||||
|
||||
# Create moderation action if needed
|
||||
@@ -327,6 +623,34 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'], permission_classes=[CanViewModerationData])
|
||||
def history(self, request, pk=None):
|
||||
"""Get transition history for this queue item."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
queue_item = self.get_object()
|
||||
content_type = ContentType.objects.get_for_model(queue_item)
|
||||
|
||||
logs = StateLog.objects.filter(
|
||||
content_type=content_type,
|
||||
object_id=queue_item.id
|
||||
).select_related('by').order_by('-timestamp')
|
||||
|
||||
history_data = [{
|
||||
'id': log.id,
|
||||
'timestamp': log.timestamp,
|
||||
'state': log.state,
|
||||
'from_state': log.source_state,
|
||||
'to_state': log.state,
|
||||
'transition': log.transition,
|
||||
'user': log.by.username if log.by else None,
|
||||
'description': log.description,
|
||||
'reason': log.description,
|
||||
} for log in logs]
|
||||
|
||||
return Response(history_data)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Moderation Action ViewSet
|
||||
@@ -453,7 +777,44 @@ class BulkOperationViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
operation.status = "CANCELLED"
|
||||
# Check if transition method exists
|
||||
transition_method = getattr(operation, "transition_to_cancelled", None)
|
||||
if transition_method is None:
|
||||
return Response(
|
||||
{"error": "Transition method not available"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition can proceed before attempting
|
||||
if not can_proceed(transition_method, request.user):
|
||||
return Response(
|
||||
format_transition_error(
|
||||
TransitionPermissionDenied(
|
||||
message="Cannot transition to CANCELLED",
|
||||
user_message="You don't have permission to cancel this operation or it cannot be cancelled from the current state.",
|
||||
)
|
||||
),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
try:
|
||||
transition_method(user=request.user)
|
||||
except TransitionPermissionDenied as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except TransitionValidationError as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except TransitionNotAllowed as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
operation.completed_at = timezone.now()
|
||||
operation.save()
|
||||
|
||||
@@ -471,8 +832,45 @@ class BulkOperationViewSet(viewsets.ModelViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition method exists
|
||||
transition_method = getattr(operation, "transition_to_pending", None)
|
||||
if transition_method is None:
|
||||
return Response(
|
||||
{"error": "Transition method not available"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if transition can proceed before attempting
|
||||
if not can_proceed(transition_method, request.user):
|
||||
return Response(
|
||||
format_transition_error(
|
||||
TransitionPermissionDenied(
|
||||
message="Cannot transition to PENDING",
|
||||
user_message="You don't have permission to retry this operation or it cannot be retried from the current state.",
|
||||
)
|
||||
),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Reset operation status
|
||||
operation.status = "PENDING"
|
||||
try:
|
||||
transition_method(user=request.user)
|
||||
except TransitionPermissionDenied as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
except TransitionValidationError as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except TransitionNotAllowed as e:
|
||||
return Response(
|
||||
format_transition_error(e),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
operation.started_at = None
|
||||
operation.completed_at = None
|
||||
operation.processed_items = 0
|
||||
@@ -517,6 +915,34 @@ class BulkOperationViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def history(self, request, pk=None):
|
||||
"""Get transition history for this bulk operation."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
operation = self.get_object()
|
||||
content_type = ContentType.objects.get_for_model(operation)
|
||||
|
||||
logs = StateLog.objects.filter(
|
||||
content_type=content_type,
|
||||
object_id=operation.id
|
||||
).select_related('by').order_by('-timestamp')
|
||||
|
||||
history_data = [{
|
||||
'id': log.id,
|
||||
'timestamp': log.timestamp,
|
||||
'state': log.state,
|
||||
'from_state': log.source_state,
|
||||
'to_state': log.state,
|
||||
'transition': log.transition,
|
||||
'user': log.by.username if log.by else None,
|
||||
'description': log.description,
|
||||
'reason': log.description,
|
||||
} for log in logs]
|
||||
|
||||
return Response(history_data)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# User Moderation ViewSet
|
||||
|
||||
Reference in New Issue
Block a user