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:
pacnpal
2025-12-21 17:33:24 -05:00
parent b9063ff4f8
commit 7ba0004c93
74 changed files with 11134 additions and 198 deletions

View File

@@ -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