From 5e900eed7d9c6636bc96802cbf1ecdeeade16595 Mon Sep 17 00:00:00 2001 From: xgc1564 Date: Fri, 14 Nov 2025 13:49:49 +0100 Subject: [PATCH 1/2] Funcionalidad CRUD productos junto con los tests --- essenza/product/forms.py | 8 + essenza/product/tests.py | 75 ++++ essenza/product/urls.py | 5 + essenza/product/views.py | 75 +++- essenza/templates/product/confirm_delete.html | 151 +++++++ essenza/templates/product/detail.html | 231 ++++++++++ essenza/templates/product/form.html | 233 ++++++++++ essenza/templates/product/list.html | 400 ++++++++++++++++++ essenza/templates/product/stock.html | 1 + 9 files changed, 1176 insertions(+), 3 deletions(-) create mode 100644 essenza/product/forms.py create mode 100644 essenza/templates/product/confirm_delete.html create mode 100644 essenza/templates/product/detail.html create mode 100644 essenza/templates/product/form.html create mode 100644 essenza/templates/product/list.html diff --git a/essenza/product/forms.py b/essenza/product/forms.py new file mode 100644 index 0000000..b3ddef9 --- /dev/null +++ b/essenza/product/forms.py @@ -0,0 +1,8 @@ +from django import forms +from .models import Product + + +class ProductForm(forms.ModelForm): + class Meta: + model = Product + fields = ['name', 'description', 'category', 'brand', 'price', 'photo', 'stock', 'is_active'] diff --git a/essenza/product/tests.py b/essenza/product/tests.py index a39b155..35f8e5e 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -1 +1,76 @@ # Create your tests here. +from django.test import TestCase + + +# Create your tests here. +from django.urls import reverse +from .models import Product + + +class ProductCRUDTests(TestCase): + def setUp(self): + self.product = Product.objects.create( + name='Test Product', + description='A product for testing', + category='maquillaje', + brand='TestBrand', + price='9.99', + stock=10, + is_active=True, + ) + def test_list_view(self): + url = reverse('product_list') + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, self.product.name) + + + def test_detail_view(self): + url = reverse('product_detail', args=[self.product.pk]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, self.product.description) + + + def test_create_view(self): + url = reverse('product_create') + data = { + 'name': 'New Product', + 'description': 'Created in test', + 'category': 'perfume', + 'brand': 'BrandX', + 'price': '5.50', + 'stock': 3, + 'is_active': True, + } + resp = self.client.post(url, data) + # redirección esperada tras creación + self.assertEqual(resp.status_code, 302) + self.assertTrue(Product.objects.filter(name='New Product').exists()) + + + def test_update_view(self): + url = reverse('product_update', args=[self.product.pk]) + data = { + 'name': 'Updated Name', + 'description': self.product.description, + 'category': self.product.category, + 'brand': self.product.brand, + 'price': str(self.product.price), + 'stock': self.product.stock, + 'is_active': self.product.is_active, + } + resp = self.client.post(url, data) + self.assertEqual(resp.status_code, 302) + self.product.refresh_from_db() + self.assertEqual(self.product.name, 'Updated Name') + + + def test_delete_view(self): + url = reverse('product_delete', args=[self.product.pk]) + # GET muestra el confirm, POST realiza borrado + resp_get = self.client.get(url) + self.assertEqual(resp_get.status_code, 200) + resp_post = self.client.post(url) + self.assertEqual(resp_post.status_code, 302) + self.assertFalse(Product.objects.filter(pk=self.product.pk).exists()) \ No newline at end of file diff --git a/essenza/product/urls.py b/essenza/product/urls.py index f7c2155..83d25b7 100644 --- a/essenza/product/urls.py +++ b/essenza/product/urls.py @@ -6,6 +6,11 @@ urlpatterns = [ path("stock/", views.StockView.as_view(), name="stock"), + path('', views.ProductListView.as_view(), name='product_list'), + path('create/', views.ProductCreateView.as_view(), name='product_create'), + path('/', views.ProductDetailView.as_view(), name='product_detail'), + path('/edit/', views.ProductUpdateView.as_view(), name='product_update'), + path('/delete/', views.ProductDeleteView.as_view(), name='product_delete'), ] if settings.DEBUG: diff --git a/essenza/product/views.py b/essenza/product/views.py index e0ed392..56d8bb8 100644 --- a/essenza/product/views.py +++ b/essenza/product/views.py @@ -1,13 +1,12 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.views import View +from .forms import ProductForm from order.models import OrderProduct - from .models import Product - class DashboardView(View): template_name = "product/dashboard.html" @@ -68,3 +67,73 @@ def post(self, request): # Recarga la misma página return redirect("stock") + + +class ProductListView(View): + template_name = 'product/list.html' + + def get(self, request): + products = Product.objects.all() + return render(request, self.template_name, {'products': products}) + +class ProductDetailView(View): + template_name = 'product/detail.html' + + + def get(self, request, pk): + product = get_object_or_404(Product, pk=pk) + return render(request, self.template_name, {'product': product}) + +class ProductCreateView(View): + template_name = 'product/form.html' + form_class = ProductForm + def test_func(self): + return self.request.user.is_authenticated and self.request.user.role == "admin" + + def get(self, request): + form = self.form_class() + return render(request, self.template_name, {'form': form}) + + def post(self, request): + form = self.form_class(request.POST, request.FILES) + if form.is_valid(): + form.save() + return redirect('product_list') + return render(request, self.template_name, {'form': form}) + +class ProductUpdateView(View): + template_name = 'product/form.html' + form_class = ProductForm + + def test_func(self): + return self.request.user.is_authenticated and self.request.user.role == "admin" + + def get(self, request, pk): + product = get_object_or_404(Product, pk=pk) + form = self.form_class(instance=product) + return render(request, self.template_name, {'form': form, 'product': product}) + + + def post(self, request, pk): + product = get_object_or_404(Product, pk=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 render(request, self.template_name, {'form': form, 'product': product}) + +class ProductDeleteView(View): + template_name = 'product/confirm_delete.html' + + def test_func(self): + return self.request.user.is_authenticated and self.request.user.role == "admin" + + def get(self, request, pk): + product = get_object_or_404(Product, pk=pk) + return render(request, self.template_name, {'product': product}) + + + def post(self, request, pk): + product = get_object_or_404(Product, pk=pk) + product.delete() + return redirect('product_list') diff --git a/essenza/templates/product/confirm_delete.html b/essenza/templates/product/confirm_delete.html new file mode 100644 index 0000000..f4bb1df --- /dev/null +++ b/essenza/templates/product/confirm_delete.html @@ -0,0 +1,151 @@ + + + + + + Confirmar Borrado - Essenza + + + +
+ ⚠️ +

