mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:31:07 -05:00
Implement search functionality improvements: optimize database queries, enhance service layer, and update frontend interactions
This commit is contained in:
119
memory-bank/features/search_improvements.md
Normal file
119
memory-bank/features/search_improvements.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Search Functionality Improvement Plan
|
||||||
|
|
||||||
|
## Technical Implementation Details
|
||||||
|
|
||||||
|
### 1. Database Optimization
|
||||||
|
```python
|
||||||
|
# parks/models.py
|
||||||
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
|
|
||||||
|
class Park(models.Model):
|
||||||
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
GinIndex(fields=['name', 'description'],
|
||||||
|
name='search_gin_idx',
|
||||||
|
opclasses=['gin_trgm_ops', 'gin_trgm_ops']),
|
||||||
|
Index(fields=['location__address_text'], name='location_addr_idx')
|
||||||
|
]
|
||||||
|
|
||||||
|
# search/services.py
|
||||||
|
from django.db.models import F, Func
|
||||||
|
from analytics.models import SearchMetric
|
||||||
|
|
||||||
|
class SearchEngine:
|
||||||
|
@classmethod
|
||||||
|
def execute_search(cls, request, filterset_class):
|
||||||
|
with timeit() as timer:
|
||||||
|
filterset = filterset_class(request.GET, queryset=cls.base_queryset())
|
||||||
|
qs = filterset.qs
|
||||||
|
results = qs.annotate(
|
||||||
|
search_rank=Func(F('name'), F('description'),
|
||||||
|
function='ts_rank')
|
||||||
|
).order_by('-search_rank')
|
||||||
|
|
||||||
|
SearchMetric.record(
|
||||||
|
query_params=dict(request.GET),
|
||||||
|
result_count=qs.count(),
|
||||||
|
duration=timer.elapsed
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Architectural Changes
|
||||||
|
```python
|
||||||
|
# search/filters.py (simplified explicit filter)
|
||||||
|
class ParkFilter(SearchableFilterMixin, django_filters.FilterSet):
|
||||||
|
search_fields = ['name', 'description', 'location__address_text']
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Park
|
||||||
|
fields = {
|
||||||
|
'ride_count': ['gte', 'lte'],
|
||||||
|
'coaster_count': ['gte', 'lte'],
|
||||||
|
'average_rating': ['gte', 'lte']
|
||||||
|
}
|
||||||
|
|
||||||
|
# search/views.py (updated)
|
||||||
|
class AdaptiveSearchView(TemplateView):
|
||||||
|
def get_queryset(self):
|
||||||
|
return SearchEngine.base_queryset()
|
||||||
|
|
||||||
|
def get_filterset(self):
|
||||||
|
return ParkFilter(self.request.GET, queryset=self.get_queryset())
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Frontend Enhancements
|
||||||
|
```javascript
|
||||||
|
// static/js/search.js
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
fetchResults(searchInput.value);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchResults(query) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/search/?search=${encodeURIComponent(query)}`);
|
||||||
|
if (!response.ok) throw new Error(response.statusText);
|
||||||
|
const html = await response.text();
|
||||||
|
updateResults(html);
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Search failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Roadmap
|
||||||
|
|
||||||
|
1. Database Migrations
|
||||||
|
```bash
|
||||||
|
uv run manage.py makemigrations parks --name add_search_indexes
|
||||||
|
uv run manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Service Layer Integration
|
||||||
|
- Create search/services.py with query instrumentation
|
||||||
|
- Update all views to use SearchEngine class
|
||||||
|
|
||||||
|
3. Frontend Updates
|
||||||
|
- Add debouncing to search inputs
|
||||||
|
- Implement error handling UI components
|
||||||
|
- Add loading spinner component
|
||||||
|
|
||||||
|
4. Monitoring Setup
|
||||||
|
```python
|
||||||
|
# analytics/models.py
|
||||||
|
class SearchMetric(models.Model):
|
||||||
|
query_params = models.JSONField()
|
||||||
|
result_count = models.IntegerField()
|
||||||
|
duration = models.FloatField()
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Performance Testing
|
||||||
|
- Use django-debug-toolbar for query analysis
|
||||||
|
- Generate load tests with locust.io
|
||||||
@@ -31,44 +31,66 @@ class ParkFilter(LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin, F
|
|||||||
model = Park
|
model = Park
|
||||||
fields = []
|
fields = []
|
||||||
|
|
||||||
# Search field
|
# Search field with better description
|
||||||
search = CharFilter(method='filter_search')
|
search = CharFilter(
|
||||||
|
method='filter_search',
|
||||||
|
label=_("Search Parks"),
|
||||||
|
help_text=_("Search by park name, description, or location")
|
||||||
|
)
|
||||||
|
|
||||||
# Status filter
|
# Status filter with clearer label
|
||||||
status = ChoiceFilter(
|
status = ChoiceFilter(
|
||||||
field_name='status',
|
field_name='status',
|
||||||
choices=Park._meta.get_field('status').choices,
|
choices=Park._meta.get_field('status').choices,
|
||||||
empty_label='Any status'
|
empty_label=_('Any status'),
|
||||||
|
label=_("Operating Status"),
|
||||||
|
help_text=_("Filter parks by their current operating status")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Owner filters
|
# Owner filters with helpful descriptions
|
||||||
owner = ModelChoiceFilter(
|
owner = ModelChoiceFilter(
|
||||||
field_name='owner',
|
field_name='owner',
|
||||||
queryset=Company.objects.all(),
|
queryset=Company.objects.all(),
|
||||||
empty_label='Any company'
|
empty_label=_('Any company'),
|
||||||
|
label=_("Operating Company"),
|
||||||
|
help_text=_("Filter parks by their operating company")
|
||||||
|
)
|
||||||
|
has_owner = BooleanFilter(
|
||||||
|
method='filter_has_owner',
|
||||||
|
label=_("Company Status"),
|
||||||
|
help_text=_("Show parks with or without an operating company")
|
||||||
)
|
)
|
||||||
has_owner = BooleanFilter(method='filter_has_owner')
|
|
||||||
|
|
||||||
# Numeric filters
|
# Ride and attraction filters
|
||||||
min_rides = NumberFilter(
|
min_rides = NumberFilter(
|
||||||
field_name='current_ride_count',
|
field_name='current_ride_count',
|
||||||
lookup_expr='gte',
|
lookup_expr='gte',
|
||||||
validators=[validate_positive_integer]
|
validators=[validate_positive_integer],
|
||||||
|
label=_("Minimum Rides"),
|
||||||
|
help_text=_("Show parks with at least this many rides")
|
||||||
)
|
)
|
||||||
min_coasters = NumberFilter(
|
min_coasters = NumberFilter(
|
||||||
field_name='current_coaster_count',
|
field_name='current_coaster_count',
|
||||||
lookup_expr='gte',
|
lookup_expr='gte',
|
||||||
validators=[validate_positive_integer]
|
validators=[validate_positive_integer],
|
||||||
|
label=_("Minimum Roller Coasters"),
|
||||||
|
help_text=_("Show parks with at least this many roller coasters")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Size filter
|
||||||
min_size = NumberFilter(
|
min_size = NumberFilter(
|
||||||
field_name='size_acres',
|
field_name='size_acres',
|
||||||
lookup_expr='gte',
|
lookup_expr='gte',
|
||||||
validators=[validate_positive_integer]
|
validators=[validate_positive_integer],
|
||||||
|
label=_("Minimum Size (acres)"),
|
||||||
|
help_text=_("Show parks of at least this size in acres")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Date filter
|
# Opening date filter with better label
|
||||||
opening_date = DateFromToRangeFilter(
|
opening_date = DateFromToRangeFilter(
|
||||||
field_name='opening_date'
|
field_name='opening_date',
|
||||||
|
label=_("Opening Date Range"),
|
||||||
|
help_text=_("Filter parks by their opening date")
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_search(self, queryset, name, value):
|
def filter_search(self, queryset, name, value):
|
||||||
@@ -94,25 +116,26 @@ class ParkFilter(LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin, F
|
|||||||
def filter_has_owner(self, queryset, name, value):
|
def filter_has_owner(self, queryset, name, value):
|
||||||
"""Filter parks based on whether they have an owner"""
|
"""Filter parks based on whether they have an owner"""
|
||||||
return queryset.filter(owner__isnull=not value)
|
return queryset.filter(owner__isnull=not value)
|
||||||
@property
|
|
||||||
def qs(self):
|
@property
|
||||||
"""Override qs property to ensure we always use base queryset with annotations"""
|
def qs(self):
|
||||||
if not hasattr(self, '_qs'):
|
"""Override qs property to ensure we always use base queryset with annotations"""
|
||||||
# Start with the base queryset that includes annotations
|
if not hasattr(self, '_qs'):
|
||||||
base_qs = get_base_park_queryset()
|
# Start with the base queryset that includes annotations
|
||||||
|
base_qs = get_base_park_queryset()
|
||||||
if not self.is_bound:
|
|
||||||
self._qs = base_qs
|
if not self.is_bound:
|
||||||
return self._qs
|
self._qs = base_qs
|
||||||
|
return self._qs
|
||||||
if not self.form.is_valid():
|
|
||||||
self._qs = base_qs.none()
|
if not self.form.is_valid():
|
||||||
return self._qs
|
self._qs = base_qs.none()
|
||||||
|
return self._qs
|
||||||
|
|
||||||
self._qs = base_qs
|
self._qs = base_qs
|
||||||
for name, value in self.form.cleaned_data.items():
|
for name, value in self.form.cleaned_data.items():
|
||||||
if value in [None, '', 0] and name not in ['has_owner']:
|
if value in [None, '', 0] and name not in ['has_owner']:
|
||||||
continue
|
continue
|
||||||
self._qs = self.filters[name].filter(self._qs, value)
|
self._qs = self.filters[name].filter(self._qs, value)
|
||||||
self._qs = self._qs.distinct()
|
self._qs = self._qs.distinct()
|
||||||
return self._qs
|
return self._qs
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load filter_utils %}
|
||||||
|
|
||||||
<div class="filter-container bg-white rounded-lg shadow p-4" x-data="{ open: false }">
|
<div class="filter-container" x-data="{ open: false }">
|
||||||
{# Mobile Filter Toggle #}
|
{# Mobile Filter Toggle #}
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden bg-white rounded-lg shadow p-4 mb-4">
|
||||||
<button @click="open = !open" type="button" class="w-full flex items-center justify-between p-2 text-gray-400 hover:text-gray-500">
|
<button @click="open = !open" type="button" class="w-full flex items-center justify-between p-2">
|
||||||
<span class="font-medium text-gray-900">Filters</span>
|
<span class="font-medium text-gray-900">
|
||||||
<span class="ml-6 flex items-center">
|
<span class="mr-2">
|
||||||
<svg class="w-5 h-5" x-show="!open" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-5 h-5 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M10 6L16 12H4L10 6Z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<svg class="w-5 h-5" x-show="open" fill="currentColor" viewBox="0 0 20 20">
|
</span>
|
||||||
<path d="M10 14L4 8H16L10 14Z"/>
|
Filter Options
|
||||||
|
</span>
|
||||||
|
<span class="text-gray-500">
|
||||||
|
<svg class="w-5 h-5 transition-transform duration-200" :class="{'rotate-180': open}" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -18,20 +23,23 @@
|
|||||||
|
|
||||||
{# Filter Form #}
|
{# Filter Form #}
|
||||||
<form hx-get="{{ request.path }}"
|
<form hx-get="{{ request.path }}"
|
||||||
hx-trigger="change delay:500ms, submit"
|
hx-trigger="change delay:500ms"
|
||||||
hx-target="#results-container"
|
hx-target="#results-container"
|
||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
class="mt-4 lg:mt-0"
|
class="space-y-6"
|
||||||
x-show="open || $screen('lg')"
|
x-show="open || $screen('lg')"
|
||||||
x-transition>
|
x-transition>
|
||||||
|
|
||||||
{# Active Filters Summary #}
|
{# Active Filters Summary #}
|
||||||
{% if applied_filters %}
|
{% if applied_filters %}
|
||||||
<div class="bg-blue-50 p-4 rounded-lg mb-4">
|
<div class="bg-blue-50 rounded-lg p-4 shadow-sm border border-blue-100">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h3 class="text-sm font-medium text-blue-800">Active Filters</h3>
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-blue-800">Active Filters</h3>
|
||||||
|
<p class="text-xs text-blue-600 mt-1">{{ applied_filters|length }} filter{{ applied_filters|length|pluralize }} applied</p>
|
||||||
|
</div>
|
||||||
<a href="{{ request.path }}"
|
<a href="{{ request.path }}"
|
||||||
class="text-sm text-blue-600 hover:text-blue-500"
|
class="text-sm font-medium text-blue-600 hover:text-blue-500 hover:underline"
|
||||||
hx-get="{{ request.path }}"
|
hx-get="{{ request.path }}"
|
||||||
hx-target="#results-container"
|
hx-target="#results-container"
|
||||||
hx-push-url="true">
|
hx-push-url="true">
|
||||||
@@ -42,21 +50,35 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Filter Groups #}
|
{# Filter Groups #}
|
||||||
<div class="space-y-4">
|
<div class="bg-white rounded-lg shadow divide-y divide-gray-200">
|
||||||
{% for fieldset in filter.form|groupby_filters %}
|
{% for fieldset in filter.form|groupby_filters %}
|
||||||
<div class="border-b border-gray-200 pb-4">
|
<div class="p-6" x-data="{ expanded: true }">
|
||||||
<h3 class="text-sm font-medium text-gray-900 mb-3">{{ fieldset.name }}</h3>
|
{# Group Header #}
|
||||||
<div class="space-y-3">
|
<button type="button"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
class="w-full flex justify-between items-center text-left">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">{{ fieldset.name }}</h3>
|
||||||
|
<svg class="w-5 h-5 text-gray-500 transform transition-transform duration-200"
|
||||||
|
:class="{'rotate-180': !expanded}"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20">
|
||||||
|
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{# Group Content #}
|
||||||
|
<div class="mt-4 space-y-4" x-show="expanded" x-collapse>
|
||||||
{% for field in fieldset.fields %}
|
{% for field in fieldset.fields %}
|
||||||
<div>
|
<div class="filter-field">
|
||||||
<label for="{{ field.id_for_label }}" class="text-sm text-gray-600">
|
<label for="{{ field.id_for_label }}"
|
||||||
|
class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
{{ field.label }}
|
{{ field.label }}
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
{{ field }}
|
{{ field|add_field_classes }}
|
||||||
</div>
|
</div>
|
||||||
{% if field.help_text %}
|
{% if field.help_text %}
|
||||||
<p class="mt-1 text-xs text-gray-500">{{ field.help_text }}</p>
|
<p class="mt-1 text-sm text-gray-500">{{ field.help_text }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -65,17 +87,25 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Submit Button - Only visible on mobile #}
|
{# Mobile Apply Button #}
|
||||||
<div class="mt-4 lg:hidden">
|
<div class="lg:hidden">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-150 ease-in-out">
|
||||||
Apply Filters
|
Apply Filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{# Required Scripts #}
|
||||||
{# Add Alpine.js for mobile menu toggle if not already included #}
|
|
||||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
{% endblock %}
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('filterForm', () => ({
|
||||||
|
expanded: true,
|
||||||
|
toggle() {
|
||||||
|
this.expanded = !this.expanded
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -32,17 +32,18 @@ def groupby_filters(form: Form) -> List[Dict[str, Any]]:
|
|||||||
"""
|
"""
|
||||||
groups = []
|
groups = []
|
||||||
|
|
||||||
# Define groups and their patterns
|
# Define groups and their patterns with specific ordering
|
||||||
group_patterns = {
|
group_patterns = {
|
||||||
'Search': lambda f: f.name in ['search', 'q'],
|
'Quick Search': lambda f: f.name in ['search', 'q'],
|
||||||
|
'Park Details': lambda f: f.name in ['status', 'has_owner', 'owner'],
|
||||||
|
'Attractions': lambda f: any(x in f.name for x in ['rides', 'coasters']),
|
||||||
|
'Park Size': lambda f: 'size' in f.name,
|
||||||
'Location': lambda f: f.name.startswith('location') or 'address' in f.name,
|
'Location': lambda f: f.name.startswith('location') or 'address' in f.name,
|
||||||
'Dates': lambda f: any(x in f.name for x in ['date', 'created', 'updated']),
|
'Ratings': lambda f: 'rating' in f.name,
|
||||||
'Rating': lambda f: 'rating' in f.name,
|
'Opening Info': lambda f: 'opening' in f.name or 'date' in f.name,
|
||||||
'Status': lambda f: f.name in ['status', 'state', 'condition'],
|
|
||||||
'Features': lambda f: f.name.startswith('has_') or f.name.endswith('_count'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize group containers
|
# Initialize group containers with ordering preserved
|
||||||
grouped_fields: Dict[str, List] = {name: [] for name in group_patterns.keys()}
|
grouped_fields: Dict[str, List] = {name: [] for name in group_patterns.keys()}
|
||||||
ungrouped = []
|
ungrouped = []
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ def groupby_filters(form: Form) -> List[Dict[str, Any]]:
|
|||||||
if not grouped:
|
if not grouped:
|
||||||
ungrouped.append(field)
|
ungrouped.append(field)
|
||||||
|
|
||||||
# Build final groups list, only including non-empty groups
|
# Build final groups list, maintaining order and only including non-empty groups
|
||||||
for name, fields in grouped_fields.items():
|
for name, fields in grouped_fields.items():
|
||||||
if fields:
|
if fields:
|
||||||
groups.append({
|
groups.append({
|
||||||
@@ -68,7 +69,7 @@ def groupby_filters(form: Form) -> List[Dict[str, Any]]:
|
|||||||
# Add ungrouped fields at the end if any exist
|
# Add ungrouped fields at the end if any exist
|
||||||
if ungrouped:
|
if ungrouped:
|
||||||
groups.append({
|
groups.append({
|
||||||
'name': 'Other',
|
'name': 'Other Filters',
|
||||||
'fields': ungrouped
|
'fields': ungrouped
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -86,15 +87,26 @@ def add_field_classes(field: Any) -> Any:
|
|||||||
"""
|
"""
|
||||||
Add appropriate Tailwind classes based on field type
|
Add appropriate Tailwind classes based on field type
|
||||||
"""
|
"""
|
||||||
|
base_classes = "transition duration-150 ease-in-out "
|
||||||
|
|
||||||
classes = {
|
classes = {
|
||||||
'default': 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
'default': base_classes + 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
||||||
'checkbox': 'rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-offset-0 focus:ring-blue-200 focus:ring-opacity-50',
|
'checkbox': base_classes + 'h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500',
|
||||||
'radio': 'border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-offset-0 focus:ring-blue-200 focus:ring-opacity-50',
|
'radio': base_classes + 'h-4 w-4 border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500',
|
||||||
'select': 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
'select': base_classes + 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
||||||
'multiselect': 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
'multiselect': base_classes + 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
||||||
|
'range': base_classes + 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
||||||
|
'dateinput': base_classes + 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50',
|
||||||
}
|
}
|
||||||
|
|
||||||
field_type = get_field_type(field)
|
field_type = get_field_type(field)
|
||||||
css_class = classes.get(field_type, classes['default'])
|
css_class = classes.get(field_type, classes['default'])
|
||||||
|
|
||||||
return field.as_widget(attrs={'class': css_class})
|
current_attrs = field.field.widget.attrs
|
||||||
|
current_attrs['class'] = css_class
|
||||||
|
|
||||||
|
# Add specific attributes for certain field types
|
||||||
|
if field_type == 'dateinput':
|
||||||
|
current_attrs['type'] = 'date'
|
||||||
|
|
||||||
|
return field.as_widget(attrs=current_attrs)
|
||||||
Reference in New Issue
Block a user