All checks were successful
Build and Push Docker Image / build (push) Successful in 51s
- Implemented `Favorite` model with unique user-item constraint and timestamped records - Added `favorites:index` and `favorites:toggle` views with AJAX support for toggling favorites - Built templates and front-end assets for the favorites page and integration with item grids - Extended header with favorites link and dynamic counter - Added tests covering models, views, and templates for the favorites functionality - Updated admin to enable management of favorites via Django admin interface
121 lines
4.8 KiB
Python
121 lines
4.8 KiB
Python
from django.contrib.auth import get_user_model
|
||
from django.db import IntegrityError
|
||
from django.test import TestCase
|
||
from django.urls import reverse
|
||
|
||
from favorites.models import Favorite
|
||
from items.models import Category, Item, ItemImage, Subcategory
|
||
|
||
|
||
User = get_user_model()
|
||
|
||
|
||
class FavoritePageTests(TestCase):
|
||
def setUp(self):
|
||
self.user = User.objects.create_user(
|
||
username="marina",
|
||
email="marina@example.com",
|
||
password="StrongPassword123",
|
||
is_active=True,
|
||
)
|
||
self.plumbing = Category.objects.create(name="Сантехника", slug="plumbing")
|
||
self.sinks = Subcategory.objects.create(name="Мойки", slug="sinks", category=self.plumbing)
|
||
self.taps = Subcategory.objects.create(name="Смесители", slug="taps", category=self.plumbing)
|
||
self.sink = Item.objects.create(
|
||
name="Кухонная мойка",
|
||
slug="kitchen-sink",
|
||
category=self.plumbing,
|
||
subcategory=self.sinks,
|
||
description="Искусственный камень",
|
||
price="1600.00",
|
||
is_available=True,
|
||
)
|
||
self.tap = Item.objects.create(
|
||
name="Смеситель Mono",
|
||
slug="mono-tap",
|
||
category=self.plumbing,
|
||
subcategory=self.taps,
|
||
description="Однорычажный смеситель",
|
||
price="320.00",
|
||
is_available=False,
|
||
)
|
||
|
||
def test_favorites_page_redirects_unauthenticated_users(self):
|
||
response = self.client.get(reverse("favorites:index"))
|
||
|
||
self.assertEqual(response.status_code, 302)
|
||
self.assertIn(reverse("users:login"), response["Location"])
|
||
|
||
def test_favorites_page_shows_empty_state(self):
|
||
self.client.force_login(self.user)
|
||
|
||
response = self.client.get(reverse("favorites:index"))
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertContains(response, "В избранном пока нет товаров.")
|
||
|
||
def test_favorites_page_shows_items_in_tile_grid(self):
|
||
self.client.force_login(self.user)
|
||
ItemImage.objects.create(
|
||
item=self.sink,
|
||
image="items/sink-primary.jpg",
|
||
alt="Главное фото мойки",
|
||
sort_order=0,
|
||
is_primary=True,
|
||
)
|
||
Favorite.objects.create(user=self.user, item=self.tap)
|
||
Favorite.objects.create(user=self.user, item=self.sink)
|
||
|
||
response = self.client.get(reverse("favorites:index"))
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertContains(response, "Избранное")
|
||
self.assertContains(response, "Кухонная мойка")
|
||
self.assertContains(response, "Смеситель Mono")
|
||
self.assertContains(response, "/media/items/sink-primary.jpg")
|
||
self.assertContains(response, 'href="/catalog/plumbing/kitchen-sink/"', html=False)
|
||
self.assertContains(response, 'href="/catalog/plumbing/mono-tap/"', html=False)
|
||
self.assertContains(response, "Удалить из избранного")
|
||
|
||
def test_toggle_endpoint_adds_and_removes_favorite(self):
|
||
self.client.force_login(self.user)
|
||
|
||
add_response = self.client.post(
|
||
reverse("favorites:toggle"),
|
||
data={"item_id": self.sink.pk, "next": reverse("favorites:index")},
|
||
)
|
||
|
||
self.assertRedirects(add_response, reverse("favorites:index"))
|
||
self.assertTrue(Favorite.objects.filter(user=self.user, item=self.sink).exists())
|
||
|
||
remove_response = self.client.post(
|
||
reverse("favorites:toggle"),
|
||
data={"item_id": self.sink.pk, "next": reverse("favorites:index")},
|
||
)
|
||
|
||
self.assertRedirects(remove_response, reverse("favorites:index"))
|
||
self.assertFalse(Favorite.objects.filter(user=self.user, item=self.sink).exists())
|
||
|
||
def test_toggle_endpoint_returns_json_for_ajax_requests(self):
|
||
self.client.force_login(self.user)
|
||
|
||
response = self.client.post(
|
||
reverse("favorites:toggle"),
|
||
data={"item_id": self.sink.pk, "next": reverse("favorites:index")},
|
||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||
HTTP_ACCEPT="application/json",
|
||
)
|
||
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertEqual(response["Content-Type"], "application/json")
|
||
self.assertEqual(response.json()["item_id"], self.sink.pk)
|
||
self.assertTrue(response.json()["is_favorite"])
|
||
self.assertEqual(response.json()["favorite_count"], 1)
|
||
self.assertTrue(Favorite.objects.filter(user=self.user, item=self.sink).exists())
|
||
|
||
def test_favorite_pair_is_unique_per_user_and_item(self):
|
||
Favorite.objects.create(user=self.user, item=self.sink)
|
||
|
||
with self.assertRaises(IntegrityError):
|
||
Favorite.objects.create(user=self.user, item=self.sink)
|