From 7453c001c64665fdd32c9c0b5cfba534611b4c6d Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Sat, 15 Nov 2025 13:01:59 +0100 Subject: [PATCH 1/6] wip: borrador correcciones dashboard --- essenza/product/tests.py | 179 ++++++++++++++++++++++- essenza/product/views.py | 51 ++++--- essenza/templates/product/dashboard.html | 4 +- 3 files changed, 212 insertions(+), 22 deletions(-) diff --git a/essenza/product/tests.py b/essenza/product/tests.py index a39b155..a72451b 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -1 +1,178 @@ -# Create your tests here. +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +from order.models import Order, OrderProduct + +from .models import Product + +User = get_user_model() + + +class DashboardViewTests(TestCase): + def setUp(self): + self.dashboard_url = reverse("dashboard") + self.login_url = reverse("login") + self.now = timezone.now() + + # --- Usuarios --- + self.admin_user = User.objects.create_user( + username="admin", email="admin@test.com", password="pass", role="admin" + ) + self.regular_user = User.objects.create_user( + username="user", email="user@test.com", password="pass", role="customer" + ) + + # --- Productos Base --- + # Estos se usarán para crear órdenes + self.p_30_day = Product.objects.create( + name="Producto 30 Días", is_active=True, stock=10 + ) + self.p_1_year = Product.objects.create( + name="Producto 1 Año", is_active=True, stock=20 + ) + self.p_stock = Product.objects.create( + name="Producto Stock Alto", is_active=True, stock=999 + ) + self.p_stock_low = Product.objects.create( + name="Producto Stock Bajo", is_active=True, stock=1 + ) + self.p_inactive = Product.objects.create( + name="Producto Inactivo", is_active=False, stock=1000 + ) + + # --- 1. Tests de Permisos (test_func) --- + + def test_anonymous_user_gets_200(self): + """Los usuarios anónimos pueden ver la vista.""" + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/dashboard.html") + + def test_regular_user_gets_200(self): + """Los usuarios logueados (no admin) pueden ver la vista.""" + self.client.login(email="user@test.com", password="pass") + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/dashboard.html") + + def test_admin_user_is_redirected(self): + """Los admins fallan el test_func y son redirigidos.""" + self.client.login(email="admin@test.com", password="pass") + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 302) + self.assertRedirects(resp, f"{self.login_url}?next={self.dashboard_url}") + + # --- 2. Tests de Lógica de Negocio (método get) --- + + def test_logic_branch_1_shows_30_day_products(self): + """Prueba el primer 'if': Muestra productos de 30 días.""" + + # 1. Crear ventas recientes (hace 10 días) + order_recent = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=10) + ) + OrderProduct.objects.create( + order=order_recent, product=self.p_30_day, quantity=100 + ) + + # 2. Crear ventas antiguas (hace 100 días) + order_old = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=100) + ) + OrderProduct.objects.create( + order=order_old, + product=self.p_1_year, + quantity=500, # Más vendido, pero antiguo + ) + + # (El 'p_stock' no tiene ventas, solo stock alto) + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: + # El producto de 30 días debe ser el primero y único + self.assertEqual(len(products_in_context), 1) + self.assertEqual(products_in_context[0], self.p_30_day) + self.assertEqual(products_in_context[0].total_quantity, 100) + + def test_logic_branch_2_falls_back_to_1_year_products(self): + """Prueba el segundo 'if': Falla 30 días, muestra 1 año.""" + + # 1. NO crear ventas recientes + + # 2. Crear ventas antiguas (hace 100 días) + order_old = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=100) + ) + OrderProduct.objects.create( + order=order_old, product=self.p_1_year, quantity=500 + ) + + # 3. Crear ventas MUY antiguas (hace 400 días) - deben ignorarse + order_ancient = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=400) + ) + OrderProduct.objects.create( + order=order_ancient, product=self.p_30_day, quantity=999 + ) + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: + # El producto de 1 año debe ser el primero y único + self.assertEqual(len(products_in_context), 1) + self.assertEqual(products_in_context[0], self.p_1_year) + self.assertEqual(products_in_context[0].total_quantity, 500) + + def test_logic_branch_3_falls_back_to_stock_products(self): + """Prueba el tercer 'if': Falla 1 año, muestra por stock.""" + + # 1. NO crear ventas recientes + + # 2. NO crear ventas en el último año + + # 3. Crear ventas MUY antiguas (hace 400 días) - para que fallen los dos 'if' + order_ancient = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=400) + ) + OrderProduct.objects.create( + order=order_ancient, product=self.p_30_day, quantity=999 + ) + + # 4. Crear ventas de productos INACTIVOS (deben ignorarse siempre) + order_inactive = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=10) + ) + OrderProduct.objects.create( + order=order_inactive, product=self.p_inactive, quantity=5000 + ) + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: + # Debe mostrar productos por stock descendente. + # p_stock (999) > p_1_year (20) > p_30_day (10) > p_stock_low (1) + # p_inactive (1000) debe ser ignorado. + + self.assertIn(self.p_stock, products_in_context) + self.assertIn(self.p_1_year, products_in_context) + self.assertNotIn(self.p_inactive, products_in_context) # Importante + + # Comprobar el orden por stock + self.assertEqual(products_in_context[0], self.p_stock) # stock 999 + self.assertEqual(products_in_context[1], self.p_1_year) # stock 20 + self.assertEqual(products_in_context[2], self.p_30_day) # stock 10 + self.assertEqual(products_in_context[3], self.p_stock_low) # stock 1 + + # Comprobar que no hay 'total_quantity' (viene de otra consulta) + self.assertFalse(hasattr(products_in_context[0], "total_quantity")) diff --git a/essenza/product/views.py b/essenza/product/views.py index e0ed392..eeb88bd 100644 --- a/essenza/product/views.py +++ b/essenza/product/views.py @@ -1,37 +1,48 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.db.models import Sum from django.shortcuts import redirect, render from django.utils import timezone from django.views import View -from order.models import OrderProduct from .models import Product -class DashboardView(View): +class DashboardView(UserPassesTestMixin, View): template_name = "product/dashboard.html" + # Todos excepto los administradores pueden acceder a esta vista + def test_func(self): + return ( + not self.request.user.is_authenticated or self.request.user.role != "admin" + ) + def get(self, request, *args, **kwargs): - # Obtenemos de los datos - order_products = OrderProduct.objects.all() month_ago = timezone.now() - timezone.timedelta(days=30) - times_purchased = {} - for order_product in order_products: - order = order_product.order - if order.placed_at >= month_ago: - if order_product.product.id in times_purchased: - times_purchased[order_product.product.id] += order_product.quantity - else: - times_purchased[order_product.product.id] = order_product.quantity - times_purchased_ordered = dict( - sorted( - times_purchased.items(), key=lambda quantity: quantity[1], reverse=True + year_ago = timezone.now() - timezone.timedelta(days=365) + + def get_top_selling_products(since): + # Paso 1: Definir el filtro base. + base_query = Product.objects.filter( + is_active=True, product_orders__order__placed_at__gte=since ) - ) - most_purchased_products = list(times_purchased_ordered.keys())[:10] - products = Product.objects.filter( - is_active=True, id__in=most_purchased_products - ) + # Paso 2: Anotar (calcular) el total vendido para esos productos. + query_with_totals = base_query.annotate( + total_quantity=Sum("product_orders__quantity") + ) + # Paso 3: Filtrar de nuevo, sobre el campo calculado. + filtered_query = query_with_totals.filter(total_quantity__gt=0) + # Paso 4: Ordenar (descendente). + ordered_query = filtered_query.order_by("-total_quantity") + # Paso 5: Limitar. + top_products = ordered_query[:10] + return top_products + + products = get_top_selling_products(since=month_ago) + if not products.exists(): + products = get_top_selling_products(since=year_ago) + if not products.exists(): + products = Product.objects.filter(is_active=True).order_by("-stock")[:10] return render(request, self.template_name, {"products": products}) diff --git a/essenza/templates/product/dashboard.html b/essenza/templates/product/dashboard.html index 92e5b7a..50f5acd 100644 --- a/essenza/templates/product/dashboard.html +++ b/essenza/templates/product/dashboard.html @@ -316,10 +316,12 @@ + {% if products.size > 1 %}
-

