All checks were successful
Build and Push Docker Image / build (push) Successful in 41s
- 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
237 lines
7.8 KiB
Python
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),
|
|
})
|