diff --git a/requirements.txt b/requirements.txt index 013f4b1..e8b7ebf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ python-decouple # Manage environment variables and settings # Static files serving in production/staging whitenoise==6.6.0 # Serve static files efficiently with compression +Pillow # Image processing for catalogue image resizing diff --git a/tienda_ilusion/config/settings/base.py b/tienda_ilusion/config/settings/base.py index 19b1541..f9fe0c8 100644 --- a/tienda_ilusion/config/settings/base.py +++ b/tienda_ilusion/config/settings/base.py @@ -138,6 +138,17 @@ USE_TZ = True STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" +# Media files +MEDIA_ROOT = BASE_DIR / "media" +MEDIA_URL = "/media/" + +# Catalogue image settings +CATALOGUE_IMAGE_WIDTH = int(os.environ.get("CATALOGUE_IMAGE_WIDTH", "600")) +CATALOGUE_IMAGE_HEIGHT = int(os.environ.get("CATALOGUE_IMAGE_HEIGHT", "600")) +CATALOGUE_STRICT_DIMENSION = os.environ.get("CATALOGUE_STRICT_DIMENSION", "False").lower() in ("true", "1", "yes") +CATALOGUE_BACKGROUND_IMAGES_COLOR = os.environ.get("CATALOGUE_BACKGROUND_IMAGES_COLOR") or None +CATALOGUE_MAX_UPLOAD_SIZE = int(os.environ.get("CATALOGUE_MAX_UPLOAD_SIZE", str(5 * 1024 * 1024))) + # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field diff --git a/tienda_ilusion/config/settings/development.py b/tienda_ilusion/config/settings/development.py index f271d51..3857cc1 100644 --- a/tienda_ilusion/config/settings/development.py +++ b/tienda_ilusion/config/settings/development.py @@ -48,6 +48,10 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_URL = "/static/" +# Media files configuration +MEDIA_ROOT = BASE_DIR / "media" +MEDIA_URL = "/media/" + # WhiteNoise configuration for development (optional, for consistency) # In development with DEBUG=True, Django serves static files automatically # But this ensures consistent behavior across all environments diff --git a/tienda_ilusion/config/urls.py b/tienda_ilusion/config/urls.py index 602cc8b..29a01d1 100644 --- a/tienda_ilusion/config/urls.py +++ b/tienda_ilusion/config/urls.py @@ -14,6 +14,8 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from rest_framework_simplejwt.views import ( @@ -32,3 +34,6 @@ urlpatterns = [ name='token_refresh'), path('api/users/', include('users.urls')), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/tienda_ilusion/don_confiao/api/__init__.py b/tienda_ilusion/don_confiao/api/__init__.py index d6f5226..275997b 100644 --- a/tienda_ilusion/don_confiao/api/__init__.py +++ b/tienda_ilusion/don_confiao/api/__init__.py @@ -1,3 +1,4 @@ +from .catalogue_images import CatalogueImageViewSet from .products import ProductView, ProductsFromTrytonView from .customers import CustomerView, CustomersFromTrytonView from .sales import ( @@ -19,6 +20,8 @@ from .payments import ( from .admin import AdminCodeValidateView __all__ = [ + # Catalogue Images + "CatalogueImageViewSet", # Products "ProductView", "ProductsFromTrytonView", diff --git a/tienda_ilusion/don_confiao/api/catalogue_images.py b/tienda_ilusion/don_confiao/api/catalogue_images.py new file mode 100644 index 0000000..1b0ac7e --- /dev/null +++ b/tienda_ilusion/don_confiao/api/catalogue_images.py @@ -0,0 +1,25 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from ..image_service import resize_catalogue_image +from ..models.catalogue_images import CatalogueImage +from ..permissions import IsAdministrator +from ..serializers.catalogue_images import CatalogueImageSerializer + + +class CatalogueImageViewSet(viewsets.ModelViewSet): + queryset = CatalogueImage.objects.all() + serializer_class = CatalogueImageSerializer + + def get_permissions(self): + if self.action in ("create", "update", "partial_update", "destroy"): + return [IsAuthenticated(), IsAdministrator()] + return [IsAuthenticated()] + + def perform_create(self, serializer): + instance = serializer.save() + resize_catalogue_image(instance.image) + + def perform_update(self, serializer): + instance = serializer.save() + resize_catalogue_image(instance.image) diff --git a/tienda_ilusion/don_confiao/image_service.py b/tienda_ilusion/don_confiao/image_service.py new file mode 100644 index 0000000..c4600ed --- /dev/null +++ b/tienda_ilusion/don_confiao/image_service.py @@ -0,0 +1,42 @@ +import os + +from django.conf import settings +from PIL import Image + + +def resize_catalogue_image(image_field): + width = settings.CATALOGUE_IMAGE_WIDTH + height = settings.CATALOGUE_IMAGE_HEIGHT + strict = settings.CATALOGUE_STRICT_DIMENSION + bg_color = settings.CATALOGUE_BACKGROUND_IMAGES_COLOR + + img = Image.open(image_field.path) + img = img.convert("RGBA") + + if strict: + img.thumbnail((width, height), Image.LANCZOS) + canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + if bg_color: + canvas = Image.new( + "RGBA", (width, height), _hex_to_rgba(bg_color) + ) + x = (width - img.width) // 2 + y = (height - img.height) // 2 + canvas.paste(img, (x, y), img) + img = canvas + else: + new_height = int(img.height * width / img.width) + img = img.resize((width, new_height), Image.LANCZOS) + + if img.mode == "RGBA": + background = Image.new("RGB", img.size, (255, 255, 255)) + background.paste(img, mask=img.split()[3]) + img = background + + img.save(image_field.path, format="JPEG", quality=85) + + +def _hex_to_rgba(hex_color): + hex_color = hex_color.lstrip("#") + r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + return r, g, b, 255 diff --git a/tienda_ilusion/don_confiao/migrations/0049_catalogueimage.py b/tienda_ilusion/don_confiao/migrations/0049_catalogueimage.py new file mode 100644 index 0000000..5017db8 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0049_catalogueimage.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.6 on 2026-06-13 18:31 + +import django.db.models.deletion +import don_confiao.models.catalogue_images +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0048_catalogsale_customer_address_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='CatalogueImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to=don_confiao.models.catalogue_images._catalogue_image_upload_path)), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='catalogue_images', to='don_confiao.product')), + ], + ), + ] diff --git a/tienda_ilusion/don_confiao/models/catalogue_images.py b/tienda_ilusion/don_confiao/models/catalogue_images.py new file mode 100644 index 0000000..60d96c6 --- /dev/null +++ b/tienda_ilusion/don_confiao/models/catalogue_images.py @@ -0,0 +1,20 @@ +from django.db import models + +from .products import Product + + +def _catalogue_image_upload_path(instance, filename): + return f"catalogue_images/{instance.product_id}/{filename}" + + +class CatalogueImage(models.Model): + product = models.ForeignKey( + Product, + on_delete=models.CASCADE, + related_name="catalogue_images", + ) + image = models.ImageField(upload_to=_catalogue_image_upload_path) + uploaded_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"CatalogueImage for {self.product.name} ({self.id})" diff --git a/tienda_ilusion/don_confiao/serializers/__init__.py b/tienda_ilusion/don_confiao/serializers/__init__.py index 6edf74f..ff3b7b2 100644 --- a/tienda_ilusion/don_confiao/serializers/__init__.py +++ b/tienda_ilusion/don_confiao/serializers/__init__.py @@ -1,3 +1,4 @@ +from .catalogue_images import CatalogueImageSerializer from .products import ProductSerializer, ListProductSerializer from .customers import CustomerSerializer, ListCustomerSerializer from .sales import ( @@ -17,6 +18,8 @@ from .payments import ( ) __all__ = [ + # Catalogue Images + "CatalogueImageSerializer", # Products "ProductSerializer", "ListProductSerializer", diff --git a/tienda_ilusion/don_confiao/serializers/catalogue_images.py b/tienda_ilusion/don_confiao/serializers/catalogue_images.py new file mode 100644 index 0000000..b76eee8 --- /dev/null +++ b/tienda_ilusion/don_confiao/serializers/catalogue_images.py @@ -0,0 +1,20 @@ +from django.conf import settings +from rest_framework import serializers + +from ..models.catalogue_images import CatalogueImage + + +class CatalogueImageSerializer(serializers.ModelSerializer): + class Meta: + model = CatalogueImage + fields = ["id", "product", "image", "uploaded_at"] + read_only_fields = ["uploaded_at"] + + def validate_image(self, value): + max_size = settings.CATALOGUE_MAX_UPLOAD_SIZE + if value.size > max_size: + raise serializers.ValidationError( + f"Image size exceeds the maximum allowed size of " + f"{max_size // (1024 * 1024)}MB." + ) + return value diff --git a/tienda_ilusion/don_confiao/serializers/products.py b/tienda_ilusion/don_confiao/serializers/products.py index 62d7c88..e026cb4 100644 --- a/tienda_ilusion/don_confiao/serializers/products.py +++ b/tienda_ilusion/don_confiao/serializers/products.py @@ -4,6 +4,8 @@ from ..models.products import Product, ProductCategory class ProductSerializer(serializers.ModelSerializer): + catalogue_images = serializers.SerializerMethodField() + class Meta: model = Product fields = [ @@ -14,10 +16,31 @@ class ProductSerializer(serializers.ModelSerializer): "measuring_unit", "categories", "external_id", + "catalogue_images", + ] + + def get_catalogue_images(self, obj): + request = self.context.get("request") + if not request: + return [img.image.url for img in obj.catalogue_images.all()] + return [ + request.build_absolute_uri(img.image.url) + for img in obj.catalogue_images.all() ] class ListProductSerializer(serializers.ModelSerializer): + catalogue_images = serializers.SerializerMethodField() + class Meta: model = Product - fields = ["id", "name"] + fields = ["id", "name", "catalogue_images"] + + def get_catalogue_images(self, obj): + request = self.context.get("request") + if not request: + return [img.image.url for img in obj.catalogue_images.all()] + return [ + request.build_absolute_uri(img.image.url) + for img in obj.catalogue_images.all() + ] diff --git a/tienda_ilusion/don_confiao/tests/test_catalogue_images.py b/tienda_ilusion/don_confiao/tests/test_catalogue_images.py new file mode 100644 index 0000000..2d7317a --- /dev/null +++ b/tienda_ilusion/don_confiao/tests/test_catalogue_images.py @@ -0,0 +1,415 @@ +import io + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import override_settings +from PIL import Image +from rest_framework import status +from rest_framework.test import APITestCase + +from ..models.products import Product +from .Mixins import LoginMixin + + +def _create_test_image(width=600, height=400, name="test.png"): + """Create a SimpleUploadedFile from an in-memory image using PIL.""" + img = Image.new("RGBA", (width, height), (255, 0, 0, 255)) + output = io.BytesIO() + img.save(output, format="PNG") + return SimpleUploadedFile( + name=name, + content=output.getvalue(), + content_type="image/png", + ) + + +class TestCatalogueImageModel(APITestCase, LoginMixin): + def setUp(self): + self.login() + self.product = Product.objects.create( + name="Test Product", price=100.00 + ) + + def test_create_catalogue_image(self): + from ..models.catalogue_images import CatalogueImage + image_file = _create_test_image() + catalogue_image = CatalogueImage.objects.create( + product=self.product, image=image_file + ) + self.assertIsInstance(catalogue_image, CatalogueImage) + self.assertEqual(catalogue_image.product, self.product) + self.assertIsNotNone(catalogue_image.image) + self.assertIsNotNone(catalogue_image.uploaded_at) + + def test_catalogue_image_product_relation(self): + from ..models.catalogue_images import CatalogueImage + image_file = _create_test_image() + ci = CatalogueImage.objects.create( + product=self.product, image=image_file + ) + self.assertIn(ci, self.product.catalogue_images.all()) + + def test_catalogue_image_str(self): + from ..models.catalogue_images import CatalogueImage + image_file = _create_test_image() + ci = CatalogueImage.objects.create( + product=self.product, image=image_file + ) + expected = f"CatalogueImage for {self.product.name} ({ci.id})" + self.assertEqual(str(ci), expected) + + def test_cascade_delete_with_product(self): + from ..models.catalogue_images import CatalogueImage + image_file = _create_test_image() + CatalogueImage.objects.create( + product=self.product, image=image_file + ) + self.assertEqual(CatalogueImage.objects.count(), 1) + self.product.delete() + self.assertEqual(CatalogueImage.objects.count(), 0) + + +class TestCatalogueImageAPIPermissions(APITestCase, LoginMixin): + def setUp(self): + self.product = Product.objects.create( + name="Perm Test Product", price=100.00 + ) + + def test_create_catalogue_image_unauthenticated(self): + url = "/don_confiao/api/catalogue_images/" + image_file = _create_test_image() + data = {"product": self.product.id, "image": image_file} + response = self.client.post(url, data, format="multipart") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_create_catalogue_image_non_admin(self): + self.user = User.objects.create_user( + username="regularuser", + email="regular@example.com", + password="regularpass", + ) + from rest_framework_simplejwt.tokens import RefreshToken + from rest_framework.test import APIClient + refresh = RefreshToken.for_user(self.user) + self.client = APIClient() + self.client.credentials( + HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}" + ) + + url = "/don_confiao/api/catalogue_images/" + image_file = _create_test_image() + data = {"product": self.product.id, "image": image_file} + response = self.client.post(url, data, format="multipart") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_get_catalogue_images_authenticated_non_admin(self): + self.user = User.objects.create_user( + username="regularuser2", + email="regular2@example.com", + password="regularpass", + ) + from rest_framework_simplejwt.tokens import RefreshToken + from rest_framework.test import APIClient + refresh = RefreshToken.for_user(self.user) + self.client = APIClient() + self.client.credentials( + HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}" + ) + + url = "/don_confiao/api/catalogue_images/" + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_delete_catalogue_image_non_admin(self): + from ..models.catalogue_images import CatalogueImage + admin_user = User.objects.create_superuser( + username="admin2", email="admin2@example.com", password="adminpass" + ) + from rest_framework_simplejwt.tokens import RefreshToken + from rest_framework.test import APIClient + admin_refresh = RefreshToken.for_user(admin_user) + admin_client = APIClient() + admin_client.credentials( + HTTP_AUTHORIZATION=f"Bearer {str(admin_refresh.access_token)}" + ) + + image_file = _create_test_image() + create_data = { + "product": self.product.id, "image": image_file + } + create_response = admin_client.post( + "/don_confiao/api/catalogue_images/", create_data, format="multipart" + ) + ci_id = create_response.json()["id"] + + regular_user = User.objects.create_user( + username="regularuser3", + email="regular3@example.com", + password="regularpass", + ) + refresh = RefreshToken.for_user(regular_user) + self.client = APIClient() + self.client.credentials( + HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}" + ) + + response = self.client.delete( + f"/don_confiao/api/catalogue_images/{ci_id}/" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + self.assertEqual(CatalogueImage.objects.count(), 1) + + +class TestCatalogueImageAPI(APITestCase, LoginMixin): + def setUp(self): + self.login() + self.product = Product.objects.create( + name="API Test Product", price=100.00 + ) + self.product2 = Product.objects.create( + name="API Test Product 2", price=200.00 + ) + + def _create_image(self, product=None, width=600, height=400): + if product is None: + product = self.product + image_file = _create_test_image(width=width, height=height) + url = "/don_confiao/api/catalogue_images/" + data = {"product": product.id, "image": image_file} + return self.client.post(url, data, format="multipart") + + def test_list_catalogue_images_empty(self): + url = "/don_confiao/api/catalogue_images/" + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), []) + + def test_create_catalogue_image(self): + response = self._create_image() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + data = response.json() + self.assertIn("id", data) + self.assertEqual(data["product"], self.product.id) + self.assertIn("image", data) + self.assertIn("uploaded_at", data) + + def test_list_catalogue_images(self): + self._create_image() + self._create_image(product=self.product2) + + url = "/don_confiao/api/catalogue_images/" + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 2) + + def test_retrieve_catalogue_image(self): + create_response = self._create_image() + ci_id = create_response.json()["id"] + + url = f"/don_confiao/api/catalogue_images/{ci_id}/" + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["id"], ci_id) + self.assertEqual(response.json()["product"], self.product.id) + + def test_update_catalogue_image(self): + create_response = self._create_image() + ci_id = create_response.json()["id"] + + new_image = _create_test_image(width=200, height=200, name="updated.png") + url = f"/don_confiao/api/catalogue_images/{ci_id}/" + response = self.client.put( + url, + {"product": self.product.id, "image": new_image}, + format="multipart", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_delete_catalogue_image(self): + create_response = self._create_image() + ci_id = create_response.json()["id"] + + url = f"/don_confiao/api/catalogue_images/{ci_id}/" + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + from ..models.catalogue_images import CatalogueImage + self.assertEqual(CatalogueImage.objects.count(), 0) + + def test_create_catalogue_image_invalid_product(self): + image_file = _create_test_image() + url = "/don_confiao/api/catalogue_images/" + data = {"product": 99999, "image": image_file} + response = self.client.post(url, data, format="multipart") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_catalogue_image_missing_file(self): + url = "/don_confiao/api/catalogue_images/" + data = {"product": self.product.id} + response = self.client.post(url, data, format="multipart") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + +class TestCatalogueImageResize(APITestCase, LoginMixin): + def setUp(self): + self.login() + self.product = Product.objects.create( + name="Resize Test Product", price=100.00 + ) + + def _create_and_get_image_obj(self, width, height, strict=False, + bg_color=None): + from ..models.catalogue_images import CatalogueImage + settings_overrides = { + "CATALOGUE_IMAGE_WIDTH": 600, + "CATALOGUE_IMAGE_HEIGHT": 600, + "CATALOGUE_STRICT_DIMENSION": strict, + } + if bg_color is not None: + settings_overrides["CATALOGUE_BACKGROUND_IMAGES_COLOR"] = bg_color + + with override_settings(**settings_overrides): + image_file = _create_test_image(width=width, height=height) + url = "/don_confiao/api/catalogue_images/" + data = {"product": self.product.id, "image": image_file} + response = self.client.post(url, data, format="multipart") + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + ci_id = response.json()["id"] + return CatalogueImage.objects.get(id=ci_id) + + def test_resize_non_strict_mode_landscape(self): + ci = self._create_and_get_image_obj( + width=800, height=600, strict=False + ) + with Image.open(ci.image.path) as img: + self.assertEqual(img.width, 600) + expected_height = int(600 * 600 / 800) + self.assertEqual(img.height, expected_height) + + def test_resize_non_strict_mode_portrait(self): + ci = self._create_and_get_image_obj( + width=600, height=800, strict=False + ) + with Image.open(ci.image.path) as img: + self.assertEqual(img.width, 600) + expected_height = int(800 * 600 / 600) + self.assertEqual(img.height, expected_height) + + def test_resize_non_strict_mode_square(self): + ci = self._create_and_get_image_obj( + width=600, height=600, strict=False + ) + with Image.open(ci.image.path) as img: + self.assertEqual(img.width, 600) + self.assertEqual(img.height, 600) + + def test_resize_strict_mode_landscape(self): + ci = self._create_and_get_image_obj( + width=800, height=600, strict=True + ) + with Image.open(ci.image.path) as img: + self.assertEqual(img.width, 600) + self.assertEqual(img.height, 600) + + def test_resize_strict_mode_portrait(self): + ci = self._create_and_get_image_obj( + width=600, height=800, strict=True + ) + with Image.open(ci.image.path) as img: + self.assertEqual(img.width, 600) + self.assertEqual(img.height, 600) + + def test_resize_strict_mode_with_color_background(self): + ci = self._create_and_get_image_obj( + width=800, height=600, strict=True, bg_color="#FFFFFF" + ) + with Image.open(ci.image.path) as img: + self.assertEqual(img.width, 600) + self.assertEqual(img.height, 600) + + @override_settings(CATALOGUE_MAX_UPLOAD_SIZE=1) + def test_resize_large_file_rejected(self): + image_file = _create_test_image(width=10, height=10) + url = "/don_confiao/api/catalogue_images/" + data = {"product": self.product.id, "image": image_file} + response = self.client.post(url, data, format="multipart") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + +class TestProductListingWithImages(APITestCase, LoginMixin): + def setUp(self): + self.login() + self.product = Product.objects.create( + name="Product With Images", price=100.00 + ) + self.product_no_images = Product.objects.create( + name="Product Without Images", price=200.00 + ) + + def test_product_list_includes_catalogue_images_field(self): + url = "/don_confiao/api/products/" + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + for product in response.json(): + self.assertIn("catalogue_images", product) + + def test_product_list_catalogue_images_urls(self): + from ..models.catalogue_images import CatalogueImage + image_file = _create_test_image() + CatalogueImage.objects.create( + product=self.product, image=image_file + ) + + url = "/don_confiao/api/products/" + response = self.client.get(url) + data = response.json() + + product_data = next( + p for p in data if p["id"] == self.product.id + ) + self.assertEqual(len(product_data["catalogue_images"]), 1) + self.assertTrue( + product_data["catalogue_images"][0].endswith(".png") + ) + + def test_product_list_catalogue_images_multiple(self): + from ..models.catalogue_images import CatalogueImage + img1 = _create_test_image(name="img1.png") + img2 = _create_test_image(name="img2.png") + CatalogueImage.objects.create(product=self.product, image=img1) + CatalogueImage.objects.create(product=self.product, image=img2) + + url = "/don_confiao/api/products/" + response = self.client.get(url) + data = response.json() + + product_data = next( + p for p in data if p["id"] == self.product.id + ) + self.assertEqual(len(product_data["catalogue_images"]), 2) + + def test_product_list_catalogue_images_empty(self): + url = "/don_confiao/api/products/" + response = self.client.get(url) + data = response.json() + + product_data = next( + p for p in data if p["id"] == self.product_no_images.id + ) + self.assertEqual(product_data["catalogue_images"], []) + + def test_product_detail_includes_catalogue_images(self): + from ..models.catalogue_images import CatalogueImage + image_file = _create_test_image() + CatalogueImage.objects.create( + product=self.product, image=image_file + ) + + url = f"/don_confiao/api/products/{self.product.id}/" + response = self.client.get(url) + data = response.json() + self.assertIn("catalogue_images", data) + self.assertEqual(len(data["catalogue_images"]), 1) diff --git a/tienda_ilusion/don_confiao/urls.py b/tienda_ilusion/don_confiao/urls.py index 63e408a..e7e5592 100644 --- a/tienda_ilusion/don_confiao/urls.py +++ b/tienda_ilusion/don_confiao/urls.py @@ -3,6 +3,8 @@ from rest_framework.routers import DefaultRouter from . import views from .api import ( + # Catalogue Images + CatalogueImageViewSet, # Products ProductView, ProductsFromTrytonView, @@ -33,6 +35,11 @@ router.register(r"sales", SaleView, basename="sale") router.register(r"catalog_sales", CatalogSaleView, basename="catalog_sale") router.register(r"customers", CustomerView, basename="customer") router.register(r"products", ProductView, basename="product") +router.register( + r"catalogue_images", + CatalogueImageViewSet, + basename="catalogue_image", +) router.register( r"reconciliate_jar", ReconciliateJarModelView,