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.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.validators import MinValueValidator, MaxValueValidator
from simple_history.models import HistoricalRecords
from django.contrib.gis.geos import Point
class Location(models.Model):
"""
@@ -27,7 +29,7 @@ class Location(models.Model):
MinValueValidator(-90),
MaxValueValidator(90)
],
help_text="Latitude coordinate",
help_text="Latitude coordinate (legacy field)",
null=True,
blank=True
)
@@ -38,12 +40,20 @@ class Location(models.Model):
MinValueValidator(-180),
MaxValueValidator(180)
],
help_text="Longitude coordinate",
help_text="Longitude coordinate (legacy field)",
null=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)
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")
@@ -58,7 +68,6 @@ class Location(models.Model):
class Meta:
indexes = [
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['latitude', 'longitude']),
models.Index(fields=['city']),
models.Index(fields=['country']),
]
@@ -73,6 +82,15 @@ class Location(models.Model):
location_str = ", ".join(location_parts) if location_parts else "Unknown location"
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):
"""Returns a formatted address string"""
components = []
@@ -91,6 +109,29 @@ class Location(models.Model):
@property
def coordinates(self):
"""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 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)
alt_text = models.CharField(max_length=255, blank=True)
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)
updated_at = models.DateTimeField(auto_now=True)
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
)
# 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
photo = Photo.objects.create(
image=request.FILES["image"],
content_type=content_type,
object_id=obj.id,
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(
content_type=content_type, object_id=obj.id
).exists(),
is_approved=is_approved # Auto-approve if the user is a moderator, admin, or superuser
)
return JsonResponse(
@@ -93,6 +96,7 @@ def upload_photo(request):
"url": photo.image.url,
"caption": photo.caption,
"is_primary": photo.is_primary,
"is_approved": photo.is_approved,
}
)

View File

