mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:11:08 -05:00
major changes, including tailwind v4
This commit is contained in:
867
memory-bank/features/location-models-design.md
Normal file
867
memory-bank/features/location-models-design.md
Normal file
@@ -0,0 +1,867 @@
|
||||
# Domain-Specific Location Models Design - ThrillWiki
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This design document outlines the complete transition from ThrillWiki's generic location system to domain-specific location models. The design builds upon existing partial implementations (ParkLocation, RideLocation, CompanyHeadquarters) and addresses the requirements for road trip planning, spatial queries, and clean domain boundaries.
|
||||
|
||||
## 1. Model Specifications
|
||||
|
||||
### 1.1 ParkLocation Model
|
||||
|
||||
#### Purpose
|
||||
Primary location model for theme parks, optimized for road trip planning and visitor navigation.
|
||||
|
||||
#### Field Specifications
|
||||
|
||||
```python
|
||||
class ParkLocation(models.Model):
|
||||
# Relationships
|
||||
park = models.OneToOneField(
|
||||
'parks.Park',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='park_location' # Changed from 'location' to avoid conflicts
|
||||
)
|
||||
|
||||
# Spatial Data (PostGIS)
|
||||
point = gis_models.PointField(
|
||||
srid=4326, # WGS84 coordinate system
|
||||
db_index=True,
|
||||
help_text="Geographic coordinates for mapping and distance calculations"
|
||||
)
|
||||
|
||||
# Core Address Fields
|
||||
street_address = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="Street number and name for the main entrance"
|
||||
)
|
||||
city = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="City where the park is located"
|
||||
)
|
||||
state = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="State/Province/Region"
|
||||
)
|
||||
country = models.CharField(
|
||||
max_length=100,
|
||||
default='USA',
|
||||
db_index=True,
|
||||
help_text="Country code or full name"
|
||||
)
|
||||
postal_code = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
help_text="ZIP or postal code"
|
||||
)
|
||||
|
||||
# Road Trip Metadata
|
||||
highway_exit = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text="Nearest highway exit information (e.g., 'I-75 Exit 234')"
|
||||
)
|
||||
parking_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="Parking tips, costs, and preferred lots"
|
||||
)
|
||||
best_arrival_time = models.TimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Recommended arrival time to minimize crowds"
|
||||
)
|
||||
seasonal_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="Seasonal considerations for visiting (weather, crowds, events)"
|
||||
)
|
||||
|
||||
# Navigation Helpers
|
||||
main_entrance_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="Specific directions to main entrance from parking"
|
||||
)
|
||||
gps_accuracy_notes = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="Notes about GPS accuracy or common navigation issues"
|
||||
)
|
||||
|
||||
# OpenStreetMap Integration
|
||||
osm_id = models.BigIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text="OpenStreetMap ID for data synchronization"
|
||||
)
|
||||
osm_last_sync = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Last time data was synchronized with OSM"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
verified_date = models.DateField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Date location was last verified as accurate"
|
||||
)
|
||||
verified_by = models.ForeignKey(
|
||||
'accounts.User',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='verified_park_locations'
|
||||
)
|
||||
```
|
||||
|
||||
#### Properties and Methods
|
||||
|
||||
```python
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Returns latitude for backward compatibility"""
|
||||
return self.point.y if self.point else None
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Returns longitude for backward compatibility"""
|
||||
return self.point.x if self.point else None
|
||||
|
||||
@property
|
||||
def 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 and self.country != 'USA':
|
||||
components.append(self.country)
|
||||
return ", ".join(components)
|
||||
|
||||
@property
|
||||
def short_address(self):
|
||||
"""Returns city, state for compact display"""
|
||||
parts = []
|
||||
if self.city:
|
||||
parts.append(self.city)
|
||||
if self.state:
|
||||
parts.append(self.state)
|
||||
return ", ".join(parts) if parts else "Location Unknown"
|
||||
|
||||
def distance_to(self, other_location):
|
||||
"""Calculate distance to another ParkLocation in miles"""
|
||||
if not self.point or not hasattr(other_location, 'point') or not other_location.point:
|
||||
return None
|
||||
# Use PostGIS distance calculation and convert to miles
|
||||
from django.contrib.gis.measure import D
|
||||
return self.point.distance(other_location.point) * 69.0 # Rough conversion
|
||||
|
||||
def nearby_parks(self, distance_miles=50):
|
||||
"""Find other parks within specified distance"""
|
||||
if not self.point:
|
||||
return ParkLocation.objects.none()
|
||||
|
||||
from django.contrib.gis.measure import D
|
||||
return ParkLocation.objects.filter(
|
||||
point__distance_lte=(self.point, D(mi=distance_miles))
|
||||
).exclude(pk=self.pk).select_related('park')
|
||||
|
||||
def get_directions_url(self):
|
||||
"""Generate Google Maps directions URL"""
|
||||
if self.point:
|
||||
return f"https://www.google.com/maps/dir/?api=1&destination={self.latitude},{self.longitude}"
|
||||
return None
|
||||
```
|
||||
|
||||
#### Meta Options
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
verbose_name = "Park Location"
|
||||
verbose_name_plural = "Park Locations"
|
||||
indexes = [
|
||||
models.Index(fields=['city', 'state']),
|
||||
models.Index(fields=['country']),
|
||||
models.Index(fields=['osm_id']),
|
||||
GistIndex(fields=['point']), # Spatial index for PostGIS
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['park'],
|
||||
name='unique_park_location'
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
### 1.2 RideLocation Model
|
||||
|
||||
#### Purpose
|
||||
Optional lightweight location tracking for individual rides within parks.
|
||||
|
||||
#### Field Specifications
|
||||
|
||||
```python
|
||||
class RideLocation(models.Model):
|
||||
# Relationships
|
||||
ride = models.OneToOneField(
|
||||
'rides.Ride',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='ride_location'
|
||||
)
|
||||
|
||||
# Optional Spatial Data
|
||||
entrance_point = gis_models.PointField(
|
||||
srid=4326,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Specific coordinates for ride entrance"
|
||||
)
|
||||
exit_point = gis_models.PointField(
|
||||
srid=4326,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Specific coordinates for ride exit (if different)"
|
||||
)
|
||||
|
||||
# Park Area Information
|
||||
park_area = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text="Themed area or land within the park"
|
||||
)
|
||||
level = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text="Floor or level if in multi-story area"
|
||||
)
|
||||
|
||||
# Accessibility
|
||||
accessible_entrance_point = gis_models.PointField(
|
||||
srid=4326,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Coordinates for accessible entrance if different"
|
||||
)
|
||||
accessible_entrance_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="Directions to accessible entrance"
|
||||
)
|
||||
|
||||
# Queue and Navigation
|
||||
queue_entrance_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="How to find the queue entrance"
|
||||
)
|
||||
fastpass_entrance_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="Location of FastPass/Express entrance"
|
||||
)
|
||||
single_rider_entrance_notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="Location of single rider entrance if available"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
#### Properties and Methods
|
||||
|
||||
```python
|
||||
@property
|
||||
def has_coordinates(self):
|
||||
"""Check if any coordinates are set"""
|
||||
return bool(self.entrance_point or self.exit_point or self.accessible_entrance_point)
|
||||
|
||||
@property
|
||||
def primary_point(self):
|
||||
"""Returns the primary location point (entrance preferred)"""
|
||||
return self.entrance_point or self.exit_point or self.accessible_entrance_point
|
||||
|
||||
def get_park_location(self):
|
||||
"""Get the parent park's location"""
|
||||
return self.ride.park.park_location if hasattr(self.ride.park, 'park_location') else None
|
||||
```
|
||||
|
||||
#### Meta Options
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
verbose_name = "Ride Location"
|
||||
verbose_name_plural = "Ride Locations"
|
||||
indexes = [
|
||||
models.Index(fields=['park_area']),
|
||||
GistIndex(fields=['entrance_point'], condition=Q(entrance_point__isnull=False)),
|
||||
]
|
||||
```
|
||||
|
||||
### 1.3 CompanyHeadquarters Model
|
||||
|
||||
#### Purpose
|
||||
Simple address storage for company headquarters without coordinate tracking.
|
||||
|
||||
#### Field Specifications
|
||||
|
||||
```python
|
||||
class CompanyHeadquarters(models.Model):
|
||||
# Relationships
|
||||
company = models.OneToOneField(
|
||||
'parks.Company',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='headquarters'
|
||||
)
|
||||
|
||||
# Address Fields (No coordinates needed)
|
||||
street_address = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="Mailing address if publicly available"
|
||||
)
|
||||
city = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="Headquarters city"
|
||||
)
|
||||
state = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text="State/Province/Region"
|
||||
)
|
||||
country = models.CharField(
|
||||
max_length=100,
|
||||
default='USA',
|
||||
db_index=True
|
||||
)
|
||||
postal_code = models.CharField(
|
||||
max_length=20,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Contact Information (Optional)
|
||||
phone = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
help_text="Corporate phone number"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
```
|
||||
|
||||
#### Properties and Methods
|
||||
|
||||
```python
|
||||
@property
|
||||
def 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 and self.country != 'USA':
|
||||
components.append(self.country)
|
||||
return ", ".join(components) if components else f"{self.city}, {self.country}"
|
||||
|
||||
@property
|
||||
def location_display(self):
|
||||
"""Simple city, country display"""
|
||||
parts = [self.city]
|
||||
if self.state:
|
||||
parts.append(self.state)
|
||||
if self.country != 'USA':
|
||||
parts.append(self.country)
|
||||
return ", ".join(parts)
|
||||
```
|
||||
|
||||
#### Meta Options
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
verbose_name = "Company Headquarters"
|
||||
verbose_name_plural = "Company Headquarters"
|
||||
indexes = [
|
||||
models.Index(fields=['city', 'country']),
|
||||
]
|
||||
```
|
||||
|
||||
## 2. Shared Functionality Design
|
||||
|
||||
### 2.1 Address Formatting Utilities
|
||||
|
||||
Create a utility module `location/utils.py`:
|
||||
|
||||
```python
|
||||
class AddressFormatter:
|
||||
"""Utility class for consistent address formatting across models"""
|
||||
|
||||
@staticmethod
|
||||
def format_full(street=None, city=None, state=None, postal=None, country=None):
|
||||
"""Format a complete address"""
|
||||
components = []
|
||||
if street:
|
||||
components.append(street)
|
||||
if city:
|
||||
components.append(city)
|
||||
if state:
|
||||
components.append(state)
|
||||
if postal:
|
||||
components.append(postal)
|
||||
if country and country != 'USA':
|
||||
components.append(country)
|
||||
return ", ".join(components)
|
||||
|
||||
@staticmethod
|
||||
def format_short(city=None, state=None, country=None):
|
||||
"""Format a short location display"""
|
||||
parts = []
|
||||
if city:
|
||||
parts.append(city)
|
||||
if state:
|
||||
parts.append(state)
|
||||
elif country and country != 'USA':
|
||||
parts.append(country)
|
||||
return ", ".join(parts) if parts else "Unknown Location"
|
||||
```
|
||||
|
||||
### 2.2 Geocoding Service
|
||||
|
||||
Create `location/services.py`:
|
||||
|
||||
```python
|
||||
class GeocodingService:
|
||||
"""Service for geocoding addresses using OpenStreetMap Nominatim"""
|
||||
|
||||
@staticmethod
|
||||
def geocode_address(street, city, state, country='USA'):
|
||||
"""Convert address to coordinates"""
|
||||
# Implementation using Nominatim API
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def reverse_geocode(latitude, longitude):
|
||||
"""Convert coordinates to address"""
|
||||
# Implementation using Nominatim API
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def validate_coordinates(latitude, longitude):
|
||||
"""Validate coordinate ranges"""
|
||||
return (-90 <= latitude <= 90) and (-180 <= longitude <= 180)
|
||||
```
|
||||
|
||||
### 2.3 Distance Calculation Mixin
|
||||
|
||||
```python
|
||||
class DistanceCalculationMixin:
|
||||
"""Mixin for models with point fields to calculate distances"""
|
||||
|
||||
def distance_to_point(self, point):
|
||||
"""Calculate distance to a point in miles"""
|
||||
if not self.point or not point:
|
||||
return None
|
||||
# Use PostGIS for calculation
|
||||
return self.point.distance(point) * 69.0 # Rough miles conversion
|
||||
|
||||
def within_radius(self, radius_miles):
|
||||
"""Get queryset of objects within radius"""
|
||||
if not self.point:
|
||||
return self.__class__.objects.none()
|
||||
|
||||
from django.contrib.gis.measure import D
|
||||
return self.__class__.objects.filter(
|
||||
point__distance_lte=(self.point, D(mi=radius_miles))
|
||||
).exclude(pk=self.pk)
|
||||
```
|
||||
|
||||
## 3. Data Flow Design
|
||||
|
||||
### 3.1 Location Data Entry Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Creates/Edits Park] --> B[Park Form]
|
||||
B --> C{Has Address?}
|
||||
C -->|Yes| D[Geocoding Service]
|
||||
C -->|No| E[Manual Coordinate Entry]
|
||||
D --> F[Validate Coordinates]
|
||||
E --> F
|
||||
F --> G[Create/Update ParkLocation]
|
||||
G --> H[Update OSM Fields]
|
||||
H --> I[Save to Database]
|
||||
```
|
||||
|
||||
### 3.2 Location Search Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Searches Location] --> B[Search View]
|
||||
B --> C[Check Cache]
|
||||
C -->|Hit| D[Return Cached Results]
|
||||
C -->|Miss| E[Query OSM Nominatim]
|
||||
E --> F[Process Results]
|
||||
F --> G[Filter by Park Existence]
|
||||
G --> H[Cache Results]
|
||||
H --> D
|
||||
```
|
||||
|
||||
### 3.3 Road Trip Planning Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Plans Road Trip] --> B[Select Starting Point]
|
||||
B --> C[Query Nearby Parks]
|
||||
C --> D[Calculate Distances]
|
||||
D --> E[Sort by Distance/Route]
|
||||
E --> F[Display with Highway Exits]
|
||||
F --> G[Show Parking/Arrival Info]
|
||||
```
|
||||
|
||||
## 4. Query Patterns
|
||||
|
||||
### 4.1 Common Spatial Queries
|
||||
|
||||
```python
|
||||
# Find parks within radius
|
||||
ParkLocation.objects.filter(
|
||||
point__distance_lte=(origin_point, D(mi=50))
|
||||
).select_related('park')
|
||||
|
||||
# Find nearest park
|
||||
ParkLocation.objects.annotate(
|
||||
distance=Distance('point', origin_point)
|
||||
).order_by('distance').first()
|
||||
|
||||
# Parks along a route (bounding box)
|
||||
from django.contrib.gis.geos import Polygon
|
||||
bbox = Polygon.from_bbox((min_lng, min_lat, max_lng, max_lat))
|
||||
ParkLocation.objects.filter(point__within=bbox)
|
||||
|
||||
# Group parks by state
|
||||
ParkLocation.objects.values('state').annotate(
|
||||
count=Count('id'),
|
||||
parks=ArrayAgg('park__name')
|
||||
)
|
||||
```
|
||||
|
||||
### 4.2 Performance Optimizations
|
||||
|
||||
```python
|
||||
# Prefetch related data for park listings
|
||||
Park.objects.select_related(
|
||||
'park_location',
|
||||
'operator',
|
||||
'property_owner'
|
||||
).prefetch_related('rides')
|
||||
|
||||
# Use database functions for formatting
|
||||
from django.db.models import Value, F
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
ParkLocation.objects.annotate(
|
||||
display_address=Concat(
|
||||
F('city'), Value(', '),
|
||||
F('state')
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 4.3 Caching Strategy
|
||||
|
||||
```python
|
||||
# Cache frequently accessed location data
|
||||
CACHE_KEYS = {
|
||||
'park_location': 'park_location_{park_id}',
|
||||
'nearby_parks': 'nearby_parks_{park_id}_{radius}',
|
||||
'state_parks': 'state_parks_{state}',
|
||||
}
|
||||
|
||||
# Cache timeout in seconds
|
||||
CACHE_TIMEOUTS = {
|
||||
'park_location': 3600, # 1 hour
|
||||
'nearby_parks': 1800, # 30 minutes
|
||||
'state_parks': 7200, # 2 hours
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Integration Points
|
||||
|
||||
### 5.1 Model Integration
|
||||
|
||||
```python
|
||||
# Park model integration
|
||||
class Park(models.Model):
|
||||
# Remove GenericRelation to Location
|
||||
# location = GenericRelation(Location) # REMOVE THIS
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
"""Backward compatibility property"""
|
||||
return self.park_location if hasattr(self, 'park_location') else None
|
||||
|
||||
@property
|
||||
def coordinates(self):
|
||||
"""Quick access to coordinates"""
|
||||
if hasattr(self, 'park_location') and self.park_location:
|
||||
return (self.park_location.latitude, self.park_location.longitude)
|
||||
return None
|
||||
```
|
||||
|
||||
### 5.2 Form Integration
|
||||
|
||||
```python
|
||||
# Park forms will need location inline
|
||||
class ParkLocationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ParkLocation
|
||||
fields = [
|
||||
'street_address', 'city', 'state', 'country', 'postal_code',
|
||||
'highway_exit', 'parking_notes', 'best_arrival_time',
|
||||
'seasonal_notes', 'point'
|
||||
]
|
||||
widgets = {
|
||||
'point': LeafletWidget(), # Map widget for coordinate selection
|
||||
}
|
||||
|
||||
class ParkForm(forms.ModelForm):
|
||||
# Include location fields as nested form
|
||||
location = ParkLocationForm()
|
||||
```
|
||||
|
||||
### 5.3 API Serialization
|
||||
|
||||
```python
|
||||
# Django REST Framework serializers
|
||||
class ParkLocationSerializer(serializers.ModelSerializer):
|
||||
latitude = serializers.ReadOnlyField()
|
||||
longitude = serializers.ReadOnlyField()
|
||||
formatted_address = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = ParkLocation
|
||||
fields = [
|
||||
'latitude', 'longitude', 'formatted_address',
|
||||
'city', 'state', 'country', 'highway_exit',
|
||||
'parking_notes', 'best_arrival_time'
|
||||
]
|
||||
|
||||
class ParkSerializer(serializers.ModelSerializer):
|
||||
location = ParkLocationSerializer(source='park_location', read_only=True)
|
||||
```
|
||||
|
||||
### 5.4 Template Integration
|
||||
|
||||
```django
|
||||
{# Park detail template #}
|
||||
{% if park.park_location %}
|
||||
<div class="park-location">
|
||||
<h3>Location</h3>
|
||||
<p>{{ park.park_location.formatted_address }}</p>
|
||||
|
||||
{% if park.park_location.highway_exit %}
|
||||
<p><strong>Highway Exit:</strong> {{ park.park_location.highway_exit }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if park.park_location.parking_notes %}
|
||||
<p><strong>Parking:</strong> {{ park.park_location.parking_notes }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div id="park-map"
|
||||
data-lat="{{ park.park_location.latitude }}"
|
||||
data-lng="{{ park.park_location.longitude }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## 6. Migration Plan
|
||||
|
||||
### 6.1 Migration Phases
|
||||
|
||||
#### Phase 1: Prepare New Models (No Downtime)
|
||||
1. Create new models alongside existing ones
|
||||
2. Add backward compatibility properties
|
||||
3. Deploy without activating
|
||||
|
||||
#### Phase 2: Data Migration (Minimal Downtime)
|
||||
1. Create migration script to copy data
|
||||
2. Run in batches to avoid locks
|
||||
3. Verify data integrity
|
||||
|
||||
#### Phase 3: Switch References (No Downtime)
|
||||
1. Update views to use new models
|
||||
2. Update forms and templates
|
||||
3. Deploy with feature flags
|
||||
|
||||
#### Phase 4: Cleanup (No Downtime)
|
||||
1. Remove GenericRelation from Park
|
||||
2. Archive old Location model
|
||||
3. Remove backward compatibility code
|
||||
|
||||
### 6.2 Migration Script
|
||||
|
||||
```python
|
||||
from django.db import migrations
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
def migrate_park_locations(apps, schema_editor):
|
||||
Location = apps.get_model('location', 'Location')
|
||||
Park = apps.get_model('parks', 'Park')
|
||||
ParkLocation = apps.get_model('parks', 'ParkLocation')
|
||||
|
||||
park_ct = ContentType.objects.get_for_model(Park)
|
||||
|
||||
for location in Location.objects.filter(content_type=park_ct):
|
||||
try:
|
||||
park = Park.objects.get(id=location.object_id)
|
||||
|
||||
# Create or update ParkLocation
|
||||
park_location, created = ParkLocation.objects.update_or_create(
|
||||
park=park,
|
||||
defaults={
|
||||
'point': location.point,
|
||||
'street_address': location.street_address or '',
|
||||
'city': location.city or '',
|
||||
'state': location.state or '',
|
||||
'country': location.country or 'USA',
|
||||
'postal_code': location.postal_code or '',
|
||||
# Map any additional fields
|
||||
}
|
||||
)
|
||||
|
||||
print(f"Migrated location for park: {park.name}")
|
||||
|
||||
except Park.DoesNotExist:
|
||||
print(f"Park not found for location: {location.id}")
|
||||
continue
|
||||
|
||||
def reverse_migration(apps, schema_editor):
|
||||
# Reverse migration if needed
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('parks', 'XXXX_create_park_location'),
|
||||
('location', 'XXXX_previous'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_park_locations, reverse_migration),
|
||||
]
|
||||
```
|
||||
|
||||
### 6.3 Data Validation
|
||||
|
||||
```python
|
||||
# Validation script to ensure migration success
|
||||
def validate_migration():
|
||||
from location.models import Location
|
||||
from parks.models import Park, ParkLocation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
park_ct = ContentType.objects.get_for_model(Park)
|
||||
old_count = Location.objects.filter(content_type=park_ct).count()
|
||||
new_count = ParkLocation.objects.count()
|
||||
|
||||
assert old_count == new_count, f"Count mismatch: {old_count} vs {new_count}"
|
||||
|
||||
# Verify data integrity
|
||||
for park_location in ParkLocation.objects.all():
|
||||
assert park_location.point is not None, f"Missing point for {park_location.park}"
|
||||
assert park_location.city, f"Missing city for {park_location.park}"
|
||||
|
||||
print("Migration validation successful!")
|
||||
```
|
||||
|
||||
### 6.4 Rollback Strategy
|
||||
|
||||
1. **Feature Flags**: Use flags to switch between old and new systems
|
||||
2. **Database Backups**: Take snapshots before migration
|
||||
3. **Parallel Running**: Keep both systems running initially
|
||||
4. **Gradual Rollout**: Migrate parks in batches
|
||||
5. **Monitoring**: Track errors and performance
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### 7.1 Unit Tests
|
||||
|
||||
```python
|
||||
# Test ParkLocation model
|
||||
class ParkLocationTestCase(TestCase):
|
||||
def test_formatted_address(self):
|
||||
location = ParkLocation(
|
||||
city="Orlando",
|
||||
state="Florida",
|
||||
country="USA"
|
||||
)
|
||||
self.assertEqual(location.formatted_address, "Orlando, Florida")
|
||||
|
||||
def test_distance_calculation(self):
|
||||
location1 = ParkLocation(point=Point(-81.5639, 28.3852))
|
||||
location2 = ParkLocation(point=Point(-81.4678, 28.4736))
|
||||
distance = location1.distance_to(location2)
|
||||
self.assertAlmostEqual(distance, 8.5, delta=0.5)
|
||||
```
|
||||
|
||||
### 7.2 Integration Tests
|
||||
|
||||
```python
|
||||
# Test location creation with park
|
||||
class ParkLocationIntegrationTest(TestCase):
|
||||
def test_create_park_with_location(self):
|
||||
park = Park.objects.create(name="Test Park", ...)
|
||||
location = ParkLocation.objects.create(
|
||||
park=park,
|
||||
point=Point(-81.5639, 28.3852),
|
||||
city="Orlando",
|
||||
state="Florida"
|
||||
)
|
||||
self.assertEqual(park.park_location, location)
|
||||
self.assertEqual(park.coordinates, (28.3852, -81.5639))
|
||||
```
|
||||
|
||||
## 8. Documentation Requirements
|
||||
|
||||
### 8.1 Developer Documentation
|
||||
- Model field descriptions
|
||||
- Query examples
|
||||
- Migration guide
|
||||
- API endpoint changes
|
||||
|
||||
### 8.2 Admin Documentation
|
||||
- Location data entry guide
|
||||
- Geocoding workflow
|
||||
- Verification process
|
||||
|
||||
### 8.3 User Documentation
|
||||
- How locations are displayed
|
||||
- Road trip planning features
|
||||
- Map interactions
|
||||
|
||||
## Conclusion
|
||||
|
||||
This design provides a comprehensive transition from generic to domain-specific location models while:
|
||||
- Maintaining all existing functionality
|
||||
- Improving query performance
|
||||
- Enabling better road trip planning features
|
||||
- Keeping clean domain boundaries
|
||||
- Supporting zero-downtime migration
|
||||
|
||||
The design prioritizes parks as the primary location entities while keeping ride locations optional and company headquarters simple. All PostGIS spatial features are retained and optimized for the specific needs of each domain model.
|
||||
Reference in New Issue
Block a user