- Updated database settings to use dj_database_url for environment-based configuration - Added dj-database-url dependency - Configured PostGIS backend for spatial data support - Set default DATABASE_URL for production PostgreSQL connection
25 KiB
Parks Listing Page - Comprehensive Improvement Plan
Executive Summary
This document outlines a comprehensive improvement plan for the ThrillWiki parks listing page, focusing on enhanced location-based filtering with a hierarchical Country → State → City approach, while preserving the current design theme, park status implementation, and user experience patterns.
Primary Focus: Hierarchical Location Filtering
1. Enhanced Location Model Structure
1.1 Country-First Approach
Objective: Implement a cascading location filter starting with countries, then drilling down to states/regions, and finally cities.
Current State:
- Flat location fields in
ParkLocationmodel - Basic country/state/city filters without hierarchy
- No standardized country/region data
Proposed Enhancement:
# New model structure to support hierarchical filtering
class Country(models.Model):
name = models.CharField(max_length=100, unique=True)
code = models.CharField(max_length=3, unique=True) # ISO 3166-1 alpha-3
region = models.CharField(max_length=100) # e.g., "Europe", "North America"
park_count = models.IntegerField(default=0) # Denormalized for performance
class Meta:
verbose_name_plural = "Countries"
ordering = ['name']
class State(models.Model):
name = models.CharField(max_length=100)
country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name='states')
code = models.CharField(max_length=10, blank=True) # State/province code
park_count = models.IntegerField(default=0)
class Meta:
unique_together = [['name', 'country']]
ordering = ['name']
class City(models.Model):
name = models.CharField(max_length=100)
state = models.ForeignKey(State, on_delete=models.CASCADE, related_name='cities')
park_count = models.IntegerField(default=0)
class Meta:
verbose_name_plural = "Cities"
unique_together = [['name', 'state']]
ordering = ['name']
# Enhanced ParkLocation model
class ParkLocation(models.Model):
park = models.OneToOneField('parks.Park', on_delete=models.CASCADE, related_name='location')
# Hierarchical location references
country = models.ForeignKey(Country, on_delete=models.PROTECT)
state = models.ForeignKey(State, on_delete=models.PROTECT, null=True, blank=True)
city = models.ForeignKey(City, on_delete=models.PROTECT, null=True, blank=True)
# Legacy fields maintained for compatibility
country_legacy = models.CharField(max_length=100, blank=True)
state_legacy = models.CharField(max_length=100, blank=True)
city_legacy = models.CharField(max_length=100, blank=True)
# Existing fields preserved
point = models.PointField(srid=4326, null=True, blank=True)
street_address = models.CharField(max_length=255, blank=True)
postal_code = models.CharField(max_length=20, blank=True)
# Trip planning fields (preserved)
highway_exit = models.CharField(max_length=100, blank=True)
parking_notes = models.TextField(blank=True)
best_arrival_time = models.TimeField(null=True, blank=True)
seasonal_notes = models.TextField(blank=True)
# OSM integration (preserved)
osm_id = models.BigIntegerField(null=True, blank=True)
osm_type = models.CharField(max_length=10, blank=True)
1.2 Data Migration Strategy
Migration Phase 1: Add new fields alongside existing ones Migration Phase 2: Populate new hierarchical data from existing location data Migration Phase 3: Update forms and views to use new structure Migration Phase 4: Deprecate legacy fields (keep for backwards compatibility)
2. Advanced Filtering Interface
2.1 Hierarchical Filter Components
Location Filter Widget:
<!-- Country Selector -->
<div class="location-filter-section">
<label class="form-label">Country</label>
<select name="location_country"
hx-get="{% url 'parks:location_states' %}"
hx-target="#state-selector"
hx-include="[name='location_country']"
class="form-input">
<option value="">All Countries</option>
{% for country in countries %}
<option value="{{ country.id }}"
data-park-count="{{ country.park_count }}">
{{ country.name }} ({{ country.park_count }} parks)
</option>
{% endfor %}
</select>
</div>
<!-- State/Region Selector (Dynamic) -->
<div id="state-selector" class="location-filter-section">
<label class="form-label">State/Region</label>
<select name="location_state"
hx-get="{% url 'parks:location_cities' %}"
hx-target="#city-selector"
hx-include="[name='location_country'], [name='location_state']"
class="form-input" disabled>
<option value="">Select Country First</option>
</select>
</div>
<!-- City Selector (Dynamic) -->
<div id="city-selector" class="location-filter-section">
<label class="form-label">City</label>
<select name="location_city" class="form-input" disabled>
<option value="">Select State First</option>
</select>
</div>
2.2 Enhanced Filter Classes
class AdvancedParkFilter(ParkFilter):
# Hierarchical location filters
location_country = ModelChoiceFilter(
field_name='location__country',
queryset=Country.objects.annotate(
park_count=Count('states__cities__parklocation')
).filter(park_count__gt=0),
empty_label='All Countries',
label='Country'
)
location_state = ModelChoiceFilter(
method='filter_location_state',
queryset=State.objects.none(), # Will be populated dynamically
empty_label='All States/Regions',
label='State/Region'
)
location_city = ModelChoiceFilter(
method='filter_location_city',
queryset=City.objects.none(), # Will be populated dynamically
empty_label='All Cities',
label='City'
)
# Geographic region filters
geographic_region = ChoiceFilter(
method='filter_geographic_region',
choices=[
('north_america', 'North America'),
('europe', 'Europe'),
('asia_pacific', 'Asia Pacific'),
('latin_america', 'Latin America'),
('middle_east_africa', 'Middle East & Africa'),
],
empty_label='All Regions',
label='Geographic Region'
)
def filter_location_state(self, queryset, name, value):
if value:
return queryset.filter(location__state=value)
return queryset
def filter_location_city(self, queryset, name, value):
if value:
return queryset.filter(location__city=value)
return queryset
def filter_geographic_region(self, queryset, name, value):
region_mapping = {
'north_america': ['USA', 'Canada', 'Mexico'],
'europe': ['United Kingdom', 'Germany', 'France', 'Spain', 'Italy'],
# ... more mappings
}
if value in region_mapping:
countries = region_mapping[value]
return queryset.filter(location__country__name__in=countries)
return queryset
3. Enhanced User Experience Features
3.1 Smart Location Suggestions
// Enhanced location autocomplete with regional intelligence
class LocationSuggestionsSystem {
constructor() {
this.userLocation = null;
this.searchHistory = [];
this.preferredRegions = [];
}
// Prioritize suggestions based on user context
prioritizeSuggestions(suggestions) {
return suggestions.sort((a, b) => {
// Prioritize user's country/region
if (this.isInPreferredRegion(a) && !this.isInPreferredRegion(b)) return -1;
if (!this.isInPreferredRegion(a) && this.isInPreferredRegion(b)) return 1;
// Then by park count
return b.park_count - a.park_count;
});
}
// Add breadcrumb navigation
buildLocationBreadcrumb(country, state, city) {
const breadcrumb = [];
if (country) breadcrumb.push({type: 'country', name: country.name, id: country.id});
if (state) breadcrumb.push({type: 'state', name: state.name, id: state.id});
if (city) breadcrumb.push({type: 'city', name: city.name, id: city.id});
return breadcrumb;
}
}
3.2 Location Statistics Display
<!-- Location Statistics Panel -->
<div class="location-stats bg-gray-50 dark:bg-gray-700 rounded-lg p-4 mb-6">
<h3 class="text-lg font-medium mb-3">Browse by Location</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{% for country in top_countries %}
<div class="text-center">
<button class="location-stat-button"
hx-get="{% url 'parks:park_list' %}?location_country={{ country.id }}"
hx-target="#results-container">
<div class="text-2xl font-bold text-primary">{{ country.park_count }}</div>
<div class="text-sm text-gray-600 dark:text-gray-300">{{ country.name }}</div>
</button>
</div>
{% endfor %}
</div>
<div class="mt-4 text-center">
<button class="text-primary hover:underline text-sm" id="view-all-countries">
View All Countries →
</button>
</div>
</div>
4. Advanced Search Capabilities
4.1 Multi-Criteria Search
class AdvancedSearchForm(forms.Form):
# Text search with field weighting
query = forms.CharField(required=False, widget=forms.TextInput(attrs={
'placeholder': 'Search parks, locations, operators...',
'class': 'form-input'
}))
# Search scope selection
search_fields = forms.MultipleChoiceField(
choices=[
('name', 'Park Name'),
('description', 'Description'),
('location', 'Location'),
('operator', 'Operator'),
('rides', 'Rides'),
],
widget=forms.CheckboxSelectMultiple,
required=False,
initial=['name', 'location', 'operator']
)
# Advanced location search
location_radius = forms.IntegerField(
required=False,
min_value=1,
max_value=500,
initial=50,
widget=forms.NumberInput(attrs={'class': 'form-input'})
)
location_center = forms.CharField(required=False, widget=forms.HiddenInput())
# Saved search functionality
save_search = forms.BooleanField(required=False, label='Save this search')
search_name = forms.CharField(required=False, max_length=100)
4.2 Search Result Enhancement
<!-- Enhanced search results with location context -->
<div class="search-result-item {{ park.status|lower }}-status">
<div class="park-header">
<h3>
<a href="{% url 'parks:park_detail' park.slug %}">{{ park.name }}</a>
<span class="status-badge status-{{ park.status|lower }}">
{{ park.get_status_display }}
</span>
</h3>
<!-- Location breadcrumb -->
<div class="location-breadcrumb">
{% if park.location.country %}
<span class="breadcrumb-item">{{ park.location.country.name }}</span>
{% if park.location.state %}
<span class="breadcrumb-separator">→</span>
<span class="breadcrumb-item">{{ park.location.state.name }}</span>
{% if park.location.city %}
<span class="breadcrumb-separator">→</span>
<span class="breadcrumb-item">{{ park.location.city.name }}</span>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
<!-- Search relevance indicators -->
<div class="search-meta">
{% if park.distance %}
<span class="distance-indicator">{{ park.distance|floatformat:1 }}km away</span>
{% endif %}
{% if park.search_score %}
<span class="relevance-score">{{ park.search_score|floatformat:0 }}% match</span>
{% endif %}
</div>
</div>
5. Map Integration Features
5.1 Location-Aware Map Views
<!-- Interactive map component -->
<div class="map-container" x-data="parkMap()">
<div id="park-map" class="h-96 rounded-lg"></div>
<div class="map-controls">
<button @click="fitToCountry(selectedCountry)"
x-show="selectedCountry"
class="btn btn-sm">
Zoom to {{ selectedCountryName }}
</button>
<button @click="showHeatmap = !showHeatmap"
class="btn btn-sm">
<span x-text="showHeatmap ? 'Hide' : 'Show'"></span> Density
</button>
</div>
</div>
5.2 Geographic Clustering
class ParkMapView(TemplateView):
template_name = 'parks/park_map.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get parks with location data
parks = get_base_park_queryset().filter(
location__point__isnull=False
).select_related('location__country', 'location__state', 'location__city')
# Apply filters
filter_form = AdvancedParkFilter(self.request.GET, queryset=parks)
parks = filter_form.qs
# Prepare map data with clustering
map_data = []
for park in parks:
map_data.append({
'id': park.id,
'name': park.name,
'slug': park.slug,
'status': park.status,
'coordinates': [park.location.latitude, park.location.longitude],
'country': park.location.country.name,
'state': park.location.state.name if park.location.state else None,
'city': park.location.city.name if park.location.city else None,
})
context.update({
'parks_json': json.dumps(map_data),
'center_point': self._calculate_center_point(parks),
'filter_form': filter_form,
})
return context
6. Performance Optimizations
6.1 Caching Strategy
from django.core.cache import cache
from django.db.models.signals import post_save, post_delete
class LocationCacheManager:
CACHE_TIMEOUT = 3600 * 24 # 24 hours
@staticmethod
def get_country_stats():
cache_key = 'park_countries_stats'
stats = cache.get(cache_key)
if stats is None:
stats = Country.objects.annotate(
park_count=Count('states__cities__parklocation__park')
).filter(park_count__gt=0).order_by('-park_count')
cache.set(cache_key, stats, LocationCacheManager.CACHE_TIMEOUT)
return stats
@staticmethod
def invalidate_location_cache():
cache.delete_many([
'park_countries_stats',
'park_states_stats',
'park_cities_stats'
])
# Signal handlers for cache invalidation
@receiver([post_save, post_delete], sender=Park)
def invalidate_park_location_cache(sender, **kwargs):
LocationCacheManager.invalidate_location_cache()
6.2 Database Indexing Strategy
class ParkLocation(models.Model):
# ... existing fields ...
class Meta:
indexes = [
models.Index(fields=['country', 'state', 'city']),
models.Index(fields=['country', 'park_count']),
models.Index(fields=['state', 'park_count']),
models.Index(fields=['city', 'park_count']),
models.Index(fields=['point']), # Spatial index
]
7. Preserve Current Design Elements
7.1 Status Implementation (Preserved)
The current park status system is well-designed and should be maintained exactly as-is:
- Status badge colors and styling remain unchanged
get_status_color()method preserved- CSS classes for status badges maintained
- Status filtering functionality kept identical
7.2 Design Theme Consistency
All new components will follow existing design patterns:
- Tailwind CSS v4 color palette (primary:
#4f46e5, secondary:#e11d48, accent:#8b5cf6) - Poppins font family
- Card design patterns with hover effects
- Dark mode support for all new elements
- Consistent spacing and typography scales
7.3 HTMX Integration Patterns
New filtering components will use established HTMX patterns:
- Form submissions with
hx-getandhx-target - URL state management with
hx-push-url - Loading indicators with
hx-indicator - Error handling with
HX-Triggerevents
8. Implementation Phases
Phase 1: Foundation (Weeks 1-2)
- Create new location models (Country, State, City)
- Build data migration scripts
- Implement location cache management
- Add database indexes
Phase 2: Backend Integration (Weeks 3-4)
- Update ParkLocation model with hierarchical references
- Enhance filtering system with new location filters
- Build dynamic location endpoint views
- Update querysets and managers
Phase 3: Frontend Enhancement (Weeks 5-6)
- Create hierarchical location filter components
- Implement HTMX dynamic loading for states/cities
- Add location statistics display
- Enhance search result presentation
Phase 4: Advanced Features (Weeks 7-8)
- Implement map integration
- Add geographic clustering
- Build advanced search capabilities
- Create location-aware suggestions
Phase 5: Testing & Optimization (Weeks 9-10)
- Performance testing and optimization
- Accessibility testing and improvements
- Mobile responsiveness verification
- User experience testing
9. Form Update Requirements
Based on the model changes, the following forms will need updates:
9.1 ParkForm Updates
class EnhancedParkForm(ParkForm):
# Location selection fields
location_country = forms.ModelChoiceField(
queryset=Country.objects.all(),
required=False,
widget=forms.Select(attrs={'class': 'form-input'})
)
location_state = forms.ModelChoiceField(
queryset=State.objects.none(),
required=False,
widget=forms.Select(attrs={'class': 'form-input'})
)
location_city = forms.ModelChoiceField(
queryset=City.objects.none(),
required=False,
widget=forms.Select(attrs={'class': 'form-input'})
)
# Keep existing coordinate fields
latitude = forms.DecimalField(...) # Unchanged
longitude = forms.DecimalField(...) # Unchanged
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Pre-populate hierarchical location fields if editing
if self.instance and self.instance.pk:
if hasattr(self.instance, 'location') and self.instance.location:
location = self.instance.location
if location.country:
self.fields['location_country'].initial = location.country
self.fields['location_state'].queryset = location.country.states.all()
if location.state:
self.fields['location_state'].initial = location.state
self.fields['location_city'].queryset = location.state.cities.all()
if location.city:
self.fields['location_city'].initial = location.city
def save(self, commit=True):
park = super().save(commit=False)
if commit:
park.save()
# Handle hierarchical location assignment
country = self.cleaned_data.get('location_country')
state = self.cleaned_data.get('location_state')
city = self.cleaned_data.get('location_city')
if country:
location, created = ParkLocation.objects.get_or_create(park=park)
location.country = country
location.state = state
location.city = city
# Maintain legacy fields for compatibility
location.country_legacy = country.name
if state:
location.state_legacy = state.name
if city:
location.city_legacy = city.name
# Handle coordinates (existing logic preserved)
if self.cleaned_data.get('latitude') and self.cleaned_data.get('longitude'):
location.set_coordinates(
float(self.cleaned_data['latitude']),
float(self.cleaned_data['longitude'])
)
location.save()
return park
9.2 Filter Form Updates
The ParkFilter class will be extended rather than replaced to maintain backward compatibility:
class ParkFilter(FilterSet):
# All existing filters preserved unchanged
search = CharFilter(...) # Unchanged
status = ChoiceFilter(...) # Unchanged
# ... all other existing filters preserved ...
# New hierarchical location filters added
country = ModelChoiceFilter(
field_name='location__country',
queryset=Country.objects.annotate(
park_count=Count('states__cities__parklocation')
).filter(park_count__gt=0).order_by('name'),
empty_label='All Countries'
)
state = ModelChoiceFilter(
method='filter_state',
queryset=State.objects.none(),
empty_label='All States/Regions'
)
city = ModelChoiceFilter(
method='filter_city',
queryset=City.objects.none(),
empty_label='All Cities'
)
# Preserve all existing filter methods
def filter_search(self, queryset, name, value):
# Existing implementation unchanged
pass
# Add new filter methods
def filter_state(self, queryset, name, value):
if value:
return queryset.filter(location__state=value)
return queryset
def filter_city(self, queryset, name, value):
if value:
return queryset.filter(location__city=value)
return queryset
10. Migration Strategy
10.1 Data Migration Plan
# Migration 0001: Create hierarchical location models
class Migration(migrations.Migration):
operations = [
migrations.CreateModel('Country', ...),
migrations.CreateModel('State', ...),
migrations.CreateModel('City', ...),
migrations.AddField('ParkLocation', 'country_ref', ...),
migrations.AddField('ParkLocation', 'state_ref', ...),
migrations.AddField('ParkLocation', 'city_ref', ...),
]
# Migration 0002: Populate hierarchical data
def populate_hierarchical_data(apps, schema_editor):
ParkLocation = apps.get_model('parks', 'ParkLocation')
Country = apps.get_model('parks', 'Country')
State = apps.get_model('parks', 'State')
City = apps.get_model('parks', 'City')
# Create country entries from existing data
countries = ParkLocation.objects.values_list('country', flat=True).distinct()
for country_name in countries:
if country_name:
country, created = Country.objects.get_or_create(
name=country_name,
defaults={'code': get_country_code(country_name)}
)
# Similar logic for states and cities...
class Migration(migrations.Migration):
operations = [
migrations.RunPython(populate_hierarchical_data, migrations.RunPython.noop),
]
Success Metrics
-
User Experience Metrics:
- Reduced average time to find parks by location (target: -30%)
- Increased filter usage rate (target: +50%)
- Improved mobile usability scores
-
Performance Metrics:
- Maintained page load times under 2 seconds
- Database query count reduction for location filters
- Cached response hit rate above 85%
-
Feature Adoption:
- Hierarchical location filter usage above 40%
- Map view engagement increase of 25%
- Advanced search feature adoption of 15%
Conclusion
This comprehensive improvement plan enhances the parks listing page with sophisticated location-based filtering while preserving all current design elements, status implementation, and user experience patterns. The hierarchical Country → State → City approach provides intuitive navigation, while advanced features like map integration and enhanced search capabilities create a more engaging user experience.
The phased implementation approach ensures minimal disruption to current functionality while progressively enhancing capabilities. All improvements maintain backward compatibility and preserve the established design language that users have come to expect from ThrillWiki.