mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 00:11:09 -05:00
feat: major API restructure and Vue.js frontend integration
- Centralize API endpoints in dedicated api app with v1 versioning - Remove individual API modules from parks and rides apps - Add event tracking system with analytics functionality - Integrate Vue.js frontend with Tailwind CSS v4 and TypeScript - Add comprehensive database migrations for event tracking - Implement user authentication and social provider setup - Add API schema documentation and serializers - Configure development environment with shared scripts - Update project structure for monorepo with frontend/backend separation
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.gis.admin import GISModelAdmin
|
||||
from .models import Park, ParkArea, ParkLocation, Company, CompanyHeadquarters
|
||||
from django.utils.html import format_html
|
||||
import pghistory.models
|
||||
from .models import (
|
||||
Park,
|
||||
ParkArea,
|
||||
ParkLocation,
|
||||
Company,
|
||||
CompanyHeadquarters,
|
||||
ParkReview,
|
||||
)
|
||||
|
||||
|
||||
class ParkLocationInline(admin.StackedInline):
|
||||
@@ -79,16 +88,14 @@ class ParkLocationAdmin(GISModelAdmin):
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Latitude")
|
||||
def latitude(self, obj):
|
||||
return obj.latitude
|
||||
|
||||
latitude.short_description = "Latitude"
|
||||
|
||||
@admin.display(description="Longitude")
|
||||
def longitude(self, obj):
|
||||
return obj.longitude
|
||||
|
||||
longitude.short_description = "Longitude"
|
||||
|
||||
|
||||
class ParkAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
@@ -112,12 +119,11 @@ class ParkAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {"slug": ("name",)}
|
||||
inlines = [ParkLocationInline]
|
||||
|
||||
@admin.display(description="Location")
|
||||
def formatted_location(self, obj):
|
||||
"""Display formatted location string"""
|
||||
return obj.formatted_location
|
||||
|
||||
formatted_location.short_description = "Location"
|
||||
|
||||
|
||||
class ParkAreaAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "park", "created_at", "updated_at")
|
||||
@@ -200,19 +206,193 @@ class CompanyAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {"slug": ("name",)}
|
||||
inlines = [CompanyHeadquartersInline]
|
||||
|
||||
@admin.display(description="Roles")
|
||||
def roles_display(self, obj):
|
||||
"""Display roles as a formatted string"""
|
||||
return ", ".join(obj.roles) if obj.roles else "No roles"
|
||||
|
||||
roles_display.short_description = "Roles"
|
||||
|
||||
@admin.display(description="Headquarters")
|
||||
def headquarters_location(self, obj):
|
||||
"""Display headquarters location if available"""
|
||||
if hasattr(obj, "headquarters"):
|
||||
return obj.headquarters.location_display
|
||||
return "No headquarters"
|
||||
|
||||
headquarters_location.short_description = "Headquarters"
|
||||
|
||||
@admin.register(ParkReview)
|
||||
class ParkReviewAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for park reviews"""
|
||||
|
||||
list_display = (
|
||||
"park",
|
||||
"user",
|
||||
"rating",
|
||||
"title",
|
||||
"visit_date",
|
||||
"is_published",
|
||||
"created_at",
|
||||
"moderation_status",
|
||||
)
|
||||
list_filter = (
|
||||
"rating",
|
||||
"is_published",
|
||||
"visit_date",
|
||||
"created_at",
|
||||
"park",
|
||||
"moderated_by",
|
||||
)
|
||||
search_fields = (
|
||||
"title",
|
||||
"content",
|
||||
"user__username",
|
||||
"park__name",
|
||||
)
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
date_hierarchy = "created_at"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
"Review Details",
|
||||
{
|
||||
"fields": (
|
||||
"user",
|
||||
"park",
|
||||
"rating",
|
||||
"title",
|
||||
"content",
|
||||
"visit_date",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Publication Status",
|
||||
{
|
||||
"fields": ("is_published",),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Moderation",
|
||||
{
|
||||
"fields": (
|
||||
"moderated_by",
|
||||
"moderated_at",
|
||||
"moderation_notes",
|
||||
),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Metadata",
|
||||
{
|
||||
"fields": ("created_at", "updated_at"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Moderation Status", boolean=True)
|
||||
def moderation_status(self, obj):
|
||||
"""Display moderation status with color coding"""
|
||||
if obj.moderated_by:
|
||||
return format_html(
|
||||
'<span style="color: {};">{}</span>',
|
||||
"green" if obj.is_published else "red",
|
||||
"Approved" if obj.is_published else "Rejected",
|
||||
)
|
||||
return format_html('<span style="color: orange;">Pending</span>')
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Auto-set moderation info when status changes"""
|
||||
if change and "is_published" in form.changed_data:
|
||||
from django.utils import timezone
|
||||
|
||||
obj.moderated_by = request.user
|
||||
obj.moderated_at = timezone.now()
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(pghistory.models.Events)
|
||||
class PgHistoryEventsAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for pghistory Events"""
|
||||
|
||||
list_display = (
|
||||
'pgh_id',
|
||||
'pgh_created_at',
|
||||
'pgh_label',
|
||||
'pgh_model',
|
||||
'pgh_obj_id',
|
||||
'pgh_context_display',
|
||||
)
|
||||
list_filter = (
|
||||
'pgh_label',
|
||||
'pgh_model',
|
||||
'pgh_created_at',
|
||||
)
|
||||
search_fields = (
|
||||
'pgh_obj_id',
|
||||
'pgh_context',
|
||||
)
|
||||
readonly_fields = (
|
||||
'pgh_id',
|
||||
'pgh_created_at',
|
||||
'pgh_label',
|
||||
'pgh_model',
|
||||
'pgh_obj_id',
|
||||
'pgh_context',
|
||||
'pgh_data',
|
||||
)
|
||||
date_hierarchy = 'pgh_created_at'
|
||||
ordering = ('-pgh_created_at',)
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
'Event Information',
|
||||
{
|
||||
'fields': (
|
||||
'pgh_id',
|
||||
'pgh_created_at',
|
||||
'pgh_label',
|
||||
'pgh_model',
|
||||
'pgh_obj_id',
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
'Context & Data',
|
||||
{
|
||||
'fields': (
|
||||
'pgh_context',
|
||||
'pgh_data',
|
||||
),
|
||||
'classes': ('collapse',),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="Context")
|
||||
def pgh_context_display(self, obj):
|
||||
"""Display context information in a readable format"""
|
||||
if obj.pgh_context:
|
||||
if isinstance(obj.pgh_context, dict):
|
||||
context_items = []
|
||||
for key, value in obj.pgh_context.items():
|
||||
context_items.append(f"{key}: {value}")
|
||||
return ", ".join(context_items)
|
||||
return str(obj.pgh_context)
|
||||
return "No context"
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Disable manual creation of history events"""
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Make history events read-only"""
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Prevent deletion of history events"""
|
||||
return getattr(request.user, 'is_superuser', False)
|
||||
|
||||
|
||||
# Register the models with their admin classes
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Parks API module
|
||||
@@ -1,304 +0,0 @@
|
||||
"""
|
||||
Serializers for Parks API following Django styleguide patterns.
|
||||
Separates Input and Output serializers for clear boundaries.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from ..models import Park
|
||||
|
||||
|
||||
class ParkLocationOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park location data."""
|
||||
|
||||
latitude = serializers.SerializerMethodField()
|
||||
longitude = serializers.SerializerMethodField()
|
||||
city = serializers.SerializerMethodField()
|
||||
state = serializers.SerializerMethodField()
|
||||
country = serializers.SerializerMethodField()
|
||||
formatted_address = serializers.SerializerMethodField()
|
||||
|
||||
def get_latitude(self, obj):
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.latitude
|
||||
return None
|
||||
|
||||
def get_longitude(self, obj):
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.longitude
|
||||
return None
|
||||
|
||||
def get_city(self, obj):
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.city
|
||||
return None
|
||||
|
||||
def get_state(self, obj):
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.state
|
||||
return None
|
||||
|
||||
def get_country(self, obj):
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.country
|
||||
return None
|
||||
|
||||
def get_formatted_address(self, obj):
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.formatted_address
|
||||
return ""
|
||||
|
||||
|
||||
class CompanyOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for company data."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
roles = serializers.ListField(child=serializers.CharField())
|
||||
|
||||
|
||||
class ParkAreaOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park area data."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
|
||||
class ParkListOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park list view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
# Statistics
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
coaster_count = serializers.IntegerField(allow_null=True)
|
||||
ride_count = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Location (simplified for list view)
|
||||
location = ParkLocationOutputSerializer(allow_null=True)
|
||||
|
||||
# Operator info
|
||||
operator = CompanyOutputSerializer()
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
class ParkDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park detail view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
# Details
|
||||
opening_date = serializers.DateField(allow_null=True)
|
||||
closing_date = serializers.DateField(allow_null=True)
|
||||
operating_season = serializers.CharField()
|
||||
size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, allow_null=True
|
||||
)
|
||||
website = serializers.URLField()
|
||||
|
||||
# Statistics
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
coaster_count = serializers.IntegerField(allow_null=True)
|
||||
ride_count = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Location (full details)
|
||||
location = ParkLocationOutputSerializer(allow_null=True)
|
||||
|
||||
# Companies
|
||||
operator = CompanyOutputSerializer()
|
||||
property_owner = CompanyOutputSerializer(allow_null=True)
|
||||
|
||||
# Areas
|
||||
areas = ParkAreaOutputSerializer(many=True)
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
class ParkCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating parks."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
status = serializers.ChoiceField(choices=Park.STATUS_CHOICES, default="OPERATING")
|
||||
|
||||
# Optional details
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
operating_season = serializers.CharField(
|
||||
max_length=255, required=False, allow_blank=True
|
||||
)
|
||||
size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
website = serializers.URLField(required=False, allow_blank=True)
|
||||
|
||||
# Required operator
|
||||
operator_id = serializers.IntegerField()
|
||||
|
||||
# Optional property owner
|
||||
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, data):
|
||||
"""Cross-field validation."""
|
||||
opening_date = data.get("opening_date")
|
||||
closing_date = data.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ParkUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating parks."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
status = serializers.ChoiceField(choices=Park.STATUS_CHOICES, required=False)
|
||||
|
||||
# Optional details
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
operating_season = serializers.CharField(
|
||||
max_length=255, required=False, allow_blank=True
|
||||
)
|
||||
size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
website = serializers.URLField(required=False, allow_blank=True)
|
||||
|
||||
# Companies
|
||||
operator_id = serializers.IntegerField(required=False)
|
||||
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, data):
|
||||
"""Cross-field validation."""
|
||||
opening_date = data.get("opening_date")
|
||||
closing_date = data.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ParkFilterInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for park filtering and search."""
|
||||
|
||||
# Search
|
||||
search = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Status filter
|
||||
status = serializers.MultipleChoiceField(
|
||||
choices=Park.STATUS_CHOICES, required=False
|
||||
)
|
||||
|
||||
# Location filters
|
||||
country = serializers.CharField(required=False, allow_blank=True)
|
||||
state = serializers.CharField(required=False, allow_blank=True)
|
||||
city = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Rating filter
|
||||
min_rating = serializers.DecimalField(
|
||||
max_digits=3,
|
||||
decimal_places=2,
|
||||
required=False,
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
)
|
||||
|
||||
# Size filter
|
||||
min_size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, min_value=0
|
||||
)
|
||||
max_size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, min_value=0
|
||||
)
|
||||
|
||||
# Company filters
|
||||
operator_id = serializers.IntegerField(required=False)
|
||||
property_owner_id = serializers.IntegerField(required=False)
|
||||
|
||||
# Ordering
|
||||
ordering = serializers.ChoiceField(
|
||||
choices=[
|
||||
"name",
|
||||
"-name",
|
||||
"opening_date",
|
||||
"-opening_date",
|
||||
"average_rating",
|
||||
"-average_rating",
|
||||
"coaster_count",
|
||||
"-coaster_count",
|
||||
"created_at",
|
||||
"-created_at",
|
||||
],
|
||||
required=False,
|
||||
default="name",
|
||||
)
|
||||
|
||||
|
||||
class ParkReviewOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park reviews."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
rating = serializers.IntegerField()
|
||||
title = serializers.CharField()
|
||||
content = serializers.CharField()
|
||||
visit_date = serializers.DateField()
|
||||
created_at = serializers.DateTimeField()
|
||||
|
||||
# User info (limited for privacy)
|
||||
user = serializers.SerializerMethodField()
|
||||
|
||||
def get_user(self, obj):
|
||||
return {
|
||||
"username": obj.user.username,
|
||||
"display_name": obj.user.get_full_name() or obj.user.username,
|
||||
}
|
||||
|
||||
|
||||
class ParkStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park statistics."""
|
||||
|
||||
total_parks = serializers.IntegerField()
|
||||
operating_parks = serializers.IntegerField()
|
||||
closed_parks = serializers.IntegerField()
|
||||
under_construction = serializers.IntegerField()
|
||||
|
||||
# Averages
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
average_coaster_count = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, allow_null=True
|
||||
)
|
||||
|
||||
# Top countries
|
||||
top_countries = serializers.ListField(child=serializers.DictField())
|
||||
|
||||
# Recently added
|
||||
recently_added_count = serializers.IntegerField()
|
||||
@@ -1,65 +0,0 @@
|
||||
"""
|
||||
URL configuration for Parks API following Django styleguide patterns.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import (
|
||||
ParkListApi,
|
||||
ParkDetailApi,
|
||||
ParkCreateApi,
|
||||
ParkUpdateApi,
|
||||
ParkDeleteApi,
|
||||
ParkApi,
|
||||
)
|
||||
|
||||
app_name = "parks_api"
|
||||
|
||||
# Option 1: Separate ViewSets for each operation (more explicit)
|
||||
router_separate = DefaultRouter()
|
||||
router_separate.register(r"list", ParkListApi, basename="park-list")
|
||||
router_separate.register(r"detail", ParkDetailApi, basename="park-detail")
|
||||
router_separate.register(r"create", ParkCreateApi, basename="park-create")
|
||||
router_separate.register(r"update", ParkUpdateApi, basename="park-update")
|
||||
router_separate.register(r"delete", ParkDeleteApi, basename="park-delete")
|
||||
|
||||
# Option 2: Unified ViewSet (more conventional DRF)
|
||||
router_unified = DefaultRouter()
|
||||
router_unified.register(r"parks", ParkApi, basename="park")
|
||||
|
||||
# Use unified approach for cleaner URLs
|
||||
urlpatterns = [
|
||||
path("v1/", include(router_unified.urls)),
|
||||
]
|
||||
|
||||
# Alternative manual URL patterns for more control
|
||||
urlpatterns_manual = [
|
||||
# List and create
|
||||
path(
|
||||
"v1/parks/",
|
||||
ParkApi.as_view({"get": "list", "post": "create"}),
|
||||
name="park-list",
|
||||
),
|
||||
# Stats endpoint
|
||||
path("v1/parks/stats/", ParkApi.as_view({"get": "stats"}), name="park-stats"),
|
||||
# Detail operations
|
||||
path(
|
||||
"v1/parks/<slug:slug>/",
|
||||
ParkApi.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="park-detail",
|
||||
),
|
||||
# Park reviews
|
||||
path(
|
||||
"v1/parks/<slug:slug>/reviews/",
|
||||
ParkApi.as_view({"get": "reviews"}),
|
||||
name="park-reviews",
|
||||
),
|
||||
]
|
||||
@@ -1,295 +0,0 @@
|
||||
"""
|
||||
Parks API views following Django styleguide patterns.
|
||||
Uses ClassNameApi naming convention and proper Input/Output serializers.
|
||||
"""
|
||||
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.permissions import (
|
||||
IsAuthenticated,
|
||||
IsAuthenticatedOrReadOnly,
|
||||
)
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from apps.core.api.mixins import (
|
||||
CreateApiMixin,
|
||||
UpdateApiMixin,
|
||||
ListApiMixin,
|
||||
RetrieveApiMixin,
|
||||
DestroyApiMixin,
|
||||
)
|
||||
from ..selectors import (
|
||||
park_list_with_stats,
|
||||
park_detail_optimized,
|
||||
park_reviews_for_park,
|
||||
park_statistics,
|
||||
)
|
||||
from ..services import ParkService
|
||||
from .serializers import (
|
||||
ParkListOutputSerializer,
|
||||
ParkDetailOutputSerializer,
|
||||
ParkCreateInputSerializer,
|
||||
ParkUpdateInputSerializer,
|
||||
ParkFilterInputSerializer,
|
||||
ParkReviewOutputSerializer,
|
||||
ParkStatsOutputSerializer,
|
||||
)
|
||||
|
||||
|
||||
class ParkListApi(ListApiMixin, GenericViewSet):
|
||||
"""
|
||||
API endpoint for listing parks with filtering and search.
|
||||
|
||||
GET /api/v1/parks/
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
search_fields = ["name", "description"]
|
||||
ordering_fields = [
|
||||
"name",
|
||||
"opening_date",
|
||||
"average_rating",
|
||||
"coaster_count",
|
||||
"created_at",
|
||||
]
|
||||
ordering = ["name"]
|
||||
|
||||
OutputSerializer = ParkListOutputSerializer
|
||||
FilterSerializer = ParkFilterInputSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Use selector to get optimized queryset."""
|
||||
# Parse filter parameters
|
||||
filter_serializer = self.FilterSerializer(data=self.request.query_params)
|
||||
filter_serializer.is_valid(raise_exception=True)
|
||||
filters = filter_serializer.validated_data
|
||||
|
||||
return park_list_with_stats(filters=filters)
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def stats(self, request: Request) -> Response:
|
||||
"""
|
||||
Get park statistics.
|
||||
|
||||
GET /api/v1/parks/stats/
|
||||
"""
|
||||
stats = park_statistics()
|
||||
serializer = ParkStatsOutputSerializer(stats)
|
||||
|
||||
return self.create_response(
|
||||
data=serializer.data,
|
||||
metadata={"cache_duration": 3600}, # 1 hour cache hint
|
||||
)
|
||||
|
||||
|
||||
class ParkDetailApi(RetrieveApiMixin, GenericViewSet):
|
||||
"""
|
||||
API endpoint for retrieving individual park details.
|
||||
|
||||
GET /api/v1/parks/{id}/
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
lookup_field = "slug"
|
||||
|
||||
OutputSerializer = ParkDetailOutputSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Use selector for optimized detail query."""
|
||||
slug = self.kwargs.get("slug")
|
||||
return park_detail_optimized(slug=slug)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def reviews(self, request: Request, slug: str = None) -> Response:
|
||||
"""
|
||||
Get reviews for a specific park.
|
||||
|
||||
GET /api/v1/parks/{slug}/reviews/
|
||||
"""
|
||||
park = self.get_object()
|
||||
reviews = park_reviews_for_park(park_id=park.id, limit=50)
|
||||
|
||||
serializer = ParkReviewOutputSerializer(reviews, many=True)
|
||||
|
||||
return self.create_response(
|
||||
data=serializer.data,
|
||||
metadata={"total_reviews": len(reviews), "park_name": park.name},
|
||||
)
|
||||
|
||||
|
||||
class ParkCreateApi(CreateApiMixin, GenericViewSet):
|
||||
"""
|
||||
API endpoint for creating parks.
|
||||
|
||||
POST /api/v1/parks/create/
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
InputSerializer = ParkCreateInputSerializer
|
||||
OutputSerializer = ParkDetailOutputSerializer
|
||||
|
||||
def perform_create(self, **validated_data):
|
||||
"""Create park using service layer."""
|
||||
return ParkService.create_park(**validated_data)
|
||||
|
||||
|
||||
class ParkUpdateApi(UpdateApiMixin, RetrieveApiMixin, GenericViewSet):
|
||||
"""
|
||||
API endpoint for updating parks.
|
||||
|
||||
PUT /api/v1/parks/{slug}/update/
|
||||
PATCH /api/v1/parks/{slug}/update/
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
lookup_field = "slug"
|
||||
|
||||
InputSerializer = ParkUpdateInputSerializer
|
||||
OutputSerializer = ParkDetailOutputSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Use selector for optimized detail query."""
|
||||
slug = self.kwargs.get("slug")
|
||||
return park_detail_optimized(slug=slug)
|
||||
|
||||
def perform_update(self, instance, **validated_data):
|
||||
"""Update park using service layer."""
|
||||
return ParkService.update_park(park_id=instance.id, **validated_data)
|
||||
|
||||
|
||||
class ParkDeleteApi(DestroyApiMixin, RetrieveApiMixin, GenericViewSet):
|
||||
"""
|
||||
API endpoint for deleting parks.
|
||||
|
||||
DELETE /api/v1/parks/{slug}/delete/
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated] # TODO: Add staff/admin permission
|
||||
lookup_field = "slug"
|
||||
|
||||
def get_object(self):
|
||||
"""Use selector for optimized detail query."""
|
||||
slug = self.kwargs.get("slug")
|
||||
return park_detail_optimized(slug=slug)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Delete park using service layer."""
|
||||
ParkService.delete_park(park_id=instance.id)
|
||||
|
||||
|
||||
# Unified API ViewSet (alternative approach)
|
||||
class ParkApi(
|
||||
CreateApiMixin,
|
||||
UpdateApiMixin,
|
||||
ListApiMixin,
|
||||
RetrieveApiMixin,
|
||||
DestroyApiMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
"""
|
||||
Unified API endpoint for parks with all CRUD operations.
|
||||
|
||||
GET /api/v1/parks/ - List parks
|
||||
POST /api/v1/parks/ - Create park
|
||||
GET /api/v1/parks/{slug}/ - Get park detail
|
||||
PUT /api/v1/parks/{slug}/ - Update park
|
||||
PATCH /api/v1/parks/{slug}/ - Partial update park
|
||||
DELETE /api/v1/parks/{slug}/ - Delete park
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
lookup_field = "slug"
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
search_fields = ["name", "description"]
|
||||
ordering_fields = [
|
||||
"name",
|
||||
"opening_date",
|
||||
"average_rating",
|
||||
"coaster_count",
|
||||
"created_at",
|
||||
]
|
||||
ordering = ["name"]
|
||||
|
||||
# Serializers for different operations
|
||||
InputSerializer = ParkCreateInputSerializer # Used for create
|
||||
UpdateInputSerializer = ParkUpdateInputSerializer # Used for update
|
||||
OutputSerializer = ParkDetailOutputSerializer # Used for retrieve
|
||||
ListOutputSerializer = ParkListOutputSerializer # Used for list
|
||||
FilterSerializer = ParkFilterInputSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Use selector to get optimized queryset."""
|
||||
if self.action == "list":
|
||||
# Parse filter parameters for list view
|
||||
filter_serializer = self.FilterSerializer(data=self.request.query_params)
|
||||
filter_serializer.is_valid(raise_exception=True)
|
||||
filters = filter_serializer.validated_data
|
||||
return park_list_with_stats(**filters)
|
||||
|
||||
# For detail views, this won't be used since we override get_object
|
||||
return []
|
||||
|
||||
def get_object(self):
|
||||
"""Use selector for optimized detail query."""
|
||||
slug = self.kwargs.get("slug")
|
||||
return park_detail_optimized(slug=slug)
|
||||
|
||||
def get_output_serializer(self, *args, **kwargs):
|
||||
"""Return appropriate output serializer based on action."""
|
||||
if self.action == "list":
|
||||
return self.ListOutputSerializer(*args, **kwargs)
|
||||
return self.OutputSerializer(*args, **kwargs)
|
||||
|
||||
def get_input_serializer(self, *args, **kwargs):
|
||||
"""Return appropriate input serializer based on action."""
|
||||
if self.action in ["update", "partial_update"]:
|
||||
return self.UpdateInputSerializer(*args, **kwargs)
|
||||
return self.InputSerializer(*args, **kwargs)
|
||||
|
||||
def perform_create(self, **validated_data):
|
||||
"""Create park using service layer."""
|
||||
return ParkService.create_park(**validated_data)
|
||||
|
||||
def perform_update(self, instance, **validated_data):
|
||||
"""Update park using service layer."""
|
||||
return ParkService.update_park(park_id=instance.id, **validated_data)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Delete park using service layer."""
|
||||
ParkService.delete_park(park_id=instance.id)
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def stats(self, request: Request) -> Response:
|
||||
"""
|
||||
Get park statistics.
|
||||
|
||||
GET /api/v1/parks/stats/
|
||||
"""
|
||||
stats = park_statistics()
|
||||
serializer = ParkStatsOutputSerializer(stats)
|
||||
|
||||
return self.create_response(
|
||||
data=serializer.data, metadata={"cache_duration": 3600}
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def reviews(self, request: Request, slug: str = None) -> Response:
|
||||
"""
|
||||
Get reviews for a specific park.
|
||||
|
||||
GET /api/v1/parks/{slug}/reviews/
|
||||
"""
|
||||
park = self.get_object()
|
||||
reviews = park_reviews_for_park(park_id=park.id, limit=50)
|
||||
|
||||
serializer = ParkReviewOutputSerializer(reviews, many=True)
|
||||
|
||||
return self.create_response(
|
||||
data=serializer.data,
|
||||
metadata={"total_reviews": len(reviews), "park_name": park.name},
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-24 18:23
|
||||
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0005_merge_20250820_2020"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="company",
|
||||
name="insert_insert",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="company",
|
||||
name="update_update",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="park",
|
||||
name="insert_insert",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="park",
|
||||
name="update_update",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="parkarea",
|
||||
name="insert_insert",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="parkarea",
|
||||
name="update_update",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="parkreview",
|
||||
name="insert_insert",
|
||||
),
|
||||
pgtrigger.migrations.RemoveTrigger(
|
||||
model_name="parkreview",
|
||||
name="update_update",
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="company",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "parks_companyevent" ("created_at", "description", "founded_year", "id", "name", "parks_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_year", NEW."id", NEW."name", NEW."parks_count", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;',
|
||||
hash="0ed33eeca3344c43d8124d1f12e3acd3e6fdef02",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_35b57",
|
||||
table="parks_company",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="company",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "parks_companyevent" ("created_at", "description", "founded_year", "id", "name", "parks_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_year", NEW."id", NEW."name", NEW."parks_count", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;',
|
||||
hash="ce9d8347090a033d0a9550419b80a1c4a339216c",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_d3286",
|
||||
table="parks_company",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="park",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "parks_parkevent" ("average_rating", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "operator_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "property_owner_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."operator_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."property_owner_id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
|
||||
hash="0a4fc95f70ad65df16aa5eaf2939266260c49213",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_66883",
|
||||
table="parks_park",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="park",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "parks_parkevent" ("average_rating", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "operator_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "property_owner_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."operator_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."property_owner_id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
|
||||
hash="24a57b318c082585e7c79da37677be7032600db9",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_19f56",
|
||||
table="parks_park",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parkarea",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;',
|
||||
hash="fa64ee07f872bf2214b2c1b638b028429752bac4",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_13457",
|
||||
table="parks_parkarea",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parkarea",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;',
|
||||
hash="59fa84527a4fd0fa51685058b6037fa22163a095",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_6e5aa",
|
||||
table="parks_parkarea",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parkreview",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "parks_parkreviewevent" ("content", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rating", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;',
|
||||
hash="fb501d2b3a0d903a03f1a1ff0ae8dd79b189791f",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_a99bc",
|
||||
table="parks_parkreview",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parkreview",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "parks_parkreviewevent" ("content", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rating", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;',
|
||||
hash="254ab0f9ccc0488ea313f1c50a2c35603f7ef02d",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_0e40d",
|
||||
table="parks_parkreview",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,232 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-24 19:25
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.db.models.deletion
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0006_remove_company_insert_insert_and_more"),
|
||||
("pghistory", "0007_auto_20250421_0444"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CompanyHeadquartersEvent",
|
||||
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()),
|
||||
(
|
||||
"street_address",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Mailing address if publicly available",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"city",
|
||||
models.CharField(help_text="Headquarters city", max_length=100),
|
||||
),
|
||||
(
|
||||
"state_province",
|
||||
models.CharField(
|
||||
blank=True, help_text="State/Province/Region", max_length=100
|
||||
),
|
||||
),
|
||||
(
|
||||
"country",
|
||||
models.CharField(
|
||||
default="USA",
|
||||
help_text="Country where headquarters is located",
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
(
|
||||
"postal_code",
|
||||
models.CharField(
|
||||
blank=True, help_text="ZIP or postal code", max_length=20
|
||||
),
|
||||
),
|
||||
(
|
||||
"mailing_address",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Complete mailing address if different from basic address",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ParkLocationEvent",
|
||||
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()),
|
||||
(
|
||||
"point",
|
||||
django.contrib.gis.db.models.fields.PointField(
|
||||
blank=True,
|
||||
help_text="Geographic coordinates (longitude, latitude)",
|
||||
null=True,
|
||||
srid=4326,
|
||||
),
|
||||
),
|
||||
("street_address", models.CharField(blank=True, max_length=255)),
|
||||
("city", models.CharField(max_length=100)),
|
||||
("state", models.CharField(max_length=100)),
|
||||
("country", models.CharField(default="USA", max_length=100)),
|
||||
("postal_code", models.CharField(blank=True, max_length=20)),
|
||||
("highway_exit", models.CharField(blank=True, max_length=100)),
|
||||
("parking_notes", models.TextField(blank=True)),
|
||||
("best_arrival_time", models.TimeField(blank=True, null=True)),
|
||||
("seasonal_notes", models.TextField(blank=True)),
|
||||
("osm_id", models.BigIntegerField(blank=True, null=True)),
|
||||
(
|
||||
"osm_type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Type of OpenStreetMap object (node, way, or relation)",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="companyheadquarters",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "parks_companyheadquartersevent" ("city", "company_id", "country", "created_at", "id", "mailing_address", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "postal_code", "state_province", "street_address", "updated_at") VALUES (NEW."city", NEW."company_id", NEW."country", NEW."created_at", NEW."id", NEW."mailing_address", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."postal_code", NEW."state_province", NEW."street_address", NEW."updated_at"); RETURN NULL;',
|
||||
hash="acf99673091ec3717f404fdccefd6e0cb228c82e",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_72259",
|
||||
table="parks_companyheadquarters",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="companyheadquarters",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "parks_companyheadquartersevent" ("city", "company_id", "country", "created_at", "id", "mailing_address", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "postal_code", "state_province", "street_address", "updated_at") VALUES (NEW."city", NEW."company_id", NEW."country", NEW."created_at", NEW."id", NEW."mailing_address", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."postal_code", NEW."state_province", NEW."street_address", NEW."updated_at"); RETURN NULL;',
|
||||
hash="bbbff3a1c9748d3ce1b2bf1b705d03ea40530c9b",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_c5392",
|
||||
table="parks_companyheadquarters",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parklocation",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "parks_parklocationevent" ("best_arrival_time", "city", "country", "highway_exit", "id", "osm_id", "osm_type", "park_id", "parking_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "seasonal_notes", "state", "street_address") VALUES (NEW."best_arrival_time", NEW."city", NEW."country", NEW."highway_exit", NEW."id", NEW."osm_id", NEW."osm_type", NEW."park_id", NEW."parking_notes", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."point", NEW."postal_code", NEW."seasonal_notes", NEW."state", NEW."street_address"); RETURN NULL;',
|
||||
hash="fcc717a6f7408f959ac1f6406d4ba42b674e3c55",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_f8c53",
|
||||
table="parks_parklocation",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parklocation",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "parks_parklocationevent" ("best_arrival_time", "city", "country", "highway_exit", "id", "osm_id", "osm_type", "park_id", "parking_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "seasonal_notes", "state", "street_address") VALUES (NEW."best_arrival_time", NEW."city", NEW."country", NEW."highway_exit", NEW."id", NEW."osm_id", NEW."osm_type", NEW."park_id", NEW."parking_notes", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."point", NEW."postal_code", NEW."seasonal_notes", NEW."state", NEW."street_address"); RETURN NULL;',
|
||||
hash="dedb3937ae968cc9f2b30309fbf72d9168039efe",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_6dd0d",
|
||||
table="parks_parklocation",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="companyheadquartersevent",
|
||||
name="company",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="parks.company",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="companyheadquartersevent",
|
||||
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.AddField(
|
||||
model_name="companyheadquartersevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="parks.companyheadquarters",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="parklocationevent",
|
||||
name="park",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="parklocationevent",
|
||||
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.AddField(
|
||||
model_name="parklocationevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="parks.parklocation",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -40,12 +40,13 @@ class Company(TrackedModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
class Meta(TrackedModel.Meta):
|
||||
app_label = "parks"
|
||||
ordering = ["name"]
|
||||
verbose_name_plural = "Companies"
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class CompanyHeadquarters(models.Model):
|
||||
"""
|
||||
Simple address storage for company headquarters without coordinate tracking.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.geos import Point
|
||||
import pghistory
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class ParkLocation(models.Model):
|
||||
"""
|
||||
Represents the geographic location and address of a park, with PostGIS support.
|
||||
|
||||
Reference in New Issue
Block a user