feat: added categories

This commit is contained in:
2024-12-31 11:19:28 +01:00
parent eae54d5198
commit 3de6bbcc29
16 changed files with 166 additions and 36 deletions
+1
View File
@@ -56,3 +56,4 @@ REDSYS_TPV_DOMAIN = env.str("REDSYS_TPV_DOMAIN", "")
ITEMS_PER_PAGE = env.int("ITEMS_PER_PAGE", 20) ITEMS_PER_PAGE = env.int("ITEMS_PER_PAGE", 20)
SHOP_COUNTRY = env.str("SHOP_COUNTRY", "ESPAÑA") SHOP_COUNTRY = env.str("SHOP_COUNTRY", "ESPAÑA")
NPM_BIN_PATH = which("npm")
+15 -2
View File
@@ -5,16 +5,17 @@ from shop.models import (
Brand, Brand,
Cart, Cart,
CartItem, CartItem,
CustomerAddress,
Order, Order,
OrderLine, OrderLine,
Product, Product,
ProductBatch, ProductBatch,
ProductCategory,
ProductPrice, ProductPrice,
Provider, Provider,
ShippingMethod,
Tag, Tag,
Tax, Tax,
ShippingMethod,
CustomerAddress,
) )
@@ -142,3 +143,15 @@ class CustomerAddressAdmin(ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.filter(hidden=False) return qs.filter(hidden=False)
@admin.register(ProductCategory)
class ProductCategoryAdmin(ModelAdmin):
list_display = (
"id",
"name",
"parent",
"promoted",
"hidden",
"show_in_navbar",
)
+2
View File
@@ -10,6 +10,7 @@ class ProductFilter(django_filters.FilterSet):
field_name="name", lookup_expr="icontains", label=_("Nombre") field_name="name", lookup_expr="icontains", label=_("Nombre")
) )
tags = django_filters.BaseInFilter(field_name="tags", label=_("Etiquetas")) 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_lt = django_filters.NumberFilter(method="filter_price_lt")
price_gt = django_filters.NumberFilter(method="filter_price_gt") price_gt = django_filters.NumberFilter(method="filter_price_gt")
o = OrderingFilter( o = OrderingFilter(
@@ -42,4 +43,5 @@ class ProductFilter(django_filters.FilterSet):
"stock", "stock",
"is_digital_asset", "is_digital_asset",
"tags", "tags",
"category",
) )
+66 -7
View File
@@ -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.db.models.deletion
import django.utils.timezone import django.utils.timezone
@@ -507,12 +507,6 @@ class Migration(migrations.Migration):
blank=True, to="files.fileupload", verbose_name="imágenes" blank=True, to="files.fileupload", verbose_name="imágenes"
), ),
), ),
(
"tags",
models.ManyToManyField(
blank=True, to="shop.tag", verbose_name="etiquetas"
),
),
], ],
options={ options={
"verbose_name": "producto", "verbose_name": "producto",
@@ -646,6 +640,64 @@ class Migration(migrations.Migration):
"verbose_name_plural": "líneas de carrito", "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( migrations.CreateModel(
name="ProductBatch", name="ProductBatch",
fields=[ fields=[
@@ -758,6 +810,13 @@ class Migration(migrations.Migration):
"verbose_name_plural": "métodos de envío", "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( migrations.CreateModel(
name="ProductPrice", name="ProductPrice",
fields=[ fields=[
+25 -1
View File
@@ -35,6 +35,27 @@ class Tag(models.Model):
ordering = ("name",) 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): class Product(TimestampedModel):
sku = models.CharField( sku = models.CharField(
max_length=64, max_length=64,
@@ -68,6 +89,7 @@ class Product(TimestampedModel):
"files.FileUpload", blank=True, verbose_name=_("imágenes") "files.FileUpload", blank=True, verbose_name=_("imágenes")
) )
tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("etiquetas")) tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("etiquetas"))
categories = models.ManyToManyField("shop.ProductCategory", blank=True, verbose_name=_("categorías"))
brand = models.ForeignKey( brand = models.ForeignKey(
"shop.Brand", "shop.Brand",
blank=True, blank=True,
@@ -415,7 +437,9 @@ class CustomerAddress(models.Model):
BILLING = "BILL", _("facturación") BILLING = "BILL", _("facturación")
SHIPPING = "SHIP", _("envío") 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( vat_id = models.CharField(
max_length=16, max_length=16,
blank=True, blank=True,
+2 -2
View File
@@ -1,11 +1,11 @@
from decimal import Decimal 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 import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from config.tests.mixins import TestUserAuthenticationMixin 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() User = get_user_model()
+1 -1
View File
@@ -3,7 +3,7 @@ from decimal import Decimal
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework.test import APITestCase as TestCase 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 from shop.utils import create_order, create_order_line_for_product
User = get_user_model() User = get_user_model()
+5 -4
View File
@@ -1,16 +1,17 @@
from decimal import Decimal from decimal import Decimal
from django.contrib.auth import get_user_model
from shop.models import ( from shop.models import (
Cart,
CustomerAddress,
Order, Order,
OrderLine, OrderLine,
Product, Product,
ProductBatch, ProductBatch,
Cart,
CustomerAddress,
ShippingMethod,
ProductPrice, ProductPrice,
ShippingMethod,
) )
from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()
+4
View File
@@ -1,8 +1,12 @@
from shop.models import ProductCategory
from web.models import WebSettings from web.models import WebSettings
def web_settings(request): def web_settings(request):
settings = WebSettings.load() settings = WebSettings.load()
navbar_categories = ProductCategory.objects.filter(hidden=False, show_in_navbar=True)
return { return {
"logo": settings.logo.url if settings.logo else "", "logo": settings.logo.url if settings.logo else "",
"navbar_categories": navbar_categories,
} }
-3
View File
@@ -1,9 +1,6 @@
from django.conf import settings from django.conf import settings
from django.core.paginator import Paginator from django.core.paginator import Paginator
from shop.models import Cart
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
class FilteredQuerysetMixin: class FilteredQuerysetMixin:
queryset = None queryset = None
+4 -14
View File
@@ -22,24 +22,14 @@
{% translate 'Inicio' %} {% translate 'Inicio' %}
</a> </a>
</li> </li>
{% for category in navbar_categories %}
<li class="shrink-0"> <li class="shrink-0">
<a href="{% url 'web:index' %}" title="" <a href="{% url 'web:category_view' slug=category.slug %}" title=""
class="flex text-sm font-medium text-gray-900 hover:text-primary-700 dark:text-white dark:hover:text-primary-500"> class="flex text-sm font-medium text-gray-900 hover:text-primary-700 dark:text-white dark:hover:text-primary-500">
{% translate 'Más vendidos' %} {{ category.name }}
</a>
</li>
<li class="shrink-0">
<a href="{% url 'web:index' %}" title=""
class="flex text-sm font-medium text-gray-900 hover:text-primary-700 dark:text-white dark:hover:text-primary-500">
{% translate 'Ideas de regalo' %}
</a>
</li>
<li class="shrink-0">
<a href="{% url 'web:index' %}" title=""
class="text-sm font-medium text-gray-900 hover:text-primary-700 dark:text-white dark:hover:text-primary-500">
{% translate 'Ofertas del día' %}
</a> </a>
</li> </li>
{% endfor %}
</ul> </ul>
</div> </div>
+1 -1
View File
@@ -7,7 +7,7 @@
{% include 'components/products/list_filters.html' %} {% include 'components/products/list_filters.html' %}
<section <section
class="p-8" class="p-8"
hx-get="{% url 'web:list_products' %}{% querystring request.GET %}" hx-get="{% url 'web:list_products' %}{% querystring request.GET %}{% if filter_by_category %}{% querystring category=category.id %}{% endif %}"
hx-trigger="load" hx-trigger="load"
hx-target="#products" hx-target="#products"
hx-swap="innerHTML" hx-swap="innerHTML"
+17
View File
@@ -0,0 +1,17 @@
from django.test import TestCase
from django.urls import reverse
from shop.models import ProductCategory
from shop.tests.mixins import CreateProductsMixin
class TestCategoryView(TestCase, CreateProductsMixin):
def setUp(self):
self.product = self.create_product()
def test_category_view(self):
category = ProductCategory.objects.create(
name="category 1", show_in_navbar=True, hidden=False
)
response = self.client.get(reverse("web:category_view", kwargs={"slug": category.slug}))
assert response.status_code == 200
+2
View File
@@ -9,12 +9,14 @@ from web.views.web import (
index, index,
manifest, manifest,
product_detail, product_detail,
category_view,
) )
app_name = "web" app_name = "web"
urlpatterns = [ urlpatterns = [
path("", index, name="index"), path("", index, name="index"),
path("categories/<str:slug>/", category_view, name="category_view"),
path("products/<int:pk>/<str:slug>/", product_detail, name="product_detail"), path("products/<int:pk>/<str:slug>/", product_detail, name="product_detail"),
path("cart/", cart_detail, name="cart_detail"), path("cart/", cart_detail, name="cart_detail"),
# users # users
+2
View File
@@ -1,6 +1,8 @@
from decimal import Decimal from decimal import Decimal
from django.db.models import Sum from django.db.models import Sum
from qrcode import constants as qr_constants
from qrcode.main import QRCode
from shop.models import Cart, CartItem from shop.models import Cart, CartItem
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
+19 -1
View File
@@ -4,7 +4,7 @@ from django.utils.text import gettext_lazy as _
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView from django.views.generic import TemplateView
from shop.models import CartItem, Product from shop.models import CartItem, Product, ProductCategory
from web.models import WebSettings from web.models import WebSettings
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
from web.utils import get_or_create_cart from web.utils import get_or_create_cart
@@ -22,6 +22,23 @@ class IndexView(TemplateView):
} }
class CategoryView(TemplateView):
template_name = "web/index.html"
def get_context_data(self, **kwargs):
settings = WebSettings.load()
slug = kwargs.get('slug')
category = ProductCategory.objects.get(slug=slug)
return {
"title": settings.web_title,
"web_title": settings.web_title,
"description": settings.web_description,
"category": category,
"filter_by_category": True,
}
class ProductDetail(TemplateView): class ProductDetail(TemplateView):
template_name = "web/product_detail.html" template_name = "web/product_detail.html"
@@ -135,5 +152,6 @@ def manifest(request, *args, **kwargs):
index = IndexView.as_view() index = IndexView.as_view()
category_view = CategoryView.as_view()
product_detail = ProductDetail.as_view() product_detail = ProductDetail.as_view()
cart_detail = CartDetail.as_view() cart_detail = CartDetail.as_view()