initial geodjango implementation

This commit is contained in:
pacnpal
2024-11-05 04:10:47 +00:00
parent c66fc2b6e3
commit 491be57ab2
22 changed files with 768 additions and 229 deletions

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.1.2 on 2024-11-04 22:30
import django.contrib.gis.db.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("location", "0003_alter_historicallocation_city_and_more"),
]
operations = [
migrations.AddField(
model_name="location",
name="point",
field=django.contrib.gis.db.models.fields.PointField(
blank=True,
help_text="Geographic coordinates as a Point",
null=True,
srid=4326,
),
),
migrations.DeleteModel(
name="HistoricalLocation",
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.1.2 on 2024-11-04 22:21
from django.db import migrations, transaction
from django.contrib.gis.geos import Point
def forwards_func(apps, schema_editor):
"""Convert existing lat/lon coordinates to points"""
Location = apps.get_model("location", "Location")
db_alias = schema_editor.connection.alias
# Update all locations with points based on existing lat/lon
with transaction.atomic():
for location in Location.objects.using(db_alias).all():
if location.latitude is not None and location.longitude is not None:
try:
location.point = Point(
float(location.longitude), # x coordinate (longitude)
float(location.latitude), # y coordinate (latitude)
srid=4326 # WGS84 coordinate system
)
location.save(update_fields=['point'])
except (ValueError, TypeError):
print(f"Warning: Could not convert coordinates for location {location.id}")
continue
def reverse_func(apps, schema_editor):
"""Convert points back to lat/lon coordinates"""
Location = apps.get_model("location", "Location")
db_alias = schema_editor.connection.alias
# Update all locations with lat/lon based on points
with transaction.atomic():
for location in Location.objects.using(db_alias).all():
if location.point:
try:
location.latitude = location.point.y
location.longitude = location.point.x
location.point = None
location.save(update_fields=['latitude', 'longitude', 'point'])
except (ValueError, TypeError, AttributeError):
print(f"Warning: Could not convert point back to coordinates for location {location.id}")
continue
class Migration(migrations.Migration):
dependencies = [
('location', '0004_add_point_field'),
]
operations = [
migrations.RunPython(forwards_func, reverse_func, atomic=True),
]

View File

@@ -0,0 +1,174 @@
# Generated by Django 5.1.2 on 2024-11-04 22:32
import django.contrib.gis.db.models.fields
import django.core.validators
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("location", "0005_convert_coordinates_to_points"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="HistoricalLocation",
fields=[
(
"id",
models.BigIntegerField(
auto_created=True, blank=True, db_index=True, verbose_name="ID"
),
),
("object_id", models.PositiveIntegerField()),
(
"name",
models.CharField(
help_text="Name of the location (e.g. business name, landmark)",
max_length=255,
),
),
(
"location_type",
models.CharField(
help_text="Type of location (e.g. business, landmark, address)",
max_length=50,
),
),
(
"latitude",
models.DecimalField(
blank=True,
decimal_places=6,
help_text="Latitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-90),
django.core.validators.MaxValueValidator(90),
],
),
),
(
"longitude",
models.DecimalField(
blank=True,
decimal_places=6,
help_text="Longitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-180),
django.core.validators.MaxValueValidator(180),
],
),
),
(
"point",
django.contrib.gis.db.models.fields.PointField(
blank=True,
help_text="Geographic coordinates as a Point",
null=True,
srid=4326,
),
),
(
"street_address",
models.CharField(blank=True, max_length=255, null=True),
),
("city", models.CharField(blank=True, max_length=100, null=True)),
(
"state",
models.CharField(
blank=True,
help_text="State/Region/Province",
max_length=100,
null=True,
),
),
("country", models.CharField(blank=True, max_length=100, null=True)),
("postal_code", models.CharField(blank=True, max_length=20, null=True)),
("created_at", models.DateTimeField(blank=True, editable=False)),
("updated_at", models.DateTimeField(blank=True, editable=False)),
("history_id", models.AutoField(primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)),
(
"history_type",
models.CharField(
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
max_length=1,
),
),
],
options={
"verbose_name": "historical location",
"verbose_name_plural": "historical locations",
"ordering": ("-history_date", "-history_id"),
"get_latest_by": ("history_date", "history_id"),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.RemoveIndex(
model_name="location",
name="location_lo_latitud_7045c4_idx",
),
migrations.AlterField(
model_name="location",
name="latitude",
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text="Latitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-90),
django.core.validators.MaxValueValidator(90),
],
),
),
migrations.AlterField(
model_name="location",
name="longitude",
field=models.DecimalField(
blank=True,
decimal_places=6,
help_text="Longitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-180),
django.core.validators.MaxValueValidator(180),
],
),
),
migrations.AddField(
model_name="historicallocation",
name="content_type",
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="contenttypes.contenttype",
),
),
migrations.AddField(
model_name="historicallocation",
name="history_user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@@ -1,8 +1,10 @@
from django.contrib.gis.db import models as gis_models
from django.db import models from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from django.contrib.gis.geos import Point
class Location(models.Model): class Location(models.Model):
""" """
@@ -27,7 +29,7 @@ class Location(models.Model):
MinValueValidator(-90), MinValueValidator(-90),
MaxValueValidator(90) MaxValueValidator(90)
], ],
help_text="Latitude coordinate", help_text="Latitude coordinate (legacy field)",
null=True, null=True,
blank=True blank=True
) )
@@ -38,12 +40,20 @@ class Location(models.Model):
MinValueValidator(-180), MinValueValidator(-180),
MaxValueValidator(180) MaxValueValidator(180)
], ],
help_text="Longitude coordinate", help_text="Longitude coordinate (legacy field)",
null=True, null=True,
blank=True blank=True
) )
# Address components - all made nullable # GeoDjango point field
point = gis_models.PointField(
srid=4326, # WGS84 coordinate system
null=True,
blank=True,
help_text="Geographic coordinates as a Point"
)
# Address components
street_address = models.CharField(max_length=255, blank=True, null=True) street_address = models.CharField(max_length=255, blank=True, null=True)
city = models.CharField(max_length=100, blank=True, null=True) city = models.CharField(max_length=100, blank=True, null=True)
state = models.CharField(max_length=100, blank=True, null=True, help_text="State/Region/Province") state = models.CharField(max_length=100, blank=True, null=True, help_text="State/Region/Province")
@@ -58,7 +68,6 @@ class Location(models.Model):
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['content_type', 'object_id']), models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['latitude', 'longitude']),
models.Index(fields=['city']), models.Index(fields=['city']),
models.Index(fields=['country']), models.Index(fields=['country']),
] ]
@@ -73,6 +82,15 @@ class Location(models.Model):
location_str = ", ".join(location_parts) if location_parts else "Unknown location" location_str = ", ".join(location_parts) if location_parts else "Unknown location"
return f"{self.name} ({location_str})" return f"{self.name} ({location_str})"
def save(self, *args, **kwargs):
# Sync point field with lat/lon fields for backward compatibility
if self.latitude is not None and self.longitude is not None and not self.point:
self.point = Point(float(self.longitude), float(self.latitude))
elif self.point and (self.latitude is None or self.longitude is None):
self.longitude = self.point.x
self.latitude = self.point.y
super().save(*args, **kwargs)
def get_formatted_address(self): def get_formatted_address(self):
"""Returns a formatted address string""" """Returns a formatted address string"""
components = [] components = []
@@ -91,6 +109,29 @@ class Location(models.Model):
@property @property
def coordinates(self): def coordinates(self):
"""Returns coordinates as a tuple""" """Returns coordinates as a tuple"""
if self.latitude is not None and self.longitude is not None: if self.point:
return (self.point.y, self.point.x) # Returns (latitude, longitude)
elif self.latitude is not None and self.longitude is not None:
return (float(self.latitude), float(self.longitude)) return (float(self.latitude), float(self.longitude))
return None return None
def distance_to(self, other_location):
"""
Calculate the distance to another location in meters.
Returns None if either location is missing coordinates.
"""
if not self.point or not other_location.point:
return None
return self.point.distance(other_location.point) * 100000 # Convert to meters
def nearby_locations(self, distance_km=10):
"""
Find locations within specified distance in kilometers.
Returns a queryset of nearby Location objects.
"""
if not self.point:
return Location.objects.none()
return Location.objects.filter(
point__distance_lte=(self.point, distance_km * 1000) # Convert km to meters
).exclude(pk=self.pk)

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-05 03:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("media", "0005_alter_photo_image"),
]
operations = [
migrations.AddField(
model_name="photo",
name="is_approved",
field=models.BooleanField(default=False),
),
]

