Add autocomplete functionality for parks: implement BaseAutocomplete class and integrate with forms

This commit is contained in:
pacnpal
2025-02-22 13:36:24 -05:00
parent 5278ad39d0
commit 4339c5c5e0
15 changed files with 633 additions and 130 deletions

39
core/forms.py Normal file
View File

@@ -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"))

View File

@@ -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.
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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -47,50 +47,25 @@
{% block filter_section %}
<div class="mb-6">
<div class="max-w-3xl mx-auto relative mb-8">
<label for="search" class="sr-only">Search parks</label>
<div class="relative">
<div x-data="{
open: false,
query: '{{ request.GET.search|default:'' }}',
focusedIndex: -1,
suggestions: []
}"
@click.away="open = false"
x-init="$watch('query', value => { console.log('query:', value); console.log('open:', open) })"
class="relative">
<input type="search"
name="search"
id="search"
class="block w-full rounded-md border-gray-300 bg-white py-3 pl-4 pr-10 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 sm:text-sm"
placeholder="Search parks by name or location..."
x-model="query"
hx-get="{% url 'parks:search_parks' %}?view_mode={{ view_mode|default:'grid' }}"
hx-trigger="input delay:500ms, search"
<div class="w-full relative">
<form hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-push-url="true"
hx-indicator="#search-indicator"
aria-label="Search parks"
@keydown.down.prevent="focusedIndex = Math.min(focusedIndex + 1, $refs.suggestionsList?.children.length - 1 || 0)"
@keydown.up.prevent="focusedIndex = Math.max(focusedIndex - 1, -1)"
@keydown.enter.prevent="if (focusedIndex >= 0) $refs.suggestionsList?.children[focusedIndex]?.click()">
<div class="relative">
<div hx-get="{% url 'parks:suggest_parks' %}?view_mode={{ view_mode|default:'grid' }}"
hx-trigger="input[target.value.length > 1] delay:300ms from:input"
hx-target="this"
hx-swap="innerHTML"
hx-include="closest input[name=search]"
x-ref="suggestionsList"
@htmx:afterRequest="open = detail.xhr.response.trim().length > 0"
@htmx:beforeRequest="open = false"
class="absolute top-full left-0 right-0 mt-1 z-50"></div>
</div>
</div>
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
<div id="search-indicator" class="htmx-indicator">
hx-trigger="change from:.park-search">
{% csrf_token %}
{{ search_form.park }}
</form>
<!-- Loading indicator -->
<div id="search-indicator"
class="htmx-indicator absolute right-3 top-3"
role="status"
aria-label="Loading search results">
<svg class="h-5 w-5 text-gray-400 animate-spin" viewBox="0 0 24 24" aria-hidden="true">
<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>
</div>
</div>
@@ -118,14 +93,3 @@
{% include "parks/partials/park_list_item.html" with parks=parks view_mode=view_mode|default:'grid' %}
</div>
{% endblock %}
{% block extra_css %}
<style>
[x-cloak] { display: none !important; }
</style>
{% endblock %}
{% block extra_js %}
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="{% static 'parks/js/search.js' %}"></script>
{% endblock %}

View File

