initial geodjango implementation

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

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