Top 10 Bestsellers

+

Top Bestsellers

Una cuidada selección de los productos estrella de la temporada.

+ {% endif %}
{% if products %} {% for p in products %} From 9722036a27a96d8c8c04a0e5af81d74baf345c02 Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Sat, 15 Nov 2025 13:01:59 +0100 Subject: [PATCH 2/6] wip: borrador correcciones dashboard --- essenza/product/tests.py | 179 ++++++++++++++++++++++- essenza/product/views.py | 51 ++++--- essenza/templates/product/dashboard.html | 4 +- 3 files changed, 212 insertions(+), 22 deletions(-) diff --git a/essenza/product/tests.py b/essenza/product/tests.py index a39b155..a72451b 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -1 +1,178 @@ -# Create your tests here. +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +from order.models import Order, OrderProduct + +from .models import Product + +User = get_user_model() + + +class DashboardViewTests(TestCase): + def setUp(self): + self.dashboard_url = reverse("dashboard") + self.login_url = reverse("login") + self.now = timezone.now() + + # --- Usuarios --- + self.admin_user = User.objects.create_user( + username="admin", email="admin@test.com", password="pass", role="admin" + ) + self.regular_user = User.objects.create_user( + username="user", email="user@test.com", password="pass", role="customer" + ) + + # --- Productos Base --- + # Estos se usarán para crear órdenes + self.p_30_day = Product.objects.create( + name="Producto 30 Días", is_active=True, stock=10 + ) + self.p_1_year = Product.objects.create( + name="Producto 1 Año", is_active=True, stock=20 + ) + self.p_stock = Product.objects.create( + name="Producto Stock Alto", is_active=True, stock=999 + ) + self.p_stock_low = Product.objects.create( + name="Producto Stock Bajo", is_active=True, stock=1 + ) + self.p_inactive = Product.objects.create( + name="Producto Inactivo", is_active=False, stock=1000 + ) + + # --- 1. Tests de Permisos (test_func) --- + + def test_anonymous_user_gets_200(self): + """Los usuarios anónimos pueden ver la vista.""" + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/dashboard.html") + + def test_regular_user_gets_200(self): + """Los usuarios logueados (no admin) pueden ver la vista.""" + self.client.login(email="user@test.com", password="pass") + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "product/dashboard.html") + + def test_admin_user_is_redirected(self): + """Los admins fallan el test_func y son redirigidos.""" + self.client.login(email="admin@test.com", password="pass") + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 302) + self.assertRedirects(resp, f"{self.login_url}?next={self.dashboard_url}") + + # --- 2. Tests de Lógica de Negocio (método get) --- + + def test_logic_branch_1_shows_30_day_products(self): + """Prueba el primer 'if': Muestra productos de 30 días.""" + + # 1. Crear ventas recientes (hace 10 días) + order_recent = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=10) + ) + OrderProduct.objects.create( + order=order_recent, product=self.p_30_day, quantity=100 + ) + + # 2. Crear ventas antiguas (hace 100 días) + order_old = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=100) + ) + OrderProduct.objects.create( + order=order_old, + product=self.p_1_year, + quantity=500, # Más vendido, pero antiguo + ) + + # (El 'p_stock' no tiene ventas, solo stock alto) + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: + # El producto de 30 días debe ser el primero y único + self.assertEqual(len(products_in_context), 1) + self.assertEqual(products_in_context[0], self.p_30_day) + self.assertEqual(products_in_context[0].total_quantity, 100) + + def test_logic_branch_2_falls_back_to_1_year_products(self): + """Prueba el segundo 'if': Falla 30 días, muestra 1 año.""" + + # 1. NO crear ventas recientes + + # 2. Crear ventas antiguas (hace 100 días) + order_old = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=100) + ) + OrderProduct.objects.create( + order=order_old, product=self.p_1_year, quantity=500 + ) + + # 3. Crear ventas MUY antiguas (hace 400 días) - deben ignorarse + order_ancient = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=400) + ) + OrderProduct.objects.create( + order=order_ancient, product=self.p_30_day, quantity=999 + ) + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: + # El producto de 1 año debe ser el primero y único + self.assertEqual(len(products_in_context), 1) + self.assertEqual(products_in_context[0], self.p_1_year) + self.assertEqual(products_in_context[0].total_quantity, 500) + + def test_logic_branch_3_falls_back_to_stock_products(self): + """Prueba el tercer 'if': Falla 1 año, muestra por stock.""" + + # 1. NO crear ventas recientes + + # 2. NO crear ventas en el último año + + # 3. Crear ventas MUY antiguas (hace 400 días) - para que fallen los dos 'if' + order_ancient = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=400) + ) + OrderProduct.objects.create( + order=order_ancient, product=self.p_30_day, quantity=999 + ) + + # 4. Crear ventas de productos INACTIVOS (deben ignorarse siempre) + order_inactive = Order.objects.create( + user=self.regular_user, placed_at=self.now - timezone.timedelta(days=10) + ) + OrderProduct.objects.create( + order=order_inactive, product=self.p_inactive, quantity=5000 + ) + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + products_in_context = list(resp.context["products"]) + + # ASERCIÓN: + # Debe mostrar productos por stock descendente. + # p_stock (999) > p_1_year (20) > p_30_day (10) > p_stock_low (1) + # p_inactive (1000) debe ser ignorado. + + self.assertIn(self.p_stock, products_in_context) + self.assertIn(self.p_1_year, products_in_context) + self.assertNotIn(self.p_inactive, products_in_context) # Importante + + # Comprobar el orden por stock + self.assertEqual(products_in_context[0], self.p_stock) # stock 999 + self.assertEqual(products_in_context[1], self.p_1_year) # stock 20 + self.assertEqual(products_in_context[2], self.p_30_day) # stock 10 + self.assertEqual(products_in_context[3], self.p_stock_low) # stock 1 + + # Comprobar que no hay 'total_quantity' (viene de otra consulta) + self.assertFalse(hasattr(products_in_context[0], "total_quantity")) diff --git a/essenza/product/views.py b/essenza/product/views.py index e0ed392..eeb88bd 100644 --- a/essenza/product/views.py +++ b/essenza/product/views.py @@ -1,37 +1,48 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.db.models import Sum from django.shortcuts import redirect, render from django.utils import timezone from django.views import View -from order.models import OrderProduct from .models import Product -class DashboardView(View): +class DashboardView(UserPassesTestMixin, View): template_name = "product/dashboard.html" + # Todos excepto los administradores pueden acceder a esta vista + def test_func(self): + return ( + not self.request.user.is_authenticated or self.request.user.role != "admin" + ) + def get(self, request, *args, **kwargs): - # Obtenemos de los datos - order_products = OrderProduct.objects.all() month_ago = timezone.now() - timezone.timedelta(days=30) - times_purchased = {} - for order_product in order_products: - order = order_product.order - if order.placed_at >= month_ago: - if order_product.product.id in times_purchased: - times_purchased[order_product.product.id] += order_product.quantity - else: - times_purchased[order_product.product.id] = order_product.quantity - times_purchased_ordered = dict( - sorted( - times_purchased.items(), key=lambda quantity: quantity[1], reverse=True + year_ago = timezone.now() - timezone.timedelta(days=365) + + def get_top_selling_products(since): + # Paso 1: Definir el filtro base. + base_query = Product.objects.filter( + is_active=True, product_orders__order__placed_at__gte=since ) - ) - most_purchased_products = list(times_purchased_ordered.keys())[:10] - products = Product.objects.filter( - is_active=True, id__in=most_purchased_products - ) + # Paso 2: Anotar (calcular) el total vendido para esos productos. + query_with_totals = base_query.annotate( + total_quantity=Sum("product_orders__quantity") + ) + # Paso 3: Filtrar de nuevo, sobre el campo calculado. + filtered_query = query_with_totals.filter(total_quantity__gt=0) + # Paso 4: Ordenar (descendente). + ordered_query = filtered_query.order_by("-total_quantity") + # Paso 5: Limitar. + top_products = ordered_query[:10] + return top_products + + products = get_top_selling_products(since=month_ago) + if not products.exists(): + products = get_top_selling_products(since=year_ago) + if not products.exists(): + products = Product.objects.filter(is_active=True).order_by("-stock")[:10] return render(request, self.template_name, {"products": products}) diff --git a/essenza/templates/product/dashboard.html b/essenza/templates/product/dashboard.html index 92e5b7a..50f5acd 100644 --- a/essenza/templates/product/dashboard.html +++ b/essenza/templates/product/dashboard.html @@ -316,10 +316,12 @@ + {% if products.size > 1 %}
-

