All checks were successful
Build and Push Docker Image / build (push) Successful in 29s
361 lines
12 KiB
Python
361 lines
12 KiB
Python
from dataclasses import dataclass
|
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
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.models import CartItem
|
|
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.quantize(Decimal("1"), rounding=ROUND_HALF_UP), "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)
|
|
|
|
filters_active = any([
|
|
search_query,
|
|
min_price_raw,
|
|
max_price_raw,
|
|
filter_sale,
|
|
filter_new,
|
|
selected_color_id_raw,
|
|
])
|
|
reset_filters_url = _build_catalog_url(selected_category.id, selected_subcategory.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,
|
|
'filters_active': filters_active,
|
|
'reset_filters_url': reset_filters_url,
|
|
'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())
|
|
colors = list(item.colors.all())
|
|
|
|
cart_color_id = None
|
|
if request.user.is_authenticated:
|
|
cart_item = CartItem.objects.filter(
|
|
cart__user=request.user, item=item
|
|
).select_related('selected_color').first()
|
|
if cart_item and cart_item.selected_color_id:
|
|
cart_color_id = cart_item.selected_color_id
|
|
|
|
selected_color_id = cart_color_id
|
|
if selected_color_id is None and colors:
|
|
selected_color_id = colors[0].id
|
|
|
|
return render(request, 'catalog/detail.html', {
|
|
'categories': Category.objects.order_by('name'),
|
|
'item': item,
|
|
'images': images,
|
|
'colors': colors,
|
|
'primary_image': images[0] if images else None,
|
|
'cart_item_ids': get_user_cart_item_ids(request.user),
|
|
'selected_color_id': selected_color_id,
|
|
'favorite_item_ids': get_user_favorite_item_ids(request.user),
|
|
})
|