diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 00000000..ac0fe3af --- /dev/null +++ b/core/forms.py @@ -0,0 +1,39 @@ +"""Core forms and form components.""" +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.utils.translation import gettext_lazy as _ + +from autocomplete import Autocomplete + + +class BaseAutocomplete(Autocomplete): + """Base autocomplete class for consistent autocomplete behavior across the project. + + This class extends django-htmx-autocomplete's base Autocomplete class to provide: + - Project-wide defaults for autocomplete behavior + - Translation strings + - Authentication enforcement + - Sensible search configuration + """ + # Search configuration + minimum_search_length = 2 # More responsive than default 3 + max_results = 10 # Reasonable limit for performance + + # UI text configuration using gettext for i18n + no_result_text = _("No matches found") + narrow_search_text = _("Showing %(page_size)s of %(total)s matches. Please refine your search.") + type_at_least_n_characters = _("Type at least %(n)s characters...") + + # Project-wide component settings + placeholder = _("Search...") + + @staticmethod + def auth_check(request): + """Enforce authentication by default. + + This can be overridden in subclasses if public access is needed. + Configure AUTOCOMPLETE_BLOCK_UNAUTHENTICATED in settings to disable. + """ + block_unauth = getattr(settings, 'AUTOCOMPLETE_BLOCK_UNAUTHENTICATED', True) + if block_unauth and not request.user.is_authenticated: + raise PermissionDenied(_("Authentication required")) \ No newline at end of file diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index e165e2c6..420c679e 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,37 +1,74 @@ -# Active Context - Laravel Migration Analysis +# Active Development Context -**Objective:** Evaluate feasibility and impact of migrating from Django to Laravel +## Recently Completed -**Key Decision:** ⛔️ Do NOT proceed with Laravel migration (see detailed analysis in `decisions/laravel_migration_analysis.md`) +### Park Search Implementation (2024-02-22) -**Analysis Summary:** -1. **High Technical Risk** - - Complex custom Django features - - Extensive model relationships - - Specialized history tracking system - - Geographic/location services integration +1. Autocomplete Base: + - Created BaseAutocomplete in core/forms.py + - Configured project-wide auth requirement + - Added test coverage for base functionality -2. **Significant Business Impact** - - Estimated 4-6 month timeline - - $180,000-230,000 direct costs - - Service disruption risks - - Resource-intensive implementation +2. Park Search: + - Implemented ParkAutocomplete class + - Created ParkSearchForm with autocomplete widget + - Updated views and templates for integration + - Added comprehensive test suite -3. **Critical Systems Affected** - - Authentication and permissions - - Data model architecture - - Template system and HTMX integration - - API and service layers +3. Documentation: + - Updated memory-bank/features/parks/search.md + - Added test documentation + - Created user interface guidelines -**Recommended Direction:** -1. Maintain and enhance current Django implementation -2. Focus on feature development and optimization -3. Consider hybrid approach for new features if needed +## Active Tasks -**Next Steps:** -1. Document current system architecture thoroughly -2. Identify optimization opportunities -3. Update dependencies and security -4. Enhance development workflows +1. Testing: + - [ ] Run the test suite with `uv run pytest parks/tests/` + - [ ] Monitor test coverage with pytest-cov + - [ ] Verify HTMX interactions work as expected -**Previous Context:** Park View Modularization work can continue as planned - the decision to maintain Django architecture means we can proceed with planned UI improvements. \ No newline at end of file +2. Performance Monitoring: + - [ ] Add database indexes if needed + - [ ] Monitor query performance + - [ ] Consider caching strategies + +3. User Experience: + - [ ] Get feedback on search responsiveness + - [ ] Monitor error rates + - [ ] Check accessibility compliance + +## Next Steps + +1. Enhancements: + - Add geographic search capabilities + - Implement result caching + - Add full-text search support + +2. Integration: + - Extend to other models (Rides, Areas) + - Add combined search functionality + - Improve filter integration + +3. Testing: + - Add Playwright e2e tests + - Implement performance benchmarks + - Add accessibility tests + +## Technical Debt + +None currently identified for the search implementation. + +## Dependencies + +- django-htmx-autocomplete +- pytest-django +- pytest-cov + +## Notes + +The implementation follows these principles: +- Authentication-first approach +- Performance optimization +- Accessibility compliance +- Test coverage +- Clean documentation \ No newline at end of file diff --git a/memory-bank/features/autocomplete/base.md b/memory-bank/features/autocomplete/base.md new file mode 100644 index 00000000..60c05c30 --- /dev/null +++ b/memory-bank/features/autocomplete/base.md @@ -0,0 +1,63 @@ +# Base Autocomplete Implementation + +The project uses `django-htmx-autocomplete` with a custom base implementation to ensure consistent behavior across all autocomplete widgets. + +## BaseAutocomplete Class + +Located in `core/forms.py`, the `BaseAutocomplete` class provides project-wide defaults and standardization: + +```python +from core.forms import BaseAutocomplete + +class MyModelAutocomplete(BaseAutocomplete): + model = MyModel + search_attrs = ['name', 'description'] +``` + +### Features + +- **Authentication Enforcement**: Requires user authentication by default + - Controlled via `AUTOCOMPLETE_BLOCK_UNAUTHENTICATED` setting + - Override `auth_check()` for custom auth logic + +- **Search Configuration** + - `minimum_search_length = 2` - More responsive than default 3 + - `max_results = 10` - Optimized for performance + +- **Internationalization** + - All text strings use Django's translation system + - Customizable messages through class attributes + +### Usage Guidelines + +1. Always extend `BaseAutocomplete` instead of using `autocomplete.Autocomplete` directly +2. Configure search_attrs based on your model's indexed fields +3. Use the AutocompleteWidget with proper options: + +```python +class MyForm(forms.ModelForm): + class Meta: + model = MyModel + fields = ['related_field'] + widgets = { + 'related_field': AutocompleteWidget( + ac_class=MyModelAutocomplete, + options={ + "multiselect": True, # For M2M fields + "placeholder": "Custom placeholder..." # Optional + } + ) + } +``` + +### Performance Considerations + +- Keep `search_attrs` minimal and indexed +- Use `select_related`/`prefetch_related` in custom querysets +- Consider caching for frequently used results + +### Security Notes + +- Authentication required by default +- Implements proper CSRF protection via HTMX +- Rate limiting should be implemented at the web server level \ No newline at end of file diff --git a/memory-bank/features/parks/search.md b/memory-bank/features/parks/search.md new file mode 100644 index 00000000..285e9eae --- /dev/null +++ b/memory-bank/features/parks/search.md @@ -0,0 +1,105 @@ +# Park Search Implementation + +## Architecture + +The park search functionality uses a combination of: +- BaseAutocomplete for search suggestions +- django-htmx for async updates +- Django filters for advanced filtering + +### Components + +1. **Forms** + - `ParkAutocomplete`: Handles search suggestions + - `ParkSearchForm`: Integrates autocomplete with search form + +2. **Views** + - `ParkSearchView`: Class-based view handling search and filters + - `suggest_parks`: Legacy endpoint maintained for backward compatibility + +3. **Templates** + - Simplified search UI using autocomplete widget + - Integrated loading indicators + - Filter form for additional search criteria + +## Implementation Details + +### Search Form +```python +class ParkSearchForm(forms.Form): + park = forms.ModelChoiceField( + queryset=Park.objects.all(), + required=False, + widget=AutocompleteWidget( + ac_class=ParkAutocomplete, + attrs={ + 'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white', + 'placeholder': 'Search parks...' + } + ) + ) +``` + +### Autocomplete +```python +class ParkAutocomplete(BaseAutocomplete): + model = Park + search_attrs = ['name'] + + def get_search_results(self, search): + return (get_base_park_queryset() + .filter(name__icontains=search) + .select_related('owner') + .order_by('name')) +``` + +### View Integration +```python +class ParkSearchView(TemplateView): + template_name = "parks/park_list.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['search_form'] = ParkSearchForm(self.request.GET) + # ... filter handling ... + return context +``` + +## Features + +1. **Security** + - Tiered access control: + * Public basic search + * Authenticated users get autocomplete + * Protected endpoints via settings + - CSRF protection + - Input validation + +2. **Real-time Search** + - Debounced input handling + - Instant results display + - Loading indicators + +3. **Accessibility** + - ARIA labels and roles + - Keyboard navigation support + - Screen reader compatibility + +4. **Integration** + - Works with existing filter system + - Maintains view mode selection + - Preserves URL state + +## Performance Considerations + +- Prefetch related owner data +- Uses base queryset optimizations +- Debounced search requests +- Proper index usage on name field + +## Future Improvements + +- Consider adding full-text search +- Implement result caching +- Add geographic search capabilities +- Enhance filter integration \ No newline at end of file diff --git a/parks/forms.py b/parks/forms.py index bb7aa705..74d7436a 100644 --- a/parks/forms.py +++ b/parks/forms.py @@ -1,7 +1,54 @@ from django import forms from decimal import Decimal, InvalidOperation, ROUND_DOWN +from autocomplete import AutocompleteWidget + +from core.forms import BaseAutocomplete from .models import Park from location.models import Location +from .querysets import get_base_park_queryset + + +class ParkAutocomplete(BaseAutocomplete): + """Autocomplete for searching parks. + + Features: + - Name-based search with partial matching + - Prefetches related owner data + - Applies standard park queryset filtering + - Includes park status and location in results + """ + model = Park + search_attrs = ['name'] # We'll match on park names + + def get_search_results(self, search): + """Return search results with related data.""" + return (get_base_park_queryset() + .filter(name__icontains=search) + .select_related('owner') + .order_by('name')) + + def format_result(self, park): + """Format each park result with status and location.""" + location = park.formatted_location + location_text = f" • {location}" if location else "" + return { + 'key': str(park.pk), + 'label': park.name, + 'extra': f"{park.get_status_display()}{location_text}" + } + + +class ParkSearchForm(forms.Form): + """Form for searching parks with autocomplete.""" + park = forms.ModelChoiceField( + queryset=Park.objects.all(), + required=False, + widget=AutocompleteWidget( + ac_class=ParkAutocomplete, + attrs={'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white', + 'placeholder': 'Search parks...'} + ) + ) class ParkForm(forms.ModelForm): diff --git a/parks/querysets.py b/parks/querysets.py index 26d41fb1..953ed40d 100644 --- a/parks/querysets.py +++ b/parks/querysets.py @@ -3,9 +3,17 @@ from .models import Park def get_base_park_queryset() -> QuerySet[Park]: """Get base queryset with all needed annotations and prefetches""" + from django.contrib.contenttypes.models import ContentType + + park_type = ContentType.objects.get_for_model(Park) return ( Park.objects.select_related('owner') - .prefetch_related('location', 'photos', 'rides') + .prefetch_related( + 'photos', + 'rides', + 'location', + 'location__content_type' + ) .annotate( current_ride_count=Count('rides', distinct=True), current_coaster_count=Count('rides', filter=Q(rides__category="RC"), distinct=True) diff --git a/parks/templates/parks/park_list.html b/parks/templates/parks/park_list.html index 73f06b3e..25a46e59 100644 --- a/parks/templates/parks/park_list.html +++ b/parks/templates/parks/park_list.html @@ -47,50 +47,25 @@ {% block filter_section %}