feat: added categories
This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user