mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 19:11:08 -05:00
Add migrations for ParkPhoto and RidePhoto models with associated events
- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model. - Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent. - Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto. - Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes. - Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
This commit is contained in:
@@ -106,9 +106,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.created_companies[company.slug] = company
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} park company: {
|
||||
company.name}'
|
||||
f" {'Created' if created else 'Found'} park company: {company.name}"
|
||||
)
|
||||
|
||||
# Ride manufacturers and designers (using rides.models.Company)
|
||||
@@ -201,9 +199,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.created_companies[company.slug] = company
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} ride company: {
|
||||
company.name}'
|
||||
f" {'Created' if created else 'Found'} ride company: {company.name}"
|
||||
)
|
||||
|
||||
def create_parks(self):
|
||||
|
||||
@@ -53,7 +53,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
companies[operator.name] = operator
|
||||
self.stdout.write(
|
||||
f'{"Created" if created else "Found"} company: {operator.name}'
|
||||
f"{'Created' if created else 'Found'} company: {operator.name}"
|
||||
)
|
||||
|
||||
# Create parks with their locations
|
||||
@@ -301,7 +301,7 @@ class Command(BaseCommand):
|
||||
"owner": company,
|
||||
},
|
||||
)
|
||||
self.stdout.write(f'{"Created" if created else "Found"} park: {park.name}')
|
||||
self.stdout.write(f"{'Created' if created else 'Found'} park: {park.name}")
|
||||
|
||||
# Create location for park
|
||||
if created:
|
||||
@@ -328,7 +328,7 @@ class Command(BaseCommand):
|
||||
defaults={"description": area_data["description"]},
|
||||
)
|
||||
self.stdout.write(
|
||||
f'{"Created" if created else "Found"} area: {area.name} in {park.name}'
|
||||
f"{'Created' if created else 'Found'} area: {area.name} in {park.name}"
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully seeded initial park data"))
|
||||
|
||||
@@ -121,8 +121,7 @@ class Command(BaseCommand):
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Error during data cleanup: {
|
||||
str(e)}",
|
||||
f"Error during data cleanup: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
self.stdout.write(
|
||||
@@ -205,7 +204,7 @@ class Command(BaseCommand):
|
||||
if missing_tables:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'Missing tables for models: {", ".join(missing_tables)}'
|
||||
f"Missing tables for models: {', '.join(missing_tables)}"
|
||||
)
|
||||
)
|
||||
return False
|
||||
@@ -353,13 +352,13 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.park_companies[data["name"]] = company
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} park company: {
|
||||
company.name}'
|
||||
f" {'Created' if created else 'Found'} park company: {
|
||||
company.name
|
||||
}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating park company {data["name"]}: {str(e)}'
|
||||
f"Error creating park company {data['name']}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -378,13 +377,13 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.ride_companies[data["name"]] = company
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} ride company: {
|
||||
company.name}'
|
||||
f" {'Created' if created else 'Found'} ride company: {
|
||||
company.name
|
||||
}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating ride company {data["name"]}: {str(e)}'
|
||||
f"Error creating ride company {data['name']}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -532,9 +531,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.parks[park_data["name"]] = park
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} park: {
|
||||
park.name}'
|
||||
f" {'Created' if created else 'Found'} park: {park.name}"
|
||||
)
|
||||
|
||||
# Create location for park
|
||||
@@ -556,15 +553,15 @@ class Command(BaseCommand):
|
||||
park_location.save()
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating location for park {
|
||||
park_data["name"]}: {
|
||||
str(e)}'
|
||||
f"Error creating location for park {
|
||||
park_data['name']
|
||||
}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating park {park_data["name"]}: {str(e)}'
|
||||
f"Error creating park {park_data['name']}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -631,15 +628,13 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.ride_models[model_data["name"]] = model
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} ride model: {
|
||||
model.name}'
|
||||
f" {'Created' if created else 'Found'} ride model: {
|
||||
model.name
|
||||
}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating ride model {
|
||||
model_data["name"]}: {
|
||||
str(e)}'
|
||||
f"Error creating ride model {model_data['name']}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -860,9 +855,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.rides[ride_data["name"]] = ride
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} ride: {
|
||||
ride.name}'
|
||||
f" {'Created' if created else 'Found'} ride: {ride.name}"
|
||||
)
|
||||
|
||||
# Create roller coaster stats if provided
|
||||
@@ -872,15 +865,15 @@ class Command(BaseCommand):
|
||||
RollerCoasterStats.objects.create(ride=ride, **stats_data)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating stats for ride {
|
||||
ride_data["name"]}: {
|
||||
str(e)}'
|
||||
f"Error creating stats for ride {ride_data['name']}: {
|
||||
str(e)
|
||||
}"
|
||||
)
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating ride {ride_data["name"]}: {str(e)}'
|
||||
f"Error creating ride {ride_data['name']}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -1013,16 +1006,13 @@ class Command(BaseCommand):
|
||||
},
|
||||
)
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} area: {
|
||||
area.name} in {
|
||||
park.name}'
|
||||
f" {'Created' if created else 'Found'} area: {
|
||||
area.name
|
||||
} in {park.name}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating areas for park {
|
||||
area_group["park"]}: {
|
||||
str(e)}'
|
||||
f"Error creating areas for park {area_group['park']}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -1095,15 +1085,15 @@ class Command(BaseCommand):
|
||||
},
|
||||
)
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} park review: {
|
||||
review.title}'
|
||||
f" {'Created' if created else 'Found'} park review: {
|
||||
review.title
|
||||
}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating park review for {
|
||||
review_data["park"]}: {
|
||||
str(e)}'
|
||||
f"Error creating park review for {review_data['park']}: {
|
||||
str(e)
|
||||
}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -1154,15 +1144,15 @@ class Command(BaseCommand):
|
||||
},
|
||||
)
|
||||
self.stdout.write(
|
||||
f' {
|
||||
"Created" if created else "Found"} ride review: {
|
||||
review.title}'
|
||||
f" {'Created' if created else 'Found'} ride review: {
|
||||
review.title
|
||||
}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f'Error creating ride review for {
|
||||
review_data["ride"]}: {
|
||||
str(e)}'
|
||||
f"Error creating ride review for {review_data['ride']}: {
|
||||
str(e)
|
||||
}"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -55,10 +55,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Test Park model integration
|
||||
self.stdout.write("\n🔍 Testing Park model integration:")
|
||||
self.stdout.write(
|
||||
f" Park formatted location: {
|
||||
park.formatted_location}"
|
||||
)
|
||||
self.stdout.write(f" Park formatted location: {park.formatted_location}")
|
||||
self.stdout.write(f" Park coordinates: {park.coordinates}")
|
||||
|
||||
# Create another location for distance testing
|
||||
@@ -112,10 +109,7 @@ class Command(BaseCommand):
|
||||
nearby_locations = ParkLocation.objects.filter(
|
||||
point__distance_lte=(search_point, D(km=100))
|
||||
)
|
||||
self.stdout.write(
|
||||
f" Found {
|
||||
nearby_locations.count()} parks within 100km"
|
||||
)
|
||||
self.stdout.write(f" Found {nearby_locations.count()} parks within 100km")
|
||||
for loc in nearby_locations:
|
||||
self.stdout.write(f" - {loc.park.name} in {loc.city}, {loc.state}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated manually for enhanced filtering performance
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0001_initial"),
|
||||
]
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0002_alter_parkarea_unique_together"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
|
||||
@@ -8,7 +8,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0003_add_business_constraints"),
|
||||
("pghistory", "0007_auto_20250421_0444"),
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0001_add_filter_indexes"),
|
||||
("parks", "0004_fix_pghistory_triggers"),
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0005_merge_20250820_2020"),
|
||||
]
|
||||
|
||||
@@ -8,7 +8,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("parks", "0006_remove_company_insert_insert_and_more"),
|
||||
("pghistory", "0007_auto_20250421_0444"),
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-26 17:39
|
||||
|
||||
import apps.parks.models.media
|
||||
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):
|
||||
dependencies = [
|
||||
("parks", "0007_companyheadquartersevent_parklocationevent_and_more"),
|
||||
("pghistory", "0007_auto_20250421_0444"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ParkPhoto",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"image",
|
||||
models.ImageField(
|
||||
max_length=255,
|
||||
upload_to=apps.parks.models.media.park_photo_upload_path,
|
||||
),
|
||||
),
|
||||
("caption", models.CharField(blank=True, max_length=255)),
|
||||
("alt_text", models.CharField(blank=True, max_length=255)),
|
||||
("is_primary", models.BooleanField(default=False)),
|
||||
("is_approved", models.BooleanField(default=False)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("date_taken", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"park",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="photos",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uploaded_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="uploaded_park_photos",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-is_primary", "-created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ParkPhotoEvent",
|
||||
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()),
|
||||
(
|
||||
"image",
|
||||
models.ImageField(
|
||||
max_length=255,
|
||||
upload_to=apps.parks.models.media.park_photo_upload_path,
|
||||
),
|
||||
),
|
||||
("caption", models.CharField(blank=True, max_length=255)),
|
||||
("alt_text", models.CharField(blank=True, max_length=255)),
|
||||
("is_primary", models.BooleanField(default=False)),
|
||||
("is_approved", models.BooleanField(default=False)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("date_taken", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"park",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="parks.park",
|
||||
),
|
||||
),
|
||||
(
|
||||
"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="parks.parkphoto",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uploaded_by",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="parkphoto",
|
||||
index=models.Index(
|
||||
fields=["park", "is_primary"], name="parks_parkp_park_id_eda26e_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="parkphoto",
|
||||
index=models.Index(
|
||||
fields=["park", "is_approved"], name="parks_parkp_park_id_5fe576_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="parkphoto",
|
||||
index=models.Index(
|
||||
fields=["created_at"], name="parks_parkp_created_033dc3_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="parkphoto",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("is_primary", True)),
|
||||
fields=("park",),
|
||||
name="unique_primary_park_photo",
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parkphoto",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
|
||||
hash="eeeb8afb335eb66cb4550a0f5abfaf7280472827",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_e2033",
|
||||
table="parks_parkphoto",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="parkphoto",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
|
||||
hash="bd95069068ba9e1a78708a0a9cc73d6507fab691",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_42711",
|
||||
table="parks_parkphoto",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -14,6 +14,7 @@ from .location import ParkLocation
|
||||
from .reviews import ParkReview
|
||||
from .companies import Company, CompanyHeadquarters
|
||||
from .media import ParkPhoto
|
||||
|
||||
# Alias Company as Operator for clarity
|
||||
Operator = Company
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from .parks import Park
|
||||
|
||||
@pghistory.track()
|
||||
class ParkArea(TrackedModel):
|
||||
|
||||
# Import managers
|
||||
from ..managers import ParkAreaManager
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import pghistory
|
||||
|
||||
@pghistory.track()
|
||||
class Company(TrackedModel):
|
||||
|
||||
# Import managers
|
||||
from ..managers import CompanyManager
|
||||
|
||||
@@ -107,13 +106,7 @@ class CompanyHeadquarters(models.Model):
|
||||
components.append(self.postal_code)
|
||||
if self.country and self.country != "USA":
|
||||
components.append(self.country)
|
||||
return (
|
||||
", ".join(components)
|
||||
if components
|
||||
else f"{
|
||||
self.city}, {
|
||||
self.country}"
|
||||
)
|
||||
return ", ".join(components) if components else f"{self.city}, {self.country}"
|
||||
|
||||
@property
|
||||
def location_display(self):
|
||||
|
||||
@@ -7,7 +7,6 @@ This module contains media models specific to parks domain.
|
||||
from typing import Any, Optional, cast
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from apps.core.history import TrackedModel
|
||||
from apps.core.services.media_service import MediaService
|
||||
import pghistory
|
||||
@@ -15,16 +14,14 @@ import pghistory
|
||||
|
||||
def park_photo_upload_path(instance: models.Model, filename: str) -> str:
|
||||
"""Generate upload path for park photos."""
|
||||
photo = cast('ParkPhoto', instance)
|
||||
photo = cast("ParkPhoto", instance)
|
||||
park = photo.park
|
||||
|
||||
if park is None:
|
||||
raise ValueError("Park cannot be None")
|
||||
|
||||
return MediaService.generate_upload_path(
|
||||
domain="park",
|
||||
identifier=park.slug,
|
||||
filename=filename
|
||||
domain="park", identifier=park.slug, filename=filename
|
||||
)
|
||||
|
||||
|
||||
@@ -33,9 +30,7 @@ class ParkPhoto(TrackedModel):
|
||||
"""Photo model specific to parks."""
|
||||
|
||||
park = models.ForeignKey(
|
||||
'parks.Park',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='photos'
|
||||
"parks.Park", on_delete=models.CASCADE, related_name="photos"
|
||||
)
|
||||
|
||||
image = models.ImageField(
|
||||
@@ -72,9 +67,9 @@ class ParkPhoto(TrackedModel):
|
||||
constraints = [
|
||||
# Only one primary photo per park
|
||||
models.UniqueConstraint(
|
||||
fields=['park'],
|
||||
fields=["park"],
|
||||
condition=models.Q(is_primary=True),
|
||||
name='unique_primary_park_photo'
|
||||
name="unique_primary_park_photo",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ if TYPE_CHECKING:
|
||||
|
||||
@pghistory.track()
|
||||
class Park(TrackedModel):
|
||||
|
||||
# Import managers
|
||||
from ..managers import ParkManager
|
||||
|
||||
@@ -226,7 +225,8 @@ class Park(TrackedModel):
|
||||
if historical:
|
||||
print(
|
||||
f"Found historical slug record for object_id: {
|
||||
historical.object_id}"
|
||||
historical.object_id
|
||||
}"
|
||||
)
|
||||
try:
|
||||
park = cls.objects.get(pk=historical.object_id)
|
||||
@@ -250,7 +250,8 @@ class Park(TrackedModel):
|
||||
if historical_event:
|
||||
print(
|
||||
f"Found pghistory event for pgh_obj_id: {
|
||||
historical_event.pgh_obj_id}"
|
||||
historical_event.pgh_obj_id
|
||||
}"
|
||||
)
|
||||
try:
|
||||
park = cls.objects.get(pk=historical_event.pgh_obj_id)
|
||||
|
||||
@@ -7,7 +7,6 @@ import pghistory
|
||||
|
||||
@pghistory.track()
|
||||
class ParkReview(TrackedModel):
|
||||
|
||||
# Import managers
|
||||
from ..managers import ParkReviewManager
|
||||
|
||||
|
||||
@@ -3,5 +3,11 @@ from .park_management import ParkService
|
||||
from .location_service import ParkLocationService
|
||||
from .filter_service import ParkFilterService
|
||||
from .media_service import ParkMediaService
|
||||
__all__ = ["RoadTripService", "ParkService",
|
||||
"ParkLocationService", "ParkFilterService", "ParkMediaService"]
|
||||
|
||||
__all__ = [
|
||||
"RoadTripService",
|
||||
"ParkService",
|
||||
"ParkLocationService",
|
||||
"ParkFilterService",
|
||||
"ParkMediaService",
|
||||
]
|
||||
|
||||
@@ -4,8 +4,7 @@ Handles geocoding, reverse geocoding, and location search for parks.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from django.conf import settings
|
||||
from typing import List, Dict, Any, Optional
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
import logging
|
||||
|
||||
@@ -27,7 +27,7 @@ class ParkMediaService:
|
||||
caption: str = "",
|
||||
alt_text: str = "",
|
||||
is_primary: bool = False,
|
||||
auto_approve: bool = False
|
||||
auto_approve: bool = False,
|
||||
) -> ParkPhoto:
|
||||
"""
|
||||
Upload a photo for a park.
|
||||
@@ -64,7 +64,7 @@ class ParkMediaService:
|
||||
alt_text=alt_text,
|
||||
is_primary=is_primary,
|
||||
is_approved=auto_approve,
|
||||
uploaded_by=user
|
||||
uploaded_by=user,
|
||||
)
|
||||
|
||||
# Extract EXIF date
|
||||
@@ -77,9 +77,7 @@ class ParkMediaService:
|
||||
|
||||
@staticmethod
|
||||
def get_park_photos(
|
||||
park: Park,
|
||||
approved_only: bool = True,
|
||||
primary_first: bool = True
|
||||
park: Park, approved_only: bool = True, primary_first: bool = True
|
||||
) -> List[ParkPhoto]:
|
||||
"""
|
||||
Get photos for a park.
|
||||
@@ -98,9 +96,9 @@ class ParkMediaService:
|
||||
queryset = queryset.filter(is_approved=True)
|
||||
|
||||
if primary_first:
|
||||
queryset = queryset.order_by('-is_primary', '-created_at')
|
||||
queryset = queryset.order_by("-is_primary", "-created_at")
|
||||
else:
|
||||
queryset = queryset.order_by('-created_at')
|
||||
queryset = queryset.order_by("-created_at")
|
||||
|
||||
return list(queryset)
|
||||
|
||||
@@ -190,7 +188,8 @@ class ParkMediaService:
|
||||
photo.delete()
|
||||
|
||||
logger.info(
|
||||
f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}")
|
||||
f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
|
||||
@@ -214,7 +213,7 @@ class ParkMediaService:
|
||||
"approved_photos": photos.filter(is_approved=True).count(),
|
||||
"pending_photos": photos.filter(is_approved=False).count(),
|
||||
"has_primary": photos.filter(is_primary=True).exists(),
|
||||
"recent_uploads": photos.order_by('-created_at')[:5].count()
|
||||
"recent_uploads": photos.order_by("-created_at")[:5].count(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -237,5 +236,6 @@ class ParkMediaService:
|
||||
approved_count += 1
|
||||
|
||||
logger.info(
|
||||
f"Bulk approved {approved_count} photos by user {approved_by.username}")
|
||||
f"Bulk approved {approved_count} photos by user {approved_by.username}"
|
||||
)
|
||||
return approved_count
|
||||
|
||||
@@ -192,8 +192,7 @@ class RoadTripService:
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
raise OSMAPIException(
|
||||
f"Failed to make request after {
|
||||
self.max_retries} attempts: {e}"
|
||||
f"Failed to make request after {self.max_retries} attempts: {e}"
|
||||
)
|
||||
|
||||
def geocode_address(self, address: str) -> Optional[Coordinates]:
|
||||
@@ -244,9 +243,7 @@ class RoadTripService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Geocoded '{address}' to {
|
||||
coords.latitude}, {
|
||||
coords.longitude}"
|
||||
f"Geocoded '{address}' to {coords.latitude}, {coords.longitude}"
|
||||
)
|
||||
return coords
|
||||
else:
|
||||
@@ -274,22 +271,18 @@ class RoadTripService:
|
||||
return None
|
||||
|
||||
# Check cache first
|
||||
cache_key = f"roadtrip:route:{
|
||||
start_coords.latitude},{
|
||||
start_coords.longitude}:{
|
||||
end_coords.latitude},{
|
||||
end_coords.longitude}"
|
||||
cache_key = f"roadtrip:route:{start_coords.latitude},{start_coords.longitude}:{
|
||||
end_coords.latitude
|
||||
},{end_coords.longitude}"
|
||||
cached_result = cache.get(cache_key)
|
||||
if cached_result:
|
||||
return RouteInfo(**cached_result)
|
||||
|
||||
try:
|
||||
# Format coordinates for OSRM (lon,lat format)
|
||||
coords_string = f"{
|
||||
start_coords.longitude},{
|
||||
start_coords.latitude};{
|
||||
end_coords.longitude},{
|
||||
end_coords.latitude}"
|
||||
coords_string = f"{start_coords.longitude},{start_coords.latitude};{
|
||||
end_coords.longitude
|
||||
},{end_coords.latitude}"
|
||||
url = f"{self.osrm_base_url}/{coords_string}"
|
||||
|
||||
params = {
|
||||
@@ -326,9 +319,9 @@ class RoadTripService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Route calculated: {
|
||||
route_info.formatted_distance}, {
|
||||
route_info.formatted_duration}"
|
||||
f"Route calculated: {route_info.formatted_distance}, {
|
||||
route_info.formatted_duration
|
||||
}"
|
||||
)
|
||||
return route_info
|
||||
else:
|
||||
@@ -350,11 +343,13 @@ class RoadTripService:
|
||||
Calculate straight-line distance as fallback when routing fails.
|
||||
"""
|
||||
# Haversine formula for great-circle distance
|
||||
lat1, lon1 = math.radians(start_coords.latitude), math.radians(
|
||||
start_coords.longitude
|
||||
lat1, lon1 = (
|
||||
math.radians(start_coords.latitude),
|
||||
math.radians(start_coords.longitude),
|
||||
)
|
||||
lat2, lon2 = math.radians(end_coords.latitude), math.radians(
|
||||
end_coords.longitude
|
||||
lat2, lon2 = (
|
||||
math.radians(end_coords.latitude),
|
||||
math.radians(end_coords.longitude),
|
||||
)
|
||||
|
||||
dlat = lat2 - lat1
|
||||
@@ -696,10 +691,7 @@ class RoadTripService:
|
||||
location.set_coordinates(coords.latitude, coords.longitude)
|
||||
location.save()
|
||||
logger.info(
|
||||
f"Geocoded park '{
|
||||
park.name}' to {
|
||||
coords.latitude}, {
|
||||
coords.longitude}"
|
||||
f"Geocoded park '{park.name}' to {coords.latitude}, {coords.longitude}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -165,7 +165,8 @@ class ParkAreaModelTests(TestCase):
|
||||
with transaction.atomic():
|
||||
with self.assertRaises(IntegrityError):
|
||||
ParkArea.objects.create(
|
||||
park=self.park, name="Test Area" # Will generate same slug
|
||||
park=self.park,
|
||||
name="Test Area", # Will generate same slug
|
||||
)
|
||||
|
||||
# Should be able to use same name in different park
|
||||
|
||||
@@ -551,14 +551,12 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
image=photo_file,
|
||||
uploaded_by=self.request.user,
|
||||
park=self.object,
|
||||
) )
|
||||
)
|
||||
uploaded_count += 1
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error uploading photo {
|
||||
photo_file.name}: {
|
||||
str(e)}",
|
||||
f"Error uploading photo {photo_file.name}: {str(e)}",
|
||||
)
|
||||
|
||||
messages.success(
|
||||
@@ -571,7 +569,8 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error creating park: {
|
||||
str(e)}. Please check your input and try again.",
|
||||
str(e)
|
||||
}. Please check your input and try again.",
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
@@ -727,9 +726,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error uploading photo {
|
||||
photo_file.name}: {
|
||||
str(e)}",
|
||||
f"Error uploading photo {photo_file.name}: {str(e)}",
|
||||
)
|
||||
|
||||
messages.success(
|
||||
@@ -742,7 +739,8 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
messages.error(
|
||||
self.request,
|
||||
f"Error updating park: {
|
||||
str(e)}. Please check your input and try again.",
|
||||
str(e)
|
||||
}. Please check your input and try again.",
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.urls import reverse
|
||||
from .models import Park
|
||||
from .services.roadtrip import RoadTripService
|
||||
from apps.core.services.map_service import unified_map_service
|
||||
from apps.core.services.data_structures import LocationType, MapFilters
|
||||
from apps.core.services.data_structures import LocationType
|
||||
|
||||
JSON_DECODE_ERROR_MSG = "Invalid JSON data"
|
||||
PARKS_ALONG_ROUTE_HTML = "parks/partials/parks_along_route.html"
|
||||
|
||||
Reference in New Issue
Block a user