diff --git a/shop/migrations/0006_cart_cartitem.py b/shop/migrations/0006_cart_cartitem.py new file mode 100644 index 0000000..4bdaf8e --- /dev/null +++ b/shop/migrations/0006_cart_cartitem.py @@ -0,0 +1,90 @@ +# Generated by Django 5.1.3 on 2024-11-27 12:10 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("shop", "0005_productprice_price_with_tax"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Cart", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + verbose_name="uuid", + ), + ), + ( + "creation_date", + models.DateTimeField( + auto_now_add=True, verbose_name="fecha de creación" + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="usuario", + ), + ), + ], + options={ + "verbose_name": "carrito", + "verbose_name_plural": "carritos", + }, + ), + migrations.CreateModel( + name="CartItem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "quantity", + models.PositiveIntegerField(default=1, verbose_name="cantidad"), + ), + ( + "cart", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="items", + to="shop.cart", + verbose_name="carrito", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="shop.product", + verbose_name="producto", + ), + ), + ], + options={ + "verbose_name": "línea de carrito", + "verbose_name_plural": "líneas de carrito", + }, + ), + ] diff --git a/shop/models.py b/shop/models.py index 533d109..9c9585d 100644 --- a/shop/models.py +++ b/shop/models.py @@ -6,6 +6,10 @@ from django.utils import timezone from django.utils.text import gettext_lazy as _ from django.utils.text import slugify +from django.contrib.auth import get_user_model + +User = get_user_model() + class TimestampedModel(models.Model): creation_date = models.DateTimeField( @@ -398,3 +402,38 @@ class Order(TimestampedModel): class Meta: verbose_name = _("pedido") verbose_name_plural = _("pedidos") + + +class Cart(models.Model): + uuid = models.UUIDField(default=uuid4, primary_key=True, verbose_name=_("uuid")) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + verbose_name=_("usuario"), + blank=True, + null=True, + ) + creation_date = models.DateTimeField( + auto_now_add=True, verbose_name=_("fecha de creación") + ) + + class Meta: + verbose_name = _("carrito") + verbose_name_plural = _("carritos") + + +class CartItem(models.Model): + cart = models.ForeignKey( + "shop.Cart", + on_delete=models.CASCADE, + related_name="items", + verbose_name=_("carrito"), + ) + product = models.ForeignKey( + "shop.Product", on_delete=models.CASCADE, verbose_name=_("producto") + ) + quantity = models.PositiveIntegerField(default=1, verbose_name=_("cantidad")) + + class Meta: + verbose_name = _("línea de carrito") + verbose_name_plural = _("líneas de carrito") diff --git a/web/components/__init__.py b/web/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/components/views.py b/web/components/views.py new file mode 100644 index 0000000..e7974fb --- /dev/null +++ b/web/components/views.py @@ -0,0 +1,35 @@ +from django.views.generic import TemplateView +from shop.filters import ProductFilter +from shop.models import Product +from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin, CartMixin + + +class ListProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin): + template_name = "web/list_products.html" + queryset = Product.objects.all() + filter_class = ProductFilter + + def get_context_data(self, **kwargs): + qs = self.get_queryset() + page = self.get_paginated_queryset(qs) + + return { + "page": page, + "has_next_page": page.has_next(), + "has_previous_page": page.has_previous(), + } + + +class CartDropdown(TemplateView, CartMixin): + template_name = "components/cart/cart_navbar.html" + + def get_context_data(self, **kwargs): + cart = self.get_cart() + + return { + "cart": cart, + } + + +list_products = ListProducts.as_view() +cart_dropdown = CartDropdown.as_view() diff --git a/web/forms.py b/web/forms.py new file mode 100644 index 0000000..3bfdf87 --- /dev/null +++ b/web/forms.py @@ -0,0 +1,13 @@ +from django import forms +from django.forms import widgets + + +class CartItemForm(forms.Form): + """ + + + + """ + + product = forms.IntegerField(widget=widgets.HiddenInput) + quantity = forms.IntegerField() diff --git a/web/mixins.py b/web/mixins.py index f0b87a4..17b12d5 100644 --- a/web/mixins.py +++ b/web/mixins.py @@ -1,6 +1,9 @@ from django.conf import settings from django.core.paginator import Paginator +from shop.models import Cart +from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME + class FilteredQuerysetMixin: queryset = None @@ -25,3 +28,13 @@ class PaginatedQuerysetMixin: page = self.request.GET.get("page", 1) qs = paginator.get_page(page) return qs + + +class CartMixin: + def get_cart(self): + if not self.request.user.is_authenticated: + uid = self.request.COOKIES.get(ANONYMOUS_CART_ID_COOKIE_NAME) + cart = Cart.objects.get(uuid=uid) + else: + cart = Cart.objects.filter(user=self.request.user).first() + return cart diff --git a/web/settings.py b/web/settings.py new file mode 100644 index 0000000..525f52f --- /dev/null +++ b/web/settings.py @@ -0,0 +1 @@ +ANONYMOUS_CART_ID_COOKIE_NAME = "caid" diff --git a/web/templates/components/cart/add_to_cart.html b/web/templates/components/cart/add_to_cart.html new file mode 100644 index 0000000..ecd27c8 --- /dev/null +++ b/web/templates/components/cart/add_to_cart.html @@ -0,0 +1,15 @@ +{% load i18n %} +
+ {% csrf_token %} + + + +
diff --git a/web/templates/components/cart/cart_dropdown.html b/web/templates/components/cart/cart_dropdown.html index 017f940..9e52769 100644 --- a/web/templates/components/cart/cart_dropdown.html +++ b/web/templates/components/cart/cart_dropdown.html @@ -1,35 +1,57 @@ {% load i18n %}
-
- Apple iPhone 15 -

