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
1 change: 1 addition & 0 deletions essenza/essenza/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
path("", DashboardView.as_view(), name="dashboard"),
path("catalog/", CatalogView.as_view(), name="catalog"),
path("catalog/<int:pk>/", CatalogDetailView.as_view(), name="catalog_detail"),
path("order/", include("order.urls")),
]

if settings.DEBUG:
Expand Down
10 changes: 6 additions & 4 deletions essenza/order/models.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
from django.db import models
from django.utils import timezone


# Create your models here.
class Status(models.TextChoices):
PENDING = "pending", "Pending"
PAID = "paid", "Paid"
SHIPPED = "shipped", "Shipped"


class Order(models.Model):
user = models.ForeignKey(
"user.Usuario", on_delete=models.CASCADE, related_name="orders"
)
address = models.CharField(max_length=255)
address = models.CharField(max_length=255, null=True, blank=True)
placed_at = models.DateTimeField(default=timezone.now)
status = models.CharField(
max_length=10, choices=Status.choices, default=Status.PENDING
Expand All @@ -23,7 +21,7 @@ class Order(models.Model):
def total_price(self):
total = 0
for product in self.order_products.all():
total += product.quantity * product.product.price
total += product.quantity * product.price
return total

def __str__(self):
Expand All @@ -39,5 +37,9 @@ class OrderProduct(models.Model):
)
quantity = models.IntegerField()

@property
def subtotal(self):
return self.quantity * self.product.price

def __str__(self):
return f"{self.quantity} of {self.product.name} in order {self.order.id}"
92 changes: 91 additions & 1 deletion essenza/order/tests.py
Original file line number Diff line number Diff line change
@@ -1 +1,91 @@
# Create your tests here.
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model

from product.models import Product
from order.models import Order, OrderProduct, Status


User = get_user_model()


class CartTests(TestCase):
def setUp(self):
self.client = Client()
# Create a sample product
self.product = Product.objects.create(
name="Test Product",
description="Desc",
category="maquillaje",
brand="Marca",
price="9.99",
stock=10,
is_active=True,
)

# Regular user
self.user = User.objects.create_user(
email="user@example.com", username="user1", password="pass1234", role="user"
)

# Admin user
self.admin = User.objects.create_user(
email="admin@example.com", username="admin1", password="adminpass", role="admin", is_staff=True
)

def test_anonymous_add_to_cart_creates_session(self):
url = reverse('add_to_cart', kwargs={'product_pk': self.product.pk})
response = self.client.post(url, {'quantity': 2}, follow=True)

# Should redirect to cart_detail
self.assertEqual(response.status_code, 200)
session = self.client.session
self.assertIn('cart_session', session)
cart = session['cart_session']
self.assertIn(str(self.product.pk), cart)
self.assertEqual(cart[str(self.product.pk)]['quantity'], 2)

def test_authenticated_user_adds_to_db_cart(self):
self.client.login(email='user@example.com', password='pass1234')
url = reverse('add_to_cart', kwargs={'product_pk': self.product.pk})
response = self.client.post(url, {'quantity': 3}, follow=True)

# After adding, there should be a pending Order for the user
self.assertEqual(response.status_code, 200)
orders = Order.objects.filter(user=self.user, status=Status.PENDING)
self.assertTrue(orders.exists())
order = orders.first()
# Check OrderProduct exists
op = OrderProduct.objects.filter(order=order, product=self.product).first()
self.assertIsNotNone(op)
self.assertEqual(op.quantity, 3)

def test_admin_get_cart_forbidden(self):
# Admin should get 403 on cart detail
self.client.login(email='admin@example.com', password='adminpass')
url = reverse('cart_detail')
response = self.client.get(url)
self.assertEqual(response.status_code, 403)

def test_admin_cannot_add_to_cart(self):
self.client.login(email='admin@example.com', password='adminpass')
url = reverse('add_to_cart', kwargs={'product_pk': self.product.pk})
response = self.client.post(url)
self.assertEqual(response.status_code, 403)

def test_cart_shows_empty_after_order_deleted(self):
# Create a pending order for the user with one OrderProduct
order = Order.objects.create(user=self.user, status=Status.PENDING, address='')
OrderProduct.objects.create(order=order, product=self.product, quantity=2)

# Delete the order
order.delete()

