Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -1,22 +1,21 @@
from django.views.generic import ListView, TemplateView
from django.views.generic import ListView
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, JsonResponse, HttpRequest
from django.http import HttpResponse, HttpRequest
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.decorators import login_required
from django.template.loader import render_to_string
from django.db.models import Q, QuerySet
from django.db.models import QuerySet
from django.core.exceptions import PermissionDenied
from typing import Optional, Any, Dict, List, Tuple, Union, cast
from django.db import models
from typing import Optional, Any, Dict, List, Tuple, cast
from django.core.serializers.json import DjangoJSONEncoder
import json
from accounts.models import User
from .models import EditSubmission, PhotoSubmission
from parks.models import Park, ParkArea
from rides.models import RideModel, Company
from rides.models import RideModel
MODERATOR_ROLES = ["MODERATOR", "ADMIN", "SUPERUSER"]
MODERATOR_ROLES = ['MODERATOR', 'ADMIN', 'SUPERUSER']
class ModeratorRequiredMixin(UserPassesTestMixin):
request: HttpRequest
@@ -24,71 +23,85 @@ class ModeratorRequiredMixin(UserPassesTestMixin):
def test_func(self) -> bool:
"""Check if user has moderator permissions."""
user = cast(User, self.request.user)
return user.is_authenticated and (user.role in MODERATOR_ROLES or user.is_superuser)
return user.is_authenticated and (
user.role in MODERATOR_ROLES or user.is_superuser
)
def handle_no_permission(self) -> HttpResponse:
if not self.request.user.is_authenticated:
return super().handle_no_permission()
raise PermissionDenied("You do not have moderator permissions.")
def get_filtered_queryset(request: HttpRequest, status: str, submission_type: str) -> QuerySet:
def get_filtered_queryset(
request: HttpRequest, status: str, submission_type: str
) -> QuerySet:
"""Get filtered queryset based on request parameters."""
if submission_type == 'photo':
return PhotoSubmission.objects.filter(status=status).order_by('-created_at')
queryset = EditSubmission.objects.filter(status=status).order_by('-created_at')
if type_filter := request.GET.get('type'):
if submission_type == "photo":
return PhotoSubmission.objects.filter(status=status).order_by("-created_at")
queryset = EditSubmission.objects.filter(status=status).order_by("-created_at")
if type_filter := request.GET.get("type"):
queryset = queryset.filter(submission_type=type_filter)
if content_type := request.GET.get('content_type'):
if content_type := request.GET.get("content_type"):
queryset = queryset.filter(content_type__model=content_type)
return queryset
def get_context_data(request: HttpRequest, queryset: QuerySet) -> Dict[str, Any]:
"""Get common context data for views."""
park_areas_by_park: Dict[int, List[Tuple[int, str]]] = {}
if isinstance(queryset.first(), EditSubmission):
for submission in queryset:
if (submission.content_type.model == 'park' and
isinstance(submission.changes, dict) and
'park' in submission.changes):
park_id = submission.changes['park']
if (
submission.content_type.model == "park"
and isinstance(submission.changes, dict)
and "park" in submission.changes
):
park_id = submission.changes["park"]
if park_id not in park_areas_by_park:
areas = ParkArea.objects.filter(park_id=park_id)
park_areas_by_park[park_id] = [(area.pk, str(area)) for area in areas]
park_areas_by_park[park_id] = [
(area.pk, str(area)) for area in areas
]
return {
'submissions': queryset,
'user': request.user,
'parks': [(park.pk, str(park)) for park in Park.objects.all()],
'ride_models': [(model.pk, str(model)) for model in RideModel.objects.all()],
'owners': [(user.pk, str(user)) for user in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])],
'park_areas_by_park': park_areas_by_park
"submissions": queryset,
"user": request.user,
"parks": [(park.pk, str(park)) for park in Park.objects.all()],
"ride_models": [(model.pk, str(model)) for model in RideModel.objects.all()],
"owners": [
(user.pk, str(user))
for user in User.objects.filter(role__in=["OWNER", "ADMIN", "SUPERUSER"])
],
"park_areas_by_park": park_areas_by_park,
}
@login_required
def search_parks(request: HttpRequest) -> HttpResponse:
"""HTMX endpoint for searching parks in moderation dashboard"""
user = cast(User, request.user)
if not (user.role in MODERATOR_ROLES or user.is_superuser):
return HttpResponse(status=403)
query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id')
parks = Park.objects.all().order_by('name')
query = request.GET.get("q", "").strip()
submission_id = request.GET.get("submission_id")
parks = Park.objects.all().order_by("name")
if query:
parks = parks.filter(name__icontains=query)
parks = parks[:10]
return render(request, 'moderation/partials/park_search_results.html', {
'parks': parks,
'search_term': query,
'submission_id': submission_id
})
return render(
request,
"moderation/partials/park_search_results.html",
{"parks": parks, "search_term": query, "submission_id": submission_id},
)
@login_required
@@ -97,190 +110,253 @@ def search_ride_models(request: HttpRequest) -> HttpResponse:
user = cast(User, request.user)
if not (user.role in MODERATOR_ROLES or user.is_superuser):
return HttpResponse(status=403)
query = request.GET.get('q', '').strip()
submission_id = request.GET.get('submission_id')
manufacturer_id = request.GET.get('manufacturer')
query = request.GET.get("q", "").strip()
submission_id = request.GET.get("submission_id")
manufacturer_id = request.GET.get("manufacturer")
queryset = RideModel.objects.all()
if manufacturer_id:
queryset = queryset.filter(manufacturer_id=manufacturer_id)
if query:
queryset = queryset.filter(name__icontains=query)
queryset = queryset.order_by('name')[:10]
return render(request, 'moderation/partials/ride_model_search_results.html', {
'ride_models': queryset,
'search_term': query,
'submission_id': submission_id
})
queryset = queryset.order_by("name")[:10]
return render(
request,
"moderation/partials/ride_model_search_results.html",
{
"ride_models": queryset,
"search_term": query,
"submission_id": submission_id,
},
)
class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
template_name = 'moderation/dashboard.html'
context_object_name = 'submissions'
template_name = "moderation/dashboard.html"
context_object_name = "submissions"
paginate_by = 10
def get_template_names(self) -> List[str]:
if self.request.headers.get('HX-Request'):
return ['moderation/partials/dashboard_content.html']
if self.request.headers.get("HX-Request"):
return ["moderation/partials/dashboard_content.html"]
return [self.template_name]
def get_queryset(self) -> QuerySet:
status = self.request.GET.get('status', 'PENDING')
submission_type = self.request.GET.get('submission_type', '')
status = self.request.GET.get("status", "PENDING")
submission_type = self.request.GET.get("submission_type", "")
return get_filtered_queryset(self.request, status, submission_type)
@login_required
def submission_list(request: HttpRequest) -> HttpResponse:
"""View for submission list with filters"""
user = cast(User, request.user)
if not (user.role in MODERATOR_ROLES or user.is_superuser):
return HttpResponse(status=403)
status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '')
status = request.GET.get("status", "PENDING")
submission_type = request.GET.get("submission_type", "")
queryset = get_filtered_queryset(request, status, submission_type)
# Process location data for park submissions
for submission in queryset:
if (submission.content_type.model == 'park' and
isinstance(submission.changes, dict)):
if submission.content_type.model == "park" and isinstance(
submission.changes, dict
):
# Extract location fields into a location object
location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']
location_data = {field: submission.changes.get(field) for field in location_fields}
location_fields = [
"latitude",
"longitude",
"street_address",
"city",
"state",
"postal_code",
"country",
]
location_data = {
field: submission.changes.get(field) for field in location_fields
}
# Add location data back as a single object
submission.changes['location'] = location_data
submission.changes["location"] = location_data
context = get_context_data(request, queryset)
template_name = ('moderation/partials/dashboard_content.html'
if request.headers.get('HX-Request')
else 'moderation/dashboard.html')
template_name = (
"moderation/partials/dashboard_content.html"
if request.headers.get("HX-Request")
else "moderation/dashboard.html"
)
return render(request, template_name, context)
@login_required
def edit_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for editing a submission"""
user = cast(User, request.user)
if not (user.role in MODERATOR_ROLES or user.is_superuser):
return HttpResponse(status=403)
submission = get_object_or_404(EditSubmission, id=submission_id)
if request.method != 'POST':
if request.method != "POST":
return HttpResponse("Invalid request method", status=405)
notes = request.POST.get('notes')
notes = request.POST.get("notes")
if not notes:
return HttpResponse("Notes are required when editing a submission", status=400)
try:
edited_changes = dict(submission.changes) if submission.changes else {}
# Update stats if present
if 'stats' in edited_changes:
if "stats" in edited_changes:
edited_stats = {}
for key in edited_changes['stats']:
if new_value := request.POST.get(f'stats.{key}'):
for key in edited_changes["stats"]:
if new_value := request.POST.get(f"stats.{key}"):
edited_stats[key] = new_value
edited_changes['stats'] = edited_stats
edited_changes["stats"] = edited_stats
# Update location fields if present
if submission.content_type.model == 'park':
location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']
if submission.content_type.model == "park":
location_fields = [
"latitude",
"longitude",
"street_address",
"city",
"state",
"postal_code",
"country",
]
location_data = {}
for field in location_fields:
if new_value := request.POST.get(field):
if field in ['latitude', 'longitude']:
if field in ["latitude", "longitude"]:
try:
location_data[field] = float(new_value)
except ValueError:
return HttpResponse(f"Invalid value for {field}", status=400)
return HttpResponse(
f"Invalid value for {field}", status=400
)
else:
location_data[field] = new_value
if location_data:
edited_changes.update(location_data)
# Update other fields
for field in edited_changes:
if field == 'stats' or field in ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']:
if field == "stats" or field in [
"latitude",
"longitude",
"street_address",
"city",
"state",
"postal_code",
"country",
]:
continue
if new_value := request.POST.get(field):
if field in ['size_acres']:
if field in ["size_acres"]:
try:
edited_changes[field] = float(new_value)
except ValueError:
return HttpResponse(f"Invalid value for {field}", status=400)
else:
edited_changes[field] = new_value
# Convert to JSON-serializable format
json_changes = json.loads(json.dumps(edited_changes, cls=DjangoJSONEncoder))
submission.moderator_changes = json_changes
submission.notes = notes
submission.save()
# Process location data for display
if submission.content_type.model == 'park':
location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']
location_data = {field: json_changes.get(field) for field in location_fields}
if submission.content_type.model == "park":
location_fields = [
"latitude",
"longitude",
"street_address",
"city",
"state",
"postal_code",
"country",
]
location_data = {
field: json_changes.get(field) for field in location_fields
}
# Add location data back as a single object
json_changes['location'] = location_data
json_changes["location"] = location_data
submission.changes = json_changes
context = get_context_data(request, EditSubmission.objects.filter(id=submission_id))
return render(request, 'moderation/partials/submission_list.html', context)
context = get_context_data(
request, EditSubmission.objects.filter(id=submission_id)
)
return render(request, "moderation/partials/submission_list.html", context)
except Exception as e:
return HttpResponse(str(e), status=400)
@login_required
def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for approving a submission"""
user = cast(User, request.user)
submission = get_object_or_404(EditSubmission, id=submission_id)
if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or
user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
if not (
(submission.status != "ESCALATED" and user.role in MODERATOR_ROLES)
or user.role in ["ADMIN", "SUPERUSER"]
or user.is_superuser
):
return HttpResponse("Insufficient permissions", status=403)
try:
submission.approve(user)
_update_submission_notes(submission, request.POST.get('notes'))
status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '')
_update_submission_notes(submission, request.POST.get("notes"))
status = request.GET.get("status", "PENDING")
submission_type = request.GET.get("submission_type", "")
queryset = get_filtered_queryset(request, status, submission_type)
return render(request, 'moderation/partials/dashboard_content.html', {
'submissions': queryset,
'user': request.user,
})
return render(
request,
"moderation/partials/dashboard_content.html",
{
"submissions": queryset,
"user": request.user,
},
)
except ValueError as e:
return HttpResponse(str(e), status=400)
@login_required
def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for rejecting a submission"""
user = cast(User, request.user)
submission = get_object_or_404(EditSubmission, id=submission_id)
if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or
user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser):
if not (
(submission.status != "ESCALATED" and user.role in MODERATOR_ROLES)
or user.role in ["ADMIN", "SUPERUSER"]
or user.is_superuser
):
return HttpResponse("Insufficient permissions", status=403)
submission.reject(user)
_update_submission_notes(submission, request.POST.get('notes'))
_update_submission_notes(submission, request.POST.get("notes"))
status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '')
status = request.GET.get("status", "PENDING")
submission_type = request.GET.get("submission_type", "")
queryset = get_filtered_queryset(request, status, submission_type)
context = get_context_data(request, queryset)
return render(request, 'moderation/partials/submission_list.html', context)
return render(request, "moderation/partials/submission_list.html", context)
@login_required
def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
@@ -299,11 +375,16 @@ def escalate_submission(request: HttpRequest, submission_id: int) -> HttpRespons
status = request.GET.get("status", "PENDING")
submission_type = request.GET.get("submission_type", "")
queryset = get_filtered_queryset(request, status, submission_type)
return render(request, "moderation/partials/dashboard_content.html", {
"submissions": queryset,
"user": request.user,
})
return render(
request,
"moderation/partials/dashboard_content.html",
{
"submissions": queryset,
"user": request.user,
},
)
@login_required
def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
@@ -315,11 +396,15 @@ def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
submission = get_object_or_404(PhotoSubmission, id=submission_id)
try:
submission.approve(user, request.POST.get("notes", ""))
return render(request, "moderation/partials/photo_submission.html",
{"submission": submission})
return render(
request,
"moderation/partials/photo_submission.html",
{"submission": submission},
)
except Exception as e:
return HttpResponse(str(e), status=400)
@login_required
def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for rejecting a photo submission"""
@@ -330,8 +415,12 @@ def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
submission = get_object_or_404(PhotoSubmission, id=submission_id)
submission.reject(user, request.POST.get("notes", ""))
return render(request, "moderation/partials/photo_submission.html",
{"submission": submission})
return render(
request,
"moderation/partials/photo_submission.html",
{"submission": submission},
)
def _update_submission_notes(submission: EditSubmission, notes: Optional[str]) -> None:
"""Update submission notes if provided."""