Files
thrillwiki_django_no_react/backend/apps/moderation/selectors.py
pacnpal 2e35f8c5d9 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.
2025-12-22 11:17:31 -05:00

282 lines
8.3 KiB
Python

"""
Selectors for moderation-related data retrieval.
Following Django styleguide pattern for separating data access from business logic.
"""
from typing import Optional, Dict, Any
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
from .models import EditSubmission
def pending_submissions_for_review(
*, content_type: Optional[str] = None, limit: int = 50
) -> QuerySet[EditSubmission]:
"""
Get pending submissions that need moderation review.
Args:
content_type: Optional filter by content type name
limit: Maximum number of submissions to return
Returns:
QuerySet of pending submissions ordered by submission date
"""
queryset = (
EditSubmission.objects.filter(status="PENDING")
.select_related("user", "content_type")
.prefetch_related("content_object")
)
if content_type:
queryset = queryset.filter(content_type__model=content_type.lower())
return queryset.order_by("created_at")[:limit]
def submissions_by_user(
*, user_id: int, status: Optional[str] = None
) -> QuerySet[EditSubmission]:
"""
Get submissions created by a specific user.
Args:
user_id: ID of the user who submitted
status: Optional filter by submission status
Returns:
QuerySet of user's submissions
"""
queryset = EditSubmission.objects.filter(user_id=user_id).select_related(
"content_type", "handled_by"
)
if status:
queryset = queryset.filter(status=status)
return queryset.order_by("-created_at")
def submissions_handled_by_moderator(
*, moderator_id: int, days: int = 30
) -> QuerySet[EditSubmission]:
"""
Get submissions handled by a specific moderator in the last N days.
Args:
moderator_id: ID of the moderator
days: Number of days to look back
Returns:
QuerySet of submissions handled by the moderator
"""
cutoff_date = timezone.now() - timedelta(days=days)
return (
EditSubmission.objects.filter(
handled_by_id=moderator_id, handled_at__gte=cutoff_date
)
.select_related("user", "content_type")
.order_by("-handled_at")
)
def recent_submissions(*, days: int = 7) -> QuerySet[EditSubmission]:
"""
Get recent submissions from the last N days.
Args:
days: Number of days to look back
Returns:
QuerySet of recent submissions
"""
cutoff_date = timezone.now() - timedelta(days=days)
return (
EditSubmission.objects.filter(created_at__gte=cutoff_date)
.select_related("user", "content_type", "handled_by")
.order_by("-created_at")
)
def submissions_by_content_type(
*, content_type: str, status: Optional[str] = None
) -> QuerySet[EditSubmission]:
"""
Get submissions for a specific content type.
Args:
content_type: Name of the content type (e.g., 'park', 'ride')
status: Optional filter by submission status
Returns:
QuerySet of submissions for the content type
"""
queryset = EditSubmission.objects.filter(
content_type__model=content_type.lower()
).select_related("user", "handled_by")
if status:
queryset = queryset.filter(status=status)
return queryset.order_by("-created_at")
def moderation_queue_summary() -> Dict[str, Any]:
"""
Get summary statistics for the moderation queue.
Returns:
Dictionary containing queue statistics
"""
pending_count = EditSubmission.objects.filter(status="PENDING").count()
approved_today = EditSubmission.objects.filter(
status="APPROVED", handled_at__date=timezone.now().date()
).count()
rejected_today = EditSubmission.objects.filter(
status="REJECTED", handled_at__date=timezone.now().date()
).count()
# Submissions by content type
submissions_by_type = (
EditSubmission.objects.filter(status="PENDING")
.values("content_type__model")
.annotate(count=Count("id"))
.order_by("-count")
)
return {
"pending_count": pending_count,
"approved_today": approved_today,
"rejected_today": rejected_today,
"submissions_by_type": list(submissions_by_type),
}
def moderation_statistics_summary(
*, days: int = 30, moderator: Optional[User] = None
) -> Dict[str, Any]:
"""
Get comprehensive moderation statistics for a time period.
Args:
days: Number of days to analyze
moderator: Optional filter by specific moderator
Returns:
Dictionary containing detailed moderation statistics
"""
cutoff_date = timezone.now() - timedelta(days=days)
base_queryset = EditSubmission.objects.filter(created_at__gte=cutoff_date)
if moderator:
handled_queryset = base_queryset.filter(handled_by=moderator)
else:
handled_queryset = base_queryset
total_submissions = base_queryset.count()
pending_submissions = base_queryset.filter(status="PENDING").count()
approved_submissions = handled_queryset.filter(status="APPROVED").count()
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)
.annotate(
response_hours=ExpressionWrapper(
Extract(F('handled_at') - F('created_at'), 'epoch') / 3600.0,
output_field=FloatField()
)
)
.values_list("response_hours", flat=True)
)
avg_response_time = None
if handled_with_times:
avg_response_time = sum(handled_with_times) / len(handled_with_times)
return {
"period_days": days,
"total_submissions": total_submissions,
"pending_submissions": pending_submissions,
"approved_submissions": approved_submissions,
"rejected_submissions": rejected_submissions,
"approval_rate": (
(approved_submissions / (approved_submissions + rejected_submissions) * 100)
if (approved_submissions + rejected_submissions) > 0
else 0
),
"average_response_time_hours": avg_response_time,
"moderator": moderator.username if moderator else None,
}
def submissions_needing_attention(*, hours: int = 24) -> QuerySet[EditSubmission]:
"""
Get pending submissions that have been waiting for more than N hours.
Args:
hours: Number of hours threshold for attention
Returns:
QuerySet of submissions needing attention
"""
cutoff_time = timezone.now() - timedelta(hours=hours)
return (
EditSubmission.objects.filter(status="PENDING", created_at__lte=cutoff_time)
.select_related("user", "content_type")
.order_by("created_at")
)
def top_contributors(*, days: int = 30, limit: int = 10) -> QuerySet[User]:
"""
Get users who have submitted the most content in the last N days.
Args:
days: Number of days to analyze
limit: Maximum number of users to return
Returns:
QuerySet of top contributing users
"""
cutoff_date = timezone.now() - timedelta(days=days)
return (
User.objects.filter(edit_submissions__created_at__gte=cutoff_date)
.annotate(submission_count=Count("edit_submissions"))
.filter(submission_count__gt=0)
.order_by("-submission_count")[:limit]
)
def moderator_workload_summary(*, days: int = 30) -> Dict[str, Any]:
"""
Get workload distribution among moderators.
Args:
days: Number of days to analyze
Returns:
Dictionary containing moderator workload statistics
"""
cutoff_date = timezone.now() - timedelta(days=days)
moderator_stats = (
User.objects.filter(handled_submissions__handled_at__gte=cutoff_date)
.annotate(handled_count=Count("handled_submissions"))
.filter(handled_count__gt=0)
.order_by("-handled_count")
.values("username", "handled_count")
)
return {"period_days": days, "moderator_stats": list(moderator_stats)}