View File

@@ -53,6 +53,7 @@ class Photo(models.Model):
caption = models.CharField(max_length=255, blank=True) caption = models.CharField(max_length=255, blank=True)
alt_text = models.CharField(max_length=255, blank=True) alt_text = models.CharField(max_length=255, blank=True)
is_primary = models.BooleanField(default=False) is_primary = models.BooleanField(default=False)
is_approved = models.BooleanField(default=False) # New field for approval status
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
uploaded_by = models.ForeignKey( uploaded_by = models.ForeignKey(

View File

@@ -75,16 +75,19 @@ def upload_photo(request):
{"error": "You do not have permission to upload photos"}, status=403 {"error": "You do not have permission to upload photos"}, status=403
) )
# Determine if the photo should be auto-approved
is_approved = request.user.is_superuser or request.user.is_staff or request.user.groups.filter(name='Moderators').exists()
# Create the photo # Create the photo
photo = Photo.objects.create( photo = Photo.objects.create(
image=request.FILES["image"], image=request.FILES["image"],
content_type=content_type, content_type=content_type,
object_id=obj.id, object_id=obj.id,
uploaded_by=request.user, # Add the user who uploaded the photo uploaded_by=request.user, # Add the user who uploaded the photo
# Set as primary if it's the first photo
is_primary=not Photo.objects.filter( is_primary=not Photo.objects.filter(
content_type=content_type, object_id=obj.id content_type=content_type, object_id=obj.id
).exists(), ).exists(),
is_approved=is_approved # Auto-approve if the user is a moderator, admin, or superuser
) )
return JsonResponse( return JsonResponse(
@@ -93,6 +96,7 @@ def upload_photo(request):
"url": photo.image.url, "url": photo.image.url,
"caption": photo.caption, "caption": photo.caption,
"is_primary": photo.is_primary, "is_primary": photo.is_primary,
"is_approved": photo.is_approved,
} }
) )

View File