Top 10 Bestsellers

+

Top Bestsellers

Una cuidada selección de los productos estrella de la temporada.

+ {% endif %}
{% if products %} {% for p in products %} From 9cb5310872a870c2b8adfbbf3d5db048214aac6c Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Sat, 15 Nov 2025 13:48:32 +0100 Subject: [PATCH 3/6] Funcionalidad dashboard lista con tests incluidos. --- essenza/product/tests.py | 154 +++++++++++++++-------- essenza/templates/product/dashboard.html | 9 +- essenza/templates/product/stock.html | 2 +- 3 files changed, 113 insertions(+), 52 deletions(-) diff --git a/essenza/product/tests.py b/essenza/product/tests.py index a72451b..5fb7b53 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -4,73 +4,106 @@ from django.utils import timezone from order.models import Order, OrderProduct -from .models import Product +from .models import Category, Product User = get_user_model() -class DashboardViewTests(TestCase): +class DashboardViewLogicTests(TestCase): def setUp(self): self.dashboard_url = reverse("dashboard") self.login_url = reverse("login") self.now = timezone.now() - # --- Usuarios --- + # --- Usuarios (Usando tus roles 'admin' y 'user') --- self.admin_user = User.objects.create_user( username="admin", email="admin@test.com", password="pass", role="admin" ) self.regular_user = User.objects.create_user( - username="user", email="user@test.com", password="pass", role="customer" + username="user", email="user@test.com", password="pass", role="user" ) - # --- Productos Base --- - # Estos se usarán para crear órdenes + # --- Productos --- self.p_30_day = Product.objects.create( - name="Producto 30 Días", is_active=True, stock=10 + name="Producto 30 Días", + description="Test Desc", + category=Category.MAQUILLAJE, + brand="TestBrand", + price=10.00, + stock=10, + is_active=True, ) self.p_1_year = Product.objects.create( - name="Producto 1 Año", is_active=True, stock=20 + name="Producto 1 Año", + description="Test Desc", + category=Category.CABELLO, + brand="TestBrand", + price=20.00, + stock=20, + is_active=True, ) self.p_stock = Product.objects.create( - name="Producto Stock Alto", is_active=True, stock=999 + name="Producto Stock Alto", + description="Test Desc", + category=Category.PERFUME, + brand="TestBrand", + price=30.00, + stock=999, + is_active=True, ) self.p_stock_low = Product.objects.create( - name="Producto Stock Bajo", is_active=True, stock=1 + name="Producto Stock Bajo", + description="Test Desc", + category=Category.TRATAMIENTO, + brand="TestBrand", + price=40.00, + stock=1, + is_active=True, ) self.p_inactive = Product.objects.create( - name="Producto Inactivo", is_active=False, stock=1000 + name="Producto Inactivo", + description="Test Desc", + category=Category.MAQUILLAJE, + brand="TestBrand", + price=50.00, + stock=1000, + is_active=False, ) # --- 1. Tests de Permisos (test_func) --- def test_anonymous_user_gets_200(self): - """Los usuarios anónimos pueden ver la vista.""" + """Prueba que los anónimos pueden ver la vista.""" resp = self.client.get(self.dashboard_url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed(resp, "product/dashboard.html") def test_regular_user_gets_200(self): - """Los usuarios logueados (no admin) pueden ver la vista.""" + """Prueba que los usuarios 'user' pueden ver la vista.""" self.client.login(email="user@test.com", password="pass") resp = self.client.get(self.dashboard_url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed(resp, "product/dashboard.html") - def test_admin_user_is_redirected(self): - """Los admins fallan el test_func y son redirigidos.""" + def test_admin_user_is_forbidden(self): + """Prueba que los 'admin' son bloqueados.""" self.client.login(email="admin@test.com", password="pass") resp = self.client.get(self.dashboard_url) - self.assertEqual(resp.status_code, 302) - self.assertRedirects(resp, f"{self.login_url}?next={self.dashboard_url}") + self.assertEqual(resp.status_code, 403) # --- 2. Tests de Lógica de Negocio (método get) --- def test_logic_branch_1_shows_30_day_products(self): - """Prueba el primer 'if': Muestra productos de 30 días.""" - + """ + Prueba el primer 'if': Muestra productos de 30 días. + Ignora ventas más antiguas aunque sean mayores. + """ # 1. Crear ventas recientes (hace 10 días) order_recent = Order.objects.create( - user=self.regular_user, placed_at=self.now - timezone.timedelta(days=10) + user=self.regular_user, + address="Test Address 1", + placed_at=self.now + - timezone.timedelta(days=10), # <-- Controlamos la fecha ) OrderProduct.objects.create( order=order_recent, product=self.p_30_day, quantity=100 @@ -78,69 +111,72 @@ def test_logic_branch_1_shows_30_day_products(self): # 2. Crear ventas antiguas (hace 100 días) order_old = Order.objects.create( - user=self.regular_user, placed_at=self.now - timezone.timedelta(days=100) + user=self.regular_user, + address="Test Address 2", + placed_at=self.now - timezone.timedelta(days=100), ) OrderProduct.objects.create( order=order_old, product=self.p_1_year, - quantity=500, # Más vendido, pero antiguo + quantity=500, # Más ventas, pero antiguo ) - # (El 'p_stock' no tiene ventas, solo stock alto) - - # Cargar la vista resp = self.client.get(self.dashboard_url) - self.assertEqual(resp.status_code, 200) products_in_context = list(resp.context["products"]) - # ASERCIÓN: - # El producto de 30 días debe ser el primero y único + # ASERCIÓN: Solo debe aparecer el producto de 30 días self.assertEqual(len(products_in_context), 1) self.assertEqual(products_in_context[0], self.p_30_day) self.assertEqual(products_in_context[0].total_quantity, 100) def test_logic_branch_2_falls_back_to_1_year_products(self): - """Prueba el segundo 'if': Falla 30 días, muestra 1 año.""" - + """ + Prueba el segundo 'if': Falla 30 días, muestra 1 año. + Ignora ventas de hace más de 1 año. + """ # 1. NO crear ventas recientes # 2. Crear ventas antiguas (hace 100 días) order_old = Order.objects.create( - user=self.regular_user, placed_at=self.now - timezone.timedelta(days=100) + user=self.regular_user, + address="Test Address 1", + placed_at=self.now - timezone.timedelta(days=100), ) OrderProduct.objects.create( order=order_old, product=self.p_1_year, quantity=500 ) - # 3. Crear ventas MUY antiguas (hace 400 días) - deben ignorarse + # 3. Crear ventas MUY antiguas (hace 400 días) - debe ignorarse order_ancient = Order.objects.create( - user=self.regular_user, placed_at=self.now - timezone.timedelta(days=400) + user=self.regular_user, + address="Test Address 2", + placed_at=self.now - timezone.timedelta(days=400), ) OrderProduct.objects.create( order=order_ancient, product=self.p_30_day, quantity=999 ) - # Cargar la vista resp = self.client.get(self.dashboard_url) - self.assertEqual(resp.status_code, 200) products_in_context = list(resp.context["products"]) - # ASERCIÓN: - # El producto de 1 año debe ser el primero y único + # ASERCIÓN: Solo debe aparecer el producto de 1 año self.assertEqual(len(products_in_context), 1) self.assertEqual(products_in_context[0], self.p_1_year) self.assertEqual(products_in_context[0].total_quantity, 500) def test_logic_branch_3_falls_back_to_stock_products(self): - """Prueba el tercer 'if': Falla 1 año, muestra por stock.""" - + """ + Prueba el tercer 'if': Falla 1 año, muestra por stock. + Ignora ventas de productos inactivos. + """ # 1. NO crear ventas recientes - # 2. NO crear ventas en el último año - # 3. Crear ventas MUY antiguas (hace 400 días) - para que fallen los dos 'if' + # 3. Crear ventas MUY antiguas (hace 400 días) - para forzar el fallback order_ancient = Order.objects.create( - user=self.regular_user, placed_at=self.now - timezone.timedelta(days=400) + user=self.regular_user, + address="Test Address 1", + placed_at=self.now - timezone.timedelta(days=400), ) OrderProduct.objects.create( order=order_ancient, product=self.p_30_day, quantity=999 @@ -148,25 +184,24 @@ def test_logic_branch_3_falls_back_to_stock_products(self): # 4. Crear ventas de productos INACTIVOS (deben ignorarse siempre) order_inactive = Order.objects.create( - user=self.regular_user, placed_at=self.now - timezone.timedelta(days=10) + user=self.regular_user, + address="Test Address 2", + placed_at=self.now - timezone.timedelta(days=10), # Reciente, pero inactivo ) OrderProduct.objects.create( order=order_inactive, product=self.p_inactive, quantity=5000 ) - # Cargar la vista resp = self.client.get(self.dashboard_url) - self.assertEqual(resp.status_code, 200) products_in_context = list(resp.context["products"]) - # ASERCIÓN: - # Debe mostrar productos por stock descendente. + # ASERCIÓN: Debe mostrar los productos activos por stock descendente # p_stock (999) > p_1_year (20) > p_30_day (10) > p_stock_low (1) # p_inactive (1000) debe ser ignorado. self.assertIn(self.p_stock, products_in_context) self.assertIn(self.p_1_year, products_in_context) - self.assertNotIn(self.p_inactive, products_in_context) # Importante + self.assertNotIn(self.p_inactive, products_in_context) # Clave # Comprobar el orden por stock self.assertEqual(products_in_context[0], self.p_stock) # stock 999 @@ -174,5 +209,26 @@ def test_logic_branch_3_falls_back_to_stock_products(self): self.assertEqual(products_in_context[2], self.p_30_day) # stock 10 self.assertEqual(products_in_context[3], self.p_stock_low) # stock 1 - # Comprobar que no hay 'total_quantity' (viene de otra consulta) + # Comprobar que no hay 'total_quantity' (viene de la consulta de stock) self.assertFalse(hasattr(products_in_context[0], "total_quantity")) + + def test_logic_branch_4_handles_empty_database(self): + """ + Prueba el caso límite final: No hay productos activos en la BBDD. + La vista debe devolver una lista vacía, no romperse. + """ + + # 1. Borrar TODOS los productos creados en el setUp + # (Esto deja la BBDD sin productos activos) + Product.objects.all().delete() + + # Cargar la vista + resp = self.client.get(self.dashboard_url) + self.assertEqual(resp.status_code, 200) + + # ASERCIÓN: + # El contexto 'products' debe existir, pero estar vacío. + self.assertIn("products", resp.context) + products_in_context = list(resp.context["products"]) + + self.assertEqual(len(products_in_context), 0) diff --git a/essenza/templates/product/dashboard.html b/essenza/templates/product/dashboard.html index 50f5acd..bbbdf4e 100644 --- a/essenza/templates/product/dashboard.html +++ b/essenza/templates/product/dashboard.html @@ -281,7 +281,10 @@
- i
ESSENZA
@@ -326,7 +329,9 @@

