diff --git a/config/settings/environ.py b/config/settings/environ.py index 386d2e8..88aa282 100644 --- a/config/settings/environ.py +++ b/config/settings/environ.py @@ -56,3 +56,4 @@ REDSYS_TPV_DOMAIN = env.str("REDSYS_TPV_DOMAIN", "") ITEMS_PER_PAGE = env.int("ITEMS_PER_PAGE", 20) SHOP_COUNTRY = env.str("SHOP_COUNTRY", "ESPAÑA") +NPM_BIN_PATH = which("npm") diff --git a/shop/admin.py b/shop/admin.py index baed5ef..9c3d472 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -5,16 +5,17 @@ from shop.models import ( Brand, Cart, CartItem, + CustomerAddress, Order, OrderLine, Product, ProductBatch, + ProductCategory, ProductPrice, Provider, + ShippingMethod, Tag, Tax, - ShippingMethod, - CustomerAddress, ) @@ -142,3 +143,15 @@ class CustomerAddressAdmin(ModelAdmin): def get_queryset(self, request): qs = super().get_queryset(request) return qs.filter(hidden=False) + + +@admin.register(ProductCategory) +class ProductCategoryAdmin(ModelAdmin): + list_display = ( + "id", + "name", + "parent", + "promoted", + "hidden", + "show_in_navbar", + ) diff --git a/shop/filters.py b/shop/filters.py index 5ab9c84..ce5973b 100644 --- a/shop/filters.py +++ b/shop/filters.py @@ -10,6 +10,7 @@ class ProductFilter(django_filters.FilterSet): field_name="name", lookup_expr="icontains", label=_("Nombre") ) tags = django_filters.BaseInFilter(field_name="tags", label=_("Etiquetas")) + category = django_filters.BaseInFilter(field_name="categories", label=_("Categorías")) price_lt = django_filters.NumberFilter(method="filter_price_lt") price_gt = django_filters.NumberFilter(method="filter_price_gt") o = OrderingFilter( @@ -42,4 +43,5 @@ class ProductFilter(django_filters.FilterSet): "stock", "is_digital_asset", "tags", + "category", ) diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py index dc4c69b..daa8039 100644 --- a/shop/migrations/0001_initial.py +++ b/shop/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.3 on 2024-12-23 12:01 +# Generated by Django 5.1.3 on 2024-12-31 09:48 import django.db.models.deletion import django.utils.timezone @@ -507,12 +507,6 @@ class Migration(migrations.Migration): blank=True, to="files.fileupload", verbose_name="imágenes" ), ), - ( - "tags", - models.ManyToManyField( - blank=True, to="shop.tag", verbose_name="etiquetas" - ), - ), ], options={ "verbose_name": "producto", @@ -646,6 +640,64 @@ class Migration(migrations.Migration): "verbose_name_plural": "líneas de carrito", }, ), + migrations.CreateModel( + name="ProductCategory", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=32)), + ( + "promoted", + models.BooleanField(default=False, verbose_name="promocionada"), + ), + ("hidden", models.BooleanField(default=False, verbose_name="oculta")), + ( + "show_in_navbar", + models.BooleanField( + default=False, verbose_name="mostrar en cabecera" + ), + ), + ( + "slug", + models.SlugField( + blank=True, + default="", + max_length=48, + unique=True, + verbose_name="slug", + ), + ), + ( + "parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="shop.productcategory", + verbose_name="categoría padre", + ), + ), + ], + options={ + "verbose_name": "categoría", + "verbose_name_plural": "categorías", + "ordering": ("name",), + }, + ), + migrations.AddField( + model_name="product", + name="categories", + field=models.ManyToManyField( + blank=True, to="shop.productcategory", verbose_name="categorías" + ), + ), migrations.CreateModel( name="ProductBatch", fields=[ @@ -758,6 +810,13 @@ class Migration(migrations.Migration): "verbose_name_plural": "métodos de envío", }, ), + migrations.AddField( + model_name="product", + name="tags", + field=models.ManyToManyField( + blank=True, to="shop.tag", verbose_name="etiquetas" + ), + ), migrations.CreateModel( name="ProductPrice", fields=[ diff --git a/shop/models.py b/shop/models.py index 32d2844..ddfe4ab 100644 --- a/shop/models.py +++ b/shop/models.py @@ -35,6 +35,27 @@ class Tag(models.Model): ordering = ("name",) +class ProductCategory(models.Model): + name = models.CharField(max_length=32) + parent = models.ForeignKey('shop.ProductCategory', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('categoría padre')) + promoted = models.BooleanField(default=False, verbose_name=_('promocionada')) + hidden = models.BooleanField(default=False, verbose_name=_('oculta')) + show_in_navbar = models.BooleanField(default=False, verbose_name=_('mostrar en cabecera')) + slug = models.SlugField(max_length=48, blank=True, default="", verbose_name=_("slug"), unique=True, db_index=True) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + super().save() + + class Meta: + verbose_name = _("categoría") + verbose_name_plural = _("categorías") + ordering = ("name",) + + class Product(TimestampedModel): sku = models.CharField( max_length=64, @@ -68,6 +89,7 @@ class Product(TimestampedModel): "files.FileUpload", blank=True, verbose_name=_("imágenes") ) tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("etiquetas")) + categories = models.ManyToManyField("shop.ProductCategory", blank=True, verbose_name=_("categorías")) brand = models.ForeignKey( "shop.Brand", blank=True, @@ -415,7 +437,9 @@ class CustomerAddress(models.Model): BILLING = "BILL", _("facturación") SHIPPING = "SHIP", _("envío") - user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name=_("usuario")) + user = models.ForeignKey( + User, on_delete=models.CASCADE, null=True, verbose_name=_("usuario") + ) vat_id = models.CharField( max_length=16, blank=True, diff --git a/shop/tests/api/test_api_orders.py b/shop/tests/api/test_api_orders.py index 35ff34e..a76a099 100644 --- a/shop/tests/api/test_api_orders.py +++ b/shop/tests/api/test_api_orders.py @@ -1,11 +1,11 @@ from decimal import Decimal -from django.contrib.auth import get_user_model +from django.contrib.auth import get_user_model from rest_framework import status from rest_framework.test import APITestCase from config.tests.mixins import TestUserAuthenticationMixin -from shop.models import Product, ProductPrice, Tax, CustomerAddress +from shop.models import CustomerAddress, Product, ProductPrice, Tax User = get_user_model() diff --git a/shop/tests/api/test_shop_models.py b/shop/tests/api/test_shop_models.py index c7a3685..53f2658 100644 --- a/shop/tests/api/test_shop_models.py +++ b/shop/tests/api/test_shop_models.py @@ -3,7 +3,7 @@ from decimal import Decimal from django.contrib.auth import get_user_model from rest_framework.test import APITestCase as TestCase -from shop.models import Product, ProductPrice, Tax, CustomerAddress +from shop.models import CustomerAddress, Product, ProductPrice, Tax from shop.utils import create_order, create_order_line_for_product User = get_user_model() diff --git a/shop/utils.py b/shop/utils.py index 9787990..ceb6f0d 100644 --- a/shop/utils.py +++ b/shop/utils.py @@ -1,16 +1,17 @@ from decimal import Decimal +from django.contrib.auth import get_user_model + from shop.models import ( + Cart, + CustomerAddress, Order, OrderLine, Product, ProductBatch, - Cart, - CustomerAddress, - ShippingMethod, ProductPrice, + ShippingMethod, ) -from django.contrib.auth import get_user_model User = get_user_model() diff --git a/web/context_processors.py b/web/context_processors.py index 7f34bd4..20ba856 100644 --- a/web/context_processors.py +++ b/web/context_processors.py @@ -1,8 +1,12 @@ +from shop.models import ProductCategory from web.models import WebSettings def web_settings(request): settings = WebSettings.load() + navbar_categories = ProductCategory.objects.filter(hidden=False, show_in_navbar=True) + return { "logo": settings.logo.url if settings.logo else "", + "navbar_categories": navbar_categories, } diff --git a/web/mixins.py b/web/mixins.py index 0acc2c7..f0b87a4 100644 --- a/web/mixins.py +++ b/web/mixins.py @@ -1,9 +1,6 @@ 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 diff --git a/web/templates/components/generic/navbar.html b/web/templates/components/generic/navbar.html index 58f6a06..38735ec 100644 --- a/web/templates/components/generic/navbar.html +++ b/web/templates/components/generic/navbar.html @@ -22,24 +22,14 @@ {% translate 'Inicio' %} + {% for category in navbar_categories %}
  • - - {% translate 'Más vendidos' %} - -
  • -
  • - - {% translate 'Ideas de regalo' %} - -
  • -
  • - - {% translate 'Ofertas del día' %} + {{ category.name }}
  • + {% endfor %} diff --git a/web/templates/web/index.html b/web/templates/web/index.html index 5cea275..3d3cd1f 100644 --- a/web/templates/web/index.html +++ b/web/templates/web/index.html @@ -7,7 +7,7 @@ {% include 'components/products/list_filters.html' %}