@@ -5,6 +5,64 @@ from .models import Park
class ParkForm(forms.ModelForm): class ParkForm(forms.ModelForm):
"""Form for creating and updating Park objects with location support""" """Form for creating and updating Park objects with location support"""
# Location fields
latitude = forms.DecimalField(
max_digits=9,
decimal_places=6,
required=False,
widget=forms.HiddenInput()
)
longitude = forms.DecimalField(
max_digits=10,
decimal_places=6,
required=False,
widget=forms.HiddenInput()
)
street_address = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
)
)
city = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
)
)
state = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
)
)
country = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
)
)
postal_code = forms.CharField(
max_length=20,
required=False,
widget=forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
)
)
class Meta: class Meta:
model = Park model = Park
@@ -18,6 +76,7 @@ class ParkForm(forms.ModelForm):
"operating_season", "operating_season",
"size_acres", "size_acres",
"website", "website",
# Location fields handled separately
"latitude", "latitude",
"longitude", "longitude",
"street_address", "street_address",
@@ -79,36 +138,21 @@ class ParkForm(forms.ModelForm):
"placeholder": "https://example.com", "placeholder": "https://example.com",
} }
), ),
# Location fields
"latitude": forms.HiddenInput(),
"longitude": forms.HiddenInput(),
"street_address": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"city": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"state": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"country": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
"postal_code": forms.TextInput(
attrs={
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
}
),
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Pre-fill location fields if editing existing park
if self.instance and self.instance.pk and self.instance.location.exists():
location = self.instance.location.first()
self.fields['latitude'].initial = location.latitude
self.fields['longitude'].initial = location.longitude
self.fields['street_address'].initial = location.street_address
self.fields['city'].initial = location.city
self.fields['state'].initial = location.state
self.fields['country'].initial = location.country
self.fields['postal_code'].initial = location.postal_code
def clean_latitude(self): def clean_latitude(self):
latitude = self.cleaned_data.get('latitude') latitude = self.cleaned_data.get('latitude')
if latitude is not None: if latitude is not None:
@@ -146,3 +190,27 @@ class ParkForm(forms.ModelForm):
except (InvalidOperation, TypeError): except (InvalidOperation, TypeError):
raise forms.ValidationError("Invalid longitude value.") raise forms.ValidationError("Invalid longitude value.")
return longitude return longitude
def save(self, commit=True):
park = super().save(commit=False)
# Prepare location data
location_data = {
'name': park.name,
'location_type': 'park',
'latitude': self.cleaned_data.get('latitude'),
'longitude': self.cleaned_data.get('longitude'),
'street_address': self.cleaned_data.get('street_address'),
'city': self.cleaned_data.get('city'),
'state': self.cleaned_data.get('state'),
'country': self.cleaned_data.get('country'),
'postal_code': self.cleaned_data.get('postal_code'),
}
# Set location data to be saved with the park
park.set_location(**location_data)
if commit:
park.save()
return park

View File

@@ -1,7 +1,6 @@
# Generated by Django 5.1.2 on 2024-11-03 19:59 # Generated by Django 5.1.2 on 2024-11-03 19:59
import django.core.validators import django.core.validators
import parks.models
from decimal import Decimal from decimal import Decimal
from django.db import migrations, models from django.db import migrations, models
@@ -25,7 +24,6 @@ class Migration(migrations.Migration):
validators=[ validators=[
django.core.validators.MinValueValidator(Decimal("-90")), django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")), django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
], ],
), ),
), ),
@@ -41,7 +39,6 @@ class Migration(migrations.Migration):
validators=[ validators=[
django.core.validators.MinValueValidator(Decimal("-180")), django.core.validators.MinValueValidator(Decimal("-180")),
django.core.validators.MaxValueValidator(Decimal("180")), django.core.validators.MaxValueValidator(Decimal("180")),
parks.models.validate_longitude_digits,
], ],
), ),
), ),
@@ -57,7 +54,6 @@ class Migration(migrations.Migration):
validators=[ validators=[
django.core.validators.MinValueValidator(Decimal("-90")), django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")), django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
], ],
), ),
), ),
@@ -73,7 +69,6 @@ class Migration(migrations.Migration):
validators=[ validators=[
django.core.validators.MinValueValidator(Decimal("-180")), django.core.validators.MinValueValidator(Decimal("-180")),
django.core.validators.MaxValueValidator(Decimal("180")), django.core.validators.MaxValueValidator(Decimal("180")),
parks.models.validate_longitude_digits,
], ],
), ),
), ),

View File

@@ -3,7 +3,6 @@
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
import history_tracking.mixins import history_tracking.mixins
import parks.models
import simple_history.models import simple_history.models
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
@@ -57,7 +56,6 @@ class Migration(migrations.Migration):
validators=[ validators=[
django.core.validators.MinValueValidator(Decimal("-90")), django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")), django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
], ],
), ),
), ),
@@ -72,7 +70,6 @@ class Migration(migrations.Migration):
validators=[ validators=[
django.core.validators.MinValueValidator(Decimal("-180")), django.core.validators.MinValueValidator(Decimal("-180")),
django.core.validators.MaxValueValidator(Decimal("180")), django.core.validators.MaxValueValidator(Decimal("180")),
parks.models.validate_longitude_digits,
], ],
), ),
), ),

View File