$599

-
-
-

Qty: 1

+ {% if cart.items.count == 0 %} +

No hay nada en el carrito

+ {% endif %} - - + +
+

{% translate 'Cantidad' %}: {{ cart_item.quantity }}

+ +
+ + +
+ + + +
+ {% endfor %} +
- {% translate 'Ir a pagar' %} - + role="button"> {% translate 'Ir a pagar' %} + +{% endif %} diff --git a/web/templates/components/cart/cart_navbar.html b/web/templates/components/cart/cart_navbar.html new file mode 100644 index 0000000..12baaf3 --- /dev/null +++ b/web/templates/components/cart/cart_navbar.html @@ -0,0 +1,87 @@ +{% load i18n %} + + + + \ No newline at end of file diff --git a/web/templates/components/generic/navbar.html b/web/templates/components/generic/navbar.html index ef8784c..42b5457 100644 --- a/web/templates/components/generic/navbar.html +++ b/web/templates/components/generic/navbar.html @@ -46,58 +46,38 @@ {% include 'components/theme-switch.html' %} - - - - + {% if request.user.is_authenticated %} - - + + - - + + {% else %} - Inicia sesión + Inicia + sesión {% endif %}
diff --git a/web/templates/components/product_card.html b/web/templates/components/product_card.html index c3a78be..1de869e 100644 --- a/web/templates/components/product_card.html +++ b/web/templates/components/product_card.html @@ -55,15 +55,7 @@

{{ product.price.price_with_tax }}€

- + {% include 'components/cart/add_to_cart.html' %} diff --git a/web/urls.py b/web/urls.py index 3a6d49c..69eaaa4 100644 --- a/web/urls.py +++ b/web/urls.py @@ -1,6 +1,7 @@ from django.urls import path -from web.views import index, list_products, product_detail +from web.views import index, product_detail, add_cart_item, delete_cart_item +from web.components.views import list_products, cart_dropdown app_name = "web" @@ -8,5 +9,10 @@ app_name = "web" urlpatterns = [ path("", index, name="index"), path("products///", product_detail, name="product_detail"), - path("shop/components/products/", list_products, name="list_products"), + # components + path("web/components/products/", list_products, name="list_products"), + path("web/components/cart-dropdown/", cart_dropdown, name="cart_dropdown"), + # api + path("web/add-cart-item/", add_cart_item, name="add_cart_item"), + path("web/delete-cart-item//", delete_cart_item, name="delete_cart_item"), ] diff --git a/web/utils.py b/web/utils.py new file mode 100644 index 0000000..6fe099d --- /dev/null +++ b/web/utils.py @@ -0,0 +1,11 @@ +from shop.models import Cart +from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME + + +def get_cart(request): + if not request.user.is_authenticated: + uid = request.COOKIES.get(ANONYMOUS_CART_ID_COOKIE_NAME) + cart = Cart.objects.get(uuid=uid) + else: + cart = Cart.objects.filter(user=request.user).first() + return cart diff --git a/web/views.py b/web/views.py index 700a931..625ec4c 100644 --- a/web/views.py +++ b/web/views.py @@ -1,9 +1,11 @@ from django.shortcuts import get_object_or_404 from django.views.generic import TemplateView +from django.http.response import HttpResponse +from django.views.decorators.http import require_http_methods -from shop.filters import ProductFilter -from shop.models import Product -from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin +from shop.models import Product, Cart, CartItem +from web.utils import get_cart +from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME class IndexView(TemplateView): @@ -15,22 +17,6 @@ class IndexView(TemplateView): } -class ListProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin): - template_name = "web/list_products.html" - queryset = Product.objects.all() - filter_class = ProductFilter - - def get_context_data(self, **kwargs): - qs = self.get_queryset() - page = self.get_paginated_queryset(qs) - - return { - "page": page, - "has_next_page": page.has_next(), - "has_previous_page": page.has_previous(), - } - - class ProductDetail(TemplateView): template_name = "web/product_detail.html" @@ -45,6 +31,49 @@ class ProductDetail(TemplateView): } +@require_http_methods( + [ + "POST", + ] +) +def add_cart_item(request, *args, **kwargs): + product = get_object_or_404(Product, pk=request.POST.get("product")) + response = HttpResponse(status=201) + + if not request.user.is_authenticated: + cart = Cart.objects.create( + user=None, + ) + else: + cart, created = Cart.objects.get_or_create(user=request.user) + response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid) + + existing_cart_item = CartItem.objects.filter(cart=cart, product=product).first() + + if existing_cart_item is not None: + existing_cart_item.quantity += int(request.POST.get("quantity")) + existing_cart_item.save() + else: + CartItem.objects.create( + cart=cart, + quantity=request.POST.get("quantity"), + product=product, + ) + + return response + + +@require_http_methods( + [ + "POST", + ] +) +def delete_cart_item(request, pk, *args, **kwargs): + cart = get_cart(request) + cart_item = get_object_or_404(CartItem, pk=pk, cart=cart) + cart_item.delete() + return HttpResponse(status=204) + + index = IndexView.as_view() -list_products = ListProducts.as_view() product_detail = ProductDetail.as_view()