mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-27 02:07:04 -05:00
Configure PostgreSQL with PostGIS support
- 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
This commit is contained in:
700
parks_listing_improvement_plan.md
Normal file
700
parks_listing_improvement_plan.md
Normal file
@@ -0,0 +1,700 @@
|
||||
# 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 `ParkLocation` model
|
||||
- Basic country/state/city filters without hierarchy
|
||||
- No standardized country/region data
|
||||
|
||||
**Proposed Enhancement**:
|
||||
```python
|
||||
# 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**:
|
||||
```html
|
||||
<!-- 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
|
||||
```python
|
||||
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
|
||||
```javascript
|
||||
// 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
|
||||
```html
|
||||
<!-- 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
|
||||
```python
|
||||
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
|
||||
```html
|
||||
<!-- 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
|
||||
```html
|
||||
<!-- 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
|
||||
```python
|
||||
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
|
||||
```python
|
||||
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
|
||||
```python
|
||||
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-get` and `hx-target`
|
||||
- URL state management with `hx-push-url`
|
||||
- Loading indicators with `hx-indicator`
|
||||
- Error handling with `HX-Trigger` events
|
||||
|
||||
### 8. Implementation Phases
|
||||
|
||||
#### Phase 1: Foundation (Weeks 1-2)
|
||||
1. Create new location models (Country, State, City)
|
||||
2. Build data migration scripts
|
||||
3. Implement location cache management
|
||||
4. Add database indexes
|
||||
|
||||
#### Phase 2: Backend Integration (Weeks 3-4)
|
||||
1. Update ParkLocation model with hierarchical references
|
||||
2. Enhance filtering system with new location filters
|
||||
3. Build dynamic location endpoint views
|
||||
4. Update querysets and managers
|
||||
|
||||
#### Phase 3: Frontend Enhancement (Weeks 5-6)
|
||||
1. Create hierarchical location filter components
|
||||
2. Implement HTMX dynamic loading for states/cities
|
||||
3. Add location statistics display
|
||||
4. Enhance search result presentation
|
||||
|
||||
#### Phase 4: Advanced Features (Weeks 7-8)
|
||||
1. Implement map integration
|
||||
2. Add geographic clustering
|
||||
3. Build advanced search capabilities
|
||||
4. Create location-aware suggestions
|
||||
|
||||
#### Phase 5: Testing & Optimization (Weeks 9-10)
|
||||
1. Performance testing and optimization
|
||||
2. Accessibility testing and improvements
|
||||
3. Mobile responsiveness verification
|
||||
4. User experience testing
|
||||
|
||||
### 9. Form Update Requirements
|
||||
|
||||
Based on the model changes, the following forms will need updates:
|
||||
|
||||
#### 9.1 ParkForm Updates
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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
|
||||
```python
|
||||
# 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
|
||||
|
||||
1. **User Experience Metrics**:
|
||||
- Reduced average time to find parks by location (target: -30%)
|
||||
- Increased filter usage rate (target: +50%)
|
||||
- Improved mobile usability scores
|
||||
|
||||
2. **Performance Metrics**:
|
||||
- Maintained page load times under 2 seconds
|
||||
- Database query count reduction for location filters
|
||||
- Cached response hit rate above 85%
|
||||
|
||||
3. **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.
|
||||
Reference in New Issue
Block a user