@@ -0,0 +1,83 @@
# Generated by Django 5.1.2 on 2024-11-04 22:21
from django.db import migrations, transaction
from django.contrib.contenttypes.models import ContentType
def forwards_func(apps, schema_editor):
"""Move park location data to Location model"""
Park = apps.get_model("parks", "Park")
Location = apps.get_model("location", "Location")
ContentType = apps.get_model("contenttypes", "ContentType")
db_alias = schema_editor.connection.alias
# Get content type for Park model
park_content_type = ContentType.objects.db_manager(db_alias).get(
app_label='parks',
model='park'
)
# Move location data for each park
with transaction.atomic():
for park in Park.objects.using(db_alias).all():
# Only create Location if park has coordinate data
if park.latitude is not None and park.longitude is not None:
Location.objects.using(db_alias).create(
content_type=park_content_type,
object_id=park.id,
name=park.name,
location_type='park',
latitude=park.latitude,
longitude=park.longitude,
street_address=park.street_address,
city=park.city,
state=park.state,
country=park.country,
postal_code=park.postal_code
)
def reverse_func(apps, schema_editor):
"""Move location data back to Park model"""
Park = apps.get_model("parks", "Park")
Location = apps.get_model("location", "Location")
ContentType = apps.get_model("contenttypes", "ContentType")
db_alias = schema_editor.connection.alias
# Get content type for Park model
park_content_type = ContentType.objects.db_manager(db_alias).get(
app_label='parks',
model='park'
)
# Move location data back to each park
with transaction.atomic():
locations = Location.objects.using(db_alias).filter(
content_type=park_content_type
)
for location in locations:
try:
park = Park.objects.using(db_alias).get(id=location.object_id)
park.latitude = location.latitude
park.longitude = location.longitude
park.street_address = location.street_address
park.city = location.city
park.state = location.state
park.country = location.country
park.postal_code = location.postal_code
park.save()
except Park.DoesNotExist:
continue
# Delete all park locations
locations.delete()
class Migration(migrations.Migration):
dependencies = [
('parks', '0008_historicalpark_historicalparkarea'),
('location', '0005_convert_coordinates_to_points'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.RunPython(forwards_func, reverse_func, atomic=True),
]

View File

@@ -0,0 +1,69 @@
# Generated by Django 5.1.2 on 2024-11-04 22:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0009_migrate_to_location_model"),
]
operations = [
migrations.RemoveField(
model_name="historicalpark",
name="latitude",
),
migrations.RemoveField(
model_name="historicalpark",
name="longitude",
),
migrations.RemoveField(
model_name="historicalpark",
name="street_address",
),
migrations.RemoveField(
model_name="historicalpark",
name="city",
),
migrations.RemoveField(
model_name="historicalpark",
name="state",
),
migrations.RemoveField(
model_name="historicalpark",
name="country",
),
migrations.RemoveField(
model_name="historicalpark",
name="postal_code",
),
migrations.RemoveField(
model_name="park",
name="latitude",
),
migrations.RemoveField(
model_name="park",
name="longitude",
),
migrations.RemoveField(
model_name="park",
name="street_address",
),
migrations.RemoveField(
model_name="park",
name="city",
),
migrations.RemoveField(
model_name="park",
name="state",
),
migrations.RemoveField(
model_name="park",
name="country",
),
migrations.RemoveField(
model_name="park",
name="postal_code",
),
]

View File

