feat: complete monorepo structure with frontend and shared resources

- Add complete backend/ directory with full Django application
- Add frontend/ directory with Vite + TypeScript setup ready for Next.js
- Add comprehensive shared/ directory with:
  - Complete documentation and memory-bank archives
  - Media files and avatars (letters, park/ride images)
  - Deployment scripts and automation tools
  - Shared types and utilities
- Add architecture/ directory with migration guides
- Configure pnpm workspace for monorepo development
- Update .gitignore to exclude .django_tailwind_cli/ build artifacts
- Preserve all historical documentation in shared/docs/memory-bank/
- Set up proper structure for full-stack development with shared resources
This commit is contained in:
pacnpal
2025-08-23 18:40:07 -04:00
parent b0e0678590
commit d504d41de2
762 changed files with 142636 additions and 0 deletions

View File

@@ -0,0 +1,429 @@
from django.views.generic import ListView
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpRequest
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.decorators import login_required
from django.db.models import QuerySet
from django.core.exceptions import PermissionDenied
from typing import Optional, Any, Dict, List, Tuple, cast
from django.core.serializers.json import DjangoJSONEncoder
import json
from apps.accounts.models import User
from .models import EditSubmission, PhotoSubmission
from apps.parks.models import Park, ParkArea
from apps.rides.models import RideModel
MODERATOR_ROLES = ["MODERATOR", "ADMIN", "SUPERUSER"]
class ModeratorRequiredMixin(UserPassesTestMixin):
request: HttpRequest
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
)
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:
"""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"):
queryset = queryset.filter(submission_type=type_filter)
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 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
]
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,
}
@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")
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},
)
@login_required
def search_ride_models(request: HttpRequest) -> HttpResponse:
"""HTMX endpoint for searching ride models 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")
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,
},
)
class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
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"]
return [self.template_name]
def get_queryset(self) -> QuerySet:
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", "")
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
):
# 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
}
# Add location data back as a single object
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"
)
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":
return HttpResponse("Invalid request method", status=405)
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:
edited_stats = {}
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
# Update location fields if present
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"]:
try:
location_data[field] = float(new_value)
except ValueError:
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",
]:
continue
if new_value := request.POST.get(field):
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
}
# Add location data back as a single object
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)
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
):
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", "")
queryset = get_filtered_queryset(request, status, submission_type)
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
):
return HttpResponse("Insufficient permissions", status=403)
submission.reject(user)
_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)
context = get_context_data(request, queryset)
return render(request, "moderation/partials/submission_list.html", context)
@login_required
def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for escalating 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 submission.status == "ESCALATED":
return HttpResponse("Submission is already escalated", status=400)
submission.escalate(user)
_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,
},
)
@login_required
def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for approving a photo 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(PhotoSubmission, id=submission_id)
try:
submission.approve(user, request.POST.get("notes", ""))
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"""
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(PhotoSubmission, id=submission_id)
submission.reject(user, request.POST.get("notes", ""))
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."""
if notes:
submission.notes = notes
submission.save()