All checks were successful
Build and Push Docker Image / build (push) Successful in 30s
- Introduced `Subcategory` model with migration and unique slug constraint per category - Enabled subcategory assignment for items with validation to ensure category matching - Updated catalog views to handle subcategories, filtering items accordingly - Enhanced templates (`index.html`, `side-bar.html`, `item-list.html`, `detail.html`) to display subcategories - Added dynamic subcategory dropdown in Django admin with category filtering - Created JavaScript for admin subcategory handling and AJAX population - Wrote comprehensive tests for subcategory functionality in views and admin
239 lines
8.7 KiB
Python
239 lines
8.7 KiB
Python
from django.test import TestCase, override_settings
|
||
from django.contrib.auth import get_user_model
|
||
from django.core.exceptions import ValidationError
|
||
|
||
from items.models import Category, Item, ItemImage, Subcategory
|
||
|
||
|
||
@override_settings(MEDIA_URL="/media/")
|
||
class CatalogViewTests(TestCase):
|
||
def setUp(self):
|
||
self.plumbing = Category.objects.create(name="Сантехника", slug="plumbing")
|
||
self.decor = Category.objects.create(name="Декор", slug="decor")
|
||
self.sinks = Subcategory.objects.create(name="Мойки", slug="sinks", category=self.plumbing)
|
||
self.taps = Subcategory.objects.create(name="Смесители", slug="taps", category=self.plumbing)
|
||
self.decor_lights = Subcategory.objects.create(name="Подсветка", slug="lights", category=self.decor)
|
||
self.sink = Item.objects.create(
|
||
name="Кухонная мойка",
|
||
slug="kitchen-sink",
|
||
category=self.plumbing,
|
||
subcategory=self.sinks,
|
||
description="Искусственный камень, врезная на столешницу",
|
||
price="1600.00",
|
||
is_available=True,
|
||
)
|
||
self.mirror = Item.objects.create(
|
||
name="Зеркало Terra",
|
||
slug="terra-mirror",
|
||
category=self.decor,
|
||
subcategory=self.decor_lights,
|
||
description="Холодная подсветка, фронтальная",
|
||
price="200.00",
|
||
is_available=False,
|
||
)
|
||
|
||
def test_catalog_page_shows_items_from_all_categories(self):
|
||
response = self.client.get("/catalog/")
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertContains(response, "Кухонная мойка")
|
||
self.assertContains(response, "Зеркало Terra")
|
||
|
||
def test_category_page_shows_only_selected_category_items(self):
|
||
response = self.client.get("/catalog/plumbing/")
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertContains(response, "Кухонная мойка")
|
||
self.assertNotContains(response, "Зеркало Terra")
|
||
|
||
def test_category_page_shows_only_selected_category_subcategories(self):
|
||
response = self.client.get("/catalog/plumbing/")
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertContains(response, "Мойки")
|
||
self.assertContains(response, "Смесители")
|
||
self.assertNotContains(response, "Подсветка")
|
||
|
||
def test_subcategory_page_shows_only_selected_subcategory_items(self):
|
||
Item.objects.create(
|
||
name="Смеситель Mono",
|
||
slug="mono-tap",
|
||
category=self.plumbing,
|
||
subcategory=self.taps,
|
||
description="Однорычажный смеситель",
|
||
price="320.00",
|
||
is_available=True,
|
||
)
|
||
|
||
response = self.client.get("/catalog/plumbing/?subcategory=sinks")
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertContains(response, "Кухонная мойка")
|
||
self.assertNotContains(response, "Смеситель Mono")
|
||
|
||
def test_unknown_subcategory_returns_404(self):
|
||
response = self.client.get("/catalog/plumbing/?subcategory=unknown")
|
||
|
||
self.assertEqual(response.status_code, 404)
|
||
|
||
def test_subcategory_from_another_category_returns_404(self):
|
||
response = self.client.get("/catalog/plumbing/?subcategory=lights")
|
||
|
||
self.assertEqual(response.status_code, 404)
|
||
|
||
def test_unknown_category_returns_404(self):
|
||
response = self.client.get("/catalog/unknown/")
|
||
|
||
self.assertEqual(response.status_code, 404)
|
||
|
||
def test_sidebar_shows_all_categories_and_all_items_link(self):
|
||
response = self.client.get("/catalog/")
|
||
|
||
self.assertContains(response, "Все товары")
|
||
self.assertContains(response, "Сантехника")
|
||
self.assertContains(response, "Декор")
|
||
|
||
def test_item_card_uses_primary_image(self):
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/secondary.jpg",
|
||
sort_order=0,
|
||
is_primary=False,
|
||
)
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/primary.jpg",
|
||
sort_order=10,
|
||
is_primary=True,
|
||
)
|
||
|
||
response = self.client.get("/catalog/")
|
||
|
||
self.assertContains(response, "/media/items/primary.jpg")
|
||
self.assertNotContains(response, "/media/items/secondary.jpg")
|
||
|
||
def test_item_card_shows_subcategory(self):
|
||
response = self.client.get("/catalog/")
|
||
|
||
self.assertContains(response, "Сантехника / Мойки")
|
||
|
||
def test_item_card_uses_first_ordered_image_without_primary(self):
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/second.jpg",
|
||
sort_order=20,
|
||
is_primary=False,
|
||
)
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/first.jpg",
|
||
sort_order=10,
|
||
is_primary=False,
|
||
)
|
||
|
||
response = self.client.get("/catalog/")
|
||
|
||
self.assertContains(response, "/media/items/first.jpg")
|
||
self.assertNotContains(response, "/media/items/second.jpg")
|
||
|
||
def test_catalog_item_card_links_to_detail_page(self):
|
||
response = self.client.get("/catalog/")
|
||
|
||
self.assertContains(response, 'href="/catalog/plumbing/kitchen-sink/"')
|
||
|
||
def test_detail_page_shows_item_data(self):
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/sink-primary.jpg",
|
||
alt="Главное фото мойки",
|
||
sort_order=0,
|
||
is_primary=True,
|
||
)
|
||
|
||
response = self.client.get("/catalog/plumbing/kitchen-sink/")
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertContains(response, "Кухонная мойка")
|
||
self.assertContains(response, "Сантехника / Мойки")
|
||
self.assertContains(response, "Искусственный камень")
|
||
self.assertContains(response, "1600,00 руб.")
|
||
self.assertContains(response, "В наличии")
|
||
self.assertContains(response, "/media/items/sink-primary.jpg")
|
||
self.assertContains(response, "Главное фото мойки")
|
||
|
||
def test_detail_page_uses_primary_image_first(self):
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/secondary.jpg",
|
||
sort_order=0,
|
||
is_primary=False,
|
||
)
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/primary.jpg",
|
||
sort_order=10,
|
||
is_primary=True,
|
||
)
|
||
|
||
response = self.client.get("/catalog/plumbing/kitchen-sink/")
|
||
|
||
self.assertContains(response, '<img src="/media/items/primary.jpg"', html=False)
|
||
|
||
def test_detail_page_unknown_item_returns_404(self):
|
||
response = self.client.get("/catalog/plumbing/unknown/")
|
||
|
||
self.assertEqual(response.status_code, 404)
|
||
|
||
def test_detail_page_wrong_category_returns_404(self):
|
||
response = self.client.get("/catalog/decor/kitchen-sink/")
|
||
|
||
self.assertEqual(response.status_code, 404)
|
||
|
||
def test_item_subcategory_must_belong_to_item_category(self):
|
||
item = Item(
|
||
name="Некорректный товар",
|
||
slug="invalid-item",
|
||
category=self.plumbing,
|
||
subcategory=self.decor_lights,
|
||
description="",
|
||
price="100.00",
|
||
is_available=True,
|
||
)
|
||
|
||
with self.assertRaises(ValidationError):
|
||
item.full_clean()
|
||
|
||
def test_admin_subcategories_endpoint_filters_by_category(self):
|
||
user = get_user_model().objects.create_superuser(
|
||
username="admin",
|
||
email="admin@example.com",
|
||
password="password",
|
||
)
|
||
self.client.force_login(user)
|
||
|
||
response = self.client.get(f"/admin/items/item/subcategories/?category_id={self.plumbing.id}")
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertEqual(
|
||
response.json(),
|
||
{
|
||
"subcategories": [
|
||
{"id": self.sinks.id, "name": "Мойки"},
|
||
{"id": self.taps.id, "name": "Смесители"},
|
||
]
|
||
},
|
||
)
|
||
|
||
def test_admin_subcategories_endpoint_returns_empty_without_category(self):
|
||
user = get_user_model().objects.create_superuser(
|
||
username="admin",
|
||
email="admin@example.com",
|
||
password="password",
|
||
)
|
||
self.client.force_login(user)
|
||
|
||
response = self.client.get("/admin/items/item/subcategories/")
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertEqual(response.json(), {"subcategories": []})
|