@@ -2,7 +2,6 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from decimal import Decimal, ROUND_DOWN, InvalidOperation from decimal import Decimal, ROUND_DOWN, InvalidOperation
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
@@ -10,48 +9,7 @@ from simple_history.models import HistoricalRecords
from companies.models import Company from companies.models import Company
from media.models import Photo from media.models import Photo
from history_tracking.models import HistoricalModel from history_tracking.models import HistoricalModel
from location.models import Location
def normalize_coordinate(value, max_digits, decimal_places):
"""Normalize coordinate to have at most max_digits total digits and decimal_places decimal places"""
try:
if value is None:
return None
# Convert to Decimal for precise handling
value = Decimal(str(value))
# Round to specified decimal places
value = Decimal(
value.quantize(Decimal("0." + "0" * decimal_places), rounding=ROUND_DOWN)
)
return value
except (TypeError, ValueError, InvalidOperation):
return None
def validate_coordinate_digits(value, max_digits, decimal_places):
"""Validate total number of digits in a coordinate value"""
if value is not None:
try:
# Convert to Decimal for precise handling
value = Decimal(str(value))
# Round to exactly 6 decimal places
value = value.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
return value
except (InvalidOperation, TypeError):
raise ValidationError("Invalid coordinate value.")
return value
def validate_latitude_digits(value):
"""Validate total number of digits in latitude"""
return validate_coordinate_digits(value, 9, 6)
def validate_longitude_digits(value):
"""Validate total number of digits in longitude"""
return validate_coordinate_digits(value, 10, 6)
class Park(HistoricalModel): class Park(HistoricalModel):
@@ -71,36 +29,8 @@ class Park(HistoricalModel):
max_length=20, choices=STATUS_CHOICES, default="OPERATING" max_length=20, choices=STATUS_CHOICES, default="OPERATING"
) )
# Location fields # Location fields using GenericRelation
latitude = models.DecimalField( location = GenericRelation(Location, related_query_name='park')
max_digits=9,
decimal_places=6,
null=True,
blank=True,
help_text="Latitude coordinate (-90 to 90)",
validators=[
MinValueValidator(Decimal("-90")),
MaxValueValidator(Decimal("90")),
validate_latitude_digits,
],
)
longitude = models.DecimalField(
max_digits=10,
decimal_places=6,
null=True,
blank=True,
help_text="Longitude coordinate (-180 to 180)",
validators=[
MinValueValidator(Decimal("-180")),
MaxValueValidator(Decimal("180")),
validate_longitude_digits,
],
)
street_address = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=255, blank=True)
state = models.CharField(max_length=255, blank=True)
country = models.CharField(max_length=255, blank=True)
postal_code = models.CharField(max_length=20, blank=True)
# Details # Details
opening_date = models.DateField(null=True, blank=True) opening_date = models.DateField(null=True, blank=True)
@@ -138,13 +68,6 @@ class Park(HistoricalModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:
self.slug = slugify(self.name) self.slug = slugify(self.name)
# Normalize coordinates before saving
if self.latitude is not None:
self.latitude = normalize_coordinate(self.latitude, 9, 6)
if self.longitude is not None:
self.longitude = normalize_coordinate(self.longitude, 10, 6)
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_absolute_url(self): def get_absolute_url(self):
@@ -152,14 +75,18 @@ class Park(HistoricalModel):
@property @property
def formatted_location(self): def formatted_location(self):
parts = [] if self.location.exists():
if self.city: location = self.location.first()
parts.append(self.city) return location.get_formatted_address()
if self.state: return ""
parts.append(self.state)
if self.country: @property
parts.append(self.country) def coordinates(self):
return ", ".join(parts) """Returns coordinates as a tuple (latitude, longitude)"""
if self.location.exists():
location = self.location.first()
return location.coordinates
return None
@classmethod @classmethod
def get_by_slug(cls, slug): def get_by_slug(cls, slug):

View File

@@ -1,4 +1,4 @@
from decimal import Decimal, ROUND_DOWN from decimal import Decimal, ROUND_DOWN, InvalidOperation
from django.views.generic import DetailView, ListView, CreateView, UpdateView from django.views.generic import DetailView, ListView, CreateView, UpdateView
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
@@ -16,6 +16,7 @@ from core.views import SlugRedirectMixin
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin
from moderation.models import EditSubmission from moderation.models import EditSubmission
from media.models import Photo from media.models import Photo
from location.models import Location
def location_search(request): def location_search(request):
@@ -101,7 +102,7 @@ class ParkListView(ListView):
context_object_name = "parks" context_object_name = "parks"
def get_queryset(self): def get_queryset(self):
queryset = Park.objects.select_related("owner").prefetch_related("photos") queryset = Park.objects.select_related("owner").prefetch_related("photos", "location")
search = self.request.GET.get("search", "").strip() search = self.request.GET.get("search", "").strip()
country = self.request.GET.get("country", "").strip() country = self.request.GET.get("country", "").strip()
@@ -111,25 +112,25 @@ class ParkListView(ListView):
if search: if search:
queryset = queryset.filter( queryset = queryset.filter(
Q(name__icontains=search) Q(name__icontains=search) |
| Q(city__icontains=search) Q(location__city__icontains=search) |
| Q(state__icontains=search) Q(location__state__icontains=search) |
| Q(country__icontains=search) Q(location__country__icontains=search)
) )
if country: if country:
queryset = queryset.filter(country__icontains=country) queryset = queryset.filter(location__country__icontains=country)
if region: if region:
queryset = queryset.filter(state__icontains=region) queryset = queryset.filter(location__state__icontains=region)
if city: if city:
queryset = queryset.filter(city__icontains=city) queryset = queryset.filter(location__city__icontains=city)
if statuses: if statuses:
queryset = queryset.filter(status__in=statuses) queryset = queryset.filter(status__in=statuses)
return queryset return queryset.distinct()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@@ -173,7 +174,8 @@ class ParkDetailView(
'rides', 'rides',
'rides__manufacturer', 'rides__manufacturer',
'photos', 'photos',
'areas' 'areas',
'location'
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -242,6 +244,22 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
submission.handled_by = self.request.user submission.handled_by = self.request.user
submission.save() submission.save()
# Create Location record
if form.cleaned_data.get("latitude") and form.cleaned_data.get("longitude"):
Location.objects.create(
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
name=self.object.name,
location_type='park',
latitude=form.cleaned_data["latitude"],
longitude=form.cleaned_data["longitude"],
street_address=form.cleaned_data.get("street_address", ""),
city=form.cleaned_data.get("city", ""),
state=form.cleaned_data.get("state", ""),
country=form.cleaned_data.get("country", ""),
postal_code=form.cleaned_data.get("postal_code", "")
)
# Handle photo uploads # Handle photo uploads
photos = self.request.FILES.getlist("photos") photos = self.request.FILES.getlist("photos")
for photo_file in photos: for photo_file in photos:
@@ -349,6 +367,31 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
submission.handled_by = self.request.user submission.handled_by = self.request.user
submission.save() submission.save()
# Update or create Location record
location_data = {
'name': self.object.name,
'location_type': 'park',
'latitude': form.cleaned_data.get("latitude"),
'longitude': form.cleaned_data.get("longitude"),
'street_address': form.cleaned_data.get("street_address", ""),
'city': form.cleaned_data.get("city", ""),
'state': form.cleaned_data.get("state", ""),
'country': form.cleaned_data.get("country", ""),
'postal_code': form.cleaned_data.get("postal_code", "")
}
if self.object.location.exists():
location = self.object.location.first()
for key, value in location_data.items():
setattr(location, key, value)
location.save()
else:
Location.objects.create(
content_type=ContentType.objects.get_for_model(Park),
object_id=self.object.id,
**location_data
)
# Handle photo uploads # Handle photo uploads
photos = self.request.FILES.getlist("photos") photos = self.request.FILES.getlist("photos")
uploaded_count = 0 uploaded_count = 0

View File

@@ -2317,10 +2317,6 @@ select {
margin-left: 1.5rem; margin-left: 1.5rem;
} }
.mr-0\.5 {
margin-right: 0.125rem;
}
.mr-1 { .mr-1 {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
@@ -3001,11 +2997,6 @@ select {
padding-right: 2rem; padding-right: 2rem;
} }
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
@@ -3881,10 +3872,6 @@ select {
margin-bottom: 4rem; margin-bottom: 4rem;
} }
.sm\:grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.sm\:grid-cols-12 { .sm\:grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr)); grid-template-columns: repeat(12, minmax(0, 1fr));
} }
@@ -3905,6 +3892,11 @@ select {
margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse))); margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
} }
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.sm\:text-2xl { .sm\:text-2xl {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
@@ -3925,13 +3917,18 @@ select {
line-height: 1.25rem; line-height: 1.25rem;
} }
.sm\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.sm\:text-xs { .sm\:text-xs {
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1rem; line-height: 1rem;
} }
.sm\:text-xl { .sm\:text-lg {
font-size: 1.25rem; font-size: 1.125rem;
line-height: 1.75rem; line-height: 1.75rem;
} }
} }
@@ -3974,6 +3971,16 @@ select {
font-size: 3rem; font-size: 3rem;
line-height: 1; line-height: 1;
} }
.md\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.md\:text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -4001,8 +4008,38 @@ select {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
.lg\:px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
.lg\:text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.lg\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.lg\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.lg\:text-6xl { .lg\:text-6xl {
font-size: 3.75rem; font-size: 3.75rem;
line-height: 1; line-height: 1;
} }
.lg\:text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.lg\:text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
} }