@@ -5,6 +5,64 @@ from .models import Park
class ParkForm(forms.ModelForm):
"""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:
model = Park
@@ -18,6 +76,7 @@ class ParkForm(forms.ModelForm):
"operating_season",
"size_acres",
"website",
# Location fields handled separately
"latitude",
"longitude",
"street_address",
@@ -79,36 +138,21 @@ class ParkForm(forms.ModelForm):
"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):
latitude = self.cleaned_data.get('latitude')
if latitude is not None:
@@ -146,3 +190,27 @@ class ParkForm(forms.ModelForm):
except (InvalidOperation, TypeError):
raise forms.ValidationError("Invalid longitude value.")
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
import django.core.validators
import parks.models
from decimal import Decimal
from django.db import migrations, models
@@ -25,7 +24,6 @@ class Migration(migrations.Migration):
validators=[
django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
],
),
),
@@ -41,7 +39,6 @@ class Migration(migrations.Migration):
validators=[
django.core.validators.MinValueValidator(Decimal("-180")),
django.core.validators.MaxValueValidator(Decimal("180")),
parks.models.validate_longitude_digits,
],
),
),
@@ -57,7 +54,6 @@ class Migration(migrations.Migration):
validators=[
django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
],
),
),
@@ -73,7 +69,6 @@ class Migration(migrations.Migration):
validators=[
django.core.validators.MinValueValidator(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.db.models.deletion
import history_tracking.mixins
import parks.models
import simple_history.models
from decimal import Decimal
from django.conf import settings
@@ -57,7 +56,6 @@ class Migration(migrations.Migration):
validators=[
django.core.validators.MinValueValidator(Decimal("-90")),
django.core.validators.MaxValueValidator(Decimal("90")),
parks.models.validate_latitude_digits,
],
),
),
@@ -72,7 +70,6 @@ class Migration(migrations.Migration):
validators=[
django.core.validators.MinValueValidator(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.utils.text import slugify
from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError
from decimal import Decimal, ROUND_DOWN, InvalidOperation
from simple_history.models import HistoricalRecords
@@ -10,48 +9,7 @@ from simple_history.models import HistoricalRecords
from companies.models import Company
from media.models import Photo
from history_tracking.models import HistoricalModel
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)
from location.models import Location
class Park(HistoricalModel):
@@ -71,36 +29,8 @@ class Park(HistoricalModel):
max_length=20, choices=STATUS_CHOICES, default="OPERATING"
)
# Location fields
latitude = models.DecimalField(
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)
# Location fields using GenericRelation
location = GenericRelation(Location, related_query_name='park')
# Details
opening_date = models.DateField(null=True, blank=True)
@@ -138,13 +68,6 @@ class Park(HistoricalModel):
def save(self, *args, **kwargs):
if not self.slug:
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)
def get_absolute_url(self):
@@ -152,14 +75,18 @@ class Park(HistoricalModel):
@property
def formatted_location(self):
parts = []
if self.city:
parts.append(self.city)
if self.state:
parts.append(self.state)
if self.country:
parts.append(self.country)
return ", ".join(parts)
if self.location.exists():
location = self.location.first()
return location.get_formatted_address()
return ""
@property
def coordinates(self):
"""Returns coordinates as a tuple (latitude, longitude)"""
if self.location.exists():
location = self.location.first()
return location.coordinates
return None
@classmethod
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.shortcuts import get_object_or_404, render
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.models import EditSubmission
from media.models import Photo
from location.models import Location
def location_search(request):
@@ -101,7 +102,7 @@ class ParkListView(ListView):
context_object_name = "parks"
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()
country = self.request.GET.get("country", "").strip()
@@ -111,25 +112,25 @@ class ParkListView(ListView):
if search:
queryset = queryset.filter(
Q(name__icontains=search)
| Q(city__icontains=search)
| Q(state__icontains=search)
| Q(country__icontains=search)
Q(name__icontains=search) |
Q(location__city__icontains=search) |
Q(location__state__icontains=search) |
Q(location__country__icontains=search)
)
if country:
queryset = queryset.filter(country__icontains=country)
queryset = queryset.filter(location__country__icontains=country)
if region:
queryset = queryset.filter(state__icontains=region)
queryset = queryset.filter(location__state__icontains=region)
if city:
queryset = queryset.filter(city__icontains=city)
queryset = queryset.filter(location__city__icontains=city)
if statuses:
queryset = queryset.filter(status__in=statuses)
return queryset
return queryset.distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -173,7 +174,8 @@ class ParkDetailView(
'rides',
'rides__manufacturer',
'photos',
'areas'
'areas',
'location'
)
def get_context_data(self, **kwargs):
@@ -242,6 +244,22 @@ class ParkCreateView(LoginRequiredMixin, CreateView):
submission.handled_by = self.request.user
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
photos = self.request.FILES.getlist("photos")
for photo_file in photos:
@@ -349,6 +367,31 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
submission.handled_by = self.request.user
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
photos = self.request.FILES.getlist("photos")
uploaded_count = 0

View File

@@ -2317,10 +2317,6 @@ select {
margin-left: 1.5rem;
}
.mr-0\.5 {
margin-right: 0.125rem;
}
.mr-1 {
margin-right: 0.25rem;
}
@@ -3001,11 +2997,6 @@ select {
padding-right: 2rem;
}
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
@@ -3881,10 +3872,6 @@ select {
margin-bottom: 4rem;
}
.sm\:grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.sm\:grid-cols-12 {
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)));
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.sm\:text-2xl {
font-size: 1.5rem;
line-height: 2rem;
@@ -3925,13 +3917,18 @@ select {
line-height: 1.25rem;
}
.sm\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.sm\:text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.sm\:text-xl {
font-size: 1.25rem;
.sm\:text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
}
@@ -3974,6 +3971,16 @@ select {
font-size: 3rem;
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) {
@@ -4001,8 +4008,38 @@ select {
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 {
font-size: 3.75rem;
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 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" />
{% endif %}
{% endblock %}
@@ -19,7 +19,7 @@
})
</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 -->
{% if user.is_authenticated %}
<div class="flex justify-end gap-2 mb-2">
@@ -37,18 +37,16 @@
{% endif %}
<!-- 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 -->
<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">{{ park.name }}</h1>
<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-lg lg:text-4xl md:text-lg dark:text-white">{{ park.name }}</h1>
{% if park.formatted_location %}
<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>
<p>{{ park.formatted_location }}</p>
</div>
{% endif %}
<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
{% elif park.status == 'CLOSED_TEMP' or park.status == 'CLOSED_PERM' %}status-closed
@@ -67,36 +65,36 @@
</div>
<!-- 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 -->
<div class="grid grid-cols-2 col-span-12 gap-4 sm:col-span-4">
<!-- Total Rides Card -->
{% if park.total_rides %}
<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">
<dt class="text-sm font-semibold text-gray-900 sm:text-base 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>
<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 lg:text-3xl dark:text-sky-400 dark:hover:text-sky-300">
{{ park.total_rides|default:"N/A" }}
</dd>
</a>
{% endif %}
<!-- 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">
<dt class="text-sm font-semibold text-gray-900 sm:text-base 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>
<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 lg:text-3xl dark:text-sky-400 dark:hover:text-sky-300">
{{ park.total_roller_coasters|default:"N/A" }}
</dd>
</div>
{% endif %}
</div>
<!-- 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">
{% if park.owner %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Owner</dt>
<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 sm:text-sm lg:text-base dark:text-gray-400">Owner</dt>
<dd>
<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 }}
</a>
</dd>
@@ -105,19 +103,19 @@
{% if park.opening_date %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Opened</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ park.opening_date }}</dd>
<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 sm:text-sm lg:text-base dark:text-gray-400">Opened</dt>
<dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ park.opening_date }}</dd>
</div>
{% endif %}
{% if park.website %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Website</dt>
<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 sm:text-sm lg:text-base dark:text-gray-400">Website</dt>
<dd>
<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">
Visit
<i class="ml-1 text-xs fas fa-external-link-alt"></i>
@@ -186,7 +184,7 @@
<!-- Right Column - Map and Additional Info -->
<div class="lg:col-span-1">
<!-- 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">
<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>
@@ -253,6 +251,7 @@
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
@@ -260,12 +259,14 @@
<script src="{% static 'js/photo-gallery.js' %}"></script>
<!-- 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="{% static 'js/park-map.js' %}"></script>
<script>
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>
{% endif %}

View File

@@ -4,7 +4,7 @@
{% block title %}{{ ride.name }} at {{ ride.park.name }} - ThrillWiki{% endblock %}
{% 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 -->
{% if user.is_authenticated %}
<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">
<!-- 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">
<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">
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 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 == 'UNDER_CONSTRUCTION' %}status-construction
{% elif ride.status == 'DEMOLISHED' %}status-demolished
{% elif ride.status == 'RELOCATED' %}status-relocated{% endif %}">
{{ ride.get_status_display }}
</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 }}
</span>
{% 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>
{{ ride.average_rating|floatformat:1 }}/10
</span>
@@ -63,28 +63,32 @@
{% if coaster_stats %}
{% 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">
<dt class="text-sm font-semibold text-gray-900 sm:text-base 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>
<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 lg:text-3xl dark:text-sky-400">{{ coaster_stats.height_ft }} ft</dd>
</div>
{% endif %}
{% 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">
<dt class="text-sm font-semibold text-gray-900 sm:text-base 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>
<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 lg:text-3xl dark:text-sky-400">{{ coaster_stats.speed_mph }} mph</dd>
</div>
{% endif %}
{% 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">
<dt class="text-sm font-semibold text-gray-900 sm:text-base 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>
<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 lg:text-3xl dark:text-sky-400">{{ coaster_stats.inversions }}</dd>
</div>
{% endif %}
{% 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">
<dt class="text-sm font-semibold text-gray-900 sm:text-base 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>
<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">{{ 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>
{% endif %}
{% endif %}
</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">
{% if ride.manufacturer %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Manufacturer</dt>
<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 sm:text-sm lg:text-base dark:text-gray-400">Manufacturer</dt>
<dd>
<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">
{{ ride.manufacturer.name }}
</a>
</dd>
</div>
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">
{% endif %}
{% if ride.designer %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Designer</dt>
<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 sm:text-sm lg:text-base dark:text-gray-400">Designer</dt>
<dd>
<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 }}
</a>
</dd>
@@ -118,41 +118,41 @@
{% if coaster_stats.roller_coaster_type %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 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>
<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 sm:text-sm lg:text-base dark:text-gray-400">Coaster Type</dt>
<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>
{% endif %}
{% if coaster_stats.track_material %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 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>
<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 sm:text-sm lg:text-base dark:text-gray-400">Track Material</dt>
<dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ coaster_stats.get_track_material_display }}</dd>
</div>
{% endif %}
{% if ride.opening_date %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 dark:text-gray-400">Opened</dt>
<dd class="text-xs text-gray-900 sm:text-sm dark:text-white">{{ ride.opening_date }}</dd>
<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 sm:text-sm lg:text-base dark:text-gray-400">Opened</dt>
<dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ ride.opening_date }}</dd>
</div>
{% endif %}
{% if ride.capacity_per_hour %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 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>
<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 sm:text-sm lg:text-base dark:text-gray-400">Capacity</dt>
<dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ ride.capacity_per_hour }}/hr</dd>
</div>
{% endif %}
{% if coaster_stats.launch_type %}
<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>
<dt class="mt-1 text-xs font-medium text-gray-500 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>
<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 sm:text-sm lg:text-base dark:text-gray-400">Launch Type</dt>
<dd class="text-xs text-gray-900 sm:text-sm lg:text-base dark:text-white">{{ coaster_stats.get_launch_type_display }}</dd>
</div>
{% endif %}
</div>

View File

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