feat: add catalogue image management (#1)
- Add CatalogueImage model with FK to Product - Add image resize service (strict/non-strict modes with configurable dimensions) - Add CRUD API endpoints with admin-only write permissions - Add catalogue_images field to product listing/detail endpoints - Serve media files in development via static() - 28 TDD tests covering model, API, permissions, resize, and product listing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
25
tienda_ilusion/don_confiao/api/catalogue_images.py
Normal file
25
tienda_ilusion/don_confiao/api/catalogue_images.py
Normal file
@@ -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)
|
||||
42
tienda_ilusion/don_confiao/image_service.py
Normal file
42
tienda_ilusion/don_confiao/image_service.py
Normal file
@@ -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
|
||||
24
tienda_ilusion/don_confiao/migrations/0049_catalogueimage.py
Normal file
24
tienda_ilusion/don_confiao/migrations/0049_catalogueimage.py
Normal file
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
20
tienda_ilusion/don_confiao/models/catalogue_images.py
Normal file
20
tienda_ilusion/don_confiao/models/catalogue_images.py
Normal file
@@ -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})"
|
||||
@@ -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",
|
||||
|
||||
20
tienda_ilusion/don_confiao/serializers/catalogue_images.py
Normal file
20
tienda_ilusion/don_confiao/serializers/catalogue_images.py
Normal file
@@ -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
|
||||
@@ -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()
|
||||
]
|
||||
|
||||
415
tienda_ilusion/don_confiao/tests/test_catalogue_images.py
Normal file
415
tienda_ilusion/don_confiao/tests/test_catalogue_images.py
Normal file
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user