Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions essenza/product/forms.py
Original file line number Diff line number Diff line change
@@ -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']
114 changes: 113 additions & 1 deletion essenza/product/tests.py
Original file line number Diff line number Diff line change
@@ -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())

5 changes: 5 additions & 0 deletions essenza/product/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('<int:pk>/', views.ProductDetailView.as_view(), name='product_detail'),
path('<int:pk>/edit/', views.ProductUpdateView.as_view(), name='product_update'),
path('<int:pk>/delete/', views.ProductDeleteView.as_view(), name='product_delete'),
]

if settings.DEBUG:
Expand Down
79 changes: 76 additions & 3 deletions essenza/product/views.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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')
151 changes: 151 additions & 0 deletions essenza/templates/product/confirm_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Confirmar Borrado - Essenza</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #faf7f2;
font-family: 'Segoe UI', Arial, sans-serif;
color: #333;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.confirm-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
padding: 40px;
max-width: 500px;
text-align: center;
}
.warning-icon {
font-size: 48px;
margin-bottom: 20px;
display: block;
}
h1 {
color: #c85a54;
font-size: 24px;
margin-bottom: 15px;
}
p {
color: #666;
font-size: 16px;
margin-bottom: 20px;
line-height: 1.5;
}
.product-info {
background-color: #f9f5f2;
border-left: 4px solid #c06b3e;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
text-align: left;
}
.product-info strong {
display: block;
color: #c06b3e;
margin-bottom: 8px;
}
.product-name {
font-size: 18px;
color: #333;
word-break: break-word;
}
.actions {
display: flex;
gap: 10px;
margin-top: 30px;
justify-content: center;
}
button,
a.btn {
padding: 12px 30px;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
text-decoration: none;
font-size: 16px;
transition: opacity 0.3s;
display: inline-block;
}
.btn-delete {
background-color: #c85a54;
color: white;
flex: 1;
}
.btn-delete:hover {
opacity: 0.85;
}
.btn-cancel {
background-color: #ddd;
color: #333;
flex: 1;
}
.btn-cancel:hover {
opacity: 0.85;
}
.note {
background-color: #fff3cd;
border: 1px solid #ffc107;
color: #856404;
padding: 12px;
border-radius: 4px;
margin-top: 20px;
font-size: 14px;
}
@media (max-width: 500px) {
.confirm-container {
padding: 30px 20px;
}
.actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="confirm-container">
<span class="warning-icon">⚠️</span>
<p>¿Está seguro de que desea borrar este producto?</p>


<div class="product-info">
<strong>Producto:</strong>
<div class="product-name">{{ product.name }}</div>
</div>


<p style="margin-bottom: 10px; font-size: 14px; color: #999;">
Esta acción es <strong>irreversible</strong> y no se puede deshacer.
</p>


<form method="post" style="margin: 0;">
{% csrf_token %}
<div class="actions">
<button type="submit" class="btn-delete">Sí, borrar producto</button>
<a href="{% url 'product_detail' product.pk %}" class="btn-cancel">← No, cancelar</a>
</div>
</form>


<div class="note">
💡 Si borras este producto, se eliminará toda la información asociada.
</div>
</div>
</body>
</html>


Loading