Add autocomplete functionality for parks: implement URLs, views, and templates for real-time suggestions

This commit is contained in:
pacnpal
2025-02-23 13:07:27 -05:00
parent 8e9b6b6a15
commit 1876af46d9
26 changed files with 862 additions and 191 deletions

49
autocomplete/__init__.py Normal file
View File

@@ -0,0 +1,49 @@
default_app_config = 'autocomplete.apps.AutocompleteConfig'
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Widget
from django.template.loader import render_to_string
class ModelAutocomplete:
"""Base class for model-based autocomplete."""
model = None # Model class to use for autocomplete
search_attrs = [] # List of model attributes to search
minimum_search_length = 2 # Minimum length of search string
max_results = 10 # Maximum number of results to return
def __init__(self):
if not self.model:
raise ImproperlyConfigured("ModelAutocomplete requires a model class")
if not self.search_attrs:
raise ImproperlyConfigured("ModelAutocomplete requires search_attrs")
def get_search_results(self, search):
"""Return search results for a given search string."""
raise NotImplementedError("Subclasses must implement get_search_results()")
def format_result(self, obj):
"""Format a single result object."""
raise NotImplementedError("Subclasses must implement format_result()")
class AutocompleteWidget(Widget):
"""Widget for autocomplete fields."""
template_name = 'autocomplete/widget.html'
def __init__(self, ac_class, attrs=None):
super().__init__(attrs)
if not issubclass(ac_class, ModelAutocomplete):
raise ImproperlyConfigured("ac_class must be a subclass of ModelAutocomplete")
self.ac_class = ac_class
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
# Add ac_name for URL resolution
context['ac_name'] = self.ac_class.__name__.lower()
return context
def render(self, name, value, attrs=None, renderer=None):
context = self.get_context(name, value, attrs)
return render_to_string(self.template_name, context)

25
autocomplete/apps.py Normal file
View File

@@ -0,0 +1,25 @@
from django.apps import AppConfig
class AutocompleteConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'autocomplete'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._registry = {}
def ready(self):
"""Register all autocomplete classes."""
from parks.forms import ParkAutocomplete
# Register autocomplete classes
self.register_autocomplete('park', ParkAutocomplete)
def register_autocomplete(self, name, ac_class):
"""Register an autocomplete class."""
self._registry[name] = ac_class
def get_autocomplete_class(self, name):
"""Get an autocomplete class by name."""
return self._registry.get(name)

View File

@@ -0,0 +1,20 @@
{% if results %}
<ul class="py-1 overflow-auto max-h-60" role="listbox">
{% for result in results %}
<li class="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
role="option"
@click="selectedId = '{{ result.key }}'; query = '{{ result.label }}'; $refs.filterForm.requestSubmit()">
<div class="flex flex-col">
<span class="font-medium">{{ result.label }}</span>
{% if result.extra %}
<span class="text-sm text-gray-500 dark:text-gray-400">{{ result.extra }}</span>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="px-4 py-2 text-gray-500 dark:text-gray-400">
No results found
</div>
{% endif %}

View File

@@ -0,0 +1,38 @@
{% load static %}
<div class="relative" x-data="{ query: '', selectedId: null }">
<input type="text"
name="{{ widget.name }}_search"
placeholder="{{ widget.attrs.placeholder|default:'Search...' }}"
class="{{ widget.attrs.class }}"
x-model="query"
@keydown.escape="query = ''"
hx-get="{% url 'autocomplete:items' ac_name %}"
hx-trigger="input changed delay:300ms"
hx-target="#{{ widget.name }}-suggestions"
hx-indicator="#{{ widget.name }}-indicator">
<input type="hidden"
name="{{ widget.name }}"
x-model="selectedId">
<!-- Loading indicator -->
<div id="{{ widget.name }}-indicator"
class="htmx-indicator absolute right-3 top-1/2 -translate-y-1/2"
role="status"
aria-label="Loading search results">
<svg class="w-5 h-5 text-gray-400 animate-spin" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<span class="sr-only">Searching...</span>
</div>
<!-- Suggestions dropdown -->
<div id="{{ widget.name }}-suggestions"
class="absolute z-50 mt-1 w-full bg-white dark:bg-gray-800 rounded-md shadow-lg"
role="listbox"
style="display: none;"
x-show="query.length > 0">
</div>
</div>

9
autocomplete/urls.py Normal file
View File

@@ -0,0 +1,9 @@
from django.urls import path
from . import views
app_name = 'autocomplete'
urlpatterns = [
path('<str:ac_name>/items/', views.items, name='items'),
path('<str:ac_name>/toggle/', views.toggle, name='toggle'),
]

52
autocomplete/views.py Normal file
View File

@@ -0,0 +1,52 @@
from django.http import JsonResponse, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
def items(request, ac_name):
"""Return autocomplete items for a given autocomplete class."""
try:
# Get the autocomplete class from the registry
ac_class = apps.get_app_config('autocomplete').get_autocomplete_class(ac_name)
if not ac_class:
raise ImproperlyConfigured(f"No autocomplete class found for {ac_name}")
# Create instance and get results
ac = ac_class()
search = request.GET.get('search', '')
# Check minimum search length
if len(search) < ac.minimum_search_length:
return HttpResponse('')
# Get and format results
results = ac.get_search_results(search)[:ac.max_results]
formatted_results = [ac.format_result(obj) for obj in results]
# Render suggestions template
return render(request, 'autocomplete/suggestions.html', {
'results': formatted_results
})
except Exception as e:
return HttpResponse(str(e), status=400)
def toggle(request, ac_name):
"""Toggle selection state for an autocomplete item."""
try:
# Get the autocomplete class from the registry
ac_class = apps.get_app_config('autocomplete').get_autocomplete_class(ac_name)
if not ac_class:
raise ImproperlyConfigured(f"No autocomplete class found for {ac_name}")
# Create instance and handle toggle
ac = ac_class()
item_id = request.POST.get('id')
if not item_id:
raise ValueError("No item ID provided")
# Get the object and format it
obj = get_object_or_404(ac.model, pk=item_id)
result = ac.format_result(obj)
return JsonResponse(result)
except Exception as e:
return JsonResponse({'error': str(e)}, status=400)