@@ -1,5 +1,7 @@
{% load filter_utils %}
{% if suggestions %}
<div class="w-full bg-white rounded-md shadow-lg border border-gray-200 max-h-96 overflow-y-auto"
<div id="search-suggestions-results"
class="w-full bg-white rounded-md shadow-lg border border-gray-200 max-h-96 overflow-y-auto"
x-show="open"
x-cloak
@keydown.escape.window="open = false"
@@ -10,20 +12,26 @@
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95">
{% for park in suggestions %}
<a href="{% url 'parks:park_detail' slug=park.slug %}"
class="block px-4 py-2 text-sm hover:bg-gray-100 cursor-pointer flex items-center justify-between"
:class="{ 'bg-gray-100': focusedIndex === {{ forloop.counter0 }} }"
hx-get="{% url 'parks:search_parks' %}?search={{ park.name }}&view_mode={{ view_mode|default:'grid' }}"
hx-target="#park-results"
hx-push-url="true"
@mousedown.prevent
@click="query = '{{ park.name }}'; open = false">
<span class="font-medium">{{ park.name }}</span>
{% with location=park.location.first %}
<button type="button"
class="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 cursor-pointer flex items-center justify-between gap-2 transition duration-150"
:class="{ 'bg-blue-50': focusedIndex === {{ forloop.counter0 }} }"
@mousedown.prevent="query = '{{ park.name }}'; $refs.search.value = '{{ park.name }}'"
@mousedown.prevent="query = '{{ park.name }}'; $dispatch('search-selected', '{{ park.name }}'); open = false;"
role="option"
:aria-selected="focusedIndex === {{ forloop.counter0 }}"
tabindex="-1"
x-effect="if(focusedIndex === {{ forloop.counter0 }}) $el.scrollIntoView({block: 'nearest'})"
aria-label="{{ park.name }}{% if location.city %} in {{ location.city }}{% endif %}{% if location.state %}, {{ location.state }}{% endif %}">
<div class="flex items-center gap-2">
<span class="font-medium" x-text="focusedIndex === {{ forloop.counter0 }} ? '▶ {{ park.name }}' : '{{ park.name }}'"></span>
<span class="text-gray-500">
{% if park.location.first.city %}{{ park.location.first.city }}, {% endif %}
{% if park.location.first.state %}{{ park.location.first.state }}{% endif %}
{% if location.city %}{{ location.city }}, {% endif %}
{% if location.state %}{{ location.state }}{% endif %}
</span>
</a>
</div>
</button>
{% endwith %}
{% endfor %}
</div>
{% endif %}

81
parks/tests/README.md Normal file
View File

@@ -0,0 +1,81 @@
# Park Search Tests
## Overview
Test suite for the park search functionality including:
- Autocomplete widget integration
- Search form validation
- Filter integration
- HTMX interaction
- View mode persistence
## Running Tests
```bash
# Run all park tests
uv run pytest parks/tests/
# Run specific search tests
uv run pytest parks/tests/test_search.py
# Run with coverage
uv run pytest --cov=parks parks/tests/
```
## Test Coverage
### Unit Tests
- `test_autocomplete_results`: Validates search result filtering
- `test_search_form_valid`: Ensures form validation works
- `test_autocomplete_class`: Checks autocomplete configuration
- `test_search_with_filters`: Verifies filter integration
### Integration Tests
- `test_empty_search`: Tests default behavior
- `test_partial_match_search`: Validates partial text matching
- `test_htmx_request_handling`: Ensures HTMX compatibility
- `test_view_mode_persistence`: Checks view state management
- `test_unauthenticated_access`: Verifies authentication requirements
### Security Tests
Parks search implements a tiered access approach:
- Basic search is public
- Autocomplete requires authentication
- Configuration set in settings.py: `AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = True`
## Configuration
Tests use pytest-django and require:
- PostgreSQL database
- HTMX middleware
- Autocomplete app configuration
## Fixtures
The test suite uses standard Django test fixtures. No additional fixtures required.
## Common Issues
1. Database Errors
- Ensure PostGIS extensions are installed
- Verify database permissions
2. HTMX Tests
- Use `HTTP_HX_REQUEST` header for HTMX requests
- Check response content for HTMX attributes
## Adding New Tests
When adding tests, ensure:
1. Database isolation using `@pytest.mark.django_db`
2. Proper test naming following `test_*` convention
3. Clear test descriptions in docstrings
4. Coverage for both success and failure cases
5. HTMX interaction testing where applicable
## Future Improvements
- Add performance benchmarks
- Include accessibility tests
- Add Playwright e2e tests
- Implement geographic search tests

119
parks/tests/test_search.py Normal file
View File

