Remove promo, add featured on main page
All checks were successful
Build and Push Docker Image / build (push) Successful in 54s

This commit is contained in:
2026-06-15 18:56:11 +02:00
parent 373cc15e4d
commit e9da339a4c
7 changed files with 325 additions and 215 deletions

View File

@@ -3,6 +3,8 @@ from django.core import mail
from django.test import SimpleTestCase, TestCase, override_settings
from django.urls import reverse, resolve
from items.models import Category, Item
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", ADMINISTRATOR_EMAIL="admin@example.com")
class FaqQuestionTests(TestCase):
@@ -39,6 +41,50 @@ class FaqQuestionTests(TestCase):
self.assertContains(response, "Необходимо согласие на обработку персональных данных.")
class HomePopularProductsTests(TestCase):
def setUp(self):
self.category = Category.objects.create(name="Plumbing", slug="plumbing")
def test_home_page_renders_available_products(self):
visible_item = Item.objects.create(
name="Visible sink",
slug="visible-sink",
category=self.category,
price="1600.00",
is_available=True,
)
Item.objects.create(
name="Hidden sink",
slug="hidden-sink",
category=self.category,
price="1800.00",
is_available=False,
)
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, visible_item.name)
self.assertContains(response, reverse("detail", args=[self.category.slug, visible_item.slug]))
self.assertContains(response, "1600,00 руб.")
self.assertNotContains(response, "Hidden sink")
def test_home_page_limits_popular_products_to_six(self):
for index in range(7):
Item.objects.create(
name=f"Product {index}",
slug=f"product-{index}",
category=self.category,
price="1000.00",
is_available=True,
)
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["home_products"]), 6)
class FaviconTests(SimpleTestCase):
def test_favicon_route_returns_icon(self):
response = self.client.get(reverse("favicon"))

View File

@@ -2,11 +2,15 @@ import logging
from django.conf import settings
from django.core.mail import send_mail
from django.db.models import BooleanField, Case, Value, When
from django.shortcuts import redirect, render
from django.urls import reverse
from django.contrib import messages
from favorites.services import get_user_favorite_item_ids
from home.forms import FaqQuestionForm
from items.models import Item
from orders.services import get_user_cart_item_ids
logger = logging.getLogger(__name__)
@@ -55,7 +59,26 @@ def index(request):
messages.success(request, "Спасибо. Ваш вопрос отправлен, мы свяжемся с вами в ближайшее время.")
return redirect(f"{reverse('home')}#faq-section")
return render(request=request, template_name='home/index.html', context={"faq_form": form})
home_products = (
Item.objects.filter(is_available=True)
.select_related("category", "subcategory")
.prefetch_related("images")
.annotate(
discount_sort=Case(
When(discount_price__isnull=False, then=Value(True)),
default=Value(False),
output_field=BooleanField(),
)
)
.order_by("-is_new", "-discount_sort", "-created_at")[:6]
)
return render(request=request, template_name='home/index.html', context={
"faq_form": form,
"home_products": home_products,
"favorite_item_ids": get_user_favorite_item_ids(request.user),
"cart_item_ids": get_user_cart_item_ids(request.user),
})
def services(request):

View File

@@ -125,6 +125,7 @@ class CartFlowTests(TestCase):
self.assertContains(response, "3200,00 руб.")
self.assertContains(response, "3745,00 руб.")
self.assertContains(response, "/media/items/cart-item.jpg")
self.assertNotContains(response, 'name="promo_code"', html=False)
def test_cart_page_redirects_guest_to_login(self):
response = self.client.get(reverse("orders:cart"))

View File

