diff --git a/Dshop/apps/products_catalogue/filters.py b/Dshop/apps/products_catalogue/filters.py index 7707544..68c15a5 100644 --- a/Dshop/apps/products_catalogue/filters.py +++ b/Dshop/apps/products_catalogue/filters.py @@ -1,11 +1,55 @@ +from django.forms import TextInput from django_filters import rest_framework as filters -from .models import Product +from .models import Product, Category class ProductFilter(filters.FilterSet): - name = filters.CharFilter(field_name='name', lookup_expr='icontains') - is_active = filters.BooleanFilter(field_name='is_active') + name = filters.CharFilter(field_name='name', lookup_expr='icontains', + widget=TextInput(attrs={'placeholder': 'Szukaj po nazwie'})) + price__lt = filters.NumberFilter(field_name='price', lookup_expr='lt', + widget=TextInput(attrs={'placeholder': 'max'})) + price__gt = filters.NumberFilter(field_name='price', lookup_expr='gt', + widget=TextInput(attrs={'placeholder': 'min'})) + category_name = filters.ChoiceFilter(field_name='category__name', + choices=Category.objects.all().values_list('name', 'name').distinct(), + label='Category', empty_label='Wybierz Kategorie') + availability = filters.ChoiceFilter( + choices=Product.AVAILABILITY_CHOICES, + label="Dostępność", + empty_label="Wszystkie", + method='filter_availability') + order_by = filters.OrderingFilter( + fields=( + ('price', 'price'), + ('name', 'name'), + ('created_at', 'created_at') + ), + field_labels={ + 'price': 'Cena rosnąco', + '-price': 'Cena malejąco', + '-name': 'Produkt A-Z', + 'name': 'Produkt Z-A', + 'created_at': 'Najnowsze', + '-created_at': 'Najstarsze', + }, + label='Sortuj', + empty_label='Domyślnie' + ) + + def filter_availability(self, queryset, name, value): + value = int(value) + if value in (1, 3, 7, 14): + return queryset.filter(availability__lte=value) + return queryset.filter(**{name: value}) + + def __init__(self, *args, **kwargs): + super(ProductFilter, self).__init__(*args, **kwargs) + self.filters['name'].label = 'Nazwa produktu' + self.filters['price__lt'].label = 'Cena do' + self.filters['price__gt'].label = 'Cena od' + self.filters['order_by'].label = 'Sortowanie' + self.filters['category_name'].label = 'Kategoria' class Meta: model = Product - fields = ['name', 'is_active'] + fields = ['name', 'price', 'category_name', 'availability'] diff --git a/Dshop/apps/products_catalogue/models.py b/Dshop/apps/products_catalogue/models.py index ecc14d3..0782b0f 100644 --- a/Dshop/apps/products_catalogue/models.py +++ b/Dshop/apps/products_catalogue/models.py @@ -63,6 +63,15 @@ def get_absolute_url(self): class Product(CatalogueItemModel): + AVAILABILITY_CHOICES = [ + (1, 'Dostępny, sklep wyśle produkt w ciągu 24 godzin'), + (3, 'Sklep wyśle produkt do 3 dni'), + (7, 'Sklep wyśle produkt w ciągu tygodnia'), + (14, 'Sklep wyśle produkt do 14 dni'), + (90, 'Towar na zamówienie'), + (99, 'Brak informacji o dostępności - status „sprawdź w sklepie”'), + (110, 'Przedsprzedaż'), + ] category = models.ForeignKey(Category, on_delete=models.CASCADE) price = models.DecimalField(max_digits=10, decimal_places=2) short_description = tinymce_models.HTMLField() @@ -70,15 +79,7 @@ class Product(CatalogueItemModel): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) availability = models.PositiveSmallIntegerField( - choices=[ - (1, 'Dostępny, sklep wyśle produkt w ciągu 24 godzin'), - (3, 'Sklep wyśle produkt do 3 dni'), - (7, 'Sklep wyśle produkt w ciągu tygodnia'), - (14, 'Sklep wyśle produkt do 14 dni'), - (90, 'Towar na zamówienie'), - (99, 'Brak informacji o dostępności - status „sprawdź w sklepie”'), - (110, 'Przedsprzedaż'), - ], + choices=AVAILABILITY_CHOICES, default=99, ) parent_product = models.ForeignKey( @@ -135,7 +136,7 @@ def get_price(self, item: CartItem) -> DecimalField: def is_available(self): return self.availability - + class ProductImage(models.Model): product = models.ForeignKey( diff --git a/Dshop/apps/products_catalogue/templates/products_catalogue/products_list.html b/Dshop/apps/products_catalogue/templates/products_catalogue/products_list.html index be4a197..5038227 100644 --- a/Dshop/apps/products_catalogue/templates/products_catalogue/products_list.html +++ b/Dshop/apps/products_catalogue/templates/products_catalogue/products_list.html @@ -14,9 +14,47 @@

DShop collection

+
+
+
+
+
+ {{ form.name.label }}: + {{ form.name}} +
+
+ {{ form.category_name.label }}: + {{ form.category_name }} +
+
+ {{ form.availability.label }}: + {{ form.availability }} +
+
+
+
+ {{ form.price__gt.label }}: + {{ form.price__gt }} +
+
+ {{ form.price__lt.label }}: + {{ form.price__lt }} +
+
+ {{ form.order_by.label }}: + {{ form.order_by }} +
+
+ +
+
+ +
+ +
+
{% for object in object_list %} -
diff --git a/Dshop/apps/products_catalogue/tests/conftest.py b/Dshop/apps/products_catalogue/tests/conftest.py index c8e2609..f715fcb 100644 --- a/Dshop/apps/products_catalogue/tests/conftest.py +++ b/Dshop/apps/products_catalogue/tests/conftest.py @@ -1,6 +1,9 @@ +import time + from django.contrib.auth.models import User import pytest from rest_framework.test import APIClient +from apps.products_catalogue.models import Category, Product @pytest.fixture @@ -8,4 +11,20 @@ def api_client(): client = APIClient() user = User.objects.create_user(username='testuser', password='testpassword') client.force_authenticate(user) - return client \ No newline at end of file + return client + + +@pytest.fixture +def products(): + category = Category.objects.create(name='Test Category', is_active=True) + for price in range(1, 11): + Product.objects.create( + name=f"main product ${price}", + category=category, + price=price, + short_description="short desc inactive", + full_description="full_description inactive", + is_active=True, + availability=3 + ) + time.sleep(0.2) diff --git a/Dshop/apps/products_catalogue/tests/test_products_filters.py b/Dshop/apps/products_catalogue/tests/test_products_filters.py new file mode 100644 index 0000000..6133c58 --- /dev/null +++ b/Dshop/apps/products_catalogue/tests/test_products_filters.py @@ -0,0 +1,122 @@ +from decimal import Decimal + +import pytest + +from django.urls import reverse +from ..models import Product + + +@pytest.mark.django_db +def test_filter_by_price(client, products): + # Products is a fixture with 9 products and diff prices. + # checking ordering + # Given is in fixture + # When + url = reverse("products-list") + response = client.get(f"{url}?order_by=-price") + products = response.context["object_list"] + # Then + for idx, price in enumerate(range(10, 1, -1)): + assert products[idx].price == price + + +@pytest.mark.django_db +def test_product(client, products): + assert Product.objects.filter(is_active=True).count() == 10 + + +@pytest.mark.django_db +def test_filter_by_price_asc(client, products): + url = reverse("products-list") + response = client.get(f"{url}?order_by=price") + products = response.context['object_list'] + for idx, price in enumerate(range(1, 11)): + assert products[idx].price == price + + +@pytest.mark.django_db +def test_filter_by_price_desc(client, products): + url = reverse("products-list") + response = client.get(f"{url}?order_by=-price") + products = response.context['object_list'] + for idx, price in enumerate(range(10, 0, -1)): + assert products[idx].price == Decimal(price) + + +@pytest.mark.django_db +def test_filter_order_by_name(client, products): + url = reverse("products-list") + response = client.get(f"{url}?order_by=name") + products = response.context['object_list'] + for idx, name in enumerate([1, 10, 2, 3, 4, 5, 6, 7, 8, 9]): + assert products[idx].name == f"main product ${name}" + + +@pytest.mark.django_db +def test_filter_order_by_name_desc(client, products): + url = reverse("products-list") + response = client.get(f"{url}?order_by=-name") + products = response.context['object_list'] + for idx, name in enumerate([9, 8, 7, 6, 5, 4, 3, 2, 10, 1]): + assert products[idx].name == f'main product ${name}' + + +@pytest.mark.django_db +def test_filter_by_created(client, products): + url = reverse("products-list") + response = client.get(f"{url}?order_by=created_at") + products = response.context['object_list'] + for idx, value in enumerate(range(1, 10)): + assert products[idx].created_at <= products[idx + 1].created_at + + +@pytest.mark.django_db +def test_filter_by_created_desc(client, products): + url = reverse("products-list") + response = client.get(f"{url}?order_by=-created_at") + products = response.context['object_list'] + for idx, value in enumerate(range(10, 1, -1)): + assert products[idx].created_at >= products[idx + 1].created_at + + +@pytest.mark.django_db +def test_filter_category(client, products): + url = reverse("products-list") + response = client.get(f"{url}?category_name=Test_Category") + products = response.context['object_list'] + for idx, value in enumerate(range(1, 11)): + assert products[idx].category.name == 'Test Category' + + +@pytest.mark.django_db +def test_filter_by_min_price(client, products): + url = reverse("products-list") + response = client.get(f"{url}?prince__gt=5") + products = response.context['object_list'] + for value in range(5, 10): + assert products[value].price == Decimal(value + 1) + + +@pytest.mark.django_db +def test_filter_by_max_price(client, products): + url = reverse("products-list") + response = client.get(f"{url}?prince__lt=5") + products = response.context['object_list'] + for idx, value in enumerate(range(5)): + assert products[idx].price == Decimal(value + 1) + + +@pytest.mark.django_db +def test_filter_by_name(client, products): + url = reverse("products-list") + response = client.get(f"{url}?name=main+product+$10") + products = response.context['object_list'] + assert products[0].name == 'main product $10' + +@pytest.mark.django_db +def test_filter_availability(client, products): + url = reverse("products-list") + response = client.get(f"{url}?availability=3") + products = response.context['object_list'] + for idx, value in enumerate(range(1, 11)): + assert products[idx].availability == 3 \ No newline at end of file diff --git a/Dshop/apps/products_catalogue/views.py b/Dshop/apps/products_catalogue/views.py index 9054ca5..24c7707 100644 --- a/Dshop/apps/products_catalogue/views.py +++ b/Dshop/apps/products_catalogue/views.py @@ -7,16 +7,25 @@ from lxml import etree from dj_shop_cart.cart import Cart - +from .filters import ProductFilter from .models import Product, Category class ProductListView(ListView): model = Product template_name = 'products_catalogue/products_list.html' + queryset = Product.objects.filter(is_active=True) + paginate_by = 24 def get_queryset(self): - return Product.objects.filter(is_active=True) + queryset = super().get_queryset() + self.filterset = ProductFilter(self.request.GET, queryset=queryset) + return self.filterset.qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['form'] = self.filterset.form + return context class ProductDetailView(DetailView):