¿Está seguro de que desea borrar este producto?

+ + +
+ Producto: +
{{ product.name }}
+
+ + +

+ Esta acción es irreversible y no se puede deshacer. +

+ + +
+ {% csrf_token %} +
+ + ← No, cancelar +
+
+ + +
+ 💡 Si borras este producto, se eliminará toda la información asociada. +
+
+ + + + diff --git a/essenza/templates/product/detail.html b/essenza/templates/product/detail.html new file mode 100644 index 0000000..5abfa4a --- /dev/null +++ b/essenza/templates/product/detail.html @@ -0,0 +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 }}

+
+ + +
+
+
Estado
+
+ {% if product.is_active %} + ✓ Activo + {% else %} + ✗ Inactivo + {% endif %} +
+
+
+
ID Producto
+
#{{ product.id }}
+
+
+ + + +
+
+ + diff --git a/essenza/templates/product/form.html b/essenza/templates/product/form.html new file mode 100644 index 0000000..c91b8ad --- /dev/null +++ b/essenza/templates/product/form.html @@ -0,0 +1,233 @@ + + + + + + {% if form.instance.pk %}Editar{% else %}Crear{% endif %} Producto - Essenza + + + +
+
+

{% if form.instance.pk %}✏️ Editar Producto{% else %} Crear nuevo producto{% endif %}

+ + + {% if form.non_field_errors %} +
+ Errores: +
    + {% for error in form.non_field_errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endif %} + + +
+ {% csrf_token %} + + + {% for field in form %} + {% if field.name == "is_active" %} +
+ + {% if field.errors %} +
+ {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endif %} +
+ {% else %} +
+ {{ field.label_tag }} + {% if field.name == "stock" %} +
+ {% endif %} + {{ field }} + {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endif %} + {% if field.name == "price" %} +
+ {% endif %} +
+ {% endif %} + {% endfor %} + + +
+ + ← Cancelar +
+
+
+
+ + diff --git a/essenza/templates/product/list.html b/essenza/templates/product/list.html new file mode 100644 index 0000000..61048a2 --- /dev/null +++ b/essenza/templates/product/list.html @@ -0,0 +1,400 @@ +{% load static %} + + + + + + Lista de Productos - Essenza + + + +
+
+ i +
ESSENZA
+ + +
+ +
+ + + +
+
+ +
+

🌸 Productos Essenza

+ + + + {% if products %} +
+ {% for product in products %} +
+
+ {% if product.photo %} + {{ product.name }} + {% else %} + Sin imagen + {% endif %} +
+
+
{{ product.name }}
+
{{ product.brand }}
+
€ {{ product.price }}
+
Stock: {{ product.stock }}
+
+ Ver + Editar + Borrar +
+
+
+ {% endfor %} +
+ {% else %} +
+

No hay productos disponibles. ¡Crea uno!

+
+ {% endif %} + + + +
+ + + \ No newline at end of file diff --git a/essenza/templates/product/stock.html b/essenza/templates/product/stock.html index 0cb3ec2..60ae8f0 100644 --- a/essenza/templates/product/stock.html +++ b/essenza/templates/product/stock.html @@ -285,6 +285,7 @@ Mi perfil {% if user.role == 'admin' %} Gestión de Stock + Gestión de Productos {% endif %} Cerrar sesión {% else %} From ad4c1a145068b3fe89ccd097be1ac3b746a04caf Mon Sep 17 00:00:00 2001 From: xgc1564 Date: Fri, 14 Nov 2025 18:57:49 +0100 Subject: [PATCH 2/2] Funcionalidad CRUD productos junto con los tests --- essenza/templates/product/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/essenza/templates/product/list.html b/essenza/templates/product/list.html index 61048a2..c47b7c0 100644 --- a/essenza/templates/product/list.html +++ b/essenza/templates/product/list.html @@ -120,7 +120,7 @@ transition: opacity 0.3s; } - /* === ESTILOS DE BOTÓN === */ + /* === ESTILO DE BOTÓN === */ .btn-detail { background-color: #c06b3e; color: white;