View File

@@ -4,7 +4,7 @@
{% block title %}{{ park.name }} - ThrillWiki{% endblock %} {% block title %}{{ park.name }} - ThrillWiki{% endblock %}
{% block extra_head %} {% block extra_head %}
{% if park.latitude and park.longitude %} {% if park.location.exists %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -19,7 +19,7 @@
}) })
</script> </script>
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto sm:px-6 lg:px-8">
<!-- Action Buttons - Above header --> <!-- Action Buttons - Above header -->
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="flex justify-end gap-2 mb-2"> <div class="flex justify-end gap-2 mb-2">
@@ -37,18 +37,16 @@
{% endif %} {% endif %}
<!-- Header Grid --> <!-- Header Grid -->
<div class="grid grid-cols-1 gap-4 mb-8 sm:grid-cols-12"> <div class="grid grid-cols-2 gap-4 mb-8 sm:grid-cols-12">
<!-- Park Info Card --> <!-- Park Info Card -->
<div class="flex flex-col items-center justify-center h-full col-span-1 p-4 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800"> <div class="flex flex-col items-center justify-center h-full col-span-2 p-4 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800">
<h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ park.name }}</h1> <h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-lg lg:text-4xl md:text-lg dark:text-white">{{ park.name }}</h1>
{% if park.formatted_location %} {% if park.formatted_location %}
<div class="flex items-center justify-center mt-2 text-sm text-gray-600 dark:text-gray-400"> <div class="flex items-center justify-center mt-2 text-sm text-gray-600 dark:text-gray-400">
<i class="mr-1 fas fa-map-marker-alt"></i> <i class="mr-1 fas fa-map-marker-alt"></i>
<p>{{ park.formatted_location }}</p> <p>{{ park.formatted_location }}</p>
</div> </div>
{% endif %} {% endif %}
<div class="flex flex-wrap items-center justify-center gap-2 mt-3"> <div class="flex flex-wrap items-center justify-center gap-2 mt-3">
<span class="status-badge text-xs sm:text-sm font-medium py-1 px-3 {% if park.status == 'OPERATING' %}status-operating <span class="status-badge text-xs sm:text-sm font-medium py-1 px-3 {% if park.status == 'OPERATING' %}status-operating
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed {% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
@@ -67,36 +65,36 @@
</div> </div>
<!-- Stats and Quick Facts --> <!-- Stats and Quick Facts -->
<div class="grid h-full grid-cols-12 col-span-1 gap-4 sm:col-span-9"> <div class="grid h-full grid-cols-2 col-span-2 gap-4 sm:col-span-9">
<!-- Stats Column --> <!-- Stats Column -->
<div class="grid grid-cols-2 col-span-12 gap-4 sm:col-span-4"> <div class="grid grid-cols-2 col-span-12 gap-4 sm:col-span-4">
<!-- Total Rides Card --> <!-- Total Rides Card -->
{% if park.total_rides %}
<a href="{% url 'parks:rides:ride_list' park.slug %}" <a href="{% url 'parks:rides:ride_list' park.slug %}"
class="flex flex-col items-center justify-center p-4 text-center transition-transform bg-white rounded-lg shadow-lg hover:scale-[1.02] dark:bg-gray-800"> class="flex flex-col items-center justify-center p-4 text-center transition-transform bg-white rounded-lg shadow-lg hover:scale-[1.02] dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Total Rides</dt> <dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Total Rides</dt>
<dd class="mt-2 text-xl font-bold text-sky-900 hover:text-sky-800 sm:text-2xl dark:text-sky-400 dark:hover:text-sky-300">{{ park.total_rides }}</dd> <dd class="mt-2 text-xl font-bold text-sky-900 hover:text-sky-800 sm:text-2xl lg:text-3xl dark:text-sky-400 dark:hover:text-sky-300">
{{ park.total_rides|default:"N/A" }}
</dd>
</a> </a>
{% endif %}
<!-- Total Roller Coasters Card --> <!-- Total Roller Coasters Card -->
{% if park.total_roller_coasters %}
<div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800"> <div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Roller Coasters</dt> <dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Roller Coasters</dt>
<dd class="mt-2 text-xl font-bold text-sky-900 hover:text-sky-800 sm:text-2xl dark:text-sky-400 dark:hover:text-sky-300">{{ park.total_roller_coasters }}</dd> <dd class="mt-2 text-xl font-bold text-sky-900 hover:text-sky-800 sm:text-2xl lg:text-3xl dark:text-sky-400 dark:hover:text-sky-300">
{{ park.total_roller_coasters|default:"N/A" }}
</dd>
</div> </div>
{% endif %}
</div> </div>
<!-- Quick Facts Grid --> <!-- Quick Facts Grid -->
<div class="grid h-full grid-cols-3 col-span-12 gap-2 p-4 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800"> <div class="grid h-full grid-cols-3 col-span-12 gap-2 p-4 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800">
{% if park.owner %} {% if park.owner %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-building dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-building dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Owner</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Owner</dt>
<dd> <dd>
<a href="{% url 'companies:company_detail' park.owner.slug %}" <a href="{% url 'companies:company_detail' park.owner.slug %}"
class="text-xs text-blue-600 sm:text-sm hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> class="text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ park.owner.name }} {{ park.owner.name }}
</a> </a>
</dd> </dd>
@@ -105,19 +103,19 @@
{% if park.opening_date %} {% if park.opening_date %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-calendar-alt dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-calendar-alt dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Opened</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Opened</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ park.opening_date }}</dd> <dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ park.opening_date }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if park.website %} {% if park.website %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-globe dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-globe dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Website</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Website</dt>
<dd> <dd>
<a href="{{ park.website }}" <a href="{{ park.website }}"
class="inline-flex items-center text-xs text-blue-600 sm:text-sm hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" class="inline-flex items-center text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
target="_blank" rel="noopener noreferrer"> target="_blank" rel="noopener noreferrer">
Visit Visit
<i class="ml-1 text-xs fas fa-external-link-alt"></i> <i class="ml-1 text-xs fas fa-external-link-alt"></i>
@@ -186,7 +184,7 @@
<!-- Right Column - Map and Additional Info --> <!-- Right Column - Map and Additional Info -->
<div class="lg:col-span-1"> <div class="lg:col-span-1">
<!-- Location Map --> <!-- Location Map -->
{% if park.latitude and park.longitude %} {% if park.location.exists %}
<div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800"> <div class="p-6 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Location</h2> <h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Location</h2>
<div id="park-map" class="relative rounded-lg" style="z-index: 0;"></div> <div id="park-map" class="relative rounded-lg" style="z-index: 0;"></div>
@@ -253,6 +251,7 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
@@ -260,12 +259,14 @@
<script src="{% static 'js/photo-gallery.js' %}"></script> <script src="{% static 'js/photo-gallery.js' %}"></script>
<!-- Map Script (if location exists) --> <!-- Map Script (if location exists) -->
{% if park.latitude and park.longitude %} {% if park.location.exists %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="{% static 'js/park-map.js' %}"></script> <script src="{% static 'js/park-map.js' %}"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
initParkMap({{ park.latitude }}, {{ park.longitude }}, "{{ park.name }}"); {% with location=park.location.first %}
initParkMap({{ location.latitude }}, {{ location.longitude }}, "{{ park.name }}");
{% endwith %}
}); });
</script> </script>
{% endif %} {% endif %}