@@ -281,20 +281,6 @@
color: var(--mid);
}
.promo-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 178px;
margin-top: 10px;
}
.promo-row button {
border: 0;
background: var(--blue-gray);
color: var(--white);
font: inherit;
cursor: pointer;
}
.checkout-summary {
display: grid;
gap: 14px;
@@ -440,15 +426,10 @@
.checkout-options--three,
.checkout-grid,
.checkout-grid--contacts,
.promo-row,
.order-success__summary {
grid-template-columns: 1fr;
}
.promo-row button {
height: 38px;
}
.checkout-submit,
.order-success__link,
.cart-empty a {

View File

@@ -500,14 +500,172 @@
}
.product {
padding: 20px;
background: #d9d9d9;
position: relative;
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
background: #ffffff;
border: 1px solid rgba(54, 33, 62, 0.12);
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
.product-actions .bi-heart {
.product:hover {
border-color: rgba(54, 33, 62, 0.28);
box-shadow: 0 18px 36px rgba(54, 33, 62, 0.1);
transform: translateY(-2px);
}
.product-media {
min-height: 0;
flex: 1 1 auto;
display: block;
overflow: hidden;
background: rgba(99, 118, 141, 0.1);
}
.product-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.product-placeholder {
width: 100%;
height: 100%;
min-height: 130px;
display: flex;
align-items: center;
justify-content: center;
color: #63768d;
font-size: 18px;
line-height: 1.2;
}
.product-actions {
position: absolute;
z-index: 2;
top: 18px;
left: 18px;
display: flex;
gap: 8px;
}
.product-actions form {
display: contents;
}
.product-actions button,
.product-actions a {
width: 30px;
height: 30px;
box-sizing: border-box;
padding: 2px;
transform: translateY(2px);
border: 1px solid rgba(54, 33, 62, 0.18);
background: rgba(255, 255, 255, 0.88);
color: #36213e;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
text-decoration: none;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
}
.product-actions button:hover,
.product-actions a:hover,
.product-actions .is-active {
border-color: #63768d;
background: #63768d;
color: #ffffff;
transform: translateY(-1px);
}
.product-actions svg {
width: 16px;
height: 16px;
}
.product-info {
flex: 0 0 auto;
padding: 18px;
background: #ffffff;
}
.product-category {
display: block;
color: #63768d;
font-size: 15px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-name {
min-height: 52px;
margin-top: 8px;
display: -webkit-box;
overflow: hidden;
color: #36213e;
font-weight: 400;
font-size: 22px;
line-height: 1.18;
text-decoration: none;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.product-name:hover {
text-decoration: underline;
}
.product-meta {
margin-top: 14px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.product-price-block {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.product-price {
color: #36213e;
font-weight: 700;
font-size: 21px;
line-height: 1.15;
}
.product-price--discount {
color: #c54c5d;
}
.product-price-original {
color: #8a8a9a;
font-size: 15px;
line-height: 1.15;
text-decoration: line-through;
}
.product-availability {
flex: 0 0 auto;
color: #4f7d4f;
font-size: 15px;
line-height: 1.2;
}
.products-empty {
grid-column: 1 / -1;
color: #554971;
font-size: 18px;
line-height: 1.4;
}
.product-1 {

View File

@@ -2,193 +2,102 @@
<section class="products-section container">
<h2 class="products-title">ПОПУЛЯРНЫЕ ТОВАРЫ</h2>
<div class="products-grid">
<article class="product product-1">
<div class="product-actions">
<button title="В избранное">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-heart" viewBox="0 0 16 16">
<path d="m8 2.748-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143q.09.083.176.171a3 3 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15"></path>
{% for item in home_products %}
{% url 'detail' item.category.slug item.slug as item_url %}
<article class="product product-{{ forloop.counter }}">
<a class="product-media" href="{{ item_url }}" aria-label="Открыть товар {{ item.name }}">
{% with primary_image=item.primary_image %}
{% if primary_image %}
<img src="{{ primary_image.image.url }}" alt="{{ primary_image.alt|default:item.name }}">
{% else %}
<span class="product-placeholder">Нет фото</span>
{% endif %}
{% endwith %}
</a>
<div class="product-actions" aria-label="Действия с товаром">
{% if request.user.is_authenticated %}
<form method="post" action="{% url 'favorites:toggle' %}">
{% csrf_token %}
<input type="hidden" name="item_id" value="{{ item.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<button
class="{% if item.id in favorite_item_ids %}is-active{% endif %}"
type="submit"
title="{% if item.id in favorite_item_ids %}Удалить из избранного{% else %}Добавить в избранное{% endif %}"
aria-label="{% if item.id in favorite_item_ids %}Удалить из избранного{% else %}Добавить в избранное{% endif %}"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="m8 2.748-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01z"></path>
</svg>
</button>
<button title="В корзину">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-cart2" viewBox="0 0 16 16">
<path d="M0 2.5A.5.5 0 0 1 .5 2H2a.5.5 0 0 1 .485.379L2.89 4H14.5a.5.5 0 0 1 .485.621l-1.5 6A.5.5 0 0 1 13 11H4a.5.5 0 0 1-.485-.379L1.61 3H.5a.5.5 0 0 1-.5-.5M3.14 5l1.25 5h8.22l1.25-5zM5 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0m9-1a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0"></path>
</form>
{% else %}
<a
href="{% url 'users:login' %}?next={{ request.get_full_path|urlencode }}"
title="Войдите, чтобы добавить в избранное"
aria-label="Войдите, чтобы добавить в избранное"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="m8 2.748-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01z"></path>
</svg>
</a>
{% endif %}
{% if request.user.is_authenticated %}
{% if item.id in cart_item_ids %}
<a href="{% url 'orders:cart' %}" title="Перейти в корзину" aria-label="Перейти в корзину">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M0 2.5A.5.5 0 0 1 .5 2H2a.5.5 0 0 1 .485.379L2.89 4H14.5a.5.5 0 0 1 .485.621l-1.5 6A.5.5 0 0 1 13 11H4a.5.5 0 0 1-.485-.379L1.61 3H.5a.5.5 0 0 1-.5-.5M3.14 5l1.25 5h8.22l1.25-5zM5 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0m9-1a1 1 0 1 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0"></path>
</svg>
</a>
{% else %}
<form method="post" action="{% url 'orders:add' %}">
{% csrf_token %}
<input type="hidden" name="item_id" value="{{ item.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<button type="submit" title="В корзину" aria-label="В корзину">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M0 2.5A.5.5 0 0 1 .5 2H2a.5.5 0 0 1 .485.379L2.89 4H14.5a.5.5 0 0 1 .485.621l-1.5 6A.5.5 0 0 1 13 11H4a.5.5 0 0 1-.485-.379L1.61 3H.5a.5.5 0 0 1-.5-.5M3.14 5l1.25 5h8.22l1.25-5zM5 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0m9-1a1 1 0 1 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0"></path>
</svg>
</button>
</form>
{% endif %}
{% else %}
<a href="{% url 'users:login' %}?next={{ request.get_full_path|urlencode }}" title="Войдите, чтобы добавить в корзину" aria-label="Войдите, чтобы добавить в корзину">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M0 2.5A.5.5 0 0 1 .5 2H2a.5.5 0 0 1 .485.379L2.89 4H14.5a.5.5 0 0 1 .485.621l-1.5 6A.5.5 0 0 1 13 11H4a.5.5 0 0 1-.485-.379L1.61 3H.5a.5.5 0 0 1-.5-.5M3.14 5l1.25 5h8.22l1.25-5zM5 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0m9-1a1 1 0 1 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0"></path>
</svg>
</a>
{% endif %}
</div>
</article>
<article class="product product-2">
</article>
<article class="product product-3">
</article>
<article class="product product-4">
</article>
<article class="product product-5">
</article>
<article class="product product-6">
</article>
</div>
{% comment %}
<!-- Card 1 — tall -->
<div class="product-card tall">
<img class="product-card-img" src="{% static 'img/kitchen-sink.jpg' %}" alt="Кухонная мойка">
<div class="product-info">
<div class="p-name">Кухонная мойка</div>
<div class="p-sub">Искусственный камень, врезная на столешницу</div>
<div class="p-price">1600 руб</div>
<span class="product-category">
{{ item.category.name }}{% if item.subcategory %} / {{ item.subcategory.name }}{% endif %}
</span>
<a class="product-name" href="{{ item_url }}">{{ item.name }}</a>
<div class="product-meta">
<div class="product-price-block">
<span class="product-price{% if item.has_discount %} product-price--discount{% endif %}">
{{ item.display_price }} руб.
</span>
{% if item.has_discount %}
<span class="product-price-original">{{ item.price }} руб.</span>
{% endif %}
</div>
<span class="product-availability">В наличии</span>
</div>
</div>
<!-- Card 2 -->
<div class="product-card">
<img class="product-card-img" src="{% static 'img/chopping-board.jpg' %}" alt="Разделочная доска"
style="aspect-ratio:4/3;">
<div class="product-actions">
<button title="В избранное">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
<button title="В корзину">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 0 1-8 0"/>
</svg>
</button>
</article>
{% empty %}
<p class="products-empty">Товары скоро появятся.</p>
{% endfor %}
</div>
<div class="product-info">
<div class="p-name">Разделочная доска</div>
<div class="p-sub">Нержавеющая сталь</div>
<div class="p-price">50 руб</div>
</div>
</div>
<!-- Card 3 — tall -->
<div class="product-card tall">
<img class="product-card-img" src="{% static 'img/terra-mirror.jpg' %}" alt="Зеркало Terra">
<div class="product-actions">
<button title="В избранное">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
<button title="В корзину">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 0 1-8 0"/>
</svg>
</button>
</div>
<div class="product-info">
<div class="p-name">Зеркало Terra</div>
<div class="p-sub">Холодная подсветка, фронтальная</div>
<div class="p-price">200 руб</div>
</div>
</div>
<!-- Card 4 -->
<div class="product-card">
<img class="product-card-img" src="{% static 'img/toilet.jpg' %}" alt="Унитаз"
style="aspect-ratio:4/3;">
<div class="product-actions">
<button title="В избранное">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
<button title="В корзину">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 0 1-8 0"/>
</svg>
</button>
</div>
<div class="product-info">
<div class="p-name">Унитаз</div>
<div class="p-sub">Напольный с бачком, безободковый</div>
<div class="p-price">1600 руб</div>
</div>
</div>
<!-- Card 5 (коврик) -->
<div class="product-card">
<img class="product-card-img" src="{% static 'img/rubber-mat.jpg' %}" alt="Коврик"
style="aspect-ratio:4/3;">
<div class="product-actions">
<button title="В избранное">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
<button title="В корзину">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 0 1-8 0"/>
</svg>
</button>
</div>
<div class="product-info">
<div class="p-name">Коврик для посуды</div>
<div class="p-sub">Мгновенно впитывает воду</div>
<div class="p-price">20 руб</div>
</div>
</div>
<!-- Card 6 (смеситель) -->
<div class="product-card">
<img class="product-card-img" src="{% static 'img/mixer.jpg' %}" alt="Смеситель"
style="aspect-ratio:4/3;">
<div class="product-actions">
<button title="В избранное">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>
<button title="В корзину">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<path d="M16 10a4 4 0 0 1-8 0"/>
</svg>
</button>
</div>
<div class="product-info">
<div class="p-name">Смеситель</div>
<div class="p-sub">Однорычажный смеситель</div>
<div class="p-price">340 руб</div>
</div>
</div>
</div>{% endcomment %}
<div class="products-footer">
<a href="#" class="btn-primary">ПЕРЕЙТИ В КАТАЛОГ</a>
<a href="/catalog/" class="btn-primary">ПЕРЕЙТИ В КАТАЛОГ</a>
</div>
</section>

View File

@@ -180,14 +180,6 @@
</label>
</fieldset>
<div class="promo-row">
<label class="checkout-input">
<span>Промокод</span>
<input type="text" name="promo_code" value="{{ checkout_form.promo_code.value|default:'' }}" placeholder="Промокод">
</label>
<button type="button">ПРИМЕНИТЬ</button>
</div>
<div class="checkout-summary">
<p><strong>Товары, {{ total_quantity }} шт.</strong><span>{{ products_total }}</span></p>
<p><strong>Скидка</strong><span>{{ discount_total }}</span></p>