mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-27 14:07:06 -05:00
feat: Introduce lists and reviews apps, refactor user list functionality from accounts, and add user profile fields.
This commit is contained in:
@@ -31,8 +31,6 @@ from apps.core.admin import (
|
||||
from .models import (
|
||||
EmailVerification,
|
||||
PasswordReset,
|
||||
TopList,
|
||||
TopListItem,
|
||||
User,
|
||||
UserProfile,
|
||||
)
|
||||
@@ -81,18 +79,6 @@ class UserProfileInline(admin.StackedInline):
|
||||
)
|
||||
|
||||
|
||||
class TopListItemInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin for TopListItem within TopList admin.
|
||||
|
||||
Shows list items ordered by rank with content linking.
|
||||
"""
|
||||
|
||||
model = TopListItem
|
||||
extra = 1
|
||||
fields = ("content_type", "object_id", "rank", "notes")
|
||||
ordering = ("rank",)
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
@@ -683,181 +669,4 @@ class PasswordResetAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
|
||||
return actions
|
||||
|
||||
|
||||
@admin.register(TopList)
|
||||
class TopListAdmin(
|
||||
QueryOptimizationMixin, ExportActionMixin, TimestampFieldsMixin, BaseModelAdmin
|
||||
):
|
||||
"""
|
||||
Admin interface for user top lists.
|
||||
|
||||
Manages user-created top lists with inline item editing
|
||||
and category filtering.
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"title",
|
||||
"user_link",
|
||||
"category",
|
||||
"item_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
list_filter = ("category", "created_at", "updated_at")
|
||||
list_select_related = ["user"]
|
||||
list_prefetch_related = ["items"]
|
||||
search_fields = ("title", "user__username", "description")
|
||||
autocomplete_fields = ["user"]
|
||||
inlines = [TopListItemInline]
|
||||
|
||||
export_fields = ["id", "title", "user", "category", "created_at", "updated_at"]
|
||||
export_filename_prefix = "top_lists"
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
"Basic Information",
|
||||
{
|
||||
"fields": ("user", "title", "category", "description"),
|
||||
"description": "List identification and categorization.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"Timestamps",
|
||||
{
|
||||
"fields": ("created_at", "updated_at"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
|
||||
@admin.display(description="User")
|
||||
def user_link(self, obj):
|
||||
"""Display user as clickable link."""
|
||||
if obj.user:
|
||||
from django.urls import reverse
|
||||
|
||||
url = reverse("admin:accounts_customuser_change", args=[obj.user.pk])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.user.username)
|
||||
return "-"
|
||||
|
||||
@admin.display(description="Items")
|
||||
def item_count(self, obj):
|
||||
"""Display count of items in the list."""
|
||||
if hasattr(obj, "_item_count"):
|
||||
return obj._item_count
|
||||
return obj.items.count()
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queryset with item count annotation."""
|
||||
qs = super().get_queryset(request)
|
||||
qs = qs.annotate(_item_count=Count("items", distinct=True))
|
||||
return qs
|
||||
|
||||
@admin.action(description="Publish selected lists")
|
||||
def publish_lists(self, request, queryset):
|
||||
"""Mark selected lists as published."""
|
||||
# Assuming there's a published field
|
||||
self.message_user(request, f"Published {queryset.count()} lists.")
|
||||
|
||||
@admin.action(description="Unpublish selected lists")
|
||||
def unpublish_lists(self, request, queryset):
|
||||
"""Mark selected lists as unpublished."""
|
||||
self.message_user(request, f"Unpublished {queryset.count()} lists.")
|
||||
|
||||
def get_actions(self, request):
|
||||
"""Add custom actions."""
|
||||
actions = super().get_actions(request)
|
||||
actions["publish_lists"] = (
|
||||
self.publish_lists,
|
||||
"publish_lists",
|
||||
"Publish selected lists",
|
||||
)
|
||||
actions["unpublish_lists"] = (
|
||||
self.unpublish_lists,
|
||||
"unpublish_lists",
|
||||
"Unpublish selected lists",
|
||||
)
|
||||
return actions
|
||||
|
||||
|
||||
@admin.register(TopListItem)
|
||||
class TopListItemAdmin(QueryOptimizationMixin, BaseModelAdmin):
|
||||
"""
|
||||
Admin interface for top list items.
|
||||
|
||||
Manages individual items within top lists with
|
||||
content type linking and reordering.
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"top_list_link",
|
||||
"content_type",
|
||||
"object_id",
|
||||
"rank",
|
||||
"content_preview",
|
||||
)
|
||||
list_filter = ("top_list__category", "content_type", "rank")
|
||||
list_select_related = ["top_list", "top_list__user", "content_type"]
|
||||
search_fields = ("top_list__title", "notes", "top_list__user__username")
|
||||
autocomplete_fields = ["top_list"]
|
||||
ordering = ("top_list", "rank")
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
"List Information",
|
||||
{
|
||||
"fields": ("top_list", "rank"),
|
||||
"description": "The list this item belongs to and its position.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"Item Details",
|
||||
{
|
||||
"fields": ("content_type", "object_id", "notes"),
|
||||
"description": "The content this item references.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Top List")
|
||||
def top_list_link(self, obj):
|
||||
"""Display top list as clickable link."""
|
||||
if obj.top_list:
|
||||
from django.urls import reverse
|
||||
|
||||
url = reverse("admin:accounts_toplist_change", args=[obj.top_list.pk])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.top_list.title)
|
||||
return "-"
|
||||
|
||||
@admin.display(description="Content")
|
||||
def content_preview(self, obj):
|
||||
"""Display preview of linked content."""
|
||||
try:
|
||||
content_obj = obj.content_type.get_object_for_this_type(pk=obj.object_id)
|
||||
return str(content_obj)[:50]
|
||||
except Exception:
|
||||
return format_html('<span style="color: red;">Not found</span>')
|
||||
|
||||
@admin.action(description="Move up in list")
|
||||
def move_up(self, request, queryset):
|
||||
"""Move selected items up in their lists."""
|
||||
for item in queryset:
|
||||
if item.rank > 1:
|
||||
item.rank -= 1
|
||||
item.save(update_fields=["rank"])
|
||||
self.message_user(request, "Items moved up.")
|
||||
|
||||
@admin.action(description="Move down in list")
|
||||
def move_down(self, request, queryset):
|
||||
"""Move selected items down in their lists."""
|
||||
for item in queryset:
|
||||
item.rank += 1
|
||||
item.save(update_fields=["rank"])
|
||||
self.message_user(request, "Items moved down.")
|
||||
|
||||
def get_actions(self, request):
|
||||
"""Add reordering actions."""
|
||||
actions = super().get_actions(request)
|
||||
actions["move_up"] = (self.move_up, "move_up", "Move up in list")
|
||||
actions["move_down"] = (self.move_down, "move_down", "Move down in list")
|
||||
return actions
|
||||
|
||||
@@ -112,6 +112,51 @@ theme_preferences = ChoiceGroup(
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UNIT SYSTEMS
|
||||
# =============================================================================
|
||||
|
||||
unit_systems = ChoiceGroup(
|
||||
name="unit_systems",
|
||||
choices=[
|
||||
RichChoice(
|
||||
value="metric",
|
||||
label="Metric",
|
||||
description="Use metric units (meters, km/h)",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "ruler",
|
||||
"css_class": "text-blue-600 bg-blue-50",
|
||||
"units": {
|
||||
"distance": "m",
|
||||
"speed": "km/h",
|
||||
"weight": "kg",
|
||||
"large_distance": "km",
|
||||
},
|
||||
"sort_order": 1,
|
||||
}
|
||||
),
|
||||
RichChoice(
|
||||
value="imperial",
|
||||
label="Imperial",
|
||||
description="Use imperial units (feet, mph)",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "ruler",
|
||||
"css_class": "text-green-600 bg-green-50",
|
||||
"units": {
|
||||
"distance": "ft",
|
||||
"speed": "mph",
|
||||
"weight": "lbs",
|
||||
"large_distance": "mi",
|
||||
},
|
||||
"sort_order": 2,
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRIVACY LEVELS
|
||||
# =============================================================================
|
||||
@@ -557,6 +602,7 @@ notification_priorities = ChoiceGroup(
|
||||
# Register each choice group individually
|
||||
register_choices("user_roles", user_roles.choices, "accounts", "User role classifications")
|
||||
register_choices("theme_preferences", theme_preferences.choices, "accounts", "Theme preference options")
|
||||
register_choices("unit_systems", unit_systems.choices, "accounts", "Unit system preferences")
|
||||
register_choices("privacy_levels", privacy_levels.choices, "accounts", "Privacy level settings")
|
||||
register_choices("top_list_categories", top_list_categories.choices, "accounts", "Top list category types")
|
||||
register_choices("notification_types", notification_types.choices, "accounts", "Notification type classifications")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ from django.utils import timezone
|
||||
from apps.core.history import TrackedModel
|
||||
from apps.core.choices import RichChoiceField
|
||||
import pghistory
|
||||
# from django_cloudflareimages_toolkit.models import CloudflareImage
|
||||
|
||||
|
||||
def generate_random_id(model_class, id_field):
|
||||
@@ -214,10 +215,11 @@ class UserProfile(models.Model):
|
||||
help_text="Legacy display name field - use User.display_name instead",
|
||||
)
|
||||
avatar = models.ForeignKey(
|
||||
'django_cloudflareimages_toolkit.CloudflareImage',
|
||||
"django_cloudflareimages_toolkit.CloudflareImage",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="user_profiles",
|
||||
help_text="User's avatar image",
|
||||
)
|
||||
pronouns = models.CharField(
|
||||
@@ -225,6 +227,16 @@ class UserProfile(models.Model):
|
||||
)
|
||||
|
||||
bio = models.TextField(max_length=500, blank=True, help_text="User biography")
|
||||
location = models.CharField(
|
||||
max_length=100, blank=True, help_text="User's location (City, Country)"
|
||||
)
|
||||
unit_system = RichChoiceField(
|
||||
choice_group="unit_systems",
|
||||
domain="accounts",
|
||||
max_length=10,
|
||||
default="metric",
|
||||
help_text="Preferred measurement system",
|
||||
)
|
||||
|
||||
# Social media links
|
||||
twitter = models.URLField(blank=True, help_text="Twitter profile URL")
|
||||
@@ -380,65 +392,6 @@ class PasswordReset(models.Model):
|
||||
verbose_name_plural = "Password Resets"
|
||||
|
||||
|
||||
# @pghistory.track()
|
||||
|
||||
|
||||
class TopList(TrackedModel):
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="top_lists",
|
||||
help_text="User who created this list",
|
||||
)
|
||||
title = models.CharField(max_length=100, help_text="Title of the top list")
|
||||
category = RichChoiceField(
|
||||
choice_group="top_list_categories",
|
||||
domain="accounts",
|
||||
max_length=2,
|
||||
help_text="Category of items in this list",
|
||||
)
|
||||
description = models.TextField(blank=True, help_text="Description of the list")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Top List"
|
||||
verbose_name_plural = "Top Lists"
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.user.get_display_name()}'s {self.category} Top List: {self.title}"
|
||||
)
|
||||
|
||||
|
||||
# @pghistory.track()
|
||||
|
||||
|
||||
class TopListItem(TrackedModel):
|
||||
top_list = models.ForeignKey(
|
||||
TopList,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="items",
|
||||
help_text="Top list this item belongs to",
|
||||
)
|
||||
content_type = models.ForeignKey(
|
||||
"contenttypes.ContentType",
|
||||
on_delete=models.CASCADE,
|
||||
help_text="Type of item (park, ride, etc.)",
|
||||
)
|
||||
object_id = models.PositiveIntegerField(help_text="ID of the item")
|
||||
rank = models.PositiveIntegerField(help_text="Position in the list")
|
||||
notes = models.TextField(blank=True, help_text="User's notes about this item")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Top List Item"
|
||||
verbose_name_plural = "Top List Items"
|
||||
ordering = ["rank"]
|
||||
unique_together = [["top_list", "rank"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"#{self.rank} in {self.top_list.title}"
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
|
||||
@@ -19,7 +19,9 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
|
||||
avatar_url = serializers.SerializerMethodField()
|
||||
display_name = serializers.SerializerMethodField()
|
||||
display_name = serializers.CharField(source="profile.display_name", required=False)
|
||||
unit_system = serializers.CharField(source="profile.unit_system", required=False)
|
||||
location = serializers.CharField(source="profile.location", required=False)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@@ -31,6 +33,8 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
"date_joined",
|
||||
"is_active",
|
||||
"avatar_url",
|
||||
"unit_system",
|
||||
"location",
|
||||
]
|
||||
read_only_fields = ["id", "date_joined", "is_active"]
|
||||
|
||||
@@ -40,9 +44,15 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
return obj.profile.avatar.url
|
||||
return None
|
||||
|
||||
def get_display_name(self, obj) -> str:
|
||||
"""Get user display name"""
|
||||
return obj.get_display_name()
|
||||
def update(self, instance, validated_data):
|
||||
profile_data = validated_data.pop("profile", {})
|
||||
profile = instance.profile
|
||||
|
||||
for attr, value in profile_data.items():
|
||||
setattr(profile, attr, value)
|
||||
profile.save()
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
|
||||
@@ -20,10 +20,10 @@ from django.core.files.uploadedfile import UploadedFile
|
||||
from apps.accounts.models import (
|
||||
User,
|
||||
PasswordReset,
|
||||
TopList,
|
||||
EmailVerification,
|
||||
UserProfile,
|
||||
)
|
||||
from apps.lists.models import UserList
|
||||
from django_forwardemail.services import EmailService
|
||||
from apps.parks.models import ParkReview
|
||||
from apps.rides.models import RideReview
|
||||
@@ -208,9 +208,9 @@ class ProfileView(DetailView):
|
||||
.order_by("-created_at")[:5]
|
||||
)
|
||||
|
||||
def _get_user_top_lists(self, user: User) -> QuerySet[TopList]:
|
||||
def _get_user_top_lists(self, user: User) -> QuerySet[UserList]:
|
||||
return (
|
||||
TopList.objects.filter(user=user)
|
||||
UserList.objects.filter(user=user)
|
||||
.select_related("user", "user__profile")
|
||||
.prefetch_related("items")
|
||||
.order_by("-created_at")[:5]
|
||||
@@ -232,6 +232,12 @@ class SettingsView(LoginRequiredMixin, TemplateView):
|
||||
if display_name := request.POST.get("display_name"):
|
||||
profile.display_name = display_name
|
||||
|
||||
if unit_system := request.POST.get("unit_system"):
|
||||
profile.unit_system = unit_system
|
||||
|
||||
if location := request.POST.get("location"):
|
||||
profile.location = location
|
||||
|
||||
if "avatar" in request.FILES:
|
||||
avatar_file = cast(UploadedFile, request.FILES["avatar"])
|
||||
profile.avatar.save(avatar_file.name, avatar_file, save=False)
|
||||
|
||||
@@ -13,7 +13,7 @@ from apps.api.v1.serializers.accounts import (
|
||||
PrivacySettingsSerializer,
|
||||
SecuritySettingsSerializer,
|
||||
UserStatisticsSerializer,
|
||||
TopListSerializer,
|
||||
UserListSerializer,
|
||||
AccountUpdateSerializer,
|
||||
ProfileUpdateSerializer,
|
||||
ThemePreferenceSerializer,
|
||||
@@ -26,10 +26,10 @@ from apps.accounts.services import UserDeletionService
|
||||
from apps.accounts.models import (
|
||||
User,
|
||||
UserProfile,
|
||||
TopList,
|
||||
UserNotification,
|
||||
NotificationPreference,
|
||||
)
|
||||
from apps.lists.models import UserList
|
||||
import logging
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
@@ -831,7 +831,7 @@ def check_user_deletion_eligibility(request, user_id):
|
||||
user, "uploaded_ride_photos", user.__class__.objects.none()
|
||||
).count(),
|
||||
"top_lists": getattr(
|
||||
user, "top_lists", user.__class__.objects.none()
|
||||
user, "user_lists", user.__class__.objects.none()
|
||||
).count(),
|
||||
"edit_submissions": getattr(
|
||||
user, "edit_submissions", user.__class__.objects.none()
|
||||
@@ -1318,7 +1318,7 @@ def get_user_statistics(request):
|
||||
"rides_ridden": RideReview.objects.filter(user=user).values("ride").distinct().count(),
|
||||
"reviews_written": ParkReview.objects.filter(user=user).count() + RideReview.objects.filter(user=user).count(),
|
||||
"photos_uploaded": total_photos_uploaded,
|
||||
"top_lists_created": TopList.objects.filter(user=user).count(),
|
||||
"top_lists_created": UserList.objects.filter(user=user).count(),
|
||||
"member_since": user.date_joined,
|
||||
"last_activity": user.last_login,
|
||||
}
|
||||
@@ -1335,7 +1335,7 @@ def get_user_statistics(request):
|
||||
summary="Get user's top lists",
|
||||
description="Get all top lists created by the authenticated user.",
|
||||
responses={
|
||||
200: TopListSerializer(many=True),
|
||||
200: UserListSerializer(many=True),
|
||||
401: {"description": "Authentication required"},
|
||||
},
|
||||
tags=["User Content"],
|
||||
@@ -1344,8 +1344,8 @@ def get_user_statistics(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_user_top_lists(request):
|
||||
"""Get user's top lists."""
|
||||
top_lists = TopList.objects.filter(user=request.user).order_by("-created_at")
|
||||
serializer = TopListSerializer(top_lists, many=True)
|
||||
top_lists = UserList.objects.filter(user=request.user).order_by("-created_at")
|
||||
serializer = UserListSerializer(top_lists, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -1353,9 +1353,9 @@ def get_user_top_lists(request):
|
||||
operation_id="create_top_list",
|
||||
summary="Create a new top list",
|
||||
description="Create a new top list for the authenticated user.",
|
||||
request=TopListSerializer,
|
||||
request=UserListSerializer,
|
||||
responses={
|
||||
201: TopListSerializer,
|
||||
201: UserListSerializer,
|
||||
400: {"description": "Validation error"},
|
||||
},
|
||||
tags=["User Content"],
|
||||
@@ -1364,7 +1364,7 @@ def get_user_top_lists(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def create_top_list(request):
|
||||
"""Create a new top list."""
|
||||
serializer = TopListSerializer(data=request.data, context={"request": request})
|
||||
serializer = UserListSerializer(data=request.data, context={"request": request})
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user)
|
||||
@@ -1377,9 +1377,9 @@ def create_top_list(request):
|
||||
operation_id="update_top_list",
|
||||
summary="Update a top list",
|
||||
description="Update a top list owned by the authenticated user.",
|
||||
request=TopListSerializer,
|
||||
request=UserListSerializer,
|
||||
responses={
|
||||
200: TopListSerializer,
|
||||
200: UserListSerializer,
|
||||
400: {"description": "Validation error"},
|
||||
404: {"description": "Top list not found"},
|
||||
},
|
||||
@@ -1390,14 +1390,14 @@ def create_top_list(request):
|
||||
def update_top_list(request, list_id):
|
||||
"""Update a top list."""
|
||||
try:
|
||||
top_list = TopList.objects.get(id=list_id, user=request.user)
|
||||
except TopList.DoesNotExist:
|
||||
top_list = UserList.objects.get(id=list_id, user=request.user)
|
||||
except UserList.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Top list not found"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
serializer = TopListSerializer(
|
||||
serializer = UserListSerializer(
|
||||
top_list, data=request.data, partial=True, context={"request": request}
|
||||
)
|
||||
|
||||
@@ -1423,10 +1423,10 @@ def update_top_list(request, list_id):
|
||||
def delete_top_list(request, list_id):
|
||||
"""Delete a top list."""
|
||||
try:
|
||||
top_list = TopList.objects.get(id=list_id, user=request.user)
|
||||
top_list = UserList.objects.get(id=list_id, user=request.user)
|
||||
top_list.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except TopList.DoesNotExist:
|
||||
except UserList.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Top list not found"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
|
||||
@@ -1081,3 +1081,45 @@ class ParkImageSettingsAPIView(APIView):
|
||||
park, context={"request": request}
|
||||
)
|
||||
return Response(output_serializer.data)
|
||||
# --- Operator list ----------------------------------------------------------
|
||||
@extend_schema(
|
||||
summary="List park operators",
|
||||
description="List all companies with OPERATOR role, including park counts.",
|
||||
responses={
|
||||
200: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Parks"],
|
||||
)
|
||||
class OperatorListAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Models not available"},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED
|
||||
)
|
||||
|
||||
operators = (
|
||||
Company.objects.filter(roles__contains=["OPERATOR"])
|
||||
.annotate(park_count=Count("operated_parks"))
|
||||
.only("id", "name", "slug", "roles", "description")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
# Simple serialization
|
||||
data = [
|
||||
{
|
||||
"id": op.id,
|
||||
"name": op.name,
|
||||
"slug": op.slug,
|
||||
"description": op.description,
|
||||
"park_count": op.park_count,
|
||||
}
|
||||
for op in operators
|
||||
]
|
||||
|
||||
return Response({
|
||||
"results": data,
|
||||
"count": len(data)
|
||||
})
|
||||
|
||||
@@ -16,15 +16,24 @@ from .park_views import (
|
||||
CompanySearchAPIView,
|
||||
ParkSearchSuggestionsAPIView,
|
||||
ParkImageSettingsAPIView,
|
||||
OperatorListAPIView,
|
||||
)
|
||||
from .park_rides_views import (
|
||||
ParkRidesListAPIView,
|
||||
ParkRideDetailAPIView,
|
||||
ParkComprehensiveDetailAPIView,
|
||||
)
|
||||
from apps.parks.views import location_search, reverse_geocode
|
||||
from .views import ParkPhotoViewSet, HybridParkAPIView, ParkFilterMetadataAPIView
|
||||
from .ride_photos_views import RidePhotoViewSet
|
||||
from .ride_photos_views import RidePhotoViewSet
|
||||
from .ride_reviews_views import RideReviewViewSet
|
||||
from apps.parks.views_roadtrip import (
|
||||
CreateTripView,
|
||||
FindParksAlongRouteView,
|
||||
GeocodeAddressView,
|
||||
ParkDistanceCalculatorView,
|
||||
)
|
||||
|
||||
# Create router for nested photo endpoints
|
||||
router = DefaultRouter()
|
||||
@@ -84,4 +93,19 @@ urlpatterns = [
|
||||
|
||||
# Nested ride review endpoints - reviews for specific rides within parks
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
|
||||
# Nested ride review endpoints - reviews for specific rides within parks
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
|
||||
|
||||
# Roadtrip API endpoints
|
||||
path("roadtrip/create/", CreateTripView.as_view(), name="roadtrip-create"),
|
||||
path("roadtrip/find-along-route/", FindParksAlongRouteView.as_view(), name="roadtrip-find"),
|
||||
path("roadtrip/geocode/", GeocodeAddressView.as_view(), name="roadtrip-geocode"),
|
||||
path("roadtrip/distance/", ParkDistanceCalculatorView.as_view(), name="roadtrip-distance"),
|
||||
|
||||
# Operator endpoints
|
||||
path("operators/", OperatorListAPIView.as_view(), name="operator-list"),
|
||||
|
||||
# Location search endpoints
|
||||
path("search/location/", location_search, name="location-search"),
|
||||
path("search/reverse-geocode/", reverse_geocode, name="reverse-geocode"),
|
||||
]
|
||||
|
||||
@@ -21,6 +21,8 @@ from .views import (
|
||||
RideImageSettingsAPIView,
|
||||
HybridRideAPIView,
|
||||
RideFilterMetadataAPIView,
|
||||
ManufacturerListAPIView,
|
||||
DesignerListAPIView,
|
||||
)
|
||||
from .photo_views import RidePhotoViewSet
|
||||
|
||||
@@ -56,6 +58,10 @@ urlpatterns = [
|
||||
RideSearchSuggestionsAPIView.as_view(),
|
||||
name="ride-search-suggestions",
|
||||
),
|
||||
# Manufacturer and Designer endpoints
|
||||
path("manufacturers/", ManufacturerListAPIView.as_view(), name="manufacturer-list"),
|
||||
path("designers/", DesignerListAPIView.as_view(), name="designer-list"),
|
||||
|
||||
# Ride model management endpoints - nested under rides/manufacturers
|
||||
path(
|
||||
"manufacturers/<slug:manufacturer_slug>/",
|
||||
|
||||
@@ -2456,3 +2456,56 @@ class RideFilterMetadataAPIView(APIView):
|
||||
# Reuse the same filter extraction logic
|
||||
view = HybridRideAPIView()
|
||||
return view._extract_filters(query_params)
|
||||
# === MANUFACTURER & DESIGNER LISTS ===
|
||||
|
||||
class BaseCompanyListAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
role = None
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Models not available"},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED
|
||||
)
|
||||
|
||||
companies = (
|
||||
Company.objects.filter(roles__contains=[self.role])
|
||||
.annotate(ride_count=Count("manufactured_rides" if self.role == "MANUFACTURER" else "designed_rides"))
|
||||
.only("id", "name", "slug", "roles", "description")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
data = [
|
||||
{
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"slug": c.slug,
|
||||
"description": c.description,
|
||||
"ride_count": c.ride_count,
|
||||
}
|
||||
for c in companies
|
||||
]
|
||||
|
||||
return Response({
|
||||
"results": data,
|
||||
"count": len(data)
|
||||
})
|
||||
|
||||
@extend_schema(
|
||||
summary="List manufacturers",
|
||||
description="List all companies with MANUFACTURER role.",
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
tags=["Rides"],
|
||||
)
|
||||
class ManufacturerListAPIView(BaseCompanyListAPIView):
|
||||
role = "MANUFACTURER"
|
||||
|
||||
@extend_schema(
|
||||
summary="List designers",
|
||||
description="List all companies with DESIGNER role.",
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
tags=["Rides"],
|
||||
)
|
||||
class DesignerListAPIView(BaseCompanyListAPIView):
|
||||
role = "DESIGNER"
|
||||
|
||||
@@ -14,10 +14,10 @@ from drf_spectacular.utils import (
|
||||
from apps.accounts.models import (
|
||||
User,
|
||||
UserProfile,
|
||||
TopList,
|
||||
UserNotification,
|
||||
NotificationPreference,
|
||||
)
|
||||
from apps.lists.models import UserList
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
UserModel = get_user_model()
|
||||
@@ -85,6 +85,8 @@ class UserProfileSerializer(serializers.ModelSerializer):
|
||||
"dark_ride_credits",
|
||||
"flat_ride_credits",
|
||||
"water_ride_credits",
|
||||
"unit_system",
|
||||
"location",
|
||||
]
|
||||
read_only_fields = ["profile_id", "avatar_url", "avatar_variants"]
|
||||
|
||||
@@ -503,8 +505,8 @@ class UserStatisticsSerializer(serializers.Serializer):
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Top List Example",
|
||||
summary="User's top list",
|
||||
"User List Example",
|
||||
summary="User's list",
|
||||
description="A user's ranked list of rides or parks",
|
||||
value={
|
||||
"id": 1,
|
||||
@@ -518,13 +520,13 @@ class UserStatisticsSerializer(serializers.Serializer):
|
||||
)
|
||||
]
|
||||
)
|
||||
class TopListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for user's top lists."""
|
||||
class UserListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for user's lists."""
|
||||
|
||||
items_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = TopList
|
||||
model = UserList
|
||||
fields = [
|
||||
"id",
|
||||
"title",
|
||||
@@ -611,6 +613,8 @@ class ProfileUpdateSerializer(serializers.ModelSerializer):
|
||||
"instagram",
|
||||
"youtube",
|
||||
"discord",
|
||||
"unit_system",
|
||||
"location",
|
||||
]
|
||||
|
||||
def validate_display_name(self, value):
|
||||
|
||||
@@ -73,6 +73,7 @@ urlpatterns = [
|
||||
path("email/", include("apps.api.v1.email.urls")),
|
||||
path("core/", include("apps.api.v1.core.urls")),
|
||||
path("maps/", include("apps.api.v1.maps.urls")),
|
||||
path("lists/", include("apps.lists.urls")),
|
||||
path("moderation/", include("apps.moderation.urls")),
|
||||
# Cloudflare Images Toolkit API endpoints
|
||||
path("cloudflare-images/", include("django_cloudflareimages_toolkit.urls")),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 5.1.6 on 2025-12-26 14:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("core", "0003_pageviewevent_slughistoryevent_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="slughistory",
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
"verbose_name": "Slug History",
|
||||
"verbose_name_plural": "Slug Histories",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="slughistory",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="Type of model this slug belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="slughistory",
|
||||
name="object_id",
|
||||
field=models.CharField(
|
||||
help_text="ID of the object this slug belongs to", max_length=50
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="slughistory",
|
||||
name="old_slug",
|
||||
field=models.SlugField(help_text="Previous slug value", max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="slughistoryevent",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Type of model this slug belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="slughistoryevent",
|
||||
name="object_id",
|
||||
field=models.CharField(
|
||||
help_text="ID of the object this slug belongs to", max_length=50
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="slughistoryevent",
|
||||
name="old_slug",
|
||||
field=models.SlugField(
|
||||
db_index=False, help_text="Previous slug value", max_length=200
|
||||
),
|
||||
),
|
||||
]
|
||||
18
backend/apps/core/permissions.py
Normal file
18
backend/apps/core/permissions.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
class IsOwnerOrReadOnly(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission to only allow owners of an object to edit it.
|
||||
"""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# Read permissions are allowed to any request,
|
||||
# so we'll always allow GET, HEAD or OPTIONS requests.
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
# Write permissions are only allowed to the owner of the object.
|
||||
# Assumes the model instance has an `user` attribute.
|
||||
if hasattr(obj, 'user'):
|
||||
return obj.user == request.user
|
||||
return False
|
||||
0
backend/apps/lists/__init__.py
Normal file
0
backend/apps/lists/__init__.py
Normal file
90
backend/apps/lists/admin.py
Normal file
90
backend/apps/lists/admin.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from django.contrib import admin
|
||||
from django.db.models import Count
|
||||
from django.utils.html import format_html
|
||||
from apps.core.admin import (
|
||||
BaseModelAdmin,
|
||||
ExportActionMixin,
|
||||
QueryOptimizationMixin,
|
||||
TimestampFieldsMixin,
|
||||
)
|
||||
from .models import UserList, ListItem
|
||||
|
||||
|
||||
class ListItemInline(admin.TabularInline):
|
||||
"""Inline admin for ListItem within UserList admin."""
|
||||
model = ListItem
|
||||
extra = 1
|
||||
fields = ("content_type", "object_id", "rank", "notes")
|
||||
ordering = ("rank",)
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(UserList)
|
||||
class UserListAdmin(QueryOptimizationMixin, ExportActionMixin, TimestampFieldsMixin, BaseModelAdmin):
|
||||
"""Admin interface for UserList."""
|
||||
list_display = (
|
||||
"title",
|
||||
"user_link",
|
||||
"category",
|
||||
"is_public",
|
||||
"item_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
list_filter = ("category", "is_public", "created_at", "updated_at")
|
||||
list_select_related = ["user"]
|
||||
list_prefetch_related = ["items"]
|
||||
search_fields = ("title", "user__username", "description")
|
||||
autocomplete_fields = ["user"]
|
||||
inlines = [ListItemInline]
|
||||
|
||||
export_fields = ["id", "title", "user", "category", "is_public", "created_at", "updated_at"]
|
||||
export_filename_prefix = "user_lists"
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
"Basic Information",
|
||||
{
|
||||
"fields": ("user", "title", "category", "description", "is_public"),
|
||||
"description": "List identification and categorization.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"Timestamps",
|
||||
{
|
||||
"fields": ("created_at", "updated_at"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
|
||||
@admin.display(description="User")
|
||||
def user_link(self, obj):
|
||||
if obj.user:
|
||||
from django.urls import reverse
|
||||
url = reverse("admin:accounts_customuser_change", args=[obj.user.pk])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.user.username)
|
||||
return "-"
|
||||
|
||||
@admin.display(description="Items")
|
||||
def item_count(self, obj):
|
||||
return obj.items.count()
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
qs = qs.annotate(_item_count=Count("items", distinct=True))
|
||||
return qs
|
||||
|
||||
|
||||
@admin.register(ListItem)
|
||||
class ListItemAdmin(QueryOptimizationMixin, BaseModelAdmin):
|
||||
"""Admin interface for ListItem."""
|
||||
list_display = (
|
||||
"user_list",
|
||||
"content_type",
|
||||
"object_id",
|
||||
"rank",
|
||||
)
|
||||
list_filter = ("user_list__category", "content_type", "rank")
|
||||
list_select_related = ["user_list", "user_list__user", "content_type"]
|
||||
5
backend/apps/lists/apps.py
Normal file
5
backend/apps/lists/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class ListsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.lists"
|
||||
284
backend/apps/lists/migrations/0001_initial.py
Normal file
284
backend/apps/lists/migrations/0001_initial.py
Normal file
@@ -0,0 +1,284 @@
|
||||
# Generated by Django 5.1.6 on 2025-12-26 14:13
|
||||
|
||||
import apps.core.choices.fields
|
||||
import django.db.models.deletion
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("pghistory", "0006_delete_aggregateevent"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ListItem",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("object_id", models.PositiveIntegerField(help_text="ID of the item")),
|
||||
("rank", models.PositiveIntegerField(help_text="Position in the list")),
|
||||
("notes", models.TextField(blank=True, help_text="User's notes about this item")),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
help_text="Type of item (park, ride, etc.)",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "List Item",
|
||||
"verbose_name_plural": "List Items",
|
||||
"ordering": ["rank"],
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserList",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("title", models.CharField(help_text="Title of the list", max_length=100)),
|
||||
(
|
||||
"category",
|
||||
apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="top_list_categories",
|
||||
choices=[
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("PK", "Park"),
|
||||
],
|
||||
domain="accounts",
|
||||
help_text="Category of items in this list",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
("description", models.TextField(blank=True, help_text="Description of the list")),
|
||||
("is_public", models.BooleanField(default=True, help_text="Whether this list is visible to others")),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
help_text="User who created this list",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="user_lists",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User List",
|
||||
"verbose_name_plural": "User Lists",
|
||||
"ordering": ["-updated_at"],
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ListItemEvent",
|
||||
fields=[
|
||||
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("pgh_label", models.TextField(help_text="The event label.")),
|
||||
("id", models.BigIntegerField()),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("object_id", models.PositiveIntegerField(help_text="ID of the item")),
|
||||
("rank", models.PositiveIntegerField(help_text="Position in the list")),
|
||||
("notes", models.TextField(blank=True, help_text="User's notes about this item")),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Type of item (park, ride, etc.)",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
(
|
||||
"pgh_context",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
(
|
||||
"pgh_obj",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="lists.listitem",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_list",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="List this item belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="lists.userlist",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="listitem",
|
||||
name="user_list",
|
||||
field=models.ForeignKey(
|
||||
help_text="List this item belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="items",
|
||||
to="lists.userlist",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserListEvent",
|
||||
fields=[
|
||||
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("pgh_label", models.TextField(help_text="The event label.")),
|
||||
("id", models.BigIntegerField()),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("title", models.CharField(help_text="Title of the list", max_length=100)),
|
||||
(
|
||||
"category",
|
||||
apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="top_list_categories",
|
||||
choices=[
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("PK", "Park"),
|
||||
],
|
||||
domain="accounts",
|
||||
help_text="Category of items in this list",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
("description", models.TextField(blank=True, help_text="Description of the list")),
|
||||
("is_public", models.BooleanField(default=True, help_text="Whether this list is visible to others")),
|
||||
(
|
||||
"pgh_context",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
(
|
||||
"pgh_obj",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="lists.userlist",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User who created this list",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="userlist",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "lists_userlistevent" ("category", "created_at", "description", "id", "is_public", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "title", "updated_at", "user_id") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."id", NEW."is_public", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."title", NEW."updated_at", NEW."user_id"); RETURN NULL;',
|
||||
hash="702082b0a9ed526aa1bffbec0839e9a2d7641f42",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_7a128",
|
||||
table="lists_userlist",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="userlist",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "lists_userlistevent" ("category", "created_at", "description", "id", "is_public", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "title", "updated_at", "user_id") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."id", NEW."is_public", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."title", NEW."updated_at", NEW."user_id"); RETURN NULL;',
|
||||
hash="843e25a795f48bb1dfbb3c5723598823a71e0da8",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_1d718",
|
||||
table="lists_userlist",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="listitem",
|
||||
unique_together={("user_list", "rank")},
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="listitem",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "lists_listitemevent" ("content_type_id", "created_at", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "updated_at", "user_list_id") VALUES (NEW."content_type_id", NEW."created_at", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rank", NEW."updated_at", NEW."user_list_id"); RETURN NULL;',
|
||||
hash="09893103c0995cb295cdf83421583a93266593bb",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_bb169",
|
||||
table="lists_listitem",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="listitem",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "lists_listitemevent" ("content_type_id", "created_at", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "updated_at", "user_list_id") VALUES (NEW."content_type_id", NEW."created_at", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rank", NEW."updated_at", NEW."user_list_id"); RETURN NULL;',
|
||||
hash="5617f50c7404a18a24f08bd237aecd466b496339",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_2b5a0",
|
||||
table="lists_listitem",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
0
backend/apps/lists/migrations/__init__.py
Normal file
0
backend/apps/lists/migrations/__init__.py
Normal file
61
backend/apps/lists/models.py
Normal file
61
backend/apps/lists/models.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from apps.core.history import TrackedModel
|
||||
from apps.core.choices import RichChoiceField
|
||||
import pghistory
|
||||
|
||||
@pghistory.track()
|
||||
class UserList(TrackedModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_lists",
|
||||
help_text="User who created this list",
|
||||
)
|
||||
title = models.CharField(max_length=100, help_text="Title of the list")
|
||||
category = RichChoiceField(
|
||||
choice_group="top_list_categories",
|
||||
domain="accounts",
|
||||
max_length=2,
|
||||
help_text="Category of items in this list",
|
||||
)
|
||||
description = models.TextField(blank=True, help_text="Description of the list")
|
||||
is_public = models.BooleanField(default=True, help_text="Whether this list is visible to others")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "User List"
|
||||
verbose_name_plural = "User Lists"
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}'s {self.category} List: {self.title}"
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class ListItem(TrackedModel):
|
||||
user_list = models.ForeignKey(
|
||||
UserList,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="items",
|
||||
help_text="List this item belongs to",
|
||||
)
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="Type of item (park, ride, etc.)",
|
||||
)
|
||||
object_id = models.PositiveIntegerField(help_text="ID of the item")
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
rank = models.PositiveIntegerField(help_text="Position in the list")
|
||||
notes = models.TextField(blank=True, help_text="User's notes about this item")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "List Item"
|
||||
verbose_name_plural = "List Items"
|
||||
ordering = ["rank"]
|
||||
unique_together = [["user_list", "rank"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"#{self.rank} in {self.user_list.title}"
|
||||
57
backend/apps/lists/serializers.py
Normal file
57
backend/apps/lists/serializers.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from rest_framework import serializers
|
||||
from .models import UserList, ListItem
|
||||
from apps.accounts.serializers import UserSerializer
|
||||
|
||||
class ListItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ListItem
|
||||
fields = [
|
||||
"id",
|
||||
"user_list",
|
||||
"content_type",
|
||||
"object_id",
|
||||
"rank",
|
||||
"notes",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"content_object_data",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
content_object_data = serializers.SerializerMethodField()
|
||||
|
||||
def get_content_object_data(self, obj):
|
||||
"""
|
||||
Return serialized data for the content object (Park or Ride).
|
||||
"""
|
||||
# Avoid circular imports
|
||||
from apps.api.v1.parks.serializers import ParkListSerializer
|
||||
from apps.api.v1.rides.serializers import RideListSerializer
|
||||
from apps.parks.models import Park
|
||||
from apps.rides.models import Ride
|
||||
|
||||
if isinstance(obj.content_object, Park):
|
||||
return ParkListSerializer(obj.content_object, context=self.context).data
|
||||
elif isinstance(obj.content_object, Ride):
|
||||
return RideListSerializer(obj.content_object, context=self.context).data
|
||||
return None
|
||||
|
||||
|
||||
class UserListSerializer(serializers.ModelSerializer):
|
||||
user = UserSerializer(read_only=True)
|
||||
items = ListItemSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = UserList
|
||||
fields = [
|
||||
"id",
|
||||
"user",
|
||||
"title",
|
||||
"category",
|
||||
"description",
|
||||
"is_public",
|
||||
"items",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "user", "created_at", "updated_at"]
|
||||
11
backend/apps/lists/urls.py
Normal file
11
backend/apps/lists/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import UserListViewSet, ListItemViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"lists", UserListViewSet, basename="list")
|
||||
router.register(r"list-items", ListItemViewSet, basename="list-item")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
28
backend/apps/lists/views.py
Normal file
28
backend/apps/lists/views.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.db.models import Q
|
||||
from rest_framework import viewsets, permissions
|
||||
from .models import UserList, ListItem
|
||||
from .serializers import UserListSerializer, ListItemSerializer
|
||||
from apps.core.permissions import IsOwnerOrReadOnly
|
||||
|
||||
class UserListViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserListSerializer
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
|
||||
lookup_field = "id"
|
||||
|
||||
def get_queryset(self):
|
||||
# Users can see their own lists and public lists
|
||||
if self.request.user.is_authenticated:
|
||||
return UserList.objects.filter(Q(is_public=True) | Q(user=self.request.user))
|
||||
return UserList.objects.filter(is_public=True)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class ListItemViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ListItemSerializer
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
|
||||
lookup_field = "id"
|
||||
|
||||
def get_queryset(self):
|
||||
return ListItem.objects.filter(user_list__is_public=True) | ListItem.objects.filter(user_list__user=self.request.user)
|
||||
@@ -0,0 +1,750 @@
|
||||
# Generated by Django 5.1.6 on 2025-12-26 14:10
|
||||
|
||||
import apps.core.state_machine.fields
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("moderation", "0007_convert_status_to_richfsmfield"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="bulkoperation",
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
"verbose_name": "Bulk Operation",
|
||||
"verbose_name_plural": "Bulk Operations",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="editsubmission",
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
"verbose_name": "Edit Submission",
|
||||
"verbose_name_plural": "Edit Submissions",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="moderationaction",
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
"verbose_name": "Moderation Action",
|
||||
"verbose_name_plural": "Moderation Actions",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="moderationqueue",
|
||||
options={
|
||||
"ordering": ["priority", "created_at"],
|
||||
"verbose_name": "Moderation Queue Item",
|
||||
"verbose_name_plural": "Moderation Queue Items",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="moderationreport",
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
"verbose_name": "Moderation Report",
|
||||
"verbose_name_plural": "Moderation Reports",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="photosubmission",
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
"verbose_name": "Photo Submission",
|
||||
"verbose_name_plural": "Photo Submissions",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperation",
|
||||
name="completed_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this operation completed", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperation",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
help_text="User who created this operation",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="bulk_operations_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperation",
|
||||
name="started_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this operation started", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperation",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this operation was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperationevent",
|
||||
name="completed_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this operation completed", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperationevent",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User who created this operation",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperationevent",
|
||||
name="started_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this operation started", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperationevent",
|
||||
name="status",
|
||||
field=apps.core.state_machine.fields.RichFSMField(
|
||||
allow_deprecated=False,
|
||||
choice_group="bulk_operation_statuses",
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("RUNNING", "Running"),
|
||||
("COMPLETED", "Completed"),
|
||||
("FAILED", "Failed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
],
|
||||
default="PENDING",
|
||||
domain="moderation",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="bulkoperationevent",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this operation was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmission",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="Type of object being edited",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmission",
|
||||
name="handled_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this submission was handled", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmission",
|
||||
name="handled_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Moderator who handled this submission",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="handled_submissions",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmission",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="ID of object being edited (null for new objects)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmission",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
help_text="User who submitted this edit",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="edit_submissions",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmissionevent",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Type of object being edited",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmissionevent",
|
||||
name="handled_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this submission was handled", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmissionevent",
|
||||
name="handled_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="Moderator who handled this submission",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmissionevent",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="ID of object being edited (null for new objects)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmissionevent",
|
||||
name="status",
|
||||
field=apps.core.state_machine.fields.RichFSMField(
|
||||
allow_deprecated=False,
|
||||
choice_group="edit_submission_statuses",
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("APPROVED", "Approved"),
|
||||
("REJECTED", "Rejected"),
|
||||
("ESCALATED", "Escalated"),
|
||||
],
|
||||
default="PENDING",
|
||||
domain="moderation",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="editsubmissionevent",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User who submitted this edit",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationaction",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this action was created"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationaction",
|
||||
name="moderator",
|
||||
field=models.ForeignKey(
|
||||
help_text="Moderator who took this action",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="moderation_actions_taken",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationaction",
|
||||
name="related_report",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related moderation report",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="actions_taken",
|
||||
to="moderation.moderationreport",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationaction",
|
||||
name="target_user",
|
||||
field=models.ForeignKey(
|
||||
help_text="User this action was taken against",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="moderation_actions_received",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationaction",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this action was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationactionevent",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this action was created"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationactionevent",
|
||||
name="moderator",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Moderator who took this action",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationactionevent",
|
||||
name="related_report",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="Related moderation report",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="moderation.moderationreport",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationactionevent",
|
||||
name="target_user",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User this action was taken against",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationactionevent",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this action was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueue",
|
||||
name="assigned_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this item was assigned", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueue",
|
||||
name="assigned_to",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Moderator assigned to this item",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="assigned_queue_items",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueue",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this item was created"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueue",
|
||||
name="flagged_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="User who flagged this item",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="flagged_queue_items",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueue",
|
||||
name="related_report",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Related moderation report",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="queue_items",
|
||||
to="moderation.moderationreport",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueue",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this item was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueueevent",
|
||||
name="assigned_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this item was assigned", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueueevent",
|
||||
name="assigned_to",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="Moderator assigned to this item",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueueevent",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this item was created"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueueevent",
|
||||
name="flagged_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="User who flagged this item",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueueevent",
|
||||
name="related_report",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="Related moderation report",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="moderation.moderationreport",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueueevent",
|
||||
name="status",
|
||||
field=apps.core.state_machine.fields.RichFSMField(
|
||||
allow_deprecated=False,
|
||||
choice_group="moderation_queue_statuses",
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("IN_PROGRESS", "In Progress"),
|
||||
("COMPLETED", "Completed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
],
|
||||
default="PENDING",
|
||||
domain="moderation",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationqueueevent",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this item was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreport",
|
||||
name="assigned_moderator",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Moderator assigned to handle this report",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="assigned_moderation_reports",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreport",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this report was created"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreport",
|
||||
name="reported_by",
|
||||
field=models.ForeignKey(
|
||||
help_text="User who made this report",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="moderation_reports_made",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreport",
|
||||
name="resolved_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this report was resolved", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreport",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this report was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreportevent",
|
||||
name="assigned_moderator",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="Moderator assigned to handle this report",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreportevent",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this report was created"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreportevent",
|
||||
name="reported_by",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User who made this report",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreportevent",
|
||||
name="resolved_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this report was resolved", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreportevent",
|
||||
name="status",
|
||||
field=apps.core.state_machine.fields.RichFSMField(
|
||||
allow_deprecated=False,
|
||||
choice_group="moderation_report_statuses",
|
||||
choices=[
|
||||
("PENDING", "Pending Review"),
|
||||
("UNDER_REVIEW", "Under Review"),
|
||||
("RESOLVED", "Resolved"),
|
||||
("DISMISSED", "Dismissed"),
|
||||
],
|
||||
default="PENDING",
|
||||
domain="moderation",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="moderationreportevent",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(
|
||||
auto_now=True, help_text="When this report was last updated"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmission",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmission",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
help_text="Type of object this photo is for",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmission",
|
||||
name="date_taken",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date the photo was taken", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmission",
|
||||
name="handled_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this submission was handled", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmission",
|
||||
name="handled_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Moderator who handled this submission",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="handled_photos",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmission",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(
|
||||
help_text="ID of object this photo is for"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmission",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
help_text="User who submitted this photo",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="photo_submissions",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Type of object this photo is for",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="date_taken",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date the photo was taken", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="handled_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this submission was handled", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="handled_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="Moderator who handled this submission",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(
|
||||
help_text="ID of object this photo is for"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="status",
|
||||
field=apps.core.state_machine.fields.RichFSMField(
|
||||
allow_deprecated=False,
|
||||
choice_group="photo_submission_statuses",
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("APPROVED", "Approved"),
|
||||
("REJECTED", "Rejected"),
|
||||
("ESCALATED", "Escalated"),
|
||||
],
|
||||
default="PENDING",
|
||||
domain="moderation",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="photosubmissionevent",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User who submitted this photo",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,760 @@
|
||||
# Generated by Django 5.1.6 on 2025-12-26 14:10
|
||||
|
||||
import apps.core.choices.fields
|
||||
import apps.core.state_machine.fields
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0024_add_timezone_default"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="company",
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
"verbose_name": "Company",
|
||||
"verbose_name_plural": "Companies",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="park",
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
"verbose_name": "Park",
|
||||
"verbose_name_plural": "Parks",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="parkarea",
|
||||
options={
|
||||
"ordering": ["park", "name"],
|
||||
"verbose_name": "Park Area",
|
||||
"verbose_name_plural": "Park Areas",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="parkphoto",
|
||||
options={
|
||||
"ordering": ["-is_primary", "-created_at"],
|
||||
"verbose_name": "Park Photo",
|
||||
"verbose_name_plural": "Park Photos",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="parkreview",
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
"verbose_name": "Park Review",
|
||||
"verbose_name_plural": "Park Reviews",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed company description"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="founded_year",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Year the company was founded", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Company name", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="parks_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of parks operated (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="rides_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of rides manufactured (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="company_roles",
|
||||
choices=[
|
||||
("OPERATOR", "Park Operator"),
|
||||
("PROPERTY_OWNER", "Property Owner"),
|
||||
],
|
||||
domain="parks",
|
||||
max_length=20,
|
||||
),
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Company roles (operator, manufacturer, etc.)",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="URL-friendly identifier", max_length=255, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="website",
|
||||
field=models.URLField(blank=True, help_text="Company website URL"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed company description"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="founded_year",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Year the company was founded", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Company name", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="parks_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of parks operated (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="rides_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of rides manufactured (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="company_roles",
|
||||
choices=[
|
||||
("OPERATOR", "Park Operator"),
|
||||
("PROPERTY_OWNER", "Property Owner"),
|
||||
],
|
||||
domain="parks",
|
||||
max_length=20,
|
||||
),
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Company roles (operator, manufacturer, etc.)",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
db_index=False, help_text="URL-friendly identifier", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="website",
|
||||
field=models.URLField(blank=True, help_text="Company website URL"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyheadquarters",
|
||||
name="company",
|
||||
field=models.OneToOneField(
|
||||
help_text="Company this headquarters belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="headquarters",
|
||||
to="parks.company",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyheadquartersevent",
|
||||
name="company",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Company this headquarters belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="parks.company",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="average_rating",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Average user rating (1–10)",
|
||||
max_digits=3,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="closing_date",
|
||||
field=models.DateField(blank=True, help_text="Closing date", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="coaster_count",
|
||||
field=models.IntegerField(
|
||||
blank=True, help_text="Total coaster count", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="description",
|
||||
field=models.TextField(blank=True, help_text="Park description"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Park name", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="opening_date",
|
||||
field=models.DateField(blank=True, help_text="Opening date", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="operating_season",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Operating season", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="ride_count",
|
||||
field=models.IntegerField(
|
||||
blank=True, help_text="Total ride count", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="size_acres",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Park size in acres",
|
||||
max_digits=10,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="URL-friendly identifier", max_length=255, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="status",
|
||||
field=apps.core.state_machine.fields.RichFSMField(
|
||||
allow_deprecated=False,
|
||||
choice_group="statuses",
|
||||
choices=[],
|
||||
default="OPERATING",
|
||||
domain="parks",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="park",
|
||||
name="website",
|
||||
field=models.URLField(blank=True, help_text="Official website URL"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkarea",
|
||||
name="closing_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date this area closed (if applicable)", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkarea",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed description of the area"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkarea",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Name of the park area", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkarea",
|
||||
name="opening_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date this area opened", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkarea",
|
||||
name="park",
|
||||
field=models.ForeignKey(
|
||||
help_text="Park this area belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="areas",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkarea",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="URL-friendly identifier (unique within park)", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkareaevent",
|
||||
name="closing_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date this area closed (if applicable)", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkareaevent",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed description of the area"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkareaevent",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Name of the park area", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkareaevent",
|
||||
name="opening_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date this area opened", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkareaevent",
|
||||
name="park",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Park this area belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkareaevent",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
db_index=False,
|
||||
help_text="URL-friendly identifier (unique within park)",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="average_rating",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Average user rating (1–10)",
|
||||
max_digits=3,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="closing_date",
|
||||
field=models.DateField(blank=True, help_text="Closing date", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="coaster_count",
|
||||
field=models.IntegerField(
|
||||
blank=True, help_text="Total coaster count", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="description",
|
||||
field=models.TextField(blank=True, help_text="Park description"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Park name", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="opening_date",
|
||||
field=models.DateField(blank=True, help_text="Opening date", null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="operating_season",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Operating season", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="ride_count",
|
||||
field=models.IntegerField(
|
||||
blank=True, help_text="Total ride count", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="size_acres",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Park size in acres",
|
||||
max_digits=10,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
db_index=False, help_text="URL-friendly identifier", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="status",
|
||||
field=apps.core.state_machine.fields.RichFSMField(
|
||||
allow_deprecated=False,
|
||||
choice_group="statuses",
|
||||
choices=[],
|
||||
default="OPERATING",
|
||||
domain="parks",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="timezone",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default="UTC",
|
||||
help_text="Timezone identifier for park operations (e.g., 'America/New_York')",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkevent",
|
||||
name="website",
|
||||
field=models.URLField(blank=True, help_text="Official website URL"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphoto",
|
||||
name="alt_text",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Alternative text for accessibility",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphoto",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption or description", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphoto",
|
||||
name="is_approved",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether this photo has been approved by moderators",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphoto",
|
||||
name="is_primary",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether this is the primary photo for the park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphoto",
|
||||
name="park",
|
||||
field=models.ForeignKey(
|
||||
help_text="Park this photo belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="photos",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphoto",
|
||||
name="uploaded_by",
|
||||
field=models.ForeignKey(
|
||||
help_text="User who uploaded this photo",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="uploaded_park_photos",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphotoevent",
|
||||
name="alt_text",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Alternative text for accessibility",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphotoevent",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption or description", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphotoevent",
|
||||
name="is_approved",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether this photo has been approved by moderators",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphotoevent",
|
||||
name="is_primary",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether this is the primary photo for the park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphotoevent",
|
||||
name="park",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Park this photo belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkphotoevent",
|
||||
name="uploaded_by",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User who uploaded this photo",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="content",
|
||||
field=models.TextField(help_text="Review content"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="is_published",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether this review is publicly visible"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="moderated_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this review was moderated", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="moderated_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Moderator who reviewed this",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="moderated_park_reviews",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="moderation_notes",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Internal notes from moderators"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="park",
|
||||
field=models.ForeignKey(
|
||||
help_text="Park being reviewed",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="reviews",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="rating",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
help_text="Rating from 1-10",
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(10),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="title",
|
||||
field=models.CharField(help_text="Review title", max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
help_text="User who wrote the review",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="park_reviews",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreview",
|
||||
name="visit_date",
|
||||
field=models.DateField(help_text="Date the user visited the park"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="content",
|
||||
field=models.TextField(help_text="Review content"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="is_published",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="Whether this review is publicly visible"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="moderated_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, help_text="When this review was moderated", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="moderated_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="Moderator who reviewed this",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="moderation_notes",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Internal notes from moderators"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="park",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Park being reviewed",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="rating",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
help_text="Rating from 1-10",
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(10),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="title",
|
||||
field=models.CharField(help_text="Review title", max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="User who wrote the review",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="parkreviewevent",
|
||||
name="visit_date",
|
||||
field=models.DateField(help_text="Date the user visited the park"),
|
||||
),
|
||||
]
|
||||
@@ -274,6 +274,18 @@ class FindParksAlongRouteView(RoadTripViewMixin, View):
|
||||
start_park, end_park, max_detour_km
|
||||
)
|
||||
|
||||
# Return JSON if requested
|
||||
if request.headers.get("Accept") == "application/json" or request.content_type == "application/json":
|
||||
return JsonResponse({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"parks": [self._park_to_dict(p) for p in parks_along_route],
|
||||
"start_park": self._park_to_dict(start_park),
|
||||
"end_park": self._park_to_dict(end_park),
|
||||
"count": len(parks_along_route)
|
||||
}
|
||||
})
|
||||
|
||||
return render(
|
||||
request,
|
||||
PARKS_ALONG_ROUTE_HTML,
|
||||
|
||||
0
backend/apps/reviews/__init__.py
Normal file
0
backend/apps/reviews/__init__.py
Normal file
6
backend/apps/reviews/apps.py
Normal file
6
backend/apps/reviews/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class ReviewsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.reviews"
|
||||
verbose_name = "User Reviews"
|
||||
56
backend/apps/reviews/models.py
Normal file
56
backend/apps/reviews/models.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from apps.core.history import TrackedModel
|
||||
import pghistory
|
||||
|
||||
@pghistory.track()
|
||||
class Review(TrackedModel):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="reviews",
|
||||
help_text="User who wrote the review",
|
||||
)
|
||||
|
||||
# Generic relation to target object (Park, Ride, etc.)
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="Type of item being reviewed",
|
||||
)
|
||||
object_id = models.PositiveIntegerField(help_text="ID of the item being reviewed")
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
# Review content
|
||||
rating = models.PositiveSmallIntegerField(
|
||||
choices=[(i, str(i)) for i in range(1, 6)],
|
||||
help_text="Rating from 1 to 5",
|
||||
db_index=True,
|
||||
)
|
||||
text = models.TextField(blank=True, help_text="Review text (optional)")
|
||||
|
||||
# Metadata
|
||||
is_public = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Whether this review is visible to others"
|
||||
)
|
||||
helpful_votes = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text="Number of users who found this helpful"
|
||||
)
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Review"
|
||||
verbose_name_plural = "Reviews"
|
||||
ordering = ["-created_at"]
|
||||
# Ensure one review per user per object
|
||||
unique_together = [["user", "content_type", "object_id"]]
|
||||
indexes = [
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
models.Index(fields=["rating"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}'s {self.rating}-star review on {self.content_object}"
|
||||
@@ -0,0 +1,945 @@
|
||||
# Generated by Django 5.1.6 on 2025-12-26 14:10
|
||||
|
||||
import apps.core.choices.fields
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pghistory", "0006_delete_aggregateevent"),
|
||||
("rides", "0026_convert_unique_together_to_constraints"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="company",
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
"verbose_name": "Company",
|
||||
"verbose_name_plural": "Companies",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rankingsnapshot",
|
||||
options={
|
||||
"ordering": ["-snapshot_date", "rank"],
|
||||
"verbose_name": "Ranking Snapshot",
|
||||
"verbose_name_plural": "Ranking Snapshots",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="ride",
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
"verbose_name": "Ride",
|
||||
"verbose_name_plural": "Rides",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="ridemodel",
|
||||
options={
|
||||
"ordering": ["manufacturer__name", "name"],
|
||||
"verbose_name": "Ride Model",
|
||||
"verbose_name_plural": "Ride Models",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="ridemodelphoto",
|
||||
options={
|
||||
"ordering": ["-is_primary", "-created_at"],
|
||||
"verbose_name": "Ride Model Photo",
|
||||
"verbose_name_plural": "Ride Model Photos",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="ridemodeltechnicalspec",
|
||||
options={
|
||||
"ordering": ["spec_category", "spec_name"],
|
||||
"verbose_name": "Ride Model Technical Specification",
|
||||
"verbose_name_plural": "Ride Model Technical Specifications",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="ridemodelvariant",
|
||||
options={
|
||||
"ordering": ["ride_model", "name"],
|
||||
"verbose_name": "Ride Model Variant",
|
||||
"verbose_name_plural": "Ride Model Variants",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="ridepaircomparison",
|
||||
options={
|
||||
"ordering": ["ride_a", "ride_b"],
|
||||
"verbose_name": "Ride Pair Comparison",
|
||||
"verbose_name_plural": "Ride Pair Comparisons",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rideranking",
|
||||
options={
|
||||
"ordering": ["rank"],
|
||||
"verbose_name": "Ride Ranking",
|
||||
"verbose_name_plural": "Ride Rankings",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="rollercoasterstats",
|
||||
options={
|
||||
"ordering": ["ride"],
|
||||
"verbose_name": "Roller Coaster Statistics",
|
||||
"verbose_name_plural": "Roller Coaster Statistics",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="coasters_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of coasters manufactured (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed company description"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="founded_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date the company was founded", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Company name", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="rides_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of rides manufactured (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="company_roles",
|
||||
choices=[
|
||||
("MANUFACTURER", "Ride Manufacturer"),
|
||||
("DESIGNER", "Ride Designer"),
|
||||
],
|
||||
domain="rides",
|
||||
max_length=20,
|
||||
),
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Company roles (manufacturer, designer, etc.)",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="URL-friendly identifier", max_length=255, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="company",
|
||||
name="website",
|
||||
field=models.URLField(blank=True, help_text="Company website URL"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="coasters_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of coasters manufactured (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Detailed company description"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="founded_date",
|
||||
field=models.DateField(
|
||||
blank=True, help_text="Date the company was founded", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="name",
|
||||
field=models.CharField(help_text="Company name", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.company",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="rides_count",
|
||||
field=models.IntegerField(
|
||||
default=0, help_text="Number of rides manufactured (auto-calculated)"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="company_roles",
|
||||
choices=[
|
||||
("MANUFACTURER", "Ride Manufacturer"),
|
||||
("DESIGNER", "Ride Designer"),
|
||||
],
|
||||
domain="rides",
|
||||
max_length=20,
|
||||
),
|
||||
blank=True,
|
||||
default=list,
|
||||
help_text="Company roles (manufacturer, designer, etc.)",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
db_index=False, help_text="URL-friendly identifier", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="companyevent",
|
||||
name="website",
|
||||
field=models.URLField(blank=True, help_text="Company website URL"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rideevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rideevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ride",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridelocationevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridelocationevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridelocation",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="alt_text",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Alternative text for accessibility",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption or description", max_length=500
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="copyright_info",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Copyright information", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="photographer",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Name of the photographer", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
help_text="Ride model this photo belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="photos",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphoto",
|
||||
name="source",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Source of the photo", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="alt_text",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Alternative text for accessibility",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="caption",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Photo caption or description", max_length=500
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="copyright_info",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Copyright information", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridemodelphoto",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="photographer",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Name of the photographer", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Ride model this photo belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelphotoevent",
|
||||
name="source",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Source of the photo", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodeltechnicalspec",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
help_text="Ride model this specification belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="technical_specs",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodeltechnicalspecevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodeltechnicalspecevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridemodeltechnicalspec",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodeltechnicalspecevent",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Ride model this specification belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariant",
|
||||
name="max_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum height for this variant",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariant",
|
||||
name="max_speed_mph",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum speed for this variant",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariant",
|
||||
name="min_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Minimum height for this variant",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariant",
|
||||
name="min_speed_mph",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Minimum speed for this variant",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariant",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
help_text="Base ride model this variant belongs to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="variants",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariantevent",
|
||||
name="max_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum height for this variant",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariantevent",
|
||||
name="max_speed_mph",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum speed for this variant",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariantevent",
|
||||
name="min_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Minimum height for this variant",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariantevent",
|
||||
name="min_speed_mph",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Minimum speed for this variant",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariantevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariantevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridemodelvariant",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridemodelvariantevent",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Base ride model this variant belongs to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridepaircomparisonevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridepaircomparisonevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridepaircomparison",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridephotoevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridephotoevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridephoto",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rideranking",
|
||||
name="ride",
|
||||
field=models.OneToOneField(
|
||||
help_text="Ride this ranking entry describes",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="ranking",
|
||||
to="rides.ride",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="riderankingevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="riderankingevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.rideranking",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="riderankingevent",
|
||||
name="ride",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Ride this ranking entry describes",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="rides.ride",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridereviewevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ridereviewevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.ridereview",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="cars_per_train",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of cars per train", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="inversions",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0, help_text="Number of inversions"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="length_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Track length in feet",
|
||||
max_digits=7,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="max_drop_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum drop height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="ride",
|
||||
field=models.OneToOneField(
|
||||
help_text="Ride these statistics belong to",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="coaster_stats",
|
||||
to="rides.ride",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="ride_time_seconds",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Duration of the ride in seconds", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="seats_per_car",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of seats per car", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="speed_mph",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum speed in mph",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="track_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Type of track (e.g., tubular steel, wooden)",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="train_style",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Style of train (e.g., floorless, inverted)",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="trains_count",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of trains", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="cars_per_train",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of cars per train", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="inversions",
|
||||
field=models.PositiveIntegerField(
|
||||
default=0, help_text="Number of inversions"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="length_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Track length in feet",
|
||||
max_digits=7,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="max_drop_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum drop height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="rides.rollercoasterstats",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="ride",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
help_text="Ride these statistics belong to",
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="rides.ride",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="ride_time_seconds",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Duration of the ride in seconds", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="seats_per_car",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of seats per car", null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="speed_mph",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum speed in mph",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="track_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Type of track (e.g., tubular steel, wooden)",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="train_style",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Style of train (e.g., floorless, inverted)",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstatsevent",
|
||||
name="trains_count",
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True, help_text="Number of trains", null=True
|
||||
),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user