View File

@@ -4,7 +4,7 @@
{% block title %}{{ ride.name }} at {{ ride.park.name }} - ThrillWiki{% endblock %} {% block title %}{{ ride.name }} at {{ ride.park.name }} - ThrillWiki{% endblock %}
{% block content %} {% block content %}
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto sm:px-6 lg:px-8">
<!-- Action Buttons - Above header --> <!-- Action Buttons - Above header -->
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="flex justify-end gap-2 mb-2"> <div class="flex justify-end gap-2 mb-2">
@@ -25,7 +25,7 @@
<div class="grid grid-cols-1 gap-4 mb-8 sm:grid-cols-12"> <div class="grid grid-cols-1 gap-4 mb-8 sm:grid-cols-12">
<!-- Ride Info Card --> <!-- Ride Info Card -->
<div class="flex flex-col items-center justify-center h-full col-span-1 p-4 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800"> <div class="flex flex-col items-center justify-center h-full col-span-1 p-4 text-center bg-white rounded-lg shadow-lg sm:col-span-3 dark:bg-gray-800">
<h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-3xl dark:text-white">{{ ride.name }}</h1> <h1 class="text-2xl font-bold leading-tight text-gray-900 sm:text-3xl lg:text-4xl dark:text-white">{{ ride.name }}</h1>
<div class="flex items-center justify-center mt-2 text-sm text-gray-600 dark:text-gray-400"> <div class="flex items-center justify-center mt-2 text-sm text-gray-600 dark:text-gray-400">
at <a href="{% url 'parks:park_detail' ride.park.slug %}" class="ml-1 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"> at <a href="{% url 'parks:park_detail' ride.park.slug %}" class="ml-1 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
@@ -37,18 +37,18 @@
</div> </div>
<div class="flex flex-wrap items-center justify-center gap-2 mt-3"> <div class="flex flex-wrap items-center justify-center gap-2 mt-3">
<span class="px-3 py-1 text-xs font-medium status-badge sm:text-sm {% if ride.status == 'OPERATING' %}status-operating <span class="px-3 py-1 text-xs font-medium status-badge sm:text-sm lg:text-base {% if ride.status == 'OPERATING' %}status-operating
{% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed {% elif ride.status == 'CLOSED_TEMP' or ride.status == 'CLOSED_PERM' %}status-closed
{% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction {% elif ride.status == 'UNDER_CONSTRUCTION' %}status-construction
{% elif ride.status == 'DEMOLISHED' %}status-demolished {% elif ride.status == 'DEMOLISHED' %}status-demolished
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}"> {% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ ride.get_status_display }} {{ ride.get_status_display }}
</span> </span>
<span class="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 status-badge sm:text-sm dark:bg-blue-700 dark:text-blue-50"> <span class="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 status-badge sm:text-sm lg:text-base dark:bg-blue-700 dark:text-blue-50">
{{ ride.get_category_display }} {{ ride.get_category_display }}
</span> </span>
{% if ride.average_rating %} {% if ride.average_rating %}
<span class="flex items-center px-3 py-1 text-xs font-medium text-yellow-800 bg-yellow-100 status-badge sm:text-sm dark:bg-yellow-600 dark:text-yellow-50"> <span class="flex items-center px-3 py-1 text-xs font-medium text-yellow-800 bg-yellow-100 status-badge sm:text-sm lg:text-base dark:bg-yellow-600 dark:text-yellow-50">
<span class="mr-1 text-yellow-500 dark:text-yellow-200"></span> <span class="mr-1 text-yellow-500 dark:text-yellow-200"></span>
{{ ride.average_rating|floatformat:1 }}/10 {{ ride.average_rating|floatformat:1 }}/10
</span> </span>
@@ -63,28 +63,32 @@
{% if coaster_stats %} {% if coaster_stats %}
{% if coaster_stats.height_ft %} {% if coaster_stats.height_ft %}
<div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800"> <div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Height</dt> <dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Height</dt>
<dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.height_ft }} ft</dd> <dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl lg:text-3xl dark:text-sky-400">{{ coaster_stats.height_ft }} ft</dd>
</div> </div>
{% endif %} {% endif %}
{% if coaster_stats.speed_mph %} {% if coaster_stats.speed_mph %}
<div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800"> <div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Speed</dt> <dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Speed</dt>
<dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.speed_mph }} mph</dd> <dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl lg:text-3xl dark:text-sky-400">{{ coaster_stats.speed_mph }} mph</dd>
</div> </div>
{% endif %} {% endif %}
{% if coaster_stats.inversions %} {% if coaster_stats.inversions %}
<div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800"> <div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Inversions</dt> <dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Inversions</dt>
<dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.inversions }}</dd> <dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl lg:text-3xl dark:text-sky-400">{{ coaster_stats.inversions }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if coaster_stats.length_ft %} {% if coaster_stats.length_ft %}
<div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800"> <div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base dark:text-white">Length</dt> <dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Length</dt>
<dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl dark:text-sky-400">{{ coaster_stats.length_ft }} ft</dd> <dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl lg:text-3xl dark:text-sky-400">{{ coaster_stats.length_ft }} ft</dd>
</div>
{% else %}
<div class="flex flex-col items-center justify-center p-4 text-center bg-white rounded-lg shadow-lg dark:bg-gray-800">
<dt class="text-sm font-semibold text-gray-900 sm:text-base lg:text-lg dark:text-white">Length</dt>
<dd class="mt-2 text-xl font-bold text-sky-900 sm:text-2xl lg:text-3xl dark:text-sky-400">N/A</dd>
</div> </div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
@@ -92,24 +96,20 @@
<div class="grid h-full grid-cols-3 col-span-12 gap-2 p-4 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800"> <div class="grid h-full grid-cols-3 col-span-12 gap-2 p-4 bg-white rounded-lg shadow-lg sm:col-span-8 dark:bg-gray-800">
{% if ride.manufacturer %} {% if ride.manufacturer %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-industry dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-industry dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Manufacturer</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Manufacturer</dt>
<dd> <dd>
<a href="{% url 'companies:manufacturer_detail' ride.manufacturer.slug %}" <a href="{% url 'companies:manufacturer_detail' ride.manufacturer.slug %}"
class="text-xs text-blue-600 sm:text-sm hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> class="text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.manufacturer.name }}
</a>
</dd>
</div>
{% endif %} {% endif %}
{% if ride.designer %} {% if ride.designer %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-drafting-compass dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-drafting-compass dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Designer</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Designer</dt>
<dd> <dd>
<a href="{% url 'designers:designer_detail' ride.designer.slug %}" <a href="{% url 'designers:designer_detail' ride.designer.slug %}"
class="text-xs text-blue-600 sm:text-sm hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> class="text-xs text-blue-600 sm:text-sm lg:text-base hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">
{{ ride.designer.name }} {{ ride.designer.name }}
</a> </a>
</dd> </dd>
@@ -118,41 +118,41 @@
{% if coaster_stats.roller_coaster_type %} {% if coaster_stats.roller_coaster_type %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-train dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-train dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Coaster Type</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Coaster Type</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ coaster_stats.get_roller_coaster_type_display }}</dd> <dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ coaster_stats.get_roller_coaster_type_display }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if coaster_stats.track_material %} {% if coaster_stats.track_material %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-layer-group dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-layer-group dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Track Material</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Track Material</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ coaster_stats.get_track_material_display }}</dd> <dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ coaster_stats.get_track_material_display }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if ride.opening_date %} {% if ride.opening_date %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-calendar-alt dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-calendar-alt dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Opened</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Opened</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ ride.opening_date }}</dd> <dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ ride.opening_date }}</dd>
</div> </div>
{% endif %} {% endif %}
{% if ride.capacity_per_hour %} {% if ride.capacity_per_hour %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-users dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-users dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Capacity</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Capacity</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ ride.capacity_per_hour }}/hr</dd> <dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ ride.capacity_per_hour }}/hr</dd>
</div> </div>
{% endif %} {% endif %}
{% if coaster_stats.launch_type %} {% if coaster_stats.launch_type %}
<div class="flex flex-col items-center justify-center p-2 text-center"> <div class="flex flex-col items-center justify-center p-2 text-center">
<i class="text-lg text-blue-600 sm:text-xl fas fa-rocket dark:text-blue-400"></i> <i class="text-lg text-blue-600 sm:text-xl lg:text-2xl fas fa-rocket dark:text-blue-400"></i>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Launch Type</dt> <dt class="mt-1 text-xs font-medium text-gray-500 sm:text-sm lg:text-base dark:text-gray-400">Launch Type</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ coaster_stats.get_launch_type_display }}</dd> <dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ coaster_stats.get_launch_type_display }}</dd>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -24,6 +24,7 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.sites", "django.contrib.sites",
"django.contrib.gis", # Add GeoDjango
"allauth", "allauth",
"allauth.account", "allauth.account",
"allauth.socialaccount", "allauth.socialaccount",
@@ -47,6 +48,7 @@ INSTALLED_APPS = [
"history_tracking", "history_tracking",
"designers", "designers",
"analytics", "analytics",
"location",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -90,7 +92,7 @@ WSGI_APPLICATION = "thrillwiki.wsgi.application"
# Database # Database
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.contrib.gis.db.backends.postgis", # Update to use PostGIS
"NAME": "thrillwiki", "NAME": "thrillwiki",
"USER": "wiki", "USER": "wiki",
"PASSWORD": "thrillwiki", "PASSWORD": "thrillwiki",