Files
santeh-ray/items/views.py
Azimkin 94facca714
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
feat: replace color dropdown with clickable links in catalog sidebar
- Updated `side-bar.html` to use clickable links instead of a dropdown for color selection
- Enhanced catalog view to generate URLs for color filters and "any color" option
- Simplified the user experience for color filtering and improved accessibility
2026-05-26 04:40:48 +02:00

333 lines
11 KiB
Python

from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from urllib.parse import urlencode
from django.core.paginator import Paginator
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, Color, 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 = "",
sale: bool = False,
new: bool = False,
color: 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 sale:
query_params["sale"] = "1"
if new:
query_params["new"] = "1"
if color:
query_params["color"] = color
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()
filter_sale = request.GET.get('sale') == '1'
filter_new = request.GET.get('new') == '1'
selected_color_id_raw = request.GET.get('color', '').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,
sale=filter_sale,
new=filter_new,
color=selected_color_id_raw,
)
)
items = (
Item.objects.select_related('category', 'subcategory')
.prefetch_related('images', 'colors')
.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()
]
if filter_sale:
items = [item for item in items if item.discount_price is not None]
if filter_new:
items = [item for item in items if item.is_new]
color_item_ids = {c.id for item in items for c in item.colors.all()}
available_colors = list(Color.objects.filter(id__in=color_item_ids))
any_color_url = _build_catalog_url(
selected_category.id,
selected_subcategory.id,
search_query=search_query,
min_price=min_price_raw,
max_price=max_price_raw,
sale=filter_sale,
new=filter_new,
)
for color in available_colors:
color.catalog_url = _build_catalog_url(
selected_category.id,
selected_subcategory.id,
search_query=search_query,
min_price=min_price_raw,
max_price=max_price_raw,
sale=filter_sale,
new=filter_new,
color=str(color.id),
)
if selected_color_id_raw:
try:
selected_color_id_int = int(selected_color_id_raw)
items = [item for item in items if any(c.id == selected_color_id_int for c in item.colors.all())]
except ValueError:
pass
item_prices = [item.display_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.display_price >= min_price]
max_price = _parse_price(max_price_raw)
if max_price is not None:
items = [item for item in items if item.display_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)
page_number = request.GET.get('page', 1)
paginator = Paginator(items, 24)
page_obj = paginator.get_page(page_number)
pagination_params = {k: v for k, v in request.GET.items() if k != 'page'}
pagination_query_string = urlencode(pagination_params)
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,
sale=filter_sale,
new=filter_new,
color=selected_color_id_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,
sale=filter_sale,
new=filter_new,
color=selected_color_id_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,
sale=filter_sale,
new=filter_new,
color=selected_color_id_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,
sale=filter_sale,
new=filter_new,
color=selected_color_id_raw,
)
price_filter_action = _build_catalog_url(selected_category.id)
sidebar_sale_items = (
Item.objects.filter(discount_price__isnull=False, is_available=True)
.prefetch_related('images')
.order_by('-created_at')[:3]
)
sidebar_new_items = (
Item.objects.filter(is_new=True, is_available=True)
.prefetch_related('images')
.order_by('-created_at')[:3]
)
sale_all_url = _build_catalog_url(sale=True)
new_all_url = _build_catalog_url(new=True)
return render(request, 'catalog/index.html', {
'categories': categories,
'subcategories': subcategories,
'items': items,
'page_obj': page_obj,
'paginator': paginator,
'pagination_query_string': pagination_query_string,
'available_colors': available_colors,
'any_color_url': any_color_url,
'selected_color_id': selected_color_id_raw,
'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),
'filter_sale': filter_sale,
'filter_new': filter_new,
'sidebar_sale_items': sidebar_sale_items,
'sidebar_new_items': sidebar_new_items,
'sale_all_url': sale_all_url,
'new_all_url': new_all_url,
})
def detail(request: HttpRequest, category: str, slug: str) -> HttpResponse:
try:
item = (
Item.objects.select_related('category', 'subcategory')
.prefetch_related('images', 'colors')
.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),
})