From 84bd7b1951bbb572c5995235dcf05be50f60a0d1 Mon Sep 17 00:00:00 2001 From: Pablo Moreno Date: Wed, 15 May 2024 22:21:20 +0200 Subject: [PATCH] feat: added tags --- files/models.py | 3 ++ shop/admin.py | 29 ++++++++++- shop/api/v1/routers.py | 2 + shop/api/v1/serializers.py | 31 ++++++++++++ shop/api/v1/viewsets.py | 10 ++++ ...5_tag_alter_product_images_product_tags.py | 41 +++++++++++++++ shop/models.py | 16 ++++++ shop/tests/test_api_products.py | 39 ++++++++++++++- shop/tests/test_api_tags.py | 50 +++++++++++++++++++ 9 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 shop/migrations/0005_tag_alter_product_images_product_tags.py create mode 100644 shop/tests/test_api_tags.py diff --git a/files/models.py b/files/models.py index aba40cc..4d58179 100644 --- a/files/models.py +++ b/files/models.py @@ -3,3 +3,6 @@ from django.db import models class FileUpload(models.Model): file = models.FileField(upload_to="uploads", blank=False, null=False) + + def __str__(self): + return self.file.url diff --git a/shop/admin.py b/shop/admin.py index 1365de8..d56a76d 100644 --- a/shop/admin.py +++ b/shop/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from shop.models import Product, ProductPrice, OrderLine, Order, Tax, Customer +from shop.models import Product, ProductPrice, OrderLine, Order, Tax, Customer, Provider, Tag, ProductBatch from unfold.admin import ModelAdmin @@ -58,3 +58,30 @@ class OrderLineAdmin(ModelAdmin): @admin.register(Order) class OrderAdmin(ModelAdmin): list_display = ("id",) + + +@admin.register(Provider) +class ProviderAdmin(ModelAdmin): + list_display = ( + "vat_id", + "name", + ) + + +@admin.register(Tag) +class TagAdmin(ModelAdmin): + list_display = ( + "id", + "name", + ) + + +@admin.register(ProductBatch) +class ProductBatchAdmin(ModelAdmin): + list_display = ( + "id", + "product", + "quantity", + "provider", + ) + diff --git a/shop/api/v1/routers.py b/shop/api/v1/routers.py index eee127f..74d31a8 100644 --- a/shop/api/v1/routers.py +++ b/shop/api/v1/routers.py @@ -8,6 +8,7 @@ from shop.api.v1.viewsets import ( OrderLineViewSet, ProviderViewSet, ProductBatchViewSet, + TagViewSet, ) @@ -17,6 +18,7 @@ app_name = "shop" router = DefaultRouter() router.register("taxes", TaxViewSet) +router.register("tags", TagViewSet) router.register("customers", CustomerViewSet) router.register("providers", ProviderViewSet) router.register("products", ProductViewSet) diff --git a/shop/api/v1/serializers.py b/shop/api/v1/serializers.py index eb69e06..bc4db35 100644 --- a/shop/api/v1/serializers.py +++ b/shop/api/v1/serializers.py @@ -11,6 +11,7 @@ from shop.models import ( Order, ProductBatch, Provider, + Tag, ) @@ -24,6 +25,15 @@ class TaxSerializer(serializers.ModelSerializer): ) +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ( + "id", + "name", + ) + + class ProductPriceSerializer(serializers.ModelSerializer): tax = TaxSerializer() @@ -60,6 +70,23 @@ class ProductSerializer(serializers.ModelSerializer): "is_digital_asset", "images", "price", + "tags", + ) + + +class ListProductSerializer(serializers.ModelSerializer): + price = ProductPriceSerializer() + tags = TagSerializer(many=True) + + class Meta: + model = Product + fields = ( + "name", + "description", + "stock", + "is_digital_asset", + "price", + "tags", ) @@ -98,6 +125,8 @@ class CreateProductSerializer(serializers.ModelSerializer): "url", "price", "tax", + "tags", + "images", ) @@ -197,3 +226,5 @@ class OrderLineSerializer(serializers.ModelSerializer): "taxes", "tax_value", ) + + diff --git a/shop/api/v1/viewsets.py b/shop/api/v1/viewsets.py index c17af6d..aa72d8f 100644 --- a/shop/api/v1/viewsets.py +++ b/shop/api/v1/viewsets.py @@ -20,6 +20,8 @@ from shop.api.v1.serializers import ( ProductBatchSerializer, CreateProductSerializer, CreateProductPriceSerializer, + TagSerializer, + ListProductSerializer, ) from shop.models import ( Product, @@ -30,6 +32,7 @@ from shop.models import ( OrderLine, Provider, ProductBatch, + Tag, ) from shop.api.v1.permissions import ProductPermissions, ProductPricePermissions @@ -46,6 +49,8 @@ class ProductViewSet(ModelViewSet): def get_serializer_class(self): if self.action == "create": return CreateProductSerializer + elif self.action == "list": + return ListProductSerializer return self.serializer_class def perform_destroy(self, instance): @@ -145,3 +150,8 @@ class OrderLineViewSet(ModelViewSet): .select_related("order") .filter(order__uuid=self.kwargs.get("order_id")) ) + + +class TagViewSet(ModelViewSet): + serializer_class = TagSerializer + queryset = Tag.objects.all() diff --git a/shop/migrations/0005_tag_alter_product_images_product_tags.py b/shop/migrations/0005_tag_alter_product_images_product_tags.py new file mode 100644 index 0000000..8f81c63 --- /dev/null +++ b/shop/migrations/0005_tag_alter_product_images_product_tags.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.6 on 2024-05-14 16:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("files", "0001_initial"), + ("shop", "0004_productbatch_provider"), + ] + + operations = [ + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=16)), + ], + ), + migrations.AlterField( + model_name="product", + name="images", + field=models.ManyToManyField( + to="files.fileupload", verbose_name="Imágenes" + ), + ), + migrations.AddField( + model_name="product", + name="tags", + field=models.ManyToManyField(to="shop.tag", verbose_name="Etiquetas"), + ), + ] diff --git a/shop/models.py b/shop/models.py index 1608931..7d50355 100644 --- a/shop/models.py +++ b/shop/models.py @@ -6,6 +6,17 @@ from django.utils import timezone from django.utils.text import gettext_lazy as _ +class Tag(models.Model): + name = models.CharField(max_length=16) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Etiqueta") + verbose_name_plural = _("Etiquetas") + + class Product(models.Model): name = models.CharField( max_length=96, @@ -30,6 +41,7 @@ class Product(models.Model): images = models.ManyToManyField( "files.FileUpload", blank=True, verbose_name=_("Imágenes") ) + tags = models.ManyToManyField("shop.Tag", blank=True, verbose_name=_("Etiquetas")) def __str__(self): return self.name @@ -103,6 +115,10 @@ class ProductBatch(models.Model): def __str__(self): return f"{self.code} - {self.product.name} - {self.quantity}" + class Meta: + verbose_name = _("Remesa de producto") + verbose_name_plural = _("Remesas de productos") + class Tax(models.Model): code = models.CharField( diff --git a/shop/tests/test_api_products.py b/shop/tests/test_api_products.py index 2bf88e4..029386d 100644 --- a/shop/tests/test_api_products.py +++ b/shop/tests/test_api_products.py @@ -5,7 +5,7 @@ from rest_framework.test import APITestCase from django.utils import timezone from config.tests.mixins import TestUserAuthenticationMixin -from shop.models import Product, ProductPrice, Tax, ProductBatch +from shop.models import Product, ProductPrice, Tax, ProductBatch, Tag class TestProductsAPI(APITestCase, TestUserAuthenticationMixin): @@ -17,6 +17,7 @@ class TestProductsAPI(APITestCase, TestUserAuthenticationMixin): code="IVA", value=21, ) + self.tag = Tag.objects.create(name="Alimentos") def create_products(self): self.product = Product.objects.create( @@ -58,6 +59,7 @@ class TestProductsAPI(APITestCase, TestUserAuthenticationMixin): "price": "1.20", "stock": "100", "tax": self.tax.pk, + "tags": [self.tag.pk], }, ) assert response.status_code == status.HTTP_201_CREATED @@ -78,6 +80,9 @@ class TestProductsAPI(APITestCase, TestUserAuthenticationMixin): "price": "1.20", "stock": "100", "tax": self.tax.pk, + "tags": [ + self.tag.pk, + ], }, ) assert response.status_code == status.HTTP_201_CREATED @@ -105,6 +110,9 @@ class TestProductsAPI(APITestCase, TestUserAuthenticationMixin): "price": "1.20", "stock": "100", "tax": self.tax.pk, + "tags": [ + self.tag.pk, + ], }, ) assert response.status_code == status.HTTP_201_CREATED @@ -127,3 +135,32 @@ class TestProductsAPI(APITestCase, TestUserAuthenticationMixin): response = self.client.delete(f"/api/v1/shop/products/{self.product.pk}/") assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_create_update_product_tags(self): + self.login() + tag = Tag.objects.create(name="Alimentación") + response = self.client.post( + "/api/v1/shop/products/", + { + "name": "Papafritas", + "description": "Las mejores papafritas", + "is_digital_asset": False, + "url": "", + "price": "1.20", + "stock": "100", + "tax": self.tax.pk, + "tags": [ + self.tag.pk, + ], + }, + ) + assert response.status_code == status.HTTP_201_CREATED + pk = response.data.get("id") + + response = self.client.get(f"/api/v1/shop/products/{pk}/") + assert response.status_code == status.HTTP_200_OK + + response = self.client.patch(f"/api/v1/shop/products/{pk}/", {"tags": [tag.pk]}) + assert response.status_code == status.HTTP_200_OK + response = self.client.get(f"/api/v1/shop/products/{pk}/") + assert response.data.get("tags", [])[0] == tag.pk diff --git a/shop/tests/test_api_tags.py b/shop/tests/test_api_tags.py new file mode 100644 index 0000000..8770013 --- /dev/null +++ b/shop/tests/test_api_tags.py @@ -0,0 +1,50 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from config.tests.mixins import TestUserAuthenticationMixin +from shop.models import Tag + + +class TestTagsAPI(APITestCase, TestUserAuthenticationMixin): + model_name = "tag" + + def setUp(self): + self.create_user() + + def create_tags(self): + Tag.objects.create(name="Deportes") + + def test_fetch_taxes(self): + self.create_tags() + self.login() + response = self.client.get("/api/v1/shop/tags/") + assert response.status_code == status.HTTP_200_OK + assert len(response.data.get("results")) == 1 + + def test_create_retrieve_tax(self): + self.login() + response = self.client.post("/api/v1/shop/tags/", {"name": "Videojuegos"}) + assert response.status_code == status.HTTP_201_CREATED + pk = response.data.get("id") + + response = self.client.get(f"/api/v1/shop/tags/{pk}/") + assert response.status_code == status.HTTP_200_OK + + def test_create_update_tax(self): + self.login() + response = self.client.post("/api/v1/shop/tags/", {"name": "Videojuegos"}) + assert response.status_code == status.HTTP_201_CREATED + pk = response.data.get("id") + + response = self.client.get(f"/api/v1/shop/tags/{pk}/") + assert response.status_code == status.HTTP_200_OK + + response = self.client.patch( + f"/api/v1/shop/tags/{pk}/", {"name": "Juegos de mesa"} + ) + assert response.status_code == status.HTTP_200_OK + + response = self.client.get(f"/api/v1/shop/tags/{pk}/") + assert response.status_code == status.HTTP_200_OK + + assert response.data.get("name") == "Juegos de mesa"