feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -52,16 +52,11 @@ class ParkListOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
status = RichChoiceFieldSerializer(choice_group="statuses", domain="parks")
description = serializers.CharField()
# Statistics
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, allow_null=True
)
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
coaster_count = serializers.IntegerField(allow_null=True)
ride_count = serializers.IntegerField(allow_null=True)
@@ -145,25 +140,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
status = RichChoiceFieldSerializer(choice_group="statuses", domain="parks")
description = serializers.CharField()
# Details
opening_date = serializers.DateField(allow_null=True)
closing_date = serializers.DateField(allow_null=True)
operating_season = serializers.CharField()
size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, allow_null=True
)
size_acres = serializers.DecimalField(max_digits=10, decimal_places=2, allow_null=True)
website = serializers.URLField()
# Statistics
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, allow_null=True
)
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
coaster_count = serializers.IntegerField(allow_null=True)
ride_count = serializers.IntegerField(allow_null=True)
@@ -211,9 +199,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"""Get all approved photos for this park."""
from apps.parks.models import ParkPhoto
photos = ParkPhoto.objects.filter(park=obj, is_approved=True).order_by(
"-is_primary", "-created_at"
)[
photos = ParkPhoto.objects.filter(park=obj, is_approved=True).order_by("-is_primary", "-created_at")[
:10
] # Limit to 10 photos
@@ -228,7 +214,9 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, photo.caption, photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "public"),
@@ -246,9 +234,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
from apps.parks.models import ParkPhoto
try:
photo = ParkPhoto.objects.filter(
park=obj, is_primary=True, is_approved=True
).first()
photo = ParkPhoto.objects.filter(park=obj, is_primary=True, is_approved=True).first()
if photo and photo.image:
return {
@@ -261,7 +247,9 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, photo.caption, photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "public"),
@@ -289,10 +277,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "public"
),
},
"caption": obj.banner_image.caption,
"alt_text": obj.banner_image.alt_text,
@@ -303,9 +299,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
try:
latest_photo = (
ParkPhoto.objects.filter(
park=obj, is_approved=True, image__isnull=False
)
ParkPhoto.objects.filter(park=obj, is_approved=True, image__isnull=False)
.order_by("-created_at")
.first()
)
@@ -321,10 +315,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "public"
),
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
@@ -350,10 +352,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "public"
),
},
"caption": obj.card_image.caption,
"alt_text": obj.card_image.alt_text,
@@ -364,9 +374,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
try:
latest_photo = (
ParkPhoto.objects.filter(
park=obj, is_approved=True, image__isnull=False
)
ParkPhoto.objects.filter(park=obj, is_approved=True, image__isnull=False)
.order_by("-created_at")
.first()
)
@@ -382,10 +390,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "public"
),
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
@@ -417,7 +433,7 @@ class ParkImageSettingsInputSerializer(serializers.Serializer):
# The park will be validated in the view
return value
except ParkPhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
raise serializers.ValidationError("Photo not found") from None
return value
def validate_card_image_id(self, value):
@@ -430,7 +446,7 @@ class ParkImageSettingsInputSerializer(serializers.Serializer):
# The park will be validated in the view
return value
except ParkPhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
raise serializers.ValidationError("Photo not found") from None
return value
@@ -439,19 +455,13 @@ class ParkCreateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
status = serializers.ChoiceField(
choices=ModelChoices.get_park_status_choices(), default="OPERATING"
)
status = serializers.ChoiceField(choices=ModelChoices.get_park_status_choices(), default="OPERATING")
# Optional details
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
operating_season = serializers.CharField(
max_length=255, required=False, allow_blank=True
)
size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, allow_null=True
)
operating_season = serializers.CharField(max_length=255, required=False, allow_blank=True)
size_acres = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
website = serializers.URLField(required=False, allow_blank=True)
# Required operator
@@ -466,9 +476,7 @@ class ParkCreateInputSerializer(serializers.Serializer):
closing_date = attrs.get("closing_date")
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs
@@ -478,19 +486,13 @@ class ParkUpdateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
status = serializers.ChoiceField(
choices=ModelChoices.get_park_status_choices(), required=False
)
status = serializers.ChoiceField(choices=ModelChoices.get_park_status_choices(), required=False)
# Optional details
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
operating_season = serializers.CharField(
max_length=255, required=False, allow_blank=True
)
size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, allow_null=True
)
operating_season = serializers.CharField(max_length=255, required=False, allow_blank=True)
size_acres = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
website = serializers.URLField(required=False, allow_blank=True)
# Companies
@@ -503,9 +505,7 @@ class ParkUpdateInputSerializer(serializers.Serializer):
closing_date = attrs.get("closing_date")
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs
@@ -537,12 +537,8 @@ class ParkFilterInputSerializer(serializers.Serializer):
)
# Size filter
min_size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, min_value=0
)
max_size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, min_value=0
)
min_size_acres = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, min_value=0)
max_size_acres = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, min_value=0)
# Company filters
operator_id = serializers.IntegerField(required=False)
@@ -625,9 +621,7 @@ class ParkAreaCreateInputSerializer(serializers.Serializer):
closing_date = attrs.get("closing_date")
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs
@@ -646,9 +640,7 @@ class ParkAreaUpdateInputSerializer(serializers.Serializer):
closing_date = attrs.get("closing_date")
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs