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..b558ece 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -1 +1,113 @@ -# Create your tests here. +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth import get_user_model +from product.models import Product +from django.core.files.uploadedfile import SimpleUploadedFile + +User = get_user_model() + + +class ProductCRUDTests(TestCase): + + def setUp(self): + self.user = User.objects.create_user( + username="user", + email="user@example.com", + password="pass1234", + role="user", + ) + # Crear usuario admin + self.admin = User.objects.create_user( + username="admin", + email="admin@example.com", + password="pass1234", + role="admin", + ) + # Crear producto inicial + self.product = Product.objects.create( + name="Producto Test", + description="Descripción", + brand="Marca X", + price=10, + photo= None, + stock=5, + category="maquillaje", + is_active=True + ) + # URLs + self.list_url = reverse("product_list") + self.detail_url = reverse("product_detail", args=[self.product.pk]) + self.create_url = reverse("product_create") + self.update_url = reverse("product_update", args=[self.product.pk]) + self.delete_url = reverse("product_delete", args=[self.product.pk]) + + # --------------------------------------------------- + # TESTS PARA USUARIO NO AUTENTICADO + # --------------------------------------------------- + + def test_list_requires_login(self): + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 302) + self.assertIn("/login", resp.url) + + def test_detail_requires_login(self): + resp = self.client.get(self.detail_url) + self.assertEqual(resp.status_code, 302) + self.assertIn("/login", resp.url) + + def test_create_requires_login(self): + resp = self.client.get(self.create_url) + self.assertEqual(resp.status_code, 302) + + # --------------------------------------------------- + # TESTS PARA USUARIO AUTENTICADO PERO NO ADMIN + # --------------------------------------------------- + + def test_user_cannot_access_list(self): + self.client.login(username="user", password="pass1234") + resp = self.client.get(self.list_url) + self.assertEqual(resp.status_code, 302) # Redirigido por permisos + + def test_user_cannot_access_detail(self): + self.client.login(username="user", password="pass1234") + resp = self.client.get(self.detail_url) + self.assertEqual(resp.status_code, 302) + + def test_user_cannot_access_create(self): + self.client.login(username="user", password="pass1234") + resp = self.client.get(self.create_url) + self.assertEqual(resp.status_code, 302) + + # --------------------------------------------------- + # TESTS PARA USUARIO ADMIN (PERMISO TOTAL) + # --------------------------------------------------- + + def test_admin_can_access_list(self): + self.client.force_login(self.admin) + url = reverse('product_list') + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, "Producto Test") + + + def test_admin_can_access_detail(self): + self.client.force_login(self.admin) + url = reverse('product_detail', args=[self.product.pk]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertContains(resp, self.product.name) + + def test_admin_can_delete_product(self): + self.client.force_login(self.admin) + url = reverse('product_delete', args=[self.product.pk]) + + # GET renderiza el confirm delete + resp_get = self.client.get(url) + self.assertEqual(resp_get.status_code, 200) + + # POST borra el producto + resp_post = self.client.post(url) + self.assertEqual(resp_post.status_code, 302) + + self.assertFalse(Product.objects.filter(pk=self.product.pk).exists()) + 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..ca0a94f 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,77 @@ def post(self, request): # Recarga la misma página return redirect("stock") + + +class ProductListView(LoginRequiredMixin, UserPassesTestMixin,View): + template_name = 'product/list.html' + + def test_func(self): + return self.request.user.is_authenticated and self.request.user.role == "admin" + + def get(self, request): + products = Product.objects.all() + return render(request, self.template_name, {'products': products}) + +class ProductDetailView(LoginRequiredMixin, UserPassesTestMixin,View): + template_name = 'product/detail.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}) + +class ProductCreateView(LoginRequiredMixin, UserPassesTestMixin,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(LoginRequiredMixin, UserPassesTestMixin,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(LoginRequiredMixin, UserPassesTestMixin,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..826993a --- /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..aa9f19c --- /dev/null +++ b/essenza/templates/product/detail.html @@ -0,0 +1,229 @@ + + + + + + {{ 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..f59cd89 --- /dev/null +++ b/essenza/templates/product/form.html @@ -0,0 +1,309 @@ + + + + + + {% 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 %} +
+ {% elif field.name == "photo" %} +
+ + + {% if form.instance.pk and field.value %} +
+ + +
+ {% endif %} + + + {{ field }} + + {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endif %} +
+ {% else %} +
+ {{ field.label_tag }} + {{ field }} + {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endif %} +
+ {% endif %} + {% endfor %} + + +
+ + ← Cancelar +
+
+
+
+ + \ No newline at end of file diff --git a/essenza/templates/product/list.html b/essenza/templates/product/list.html new file mode 100644 index 0000000..c47b7c0 --- /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 %}