@@ -0,0 +1,119 @@
import pytest
from django.urls import reverse
from django.test import Client
from parks.models import Park
from parks.forms import ParkAutocomplete, ParkSearchForm
@pytest.mark.django_db
class TestParkSearch:
def test_autocomplete_results(self, client: Client):
"""Test that autocomplete returns correct results"""
# Create test parks
park1 = Park.objects.create(name="Test Park")
park2 = Park.objects.create(name="Another Park")
park3 = Park.objects.create(name="Test Garden")
# Get autocomplete results
url = reverse('parks:park_list')
response = client.get(url, {'park': 'Test'})
# Check response
assert response.status_code == 200
content = response.content.decode()
assert park1.name in content
assert park3.name in content
assert park2.name not in content
def test_search_form_valid(self):
"""Test ParkSearchForm validation"""
form = ParkSearchForm(data={'park': ''})
assert form.is_valid()
def test_autocomplete_class(self):
"""Test ParkAutocomplete configuration"""
ac = ParkAutocomplete()
assert ac.model == Park
assert 'name' in ac.search_attrs
def test_search_with_filters(self, client: Client):
"""Test search works with filters"""
park = Park.objects.create(name="Test Park", status="OPERATING")
# Search with status filter
url = reverse('parks:park_list')
response = client.get(url, {
'park': str(park.pk),
'status': 'OPERATING'
})
assert response.status_code == 200
assert park.name in response.content.decode()
def test_empty_search(self, client: Client):
"""Test empty search returns all parks"""
Park.objects.create(name="Test Park")
Park.objects.create(name="Another Park")
url = reverse('parks:park_list')
response = client.get(url)
assert response.status_code == 200
content = response.content.decode()
assert "Test Park" in content
assert "Another Park" in content
def test_partial_match_search(self, client: Client):
"""Test partial matching in search"""
Park.objects.create(name="Adventure World")
Park.objects.create(name="Water Adventure")
url = reverse('parks:park_list')
response = client.get(url, {'park': 'Adv'})
assert response.status_code == 200
content = response.content.decode()
assert "Adventure World" in content
assert "Water Adventure" in content
def test_htmx_request_handling(self, client: Client):
"""Test HTMX-specific request handling"""
Park.objects.create(name="Test Park")
url = reverse('parks:park_list')
response = client.get(
url,
{'park': 'Test'},
HTTP_HX_REQUEST='true'
)
assert response.status_code == 200
assert "Test Park" in response.content.decode()
def test_view_mode_persistence(self, client: Client):
"""Test view mode is maintained during search"""
Park.objects.create(name="Test Park")
url = reverse('parks:park_list')
response = client.get(url, {
'park': 'Test',
'view_mode': 'list'
})
assert response.status_code == 200
assert 'data-view-mode="list"' in response.content.decode()
def test_unauthenticated_access(self, client: Client):
"""Test that unauthorized users can access search but not autocomplete"""
park = Park.objects.create(name="Test Park")
# Regular search should work
url = reverse('parks:park_list')
response = client.get(url, {'park_name': 'Test'})
assert response.status_code == 200
assert "Test Park" in response.content.decode()
# Autocomplete should require authentication
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
assert response.status_code == 302 # Redirects to login

View File

