feat: add public profiles list endpoint with search and pagination

- Add new /profiles/ endpoint for listing user profiles
- Support search by username/display name with ordering options
- Include pagination with configurable page size (max 100)
- Add comprehensive OpenAPI schema documentation
- Refactor passkey authentication state management in MFA flow
- Update URL routing and imports for new list_profiles view

This enables user discovery, leaderboards, and friend-finding features
with a publicly accessible, well-documented API endpoint.
This commit is contained in:
pacnpal
2026-01-10 13:00:02 -05:00
parent 22ff0d1c49
commit 692c0bbbbf
9 changed files with 424 additions and 45 deletions

View File

@@ -211,6 +211,18 @@ class RideDetailOutputSerializer(serializers.Serializer):
# Former names (name history)
former_names = serializers.SerializerMethodField()
# Coaster statistics - includes both imperial and metric units for frontend flexibility
coaster_statistics = serializers.SerializerMethodField()
# Metric unit fields for frontend (converted from imperial)
height_meters = serializers.SerializerMethodField()
length_meters = serializers.SerializerMethodField()
max_speed_kmh = serializers.SerializerMethodField()
drop_meters = serializers.SerializerMethodField()
# Technical specifications list
technical_specifications = serializers.SerializerMethodField()
# URL
url = serializers.SerializerMethodField()
@@ -427,6 +439,99 @@ class RideDetailOutputSerializer(serializers.Serializer):
for entry in former_names
]
@extend_schema_field(serializers.DictField(allow_null=True))
def get_coaster_statistics(self, obj):
"""Get coaster statistics with both imperial and metric units."""
try:
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
stats = obj.coaster_stats
return {
# Imperial units (stored in DB)
"height_ft": float(stats.height_ft) if stats.height_ft else None,
"length_ft": float(stats.length_ft) if stats.length_ft else None,
"speed_mph": float(stats.speed_mph) if stats.speed_mph else None,
"max_drop_height_ft": float(stats.max_drop_height_ft) if stats.max_drop_height_ft else None,
# Metric conversions for frontend
"height_meters": round(float(stats.height_ft) * 0.3048, 2) if stats.height_ft else None,
"length_meters": round(float(stats.length_ft) * 0.3048, 2) if stats.length_ft else None,
"max_speed_kmh": round(float(stats.speed_mph) * 1.60934, 2) if stats.speed_mph else None,
"drop_meters": round(float(stats.max_drop_height_ft) * 0.3048, 2) if stats.max_drop_height_ft else None,
# Other stats
"inversions": stats.inversions,
"ride_time_seconds": stats.ride_time_seconds,
"track_type": stats.track_type,
"track_material": stats.track_material,
"roller_coaster_type": stats.roller_coaster_type,
"propulsion_system": stats.propulsion_system,
"train_style": stats.train_style,
"trains_count": stats.trains_count,
"cars_per_train": stats.cars_per_train,
"seats_per_car": stats.seats_per_car,
}
except AttributeError:
pass
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_height_meters(self, obj):
"""Convert height from feet to meters for frontend."""
try:
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.height_ft:
return round(float(obj.coaster_stats.height_ft) * 0.3048, 2)
except (AttributeError, TypeError):
pass
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_length_meters(self, obj):
"""Convert length from feet to meters for frontend."""
try:
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.length_ft:
return round(float(obj.coaster_stats.length_ft) * 0.3048, 2)
except (AttributeError, TypeError):
pass
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_max_speed_kmh(self, obj):
"""Convert max speed from mph to km/h for frontend."""
try:
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.speed_mph:
return round(float(obj.coaster_stats.speed_mph) * 1.60934, 2)
except (AttributeError, TypeError):
pass
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_drop_meters(self, obj):
"""Convert drop height from feet to meters for frontend."""
try:
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.max_drop_height_ft:
return round(float(obj.coaster_stats.max_drop_height_ft) * 0.3048, 2)
except (AttributeError, TypeError):
pass
return None
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_technical_specifications(self, obj):
"""Get technical specifications list for this ride."""
try:
from apps.rides.models import RideTechnicalSpec
specs = RideTechnicalSpec.objects.filter(ride=obj).order_by("category", "name")
return [
{
"id": spec.id,
"name": spec.name,
"value": spec.value,
"unit": spec.unit,
"category": spec.category,
}
for spec in specs
]
except Exception:
return []
class RideImageSettingsInputSerializer(serializers.Serializer):
"""Input serializer for setting ride banner and card images."""