# Login as user and request cart detail
self.client.login(email='user@example.com', password='pass1234')
url = reverse('cart_detail')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertIn('cart_items', response.context)
self.assertEqual(len(response.context['cart_items']), 0)

13 changes: 13 additions & 0 deletions essenza/order/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# order/urls.py
from django.urls import include, path
from . import views

urlpatterns = [
#path('order/', include('order.urls')),
path('', views.CartDetailView.as_view(), name='order_home'),
path('cart/', views.CartDetailView.as_view(), name='cart_detail'),
path('add/<int:product_pk>/', views.AddToCartView.as_view(), name='add_to_cart'),
path('update/<int:item_pk>/', views.UpdateCartItemView.as_view(), name='update_cart_item'),
path('update/session/<int:product_pk>/', views.UpdateCartSessionView.as_view(), name='update_cart_session'),

]
230 changes: 228 additions & 2 deletions essenza/order/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,229 @@
from django.shortcuts import render
from django.shortcuts import get_object_or_404, redirect, render
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.db.models import F # Importado para operaciones atómicas
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied

# Create your views here.
# Importaciones de tus modelos
from .models import Order, OrderProduct, Status
from product.models import Product

# --------------------------------------------------------------------
# 1. FUNCIÓN AUXILIAR NECESARIA
# --------------------------------------------------------------------
def get_or_create_cart(request):
"""
Obtiene la Order más reciente con status='PENDING' (asumida como carrito)
o crea una nueva Order en estado 'PENDING'. Solo para usuarios logueados.
"""
# Deny access to admin/staff users explicitly
if request.user.is_authenticated and (getattr(request.user, 'role', None) == 'admin' or getattr(request.user, 'is_staff', False)):
raise PermissionDenied("Acceso denegado: administradores no pueden usar el carrito.")

if request.user.is_authenticated:
# Lógica para usuarios logueados
try:
cart = Order.objects.filter(
user=request.user,
status=Status.PENDING
).order_by('-placed_at').first()

if cart is None:
raise ObjectDoesNotExist

except ObjectDoesNotExist:
cart = Order.objects.create(
user=request.user,
status=Status.PENDING,
address="", # Provide an empty string to avoid IntegrityError
)

return cart
else:
# Los anónimos usan la sesión
return None

# --------------------------------------------------------------------
# 2. VISTAS
# --------------------------------------------------------------------

# order/views.py (Fragmento de CartDetailView)

class CartDetailView(View):
"""Muestra el contenido del carrito activo del usuario (DB) o de la sesión (Anónimo)."""
template_name = 'order/cart_detail.html'

def get(self, request):
cart = None
if request.user.is_authenticated:
# LÓGICA 1: Usuario logueado (lee de la DB)
cart = get_or_create_cart(request)
cart_items = cart.order_products.all()
cart_total = sum(item.product.price * item.quantity for item in cart_items) if cart_items else 0
else:
# LÓGICA 2: Usuario anónimo (lee de la Sesión)
cart_session = request.session.get('cart_session', {})
cart_items = []
cart_total = 0

# Si hay ítems en la sesión, construimos una lista para la plantilla
if cart_session:
product_pks = [int(pk) for pk in cart_session.keys()]

# Buscamos todos los objetos Product de la DB de una vez
products = Product.objects.filter(pk__in=product_pks)

# Iteramos sobre los productos para crear la lista de ítems del carrito
for product in products:
pk_str = str(product.pk)
quantity = cart_session[pk_str]['quantity']

# Creamos un objeto temporal para pasarlo al template
cart_items.append({
'product': product,
'quantity': quantity,
'subtotal': quantity * product.price,
'pk': product.pk,
})
cart_total = sum(item['product'].price * item['quantity'] for item in cart_items)

context = {
'cart': cart, # Será None para anónimos
'cart_items': cart_items, # Lista de DB objects o dicts/temp objects
'cart_total': cart_total, # Total calculado
}
return render(request, self.template_name, context)
# =======================================================
# AÑADIR AL CARRITO (Añadido/Corregido)
# =======================================================
class AddToCartView(View): # LoginRequiredMixin eliminado
"""
Añade un producto al carrito, usando DB (Logueado) o Session (Anónimo).
"""
def post(self, request, product_pk):
product = get_object_or_404(Product, pk=product_pk)