Top Bestsellers

{% if products %} {% for p in products %}
- + {# {% url 'product_detail' p.pk %} #} {% if p.photo %} diff --git a/essenza/templates/product/stock.html b/essenza/templates/product/stock.html index 0cb3ec2..6aa0a6d 100644 --- a/essenza/templates/product/stock.html +++ b/essenza/templates/product/stock.html @@ -157,7 +157,7 @@ .product-card img { width: 140px; height: 140px; - object-fit: cover; + object-fit: contain; border-radius: 10px; flex-shrink: 0; } From 3263e00c89b5c228a039b6e57aa5b07f97f81ef3 Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Sat, 15 Nov 2025 19:23:57 +0100 Subject: [PATCH 4/6] =?UTF-8?q?wip:=20revisi=C3=B3n=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- essenza/product/views.py | 2 +- essenza/templates/product/dashboard.html | 2 +- essenza/templates/product/detail.html | 416 ++++++++++++----------- 3 files changed, 211 insertions(+), 209 deletions(-) diff --git a/essenza/product/views.py b/essenza/product/views.py index 934060d..80d6067 100644 --- a/essenza/product/views.py +++ b/essenza/product/views.py @@ -9,7 +9,7 @@ from .models import Product -class DashboardView(View): +class DashboardView(UserPassesTestMixin, View): template_name = "product/dashboard.html" # Todos excepto los administradores pueden acceder a esta vista diff --git a/essenza/templates/product/dashboard.html b/essenza/templates/product/dashboard.html index 57a3c19..14dc104 100644 --- a/essenza/templates/product/dashboard.html +++ b/essenza/templates/product/dashboard.html @@ -319,7 +319,7 @@
- {% if products.size > 1 %} + {% if products|length > 1 %}

Top Bestsellers

Una cuidada selección de los productos estrella de la temporada.

diff --git a/essenza/templates/product/detail.html b/essenza/templates/product/detail.html index aa9f19c..29d082b 100644 --- a/essenza/templates/product/detail.html +++ b/essenza/templates/product/detail.html @@ -1,229 +1,231 @@ - - - + + + {{ product.name }} - Essenza - - + +
-
-
-
- {% if product.photo %} - {{ product.name }} - {% else %} - Sin imagen - {% endif %} -
-
-

{{ product.name }}

-
{{ product.brand }}
-
€ {{ product.price }}
- {% if product.get_categoria_display %} - {{ product.get_categoria_display }} - {% endif %} -
- Stock: {{ product.stock }} unidades -
-
-
- - -
- Descripción: -

{{ product.description }}

+
+
+
+ {% if product.photo %} + {{ product.name }} + {% else %} + Foto de producto por defecto + {% endif %} +
+
+

{{ product.name }}

+
{{ product.brand }}
+
€ {{ product.price }}
+ {% if product.get_categoria_display %} + {{ product.get_categoria_display }} + {% endif %} +
+ Stock: {{ product.stock }} unidades
+
+
+
+ Descripción: +

{{ product.description }}

+
-
-
-
Estado
-
- {% if product.is_active %} - ✓ Activo - {% else %} - ✗ Inactivo - {% endif %} -
-
-
-
ID Producto
-
#{{ product.id }}
-
+
+
+
Estado
+
+ {% if product.is_active %} + ✓ Activo + {% else %} + ✗ Inactivo + {% endif %}
+
+
+
ID Producto
+
#{{ product.id }}
+
+
- - + +
- + From aaa8272c90c3ee055dec90757ea181a928e36711 Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Sat, 15 Nov 2025 20:30:19 +0100 Subject: [PATCH 5/6] =?UTF-8?q?a=C3=B1adida=20correcci=C3=B3n=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- essenza/product/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/essenza/product/tests.py b/essenza/product/tests.py index 8fc90bf..a68511e 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.urls import reverse from django.utils import timezone -from order.models import Category, Order, OrderProduct +from order.models import Order, OrderProduct from .models import Category, Product From 294b489c3c531fbc104f74866f86f286dc7ec546 Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Sat, 15 Nov 2025 21:10:36 +0100 Subject: [PATCH 6/6] =?UTF-8?q?Funcionalidad=20dashboard=20terminada=20con?= =?UTF-8?q?=20tests=20y=20cambios=20menores=20en=20aplicaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- essenza/product/views.py | 3 +-- essenza/templates/product/catalog.html | 28 +++++++++++++++++----- essenza/templates/product/dashboard.html | 2 +- essenza/templates/product/detail.html | 8 ++++++- essenza/templates/product/detail_user.html | 8 ++++++- essenza/templates/product/list.html | 7 ++++-- 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/essenza/product/views.py b/essenza/product/views.py index 0fadbae..4869951 100644 --- a/essenza/product/views.py +++ b/essenza/product/views.py @@ -140,7 +140,7 @@ def post(self, request, pk): form = self.form_class(request.POST, request.FILES, instance=product) if form.is_valid(): form.save() - return redirect("product_detail", pk=product.pk) + return redirect("product_list") return render(request, self.template_name, {"form": form, "product": product}) @@ -174,4 +174,3 @@ class CatalogDetailView(View): def get(self, request, pk): product = get_object_or_404(Product, pk=pk, is_active=True) return render(request, self.template_name, {"product": product}) - return redirect("product_list") diff --git a/essenza/templates/product/catalog.html b/essenza/templates/product/catalog.html index d64c139..974a580 100644 --- a/essenza/templates/product/catalog.html +++ b/essenza/templates/product/catalog.html @@ -181,17 +181,33 @@ -
- i + i
ESSENZA
- -
- +
diff --git a/essenza/templates/product/detail_user.html b/essenza/templates/product/detail_user.html index ef2dc8a..19d40c1 100644 --- a/essenza/templates/product/detail_user.html +++ b/essenza/templates/product/detail_user.html @@ -314,7 +314,13 @@

{{ product.name }}

✓ Añadido - ← Volver + ← Volver
diff --git a/essenza/templates/product/list.html b/essenza/templates/product/list.html index 48ec6e7..38f4cb6 100644 --- a/essenza/templates/product/list.html +++ b/essenza/templates/product/list.html @@ -388,8 +388,11 @@

🌸 Productos Essenza