mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 06:51:09 -05:00
feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application - Add frontend/ directory with Vite + TypeScript setup ready for Next.js - Add comprehensive shared/ directory with: - Complete documentation and memory-bank archives - Media files and avatars (letters, park/ride images) - Deployment scripts and automation tools - Shared types and utilities - Add architecture/ directory with migration guides - Configure pnpm workspace for monorepo development - Update .gitignore to exclude .django_tailwind_cli/ build artifacts - Preserve all historical documentation in shared/docs/memory-bank/ - Set up proper structure for full-stack development with shared resources
This commit is contained in:
67
backend/apps/location/admin.py
Normal file
67
backend/apps/location/admin.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from django.contrib import admin
|
||||
from .models import Location
|
||||
|
||||
# DEPRECATED: This admin interface is deprecated.
|
||||
# Location data has been migrated to domain-specific models:
|
||||
# - ParkLocation in parks.models.location
|
||||
# - RideLocation in rides.models.location
|
||||
# - CompanyHeadquarters in parks.models.companies
|
||||
#
|
||||
# This admin interface is kept for data migration and cleanup purposes only.
|
||||
|
||||
|
||||
@admin.register(Location)
|
||||
class LocationAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"name",
|
||||
"location_type",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"created_at",
|
||||
)
|
||||
list_filter = ("location_type", "country", "state", "city")
|
||||
search_fields = ("name", "street_address", "city", "state", "country")
|
||||
readonly_fields = ("created_at", "updated_at", "content_type", "object_id")
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
"⚠️ DEPRECATED MODEL",
|
||||
{
|
||||
"description": "This model is deprecated. Use domain-specific location models instead.",
|
||||
"fields": (),
|
||||
},
|
||||
),
|
||||
("Basic Information", {"fields": ("name", "location_type")}),
|
||||
("Geographic Coordinates", {"fields": ("latitude", "longitude")}),
|
||||
(
|
||||
"Address",
|
||||
{
|
||||
"fields": (
|
||||
"street_address",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"postal_code",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Content Type (Read Only)",
|
||||
{
|
||||
"fields": ("content_type", "object_id"),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Metadata",
|
||||
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("content_type")
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Prevent creating new generic Location objects
|
||||
return False
|
||||
8
backend/apps/location/apps.py
Normal file
8
backend/apps/location/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
import os
|
||||
|
||||
|
||||
class LocationConfig(AppConfig):
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.location"
|
||||
42
backend/apps/location/forms.py
Normal file
42
backend/apps/location/forms.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# DEPRECATED: These forms are deprecated and no longer used.
|
||||
#
|
||||
# Domain-specific location models now have their own forms:
|
||||
# - ParkLocationForm in parks.forms (for ParkLocation)
|
||||
# - RideLocationForm in rides.forms (for RideLocation)
|
||||
# - CompanyHeadquartersForm in parks.forms (for CompanyHeadquarters)
|
||||
#
|
||||
# This file is kept for reference during migration cleanup only.
|
||||
|
||||
from django import forms
|
||||
from .models import Location
|
||||
|
||||
# NOTE: All classes below are DEPRECATED
|
||||
# Use domain-specific location forms instead
|
||||
|
||||
|
||||
class LocationForm(forms.ModelForm):
|
||||
"""DEPRECATED: Use domain-specific location forms instead"""
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = [
|
||||
"name",
|
||||
"location_type",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"street_address",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"postal_code",
|
||||
]
|
||||
|
||||
|
||||
class LocationSearchForm(forms.Form):
|
||||
"""DEPRECATED: Location search functionality has been moved to parks app"""
|
||||
|
||||
query = forms.CharField(
|
||||
max_length=255,
|
||||
required=True,
|
||||
help_text="This form is deprecated. Use location search in the parks app.",
|
||||
)
|
||||
293
backend/apps/location/migrations/0001_initial.py
Normal file
293
backend/apps/location/migrations/0001_initial.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# Generated by Django 5.1.4 on 2025-08-13 21:35
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("pghistory", "0006_delete_aggregateevent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Location",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
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(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LocationEvent",
|
||||
fields=[
|
||||
(
|
||||
"pgh_id",
|
||||
models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("pgh_label", models.TextField(help_text="The event label.")),
|
||||
("id", models.BigIntegerField()),
|
||||
("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(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
(
|
||||
"pgh_context",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
(
|
||||
"pgh_obj",
|
||||
models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
to="location.location",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="location",
|
||||
index=models.Index(
|
||||
fields=["content_type", "object_id"],
|
||||
name="location_lo_content_9ee1bd_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="location",
|
||||
index=models.Index(fields=["city"], name="location_lo_city_99f908_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="location",
|
||||
index=models.Index(
|
||||
fields=["country"], name="location_lo_country_b75eba_idx"
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="location",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "location_locationevent" ("city", "content_type_id", "country", "created_at", "id", "latitude", "location_type", "longitude", "name", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "state", "street_address", "updated_at") VALUES (NEW."city", NEW."content_type_id", NEW."country", NEW."created_at", NEW."id", NEW."latitude", NEW."location_type", NEW."longitude", NEW."name", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."point", NEW."postal_code", NEW."state", NEW."street_address", NEW."updated_at"); RETURN NULL;',
|
||||
hash="[AWS-SECRET-REMOVED]",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_98cd4",
|
||||
table="location_location",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="location",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "location_locationevent" ("city", "content_type_id", "country", "created_at", "id", "latitude", "location_type", "longitude", "name", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "state", "street_address", "updated_at") VALUES (NEW."city", NEW."content_type_id", NEW."country", NEW."created_at", NEW."id", NEW."latitude", NEW."location_type", NEW."longitude", NEW."name", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."point", NEW."postal_code", NEW."state", NEW."street_address", NEW."updated_at"); RETURN NULL;',
|
||||
hash="[AWS-SECRET-REMOVED]",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_471d2",
|
||||
table="location_location",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-16 17:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("location", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name="location",
|
||||
constraint=models.CheckConstraint(
|
||||
condition=models.Q(
|
||||
("latitude__isnull", True),
|
||||
models.Q(("latitude__gte", -90), ("latitude__lte", 90)),
|
||||
_connector="OR",
|
||||
),
|
||||
name="location_latitude_range",
|
||||
violation_error_message="Latitude must be between -90 and 90 degrees",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="location",
|
||||
constraint=models.CheckConstraint(
|
||||
condition=models.Q(
|
||||
("longitude__isnull", True),
|
||||
models.Q(("longitude__gte", -180), ("longitude__lte", 180)),
|
||||
_connector="OR",
|
||||
),
|
||||
name="location_longitude_range",
|
||||
violation_error_message="Longitude must be between -180 and 180 degrees",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="location",
|
||||
constraint=models.CheckConstraint(
|
||||
condition=models.Q(
|
||||
models.Q(("latitude__isnull", True), ("longitude__isnull", True)),
|
||||
models.Q(
|
||||
("latitude__isnull", False),
|
||||
("longitude__isnull", False),
|
||||
),
|
||||
_connector="OR",
|
||||
),
|
||||
name="location_coordinates_complete",
|
||||
violation_error_message="Both latitude and longitude must be provided together",
|
||||
),
|
||||
),
|
||||
]
|
||||
0
backend/apps/location/migrations/__init__.py
Normal file
0
backend/apps/location/migrations/__init__.py
Normal file
175
backend/apps/location/models.py
Normal file
175
backend/apps/location/models.py
Normal file
@@ -0,0 +1,175 @@
|
||||
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 django.contrib.gis.geos import Point
|
||||
import pghistory
|
||||
from apps.core.history import TrackedModel
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class Location(TrackedModel):
|
||||
"""
|
||||
A generic location model that can be associated with any model
|
||||
using GenericForeignKey. Stores detailed location information
|
||||
including coordinates and address components.
|
||||
"""
|
||||
|
||||
# Generic relation fields
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
# Location name and type
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
help_text="Name of the location (e.g. business name, landmark)",
|
||||
)
|
||||
location_type = models.CharField(
|
||||
max_length=50,
|
||||
help_text="Type of location (e.g. business, landmark, address)",
|
||||
)
|
||||
|
||||
# Geographic coordinates
|
||||
latitude = models.DecimalField(
|
||||
max_digits=9,
|
||||
decimal_places=6,
|
||||
validators=[MinValueValidator(-90), MaxValueValidator(90)],
|
||||
help_text="Latitude coordinate (legacy field)",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
longitude = models.DecimalField(
|
||||
max_digits=9,
|
||||
decimal_places=6,
|
||||
validators=[MinValueValidator(-180), MaxValueValidator(180)],
|
||||
help_text="Longitude coordinate (legacy field)",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# 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",
|
||||
)
|
||||
country = models.CharField(max_length=100, blank=True, null=True)
|
||||
postal_code = models.CharField(max_length=20, blank=True, null=True)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
models.Index(fields=["city"]),
|
||||
models.Index(fields=["country"]),
|
||||
]
|
||||
ordering = ["name"]
|
||||
constraints = [
|
||||
# Business rule: Latitude must be within valid range (-90 to 90)
|
||||
models.CheckConstraint(
|
||||
name="location_latitude_range",
|
||||
check=models.Q(latitude__isnull=True)
|
||||
| (models.Q(latitude__gte=-90) & models.Q(latitude__lte=90)),
|
||||
violation_error_message="Latitude must be between -90 and 90 degrees",
|
||||
),
|
||||
# Business rule: Longitude must be within valid range (-180 to 180)
|
||||
models.CheckConstraint(
|
||||
name="location_longitude_range",
|
||||
check=models.Q(longitude__isnull=True)
|
||||
| (models.Q(longitude__gte=-180) & models.Q(longitude__lte=180)),
|
||||
violation_error_message="Longitude must be between -180 and 180 degrees",
|
||||
),
|
||||
# Business rule: If coordinates are provided, both lat and lng must
|
||||
# be present
|
||||
models.CheckConstraint(
|
||||
name="location_coordinates_complete",
|
||||
check=models.Q(latitude__isnull=True, longitude__isnull=True)
|
||||
| models.Q(latitude__isnull=False, longitude__isnull=False),
|
||||
violation_error_message="Both latitude and longitude must be provided together",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
location_parts = []
|
||||
if self.city:
|
||||
location_parts.append(self.city)
|
||||
if self.country:
|
||||
location_parts.append(self.country)
|
||||
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 = []
|
||||
if self.street_address:
|
||||
components.append(self.street_address)
|
||||
if self.city:
|
||||
components.append(self.city)
|
||||
if self.state:
|
||||
components.append(self.state)
|
||||
if self.postal_code:
|
||||
components.append(self.postal_code)
|
||||
if self.country:
|
||||
components.append(self.country)
|
||||
return ", ".join(components) if components else ""
|
||||
|
||||
@property
|
||||
def coordinates(self):
|
||||
"""Returns coordinates as a tuple"""
|
||||
if self.point:
|
||||
# Returns (latitude, longitude)
|
||||
return (self.point.y, self.point.x)
|
||||
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)
|
||||
181
backend/apps/location/tests.py
Normal file
181
backend/apps/location/tests.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.gis.geos import Point
|
||||
from .models import Location
|
||||
from apps.parks.models import Park, Company as Operator
|
||||
|
||||
|
||||
class LocationModelTests(TestCase):
|
||||
def setUp(self):
|
||||
# Create test company
|
||||
self.operator = Operator.objects.create(
|
||||
name="Test Operator", website="http://example.com"
|
||||
)
|
||||
|
||||
# Create test park
|
||||
self.park = Park.objects.create(
|
||||
name="Test Park", owner=self.operator, status="OPERATING"
|
||||
)
|
||||
|
||||
# Create test location for company
|
||||
self.operator_location = Location.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(Operator),
|
||||
object_id=self.operator.pk,
|
||||
name="Test Operator HQ",
|
||||
location_type="business",
|
||||
street_address="123 Operator St",
|
||||
city="Operator City",
|
||||
state="CS",
|
||||
country="Test Country",
|
||||
postal_code="12345",
|
||||
point=Point(-118.2437, 34.0522), # Los Angeles coordinates
|
||||
)
|
||||
|
||||
# Create test location for park
|
||||
self.park_location = Location.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(Park),
|
||||
object_id=self.park.pk,
|
||||
name="Test Park Location",
|
||||
location_type="park",
|
||||
street_address="456 Park Ave",
|
||||
city="Park City",
|
||||
state="PC",
|
||||
country="Test Country",
|
||||
postal_code="67890",
|
||||
point=Point(-111.8910, 40.7608), # Park City coordinates
|
||||
)
|
||||
|
||||
def test_location_creation(self):
|
||||
"""Test location instance creation and field values"""
|
||||
# Test company location
|
||||
self.assertEqual(self.operator_location.name, "Test Operator HQ")
|
||||
self.assertEqual(self.operator_location.location_type, "business")
|
||||
self.assertEqual(self.operator_location.street_address, "123 Operator St")
|
||||
self.assertEqual(self.operator_location.city, "Operator City")
|
||||
self.assertEqual(self.operator_location.state, "CS")
|
||||
self.assertEqual(self.operator_location.country, "Test Country")
|
||||
self.assertEqual(self.operator_location.postal_code, "12345")
|
||||
self.assertIsNotNone(self.operator_location.point)
|
||||
|
||||
# Test park location
|
||||
self.assertEqual(self.park_location.name, "Test Park Location")
|
||||
self.assertEqual(self.park_location.location_type, "park")
|
||||
self.assertEqual(self.park_location.street_address, "456 Park Ave")
|
||||
self.assertEqual(self.park_location.city, "Park City")
|
||||
self.assertEqual(self.park_location.state, "PC")
|
||||
self.assertEqual(self.park_location.country, "Test Country")
|
||||
self.assertEqual(self.park_location.postal_code, "67890")
|
||||
self.assertIsNotNone(self.park_location.point)
|
||||
|
||||
def test_location_str_representation(self):
|
||||
"""Test string representation of location"""
|
||||
expected_company_str = "Test Operator HQ (Operator City, Test Country)"
|
||||
self.assertEqual(str(self.operator_location), expected_company_str)
|
||||
|
||||
expected_park_str = "Test Park Location (Park City, Test Country)"
|
||||
self.assertEqual(str(self.park_location), expected_park_str)
|
||||
|
||||
def test_get_formatted_address(self):
|
||||
"""Test get_formatted_address method"""
|
||||
expected_address = "123 Operator St, Operator City, CS, 12345, Test Country"
|
||||
self.assertEqual(
|
||||
self.operator_location.get_formatted_address(), expected_address
|
||||
)
|
||||
|
||||
def test_point_coordinates(self):
|
||||
"""Test point coordinates"""
|
||||
# Test company location point
|
||||
self.assertIsNotNone(self.operator_location.point)
|
||||
self.assertAlmostEqual(
|
||||
self.operator_location.point.y, 34.0522, places=4
|
||||
) # latitude
|
||||
self.assertAlmostEqual(
|
||||
self.operator_location.point.x, -118.2437, places=4
|
||||
) # longitude
|
||||
|
||||
# Test park location point
|
||||
self.assertIsNotNone(self.park_location.point)
|
||||
self.assertAlmostEqual(
|
||||
self.park_location.point.y, 40.7608, places=4
|
||||
) # latitude
|
||||
self.assertAlmostEqual(
|
||||
self.park_location.point.x, -111.8910, places=4
|
||||
) # longitude
|
||||
|
||||
def test_coordinates_property(self):
|
||||
"""Test coordinates property"""
|
||||
company_coords = self.operator_location.coordinates
|
||||
self.assertIsNotNone(company_coords)
|
||||
self.assertAlmostEqual(company_coords[0], 34.0522, places=4) # latitude
|
||||
self.assertAlmostEqual(company_coords[1], -118.2437, places=4) # longitude
|
||||
|
||||
park_coords = self.park_location.coordinates
|
||||
self.assertIsNotNone(park_coords)
|
||||
self.assertAlmostEqual(park_coords[0], 40.7608, places=4) # latitude
|
||||
self.assertAlmostEqual(park_coords[1], -111.8910, places=4) # longitude
|
||||
|
||||
def test_distance_calculation(self):
|
||||
"""Test distance_to method"""
|
||||
distance = self.operator_location.distance_to(self.park_location)
|
||||
self.assertIsNotNone(distance)
|
||||
self.assertGreater(distance, 0)
|
||||
|
||||
def test_nearby_locations(self):
|
||||
"""Test nearby_locations method"""
|
||||
# Create another location near the company location
|
||||
nearby_location = Location.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(Operator),
|
||||
object_id=self.operator.pk,
|
||||
name="Nearby Location",
|
||||
location_type="business",
|
||||
street_address="789 Nearby St",
|
||||
city="Operator City",
|
||||
country="Test Country",
|
||||
point=Point(-118.2438, 34.0523), # Very close to company location
|
||||
)
|
||||
|
||||
nearby = self.operator_location.nearby_locations(distance_km=1)
|
||||
self.assertEqual(nearby.count(), 1)
|
||||
self.assertEqual(nearby.first(), nearby_location)
|
||||
|
||||
def test_content_type_relations(self):
|
||||
"""Test generic relations work correctly"""
|
||||
# Test company location relation
|
||||
company_location = Location.objects.get(
|
||||
content_type=ContentType.objects.get_for_model(Operator),
|
||||
object_id=self.operator.pk,
|
||||
)
|
||||
self.assertEqual(company_location, self.operator_location)
|
||||
|
||||
# Test park location relation
|
||||
park_location = Location.objects.get(
|
||||
content_type=ContentType.objects.get_for_model(Park),
|
||||
object_id=self.park.pk,
|
||||
)
|
||||
self.assertEqual(park_location, self.park_location)
|
||||
|
||||
def test_location_updates(self):
|
||||
"""Test location updates"""
|
||||
# Update company location
|
||||
self.operator_location.street_address = "Updated Address"
|
||||
self.operator_location.city = "Updated City"
|
||||
self.operator_location.save()
|
||||
|
||||
updated_location = Location.objects.get(pk=self.operator_location.pk)
|
||||
self.assertEqual(updated_location.street_address, "Updated Address")
|
||||
self.assertEqual(updated_location.city, "Updated City")
|
||||
|
||||
def test_point_sync_with_lat_lon(self):
|
||||
"""Test point synchronization with latitude/longitude fields"""
|
||||
location = Location.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(Operator),
|
||||
object_id=self.operator.pk,
|
||||
name="Test Sync Location",
|
||||
location_type="business",
|
||||
latitude=34.0522,
|
||||
longitude=-118.2437,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(location.point)
|
||||
self.assertAlmostEqual(location.point.y, 34.0522, places=4)
|
||||
self.assertAlmostEqual(location.point.x, -118.2437, places=4)
|
||||
31
backend/apps/location/urls.py
Normal file
31
backend/apps/location/urls.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# DEPRECATED: These URLs are deprecated and no longer used.
|
||||
#
|
||||
# Location search functionality has been moved to the parks app:
|
||||
# - /parks/search/location/ (replaces /location/search/)
|
||||
# - /parks/search/reverse-geocode/ (replaces /location/reverse-geocode/)
|
||||
#
|
||||
# Domain-specific location models are managed through their respective apps:
|
||||
# - Parks app for ParkLocation
|
||||
# - Rides app for RideLocation
|
||||
# - Parks app for CompanyHeadquarters
|
||||
#
|
||||
# This file is kept for reference during migration cleanup only.
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = "location"
|
||||
|
||||
# NOTE: All URLs below are DEPRECATED
|
||||
# The location app URLs should not be included in the main URLconf
|
||||
|
||||
urlpatterns = [
|
||||
# DEPRECATED: Use /parks/search/location/ instead
|
||||
path("search/", views.LocationSearchView.as_view(), name="search"),
|
||||
# DEPRECATED: Use /parks/search/reverse-geocode/ instead
|
||||
path("reverse-geocode/", views.reverse_geocode, name="reverse_geocode"),
|
||||
# DEPRECATED: Use domain-specific location models instead
|
||||
path("create/", views.LocationCreateView.as_view(), name="create"),
|
||||
path("<int:pk>/update/", views.LocationUpdateView.as_view(), name="update"),
|
||||
path("<int:pk>/delete/", views.LocationDeleteView.as_view(), name="delete"),
|
||||
]
|
||||
48
backend/apps/location/views.py
Normal file
48
backend/apps/location/views.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# DEPRECATED: These views are deprecated and no longer used.
|
||||
#
|
||||
# Location search functionality has been moved to the parks app:
|
||||
# - parks.views.location_search
|
||||
# - parks.views.reverse_geocode
|
||||
#
|
||||
# Domain-specific location models are now used instead of the generic Location model:
|
||||
# - ParkLocation in parks.models.location
|
||||
# - RideLocation in rides.models.location
|
||||
# - CompanyHeadquarters in parks.models.companies
|
||||
#
|
||||
# This file is kept for reference during migration cleanup only.
|
||||
|
||||
from django.views.generic import View
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
|
||||
# NOTE: All classes and functions below are DEPRECATED
|
||||
# Use the equivalent functionality in the parks app instead
|
||||
|
||||
|
||||
class LocationSearchView(View):
|
||||
"""DEPRECATED: Use parks.views.location_search instead"""
|
||||
|
||||
|
||||
class LocationCreateView(LoginRequiredMixin, View):
|
||||
"""DEPRECATED: Use domain-specific location models instead"""
|
||||
|
||||
|
||||
class LocationUpdateView(LoginRequiredMixin, View):
|
||||
"""DEPRECATED: Use domain-specific location models instead"""
|
||||
|
||||
|
||||
class LocationDeleteView(LoginRequiredMixin, View):
|
||||
"""DEPRECATED: Use domain-specific location models instead"""
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def reverse_geocode(request):
|
||||
"""DEPRECATED: Use parks.views.reverse_geocode instead"""
|
||||
return JsonResponse(
|
||||
{
|
||||
"error": "This endpoint is deprecated. Use /parks/search/reverse-geocode/ instead"
|
||||
},
|
||||
status=410,
|
||||
)
|
||||
Reference in New Issue
Block a user