@@ -5,8 +5,8 @@ from rides.views import ParkSingleCategoryListView
app_name = "parks"
urlpatterns = [
# Park views
path("", views.ParkListView.as_view(), name="park_list"),
# Park views with autocomplete search
path("", views_search.ParkSearchView.as_view(), name="park_list"),
path("create/", views.ParkCreateView.as_view(), name="park_create"),
# Add park button endpoint (moved before park detail pattern)
@@ -18,7 +18,7 @@ urlpatterns = [
# Areas and search endpoints for HTMX
path("areas/", views.get_park_areas, name="get_park_areas"),
path("suggestions/", views_search.suggest_parks, name="suggest_parks"),
path("suggest_parks/", views_search.suggest_parks, name="suggest_parks"),
path("search/", views.search_parks, name="search_parks"),

View File

@@ -1,32 +1,43 @@
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import render
from django.views.generic import TemplateView
from django.urls import reverse
from .filters import ParkFilter
from .forms import ParkSearchForm
from .querysets import get_base_park_queryset
class ParkSearchView(TemplateView):
"""View for handling park search with autocomplete."""
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)
# Initialize filter with current querystring
queryset = get_base_park_queryset()
filter_instance = ParkFilter(self.request.GET, queryset=queryset)
context['filter'] = filter_instance
# Apply search if park ID selected via autocomplete
park_id = self.request.GET.get('park')
if park_id:
queryset = filter_instance.qs.filter(id=park_id)
else:
queryset = filter_instance.qs
# Handle view mode
context['view_mode'] = self.request.GET.get('view_mode', 'grid')
context['parks'] = queryset
return context
def suggest_parks(request: HttpRequest) -> HttpResponse:
"""Return park search suggestions as a dropdown"""
try:
"""Legacy endpoint for old search UI - redirects to autocomplete."""
query = request.GET.get('search', '').strip()
if not query or len(query) < 2:
if query:
return JsonResponse({
'redirect': f"{reverse('parks:park_list')}?park_name={query}"
})
return HttpResponse('')
# Get current view mode from request
current_view_mode = request.GET.get('view_mode', 'grid')
park_filter = ParkFilter({
'search': query
}, queryset=get_base_park_queryset())
parks = park_filter.qs[:8] # Limit to 8 suggestions
response = render(
request,
'parks/partials/search_suggestions.html',
{
'suggestions': parks,
'view_mode': current_view_mode
}
)
response['HX-Trigger'] = 'showSuggestions'
return response
except Exception as e:
return HttpResponse(f'Error getting suggestions: {str(e)}')

View File

@@ -57,4 +57,5 @@ dependencies = [
"playwright>=1.41.0",
"pytest-playwright>=0.4.3",
"django-pghistory>=3.5.2",
"django-htmx-autocomplete>=1.0.5",
]

View File

@@ -42,6 +42,7 @@ INSTALLED_APPS = [
"django_htmx",
"whitenoise",
"django_tailwind_cli",
"autocomplete", # Django HTMX Autocomplete
"core",
"accounts",
"companies",
@@ -208,10 +209,14 @@ SOCIALACCOUNT_STORE_TOKENS = True
EMAIL_BACKEND = "email_service.backends.ForwardEmailBackend"
FORWARD_EMAIL_BASE_URL = "https://api.forwardemail.net"
SERVER_EMAIL = "django_webmaster@thrillwiki.com"
# Custom User Model
AUTH_USER_MODEL = "accounts.User"
# Autocomplete configuration
# Enable project-wide authentication requirement for autocomplete
AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = True
# Tailwind configuration
# Tailwind configuration
TAILWIND_CLI_CONFIG_FILE = os.path.join(BASE_DIR, "tailwind.config.js")
TAILWIND_CLI_SRC_CSS = os.path.join(BASE_DIR, "static/css/src/input.css")

15
uv.lock generated
View File

@@ -1,4 +1,5 @@
version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
@@ -313,6 +314,18 @@ wheels = [
{ url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]django_htmx-1.21.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]e7ccd2963495e69afbdb6abe", size = 6901 },
]
[[package]]
name = "django-htmx-autocomplete"
version = "1.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]django_htmx_autocomplete-1.0.5.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]17bcac3ff0b70766e354ad80", size = 41127 }
wheels = [
{ url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]django_htmx_autocomplete-1.0.5-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]3572e8742fe5dfa848298735", size = 52127 },
]
[[package]]
name = "django-oauth-toolkit"
version = "3.0.1"
@@ -912,6 +925,7 @@ dependencies = [
{ name = "django-cors-headers" },
{ name = "django-filter" },
{ name = "django-htmx" },
{ name = "django-htmx-autocomplete" },
{ name = "django-oauth-toolkit" },
{ name = "django-pghistory" },
{ name = "django-simple-history" },
@@ -946,6 +960,7 @@ requires-dist = [
{ name = "django-cors-headers", specifier = ">=4.3.1" },
{ name = "django-filter", specifier = ">=23.5" },
{ name = "django-htmx", specifier = ">=1.17.2" },
{ name = "django-htmx-autocomplete", specifier = ">=1.0.5" },
{ name = "django-oauth-toolkit", specifier = ">=3.0.1" },
{ name = "django-pghistory", specifier = ">=3.5.2" },
{ name = "django-simple-history", specifier = ">=3.5.0" },