try:
quantity = int(request.POST.get('quantity', 1))
if quantity < 1:
quantity = 1
except ValueError:
quantity = 1

cart = get_or_create_cart(request) # Devuelve Order (logueado) o None (anónimo)

# --- LÓGICA DE MANEJO DEL CARRITO ---
if cart:
# 1. USUARIO LOGUEADO (cart es un objeto Order)

# Línea 88: Ya no falla porque 'cart' es un objeto Order.
cart_item = cart.order_products.filter(product=product).first()

if cart_item:
# UPDATE (DB)
cart_item.quantity = F('quantity') + quantity
cart_item.save(update_fields=['quantity'])
cart_item.refresh_from_db()
messages.success(request, f"Se ha añadido {quantity} unidad(es) de '{product.name}'. Cantidad total: {cart_item.quantity}")
else:
# CREATE (DB)
OrderProduct.objects.create(
order=cart,
product=product,
quantity=quantity
)
messages.success(request, f"'{product.name}' se ha añadido al carrito.")

else:
# 2. USUARIO ANÓNIMO (cart es None, usamos la sesión)

cart_session = request.session.get('cart_session', {})
product_pk_str = str(product_pk)

if product_pk_str in cart_session:
# UPDATE (SESSION)
cart_session[product_pk_str]['quantity'] += quantity
messages.success(request, f"Se ha añadido {quantity} unidad(es) de '{product.name}'. Cantidad total en carrito: {cart_session[product_pk_str]['quantity']}")
else:
# CREATE (SESSION)
cart_session[product_pk_str] = {
'quantity': quantity,
'price': str(product.price)
}
messages.success(request, f"'{product.name}' se ha añadido al carrito.")

# Guardar y marcar la sesión
request.session['cart_session'] = cart_session
request.session.modified = True

return redirect('cart_detail')
# =======================================================
# ACTUALIZAR CANTIDAD EN EL CARRITO
# =======================================================
class UpdateCartSessionView(View):
"""Actualiza la cantidad de un ítem existente en el carrito de la sesión (Anónimo)."""
def post(self, request, product_pk):
if request.user.is_authenticated:
# Protección: si un usuario logueado intenta usar esta URL, redirigir a la vista DB
return redirect('cart_detail')

cart_session = request.session.get('cart_session', {})
product_pk_str = str(product_pk)

# 1. Obtener la nueva cantidad
try:
new_quantity = int(request.POST.get('quantity', 0))
except ValueError:
new_quantity = -1

# Necesitamos el objeto Product para el nombre y el stock
product = get_object_or_404(Product, pk=product_pk)

# 2. Lógica de Actualización/Eliminación
if product_pk_str in cart_session:
if new_quantity <= 0:
# ELIMINAR
del cart_session[product_pk_str]
messages.info(request, f"'{product.name}' ha sido eliminado del carrito.")
else:
# ACTUALIZAR
# Opcional: limitar al stock disponible
if new_quantity > product.stock:
new_quantity = product.stock
messages.warning(request, f"Solo quedan {product.stock} unidades de '{product.name}'. Cantidad limitada.")
else:
cart_session[product_pk_str]['quantity'] = new_quantity
messages.success(request, f"Cantidad de '{product.name}' actualizada a {new_quantity}.")

# 3. Guardar sesión
request.session['cart_session'] = cart_session
request.session.modified = True

return redirect('cart_detail')

class UpdateCartItemView(View):
"""Actualiza la cantidad de un ítem existente en el carrito."""
def post(self, request, item_pk):
cart_item = get_object_or_404(OrderProduct, pk=item_pk)
cart = get_or_create_cart(request)

if cart_item.order.pk != cart.pk:
messages.error(request, "El ítem no pertenece a tu carrito activo.")
return redirect('cart_detail')

try:
new_quantity = int(request.POST.get('quantity', 0))
except ValueError:
new_quantity = -1

if new_quantity <= 0:
item_name = cart_item.product.name
cart_item.delete()
messages.info(request, f"'{item_name}' ha sido eliminado del carrito.")
else:
cart_item.quantity = new_quantity
cart_item.save(update_fields=['quantity'])
messages.success(request, f"Cantidad de '{cart_item.product.name}' actualizada a {new_quantity}.")

return redirect('cart_detail')
Loading