#51 feat: add editable store logo to StoreSettings
This commit is contained in:
@@ -153,6 +153,7 @@ CATALOGUE_BACKGROUND_IMAGES_RGBA = os.environ.get(
|
|||||||
_rgba_parts = CATALOGUE_BACKGROUND_IMAGES_RGBA.split(",")
|
_rgba_parts = CATALOGUE_BACKGROUND_IMAGES_RGBA.split(",")
|
||||||
CATALOGUE_BACKGROUND_IMAGES_RGBA = tuple(int(p.strip()) for p in _rgba_parts)
|
CATALOGUE_BACKGROUND_IMAGES_RGBA = tuple(int(p.strip()) for p in _rgba_parts)
|
||||||
CATALOGUE_MAX_UPLOAD_SIZE = int(os.environ.get("CATALOGUE_MAX_UPLOAD_SIZE", str(5 * 1024 * 1024)))
|
CATALOGUE_MAX_UPLOAD_SIZE = int(os.environ.get("CATALOGUE_MAX_UPLOAD_SIZE", str(5 * 1024 * 1024)))
|
||||||
|
LOGO_MAX_UPLOAD_SIZE = int(os.environ.get("LOGO_MAX_UPLOAD_SIZE", str(5 * 1024 * 1024)))
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ class StoreSettingsView(APIView):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
settings = StoreSettings.get_singleton()
|
settings = StoreSettings.get_singleton()
|
||||||
return Response(StoreSettingsSerializer(settings).data)
|
return Response(
|
||||||
|
StoreSettingsSerializer(
|
||||||
|
settings, context={"request": request}
|
||||||
|
).data
|
||||||
|
)
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
settings = StoreSettings.get_singleton()
|
settings = StoreSettings.get_singleton()
|
||||||
serializer = StoreSettingsSerializer(
|
serializer = StoreSettingsSerializer(
|
||||||
settings, data=request.data, partial=True
|
settings, data=request.data, partial=True, context={"request": request}
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-08 19:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0050_storesettings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='storesettings',
|
||||||
|
name='logo',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='logos/'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -5,6 +5,7 @@ class StoreSettings(models.Model):
|
|||||||
address = models.TextField()
|
address = models.TextField()
|
||||||
latitude = models.FloatField(null=True, blank=True)
|
latitude = models.FloatField(null=True, blank=True)
|
||||||
longitude = models.FloatField(null=True, blank=True)
|
longitude = models.FloatField(null=True, blank=True)
|
||||||
|
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.conf import settings
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..models.store_settings import StoreSettings
|
from ..models.store_settings import StoreSettings
|
||||||
@@ -6,5 +7,16 @@ from ..models.store_settings import StoreSettings
|
|||||||
class StoreSettingsSerializer(serializers.ModelSerializer):
|
class StoreSettingsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StoreSettings
|
model = StoreSettings
|
||||||
fields = ["id", "address", "latitude", "longitude", "updated_at"]
|
fields = ["id", "address", "latitude", "longitude", "logo", "updated_at"]
|
||||||
read_only_fields = ["id", "updated_at"]
|
read_only_fields = ["id", "updated_at"]
|
||||||
|
|
||||||
|
def validate_logo(self, value):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
max_size = settings.LOGO_MAX_UPLOAD_SIZE
|
||||||
|
if value.size > max_size:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Logo size exceeds the maximum allowed size of "
|
||||||
|
f"{max_size // (1024 * 1024)}MB."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import override_settings
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient, APITestCase
|
from rest_framework.test import APIClient, APITestCase
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
@@ -8,6 +12,11 @@ from .Mixins import LoginMixin
|
|||||||
|
|
||||||
URL = "/don_confiao/api/store_settings"
|
URL = "/don_confiao/api/store_settings"
|
||||||
|
|
||||||
|
PNG_1X1 = base64.b64decode(
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
|
||||||
|
"+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestStoreSettingsModel(APITestCase, LoginMixin):
|
class TestStoreSettingsModel(APITestCase, LoginMixin):
|
||||||
def test_singleton_returns_single_instance(self):
|
def test_singleton_returns_single_instance(self):
|
||||||
@@ -97,6 +106,7 @@ class TestStoreSettingsAPI(APITestCase, LoginMixin):
|
|||||||
self.assertIn("address", data)
|
self.assertIn("address", data)
|
||||||
self.assertIn("latitude", data)
|
self.assertIn("latitude", data)
|
||||||
self.assertIn("longitude", data)
|
self.assertIn("longitude", data)
|
||||||
|
self.assertIn("logo", data)
|
||||||
self.assertIn("updated_at", data)
|
self.assertIn("updated_at", data)
|
||||||
|
|
||||||
def test_patch_updates_settings(self):
|
def test_patch_updates_settings(self):
|
||||||
@@ -155,3 +165,82 @@ class TestStoreSettingsAPI(APITestCase, LoginMixin):
|
|||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_patch_upload_logo(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": logo},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("/media/logos/", data["logo"])
|
||||||
|
self.assertTrue(data["logo"].startswith("http"))
|
||||||
|
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertIsNotNone(settings.logo)
|
||||||
|
self.assertTrue(settings.logo.name.startswith("logos/logo"))
|
||||||
|
|
||||||
|
def test_get_returns_absolute_logo_url(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
self.client.patch(self.url, {"logo": logo}, format="multipart")
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data["logo"].startswith("http"))
|
||||||
|
|
||||||
|
def test_patch_logo_non_image(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
fake = SimpleUploadedFile("logo.txt", b"not an image", content_type="text/plain")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": fake},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@override_settings(LOGO_MAX_UPLOAD_SIZE=10)
|
||||||
|
def test_patch_logo_too_large(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": logo},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertFalse(settings.logo)
|
||||||
|
|
||||||
|
def test_patch_clear_logo(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
self.client.patch(self.url, {"logo": logo}, format="multipart")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"logo": None},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIsNone(data["logo"])
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertFalse(settings.logo)
|
||||||
|
|
||||||
|
def test_patch_upload_logo_keeps_other_fields(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
logo = SimpleUploadedFile("logo.png", PNG_1X1, content_type="image/png")
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{
|
||||||
|
"address": "Avenida Siempre Viva 742",
|
||||||
|
"logo": logo,
|
||||||
|
},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertEqual(settings.address, "Avenida Siempre Viva 742")
|
||||||
|
self.assertIsNotNone(settings.logo)
|
||||||
|
|||||||
Reference in New Issue
Block a user