Files
santeh-ray/items/views.py
Azimkin 3d356c7d6a
All checks were successful
Build and Push Docker Image / build (push) Successful in 41s
feat: add price filter and dynamic slider to catalog
- Implemented JavaScript-based price slider for catalog filtering
- Updated catalog views to handle min/max price inputs and compute slider bounds
- Enhanced templates (`index.html`, `side-bar.html`) with filtering form and updated URLs
- Added CSS for price slider styling and form layout
- Introduced utility functions for URL building and price parsing
- Expanded tests for price filtering, sliders, and sidebar active states
- Updated context processor and header for dynamic navigation with price filters
2026-05-16 11:12:24 +02:00

237 lines
7.8 KiB
Python

from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from urllib.parse import urlencode
from django.http import HttpRequest, HttpResponse, Http404
from django.shortcuts import redirect, render
from favorites.services import get_user_favorite_item_ids
from items.models import Category, Item, Subcategory
from orders.services import get_user_cart_item_ids
@dataclass
class SelectedCategory:
name: str | None
id: str | None
@dataclass
class SelectedSubcategory:
name: str | None
id: str | None
def _parse_price(value: str | None) -> Decimal | None:
if value is None:
return None
normalized_value = value.strip()
if not normalized_value:
return None
try:
return Decimal(normalized_value)
except InvalidOperation:
return None
def _build_catalog_url(
category_slug: str | None = None,
subcategory_slug: str | None = None,
*,
search_query: str = "",
min_price: str = "",
max_price: str = "",
) -> str:
base_url = "/catalog/"
if category_slug:
base_url = f"/catalog/{category_slug}/"
query_params: dict[str, str] = {}
if subcategory_slug:
query_params["subcategory"] = subcategory_slug
if search_query:
query_params["q"] = search_query
if min_price:
query_params["min_price"] = min_price
if max_price:
query_params["max_price"] = max_price
if not query_params:
return base_url
return f"{base_url}?{urlencode(query_params)}"
def _resolve_catalog_scope(scope_value: str | None) -> tuple[str | None, str | None]:
if not scope_value:
return None, None
parts = [part.strip().lower() for part in scope_value.split(":")]
if len(parts) == 2 and parts[0] == "category" and parts[1]:
return parts[1], None
if len(parts) == 3 and parts[0] == "subcategory" and parts[1] and parts[2]:
return parts[1], parts[2]
return None, None
def _format_price_value(value: Decimal) -> str:
return format(value, "f")
def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
selected_category = SelectedCategory(name=None, id=None)
selected_subcategory = SelectedSubcategory(name=None, id=None)
subcategories = Subcategory.objects.none()
subcategory_slug = request.GET.get('subcategory')
search_query = request.GET.get('q', '').strip()
min_price_raw = request.GET.get('min_price', '').strip()
max_price_raw = request.GET.get('max_price', '').strip()
selected_scope = request.GET.get('category', '').strip()
if category is None and selected_scope:
category_slug, selected_subcategory_slug = _resolve_catalog_scope(selected_scope)
if category_slug:
return redirect(
_build_catalog_url(
category_slug,
selected_subcategory_slug,
search_query=search_query,
min_price=min_price_raw,
max_price=max_price_raw,
)
)
items = (
Item.objects.select_related('category', 'subcategory')
.prefetch_related('images')
.order_by('category__name', 'name')
)
if category is None and subcategory_slug:
raise Http404()
if category is not None:
try:
cat = Category.objects.get(slug=category.lower())
selected_category = SelectedCategory(name=cat.name, id=cat.slug)
except Category.DoesNotExist:
raise Http404()
subcategories = Subcategory.objects.filter(category=cat).order_by('name')
items = items.filter(category=cat)
if subcategory_slug:
try:
subcategory_obj = subcategories.get(slug=subcategory_slug.lower())
selected_subcategory = SelectedSubcategory(name=subcategory_obj.name, id=subcategory_obj.slug)
except Subcategory.DoesNotExist:
raise Http404()
items = items.filter(subcategory=subcategory_obj)
items = list(items)
if search_query:
normalized_search_query = search_query.casefold()
items = [
item
for item in items
if normalized_search_query in item.name.casefold()
]
item_prices = [item.price for item in items]
slider_min_bound = min(item_prices, default=Decimal('0'))
slider_max_bound = max(item_prices, default=slider_min_bound)
min_price = _parse_price(min_price_raw)
if min_price is not None:
items = [item for item in items if item.price >= min_price]
max_price = _parse_price(max_price_raw)
if max_price is not None:
items = [item for item in items if item.price <= max_price]
slider_selected_min = min_price if min_price is not None else slider_min_bound
slider_selected_max = max_price if max_price is not None else slider_max_bound
slider_selected_min = min(max(slider_selected_min, slider_min_bound), slider_max_bound)
slider_selected_max = min(max(slider_selected_max, slider_min_bound), slider_max_bound)
categories = list(Category.objects.order_by('name'))
all_items_url = _build_catalog_url(
search_query=search_query,
min_price=min_price_raw,
max_price=max_price_raw,
)
for catalog_category in categories:
catalog_category.catalog_url = _build_catalog_url(
catalog_category.slug,
search_query=search_query,
min_price=min_price_raw,
max_price=max_price_raw,
)
subcategories = list(subcategories)
selected_category_url = None
if selected_category.id is not None:
selected_category_url = _build_catalog_url(
selected_category.id,
search_query=search_query,
min_price=min_price_raw,
max_price=max_price_raw,
)
for catalog_subcategory in subcategories:
catalog_subcategory.catalog_url = _build_catalog_url(
selected_category.id,
catalog_subcategory.slug,
search_query=search_query,
min_price=min_price_raw,
max_price=max_price_raw,
)
price_filter_action = _build_catalog_url(selected_category.id)
return render(request, 'catalog/index.html', {
'categories': categories,
'subcategories': subcategories,
'items': items,
'selected_category': selected_category,
'selected_subcategory': selected_subcategory,
'all_items_url': all_items_url,
'selected_category_url': selected_category_url,
'price_filter_action': price_filter_action,
'current_search_query': search_query,
'current_min_price': min_price_raw,
'current_max_price': max_price_raw,
'price_slider_min_bound': _format_price_value(slider_min_bound),
'price_slider_max_bound': _format_price_value(slider_max_bound),
'price_slider_selected_min': _format_price_value(slider_selected_min),
'price_slider_selected_max': _format_price_value(slider_selected_max),
'favorite_item_ids': get_user_favorite_item_ids(request.user),
})
def detail(request: HttpRequest, category: str, slug: str) -> HttpResponse:
try:
item = (
Item.objects.select_related('category', 'subcategory')
.prefetch_related('images')
.get(category__slug=category.lower(), slug=slug)
)
except Item.DoesNotExist:
raise Http404()
images = list(item.images.all())
return render(request, 'catalog/detail.html', {
'categories': Category.objects.order_by('name'),
'item': item,
'images': images,
'primary_image': images[0] if images else None,
'cart_item_ids': get_user_cart_item_ids(request.user),
'favorite_item_ids': get_user_favorite_item_ids(request.user),
})