feat: Refactor rides app with unique constraints, mixins, and enhanced documentation

- Added migration to convert unique_together constraints to UniqueConstraint for RideModel.
- Introduced RideFormMixin for handling entity suggestions in ride forms.
- Created comprehensive code standards documentation outlining formatting, docstring requirements, complexity guidelines, and testing requirements.
- Established error handling guidelines with a structured exception hierarchy and best practices for API and view error handling.
- Documented view pattern guidelines, emphasizing the use of CBVs, FBVs, and ViewSets with examples.
- Implemented a benchmarking script for query performance analysis and optimization.
- Developed security documentation detailing measures, configurations, and a security checklist.
- Compiled a database optimization guide covering indexing strategies, query optimization patterns, and computed fields.
This commit is contained in:
pacnpal
2025-12-22 11:17:31 -05:00
parent 45d97b6e68
commit 2e35f8c5d9
71 changed files with 8036 additions and 1462 deletions

View File

@@ -7,6 +7,7 @@ across all models using django-fsm-log.
from django.core.management.base import BaseCommand
from django.db.models import Count, Avg, F
from django.db.models.functions import TruncDate, ExtractHour
from django.utils import timezone
from datetime import timedelta
from django_fsm_log.models import StateLog
@@ -148,9 +149,10 @@ class Command(BaseCommand):
self.stdout.write(f" System-initiated: {system_count} ({(system_count/total_transitions)*100:.1f}%)")
# Daily transition volume
# Security: Using Django ORM functions instead of raw SQL .extra() to prevent SQL injection
self.stdout.write(self.style.SUCCESS('\n--- Daily Transition Volume ---'))
daily_stats = (
queryset.extra(select={'day': 'date(timestamp)'})
queryset.annotate(day=TruncDate('timestamp'))
.values('day')
.annotate(count=Count('id'))
.order_by('-day')[:7]
@@ -162,9 +164,10 @@ class Command(BaseCommand):
self.stdout.write(f" {date}: {count} transitions")
# Busiest hours
# Security: Using Django ORM functions instead of raw SQL .extra() to prevent SQL injection
self.stdout.write(self.style.SUCCESS('\n--- Busiest Hours (UTC) ---'))
hourly_stats = (
queryset.extra(select={'hour': 'extract(hour from timestamp)'})
queryset.annotate(hour=ExtractHour('timestamp'))
.values('hour')
.annotate(count=Count('id'))
.order_by('-count')[:5]

View File

@@ -4,7 +4,8 @@ Following Django styleguide pattern for separating data access from business log
"""
from typing import Optional, Dict, Any
from django.db.models import QuerySet, Count
from django.db.models import QuerySet, Count, F, ExpressionWrapper, FloatField
from django.db.models.functions import Extract
from django.utils import timezone
from datetime import timedelta
from django.contrib.auth.models import User
@@ -185,12 +186,14 @@ def moderation_statistics_summary(
rejected_submissions = handled_queryset.filter(status="REJECTED").count()
# Response time analysis (only for handled submissions)
# Security: Using Django ORM instead of raw SQL .extra() to prevent SQL injection
handled_with_times = (
handled_queryset.exclude(handled_at__isnull=True)
.extra(
select={
"response_hours": "EXTRACT(EPOCH FROM (handled_at - created_at)) / 3600"
}
.annotate(
response_hours=ExpressionWrapper(
Extract(F('handled_at') - F('created_at'), 'epoch') / 3600.0,
output_field=FloatField()
)
)
.values_list("response_hours", flat=True)
)

View File

@@ -306,35 +306,39 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
return Response(stats_data)
@action(detail=True, methods=['get'], permission_classes=[CanViewModerationData])
@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]
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])
@action(detail=False, methods=["get"], permission_classes=[CanViewModerationData])
def all_history(self, request):
"""Get all transition history with filtering.
@@ -343,61 +347,77 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
from django_fsm_log.models import StateLog
from django.contrib.contenttypes.models import ContentType
queryset = StateLog.objects.select_related('by', 'content_type').all()
queryset = StateLog.objects.select_related("by", "content_type").all()
# Filter by id (for detail view)
log_id = request.query_params.get('id')
log_id = request.query_params.get("id")
if log_id:
try:
log = queryset.get(id=log_id)
# Check if HTMX request for detail view
if request.headers.get('HX-Request'):
return render(request, 'moderation/partials/history_detail_content.html', {
'log': log,
})
if request.headers.get("HX-Request"):
return render(
request,
"moderation/partials/history_detail_content.html",
{
"log": log,
},
)
# Return JSON for API request
return Response({
'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,
})
return Response(
{
"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,
}
)
except StateLog.DoesNotExist:
if request.headers.get('HX-Request'):
return render(request, 'moderation/partials/history_detail_content.html', {
'log': None,
})
return Response({'error': 'Log not found'}, status=status.HTTP_404_NOT_FOUND)
if request.headers.get("HX-Request"):
return render(
request,
"moderation/partials/history_detail_content.html",
{
"log": None,
},
)
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')
app_label = request.query_params.get('app_label')
model_type = request.query_params.get("model_type")
app_label = request.query_params.get("app_label")
if model_type:
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 = {
'park': 'parks',
'ride': 'rides',
'editsubmission': 'submissions',
'photosubmission': 'submissions',
'moderationreport': 'moderation',
'moderationqueue': 'moderation',
'bulkoperation': 'moderation',
"park": "parks",
"ride": "rides",
"editsubmission": "submissions",
"photosubmission": "submissions",
"moderationreport": "moderation",
"moderationqueue": "moderation",
"bulkoperation": "moderation",
}
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)
@@ -406,88 +426,98 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
pass
# Filter by object_id (for object-level history)
object_id = request.query_params.get('object_id')
object_id = request.query_params.get("object_id")
if object_id:
queryset = queryset.filter(object_id=object_id)
# Filter by user
user_id = request.query_params.get('user_id')
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')
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')
state = request.query_params.get("state")
if state:
queryset = queryset.filter(state=state)
# Search filter (case-insensitive across relevant fields)
search_query = request.query_params.get('q')
search_query = request.query_params.get("q")
if search_query:
queryset = queryset.filter(
Q(transition__icontains=search_query) |
Q(description__icontains=search_query) |
Q(state__icontains=search_query) |
Q(source_state__icontains=search_query) |
Q(object_id__icontains=search_query) |
Q(by__username__icontains=search_query)
Q(transition__icontains=search_query)
| Q(description__icontains=search_query)
| Q(state__icontains=search_query)
| Q(source_state__icontains=search_query)
| Q(object_id__icontains=search_query)
| Q(by__username__icontains=search_query)
)
# Order queryset
queryset = queryset.order_by('-timestamp')
queryset = queryset.order_by("-timestamp")
# Check if HTMX request
if request.headers.get('HX-Request'):
if request.headers.get("HX-Request"):
# Use Django's Paginator for HTMX responses
paginator = Paginator(queryset, 20)
page_number = request.query_params.get('page', 1)
page_number = request.query_params.get("page", 1)
page_obj = paginator.get_page(page_number)
return render(request, 'moderation/partials/history_table.html', {
'history_logs': page_obj,
'page_obj': page_obj,
'request': request,
})
return render(
request,
"moderation/partials/history_table.html",
{
"history_logs": page_obj,
"page_obj": page_obj,
"request": request,
},
)
# Paginate for API response
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]
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]
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)
@@ -704,7 +734,7 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'], permission_classes=[CanViewModerationData])
@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
@@ -713,22 +743,26 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
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')
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]
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)
@@ -996,7 +1030,7 @@ class BulkOperationViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
@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
@@ -1005,22 +1039,26 @@ class BulkOperationViewSet(viewsets.ModelViewSet):
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')
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]
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)