Refactor API structure and add comprehensive user management features

- Restructure API v1 with improved serializers organization
- Add user deletion requests and moderation queue system
- Implement bulk moderation operations and permissions
- Add user profile enhancements with display names and avatars
- Expand ride and park API endpoints with better filtering
- Add manufacturer API with detailed ride relationships
- Improve authentication flows and error handling
- Update frontend documentation and API specifications
This commit is contained in:
pacnpal
2025-08-29 16:03:51 -04:00
parent 7b9f64be72
commit bb7da85516
92 changed files with 19690 additions and 9076 deletions

View File

@@ -3,6 +3,7 @@ from apps.core.mixins import HTMXFilterableMixin
from .models.location import ParkLocation
from .models.media import ParkPhoto
from apps.moderation.models import EditSubmission
from apps.moderation.services import ModerationService
from apps.moderation.mixins import (
EditSubmissionMixin,
PhotoSubmissionMixin,
@@ -501,88 +502,85 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
self.normalize_coordinates(form)
changes = self.prepare_changes_data(form.cleaned_data)
submission = EditSubmission.objects.create(
user=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
submission_type="CREATE",
# Use the new queue routing service
result = ModerationService.create_edit_submission_with_queue(
content_object=None, # None for CREATE
changes=changes,
submitter=self.request.user,
submission_type="CREATE",
reason=self.request.POST.get("reason", ""),
source=self.request.POST.get("source", ""),
)
if (
hasattr(self.request.user, "role")
and getattr(self.request.user, "role", None) in ALLOWED_ROLES
):
try:
self.object = form.save()
submission.object_id = self.object.id
submission.status = "APPROVED"
submission.handled_by = self.request.user
submission.save()
if result['status'] == 'auto_approved':
# Moderator submission was auto-approved
self.object = result['created_object']
if form.cleaned_data.get("latitude") and form.cleaned_data.get("longitude"):
# Create or update ParkLocation
park_location, created = ParkLocation.objects.get_or_create(
park=self.object,
defaults={
"street_address": form.cleaned_data.get("street_address", ""),
"city": form.cleaned_data.get("city", ""),
"state": form.cleaned_data.get("state", ""),
"country": form.cleaned_data.get("country", "USA"),
"postal_code": form.cleaned_data.get("postal_code", ""),
},
)
park_location.set_coordinates(
form.cleaned_data["latitude"],
form.cleaned_data["longitude"],
)
park_location.save()
if form.cleaned_data.get("latitude") and form.cleaned_data.get(
"longitude"
):
# Create or update ParkLocation
park_location, created = ParkLocation.objects.get_or_create(
photos = self.request.FILES.getlist("photos")
uploaded_count = 0
for photo_file in photos:
try:
ParkPhoto.objects.create(
image=photo_file,
uploaded_by=self.request.user,
park=self.object,
defaults={
"street_address": form.cleaned_data.get(
"street_address", ""
),
"city": form.cleaned_data.get("city", ""),
"state": form.cleaned_data.get("state", ""),
"country": form.cleaned_data.get("country", "USA"),
"postal_code": form.cleaned_data.get("postal_code", ""),
},
)
park_location.set_coordinates(
form.cleaned_data["latitude"],
form.cleaned_data["longitude"],
uploaded_count += 1
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
)
park_location.save()
photos = self.request.FILES.getlist("photos")
uploaded_count = 0
for photo_file in photos:
try:
ParkPhoto.objects.create(
image=photo_file,
uploaded_by=self.request.user,
park=self.object,
)
uploaded_count += 1
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
)
messages.success(
self.request,
f"Successfully created {self.object.name}. "
f"Added {uploaded_count} photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
except Exception as e:
messages.error(
self.request,
f"Error creating park: {
str(e)
}. Please check your input and try again.",
)
return self.form_invalid(form)
messages.success(
messages.success(
self.request,
f"Successfully created {self.object.name}. "
f"Added {uploaded_count} photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
elif result['status'] == 'queued':
# Regular user submission was queued
messages.success(
self.request,
"Your park submission has been sent for review. "
"You will be notified when it is approved.",
)
# Redirect to parks list since we don't have an object yet
return HttpResponseRedirect(reverse("parks:park_list"))
elif result['status'] == 'failed':
# Auto-approval failed
messages.error(
self.request,
f"Error creating park: {result['message']}. Please check your input and try again.",
)
return self.form_invalid(form)
# Fallback error case
messages.error(
self.request,
"Your park submission has been sent for review. "
"You will be notified when it is approved.",
"An unexpected error occurred. Please try again.",
)
for field, errors in form.errors.items():
for error in errors:
messages.error(self.request, f"{field}: {error}")
return super().form_invalid(form)
return self.form_invalid(form)
def get_success_url(self) -> str:
return reverse(PARK_DETAIL_URL, kwargs={"slug": self.object.slug})
@@ -633,125 +631,129 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
self.normalize_coordinates(form)
changes = self.prepare_changes_data(form.cleaned_data)
submission = EditSubmission.objects.create(
user=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
submission_type="EDIT",
# Use the new queue routing service
result = ModerationService.create_edit_submission_with_queue(
content_object=self.object,
changes=changes,
submitter=self.request.user,
submission_type="EDIT",
reason=self.request.POST.get("reason", ""),
source=self.request.POST.get("source", ""),
)
if (
hasattr(self.request.user, "role")
and getattr(self.request.user, "role", None) in ALLOWED_ROLES
):
if result['status'] == 'auto_approved':
# Moderator submission was auto-approved
# The object was already updated by the service
self.object = result['created_object']
location_data = {
"name": self.object.name,
"location_type": "park",
"latitude": form.cleaned_data.get("latitude"),
"longitude": form.cleaned_data.get("longitude"),
"street_address": form.cleaned_data.get("street_address", ""),
"city": form.cleaned_data.get("city", ""),
"state": form.cleaned_data.get("state", ""),
"country": form.cleaned_data.get("country", ""),
"postal_code": form.cleaned_data.get("postal_code", ""),
}
# Create or update ParkLocation
try:
self.object = form.save()
submission.status = "APPROVED"
submission.handled_by = self.request.user
submission.save()
park_location = self.object.location
# Update existing location
for key, value in location_data.items():
if key in ["latitude", "longitude"] and value:
continue # Handle coordinates separately
if hasattr(park_location, key):
setattr(park_location, key, value)
location_data = {
"name": self.object.name,
"location_type": "park",
"latitude": form.cleaned_data.get("latitude"),
"longitude": form.cleaned_data.get("longitude"),
"street_address": form.cleaned_data.get("street_address", ""),
"city": form.cleaned_data.get("city", ""),
"state": form.cleaned_data.get("state", ""),
"country": form.cleaned_data.get("country", ""),
"postal_code": form.cleaned_data.get("postal_code", ""),
# Handle coordinates if provided
if "latitude" in location_data and "longitude" in location_data:
if location_data["latitude"] and location_data["longitude"]:
park_location.set_coordinates(
float(location_data["latitude"]),
float(location_data["longitude"]),
)
park_location.save()
except ParkLocation.DoesNotExist:
# Create new ParkLocation
coordinates_data = {}
if "latitude" in location_data and "longitude" in location_data:
if location_data["latitude"] and location_data["longitude"]:
coordinates_data = {
"latitude": float(location_data["latitude"]),
"longitude": float(location_data["longitude"]),
}
# Remove coordinate fields from location_data for creation
creation_data = {
k: v
for k, v in location_data.items()
if k not in ["latitude", "longitude"]
}
creation_data.setdefault("country", "USA")
# Create or update ParkLocation
try:
park_location = self.object.location
# Update existing location
for key, value in location_data.items():
if key in ["latitude", "longitude"] and value:
continue # Handle coordinates separately
if hasattr(park_location, key):
setattr(park_location, key, value)
park_location = ParkLocation.objects.create(
park=self.object, **creation_data
)
# Handle coordinates if provided
if "latitude" in location_data and "longitude" in location_data:
if location_data["latitude"] and location_data["longitude"]:
park_location.set_coordinates(
float(location_data["latitude"]),
float(location_data["longitude"]),
)
if coordinates_data:
park_location.set_coordinates(
coordinates_data["latitude"],
coordinates_data["longitude"],
)
park_location.save()
except ParkLocation.DoesNotExist:
# Create new ParkLocation
coordinates_data = {}
if "latitude" in location_data and "longitude" in location_data:
if location_data["latitude"] and location_data["longitude"]:
coordinates_data = {
"latitude": float(location_data["latitude"]),
"longitude": float(location_data["longitude"]),
}
# Remove coordinate fields from location_data for creation
creation_data = {
k: v
for k, v in location_data.items()
if k not in ["latitude", "longitude"]
}
creation_data.setdefault("country", "USA")
park_location = ParkLocation.objects.create(
park=self.object, **creation_data
photos = self.request.FILES.getlist("photos")
uploaded_count = 0
for photo_file in photos:
try:
ParkPhoto.objects.create(
image=photo_file,
uploaded_by=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
)
uploaded_count += 1
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
)
if coordinates_data:
park_location.set_coordinates(
coordinates_data["latitude"],
coordinates_data["longitude"],
)
park_location.save()
photos = self.request.FILES.getlist("photos")
uploaded_count = 0
for photo_file in photos:
try:
ParkPhoto.objects.create(
image=photo_file,
uploaded_by=self.request.user,
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
)
uploaded_count += 1
except Exception as e:
messages.error(
self.request,
f"Error uploading photo {photo_file.name}: {str(e)}",
)
messages.success(
self.request,
f"Successfully updated {self.object.name}. "
f"Added {uploaded_count} new photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
except Exception as e:
messages.error(
self.request,
f"Error updating park: {
str(e)
}. Please check your input and try again.",
)
return self.form_invalid(form)
messages.success(
messages.success(
self.request,
f"Successfully updated {self.object.name}. "
f"Added {uploaded_count} new photo(s).",
)
return HttpResponseRedirect(self.get_success_url())
elif result['status'] == 'queued':
# Regular user submission was queued
messages.success(
self.request,
f"Your changes to {self.object.name} have been sent for review. "
"You will be notified when they are approved.",
)
return HttpResponseRedirect(
reverse(PARK_DETAIL_URL, kwargs={"slug": self.object.slug})
)
elif result['status'] == 'failed':
# Auto-approval failed
messages.error(
self.request,
f"Error updating park: {result['message']}. Please check your input and try again.",
)
return self.form_invalid(form)
# Fallback error case
messages.error(
self.request,
f"Your changes to {self.object.name} have been sent for review. "
"You will be notified when they are approved.",
)
return HttpResponseRedirect(
reverse(PARK_DETAIL_URL, kwargs={"slug": self.object.slug})
"An unexpected error occurred. Please try again.",
)
return self.form_invalid(form)
def form_invalid(self, form: ParkForm) -> HttpResponse:
messages.error(self.request, REQUIRED_FIELDS_ERROR)