#50 feat: add StoreSettings model and API for store address and map coordinates
This commit is contained in:
@@ -10,6 +10,12 @@ from .models.sales import (
|
|||||||
)
|
)
|
||||||
from .models.products import Product, ProductCategory
|
from .models.products import Product, ProductCategory
|
||||||
from .models.payments import ReconciliationJar
|
from .models.payments import ReconciliationJar
|
||||||
|
from .models.store_settings import StoreSettings
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(StoreSettings)
|
||||||
|
class StoreSettingsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("id", "address", "latitude", "longitude", "updated_at")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Customer)
|
@admin.register(Customer)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from .payments import (
|
|||||||
Pagination,
|
Pagination,
|
||||||
)
|
)
|
||||||
from .admin import AdminCodeValidateView
|
from .admin import AdminCodeValidateView
|
||||||
|
from .store_settings import StoreSettingsView
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Catalogue Images
|
# Catalogue Images
|
||||||
@@ -44,4 +45,6 @@ __all__ = [
|
|||||||
"Pagination",
|
"Pagination",
|
||||||
# Admin
|
# Admin
|
||||||
"AdminCodeValidateView",
|
"AdminCodeValidateView",
|
||||||
|
# Store Settings
|
||||||
|
"StoreSettingsView",
|
||||||
]
|
]
|
||||||
|
|||||||
28
tienda_ilusion/don_confiao/api/store_settings.py
Normal file
28
tienda_ilusion/don_confiao/api/store_settings.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from ..models.store_settings import StoreSettings
|
||||||
|
from ..permissions import IsAdministrator
|
||||||
|
from ..serializers.store_settings import StoreSettingsSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class StoreSettingsView(APIView):
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.request.method == "GET":
|
||||||
|
return [AllowAny()]
|
||||||
|
return [IsAuthenticated(), IsAdministrator()]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
settings = StoreSettings.get_singleton()
|
||||||
|
return Response(StoreSettingsSerializer(settings).data)
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
settings = StoreSettings.get_singleton()
|
||||||
|
serializer = StoreSettingsSerializer(
|
||||||
|
settings, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
27
tienda_ilusion/don_confiao/migrations/0050_storesettings.py
Normal file
27
tienda_ilusion/don_confiao/migrations/0050_storesettings.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-08 17:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0049_catalogueimage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='StoreSettings',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('address', models.TextField()),
|
||||||
|
('latitude', models.FloatField(blank=True, null=True)),
|
||||||
|
('longitude', models.FloatField(blank=True, null=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Configuración de la tienda',
|
||||||
|
'verbose_name_plural': 'Configuración de la tienda',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .store_settings import StoreSettings
|
||||||
|
|
||||||
|
__all__ = ["StoreSettings"]
|
||||||
|
|||||||
20
tienda_ilusion/don_confiao/models/store_settings.py
Normal file
20
tienda_ilusion/don_confiao/models/store_settings.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class StoreSettings(models.Model):
|
||||||
|
address = models.TextField()
|
||||||
|
latitude = models.FloatField(null=True, blank=True)
|
||||||
|
longitude = models.FloatField(null=True, blank=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Configuración de la tienda"
|
||||||
|
verbose_name_plural = "Configuración de la tienda"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"StoreSettings (id={self.id})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_singleton(cls):
|
||||||
|
instance, _ = cls.objects.get_or_create(pk=1)
|
||||||
|
return instance
|
||||||
@@ -16,6 +16,7 @@ from .payments import (
|
|||||||
ReconciliationJarSerializer,
|
ReconciliationJarSerializer,
|
||||||
PaymentMethodSerializer,
|
PaymentMethodSerializer,
|
||||||
)
|
)
|
||||||
|
from .store_settings import StoreSettingsSerializer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Catalogue Images
|
# Catalogue Images
|
||||||
@@ -39,4 +40,6 @@ __all__ = [
|
|||||||
# Payments
|
# Payments
|
||||||
"ReconciliationJarSerializer",
|
"ReconciliationJarSerializer",
|
||||||
"PaymentMethodSerializer",
|
"PaymentMethodSerializer",
|
||||||
|
# Store Settings
|
||||||
|
"StoreSettingsSerializer",
|
||||||
]
|
]
|
||||||
|
|||||||
10
tienda_ilusion/don_confiao/serializers/store_settings.py
Normal file
10
tienda_ilusion/don_confiao/serializers/store_settings.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from ..models.store_settings import StoreSettings
|
||||||
|
|
||||||
|
|
||||||
|
class StoreSettingsSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = StoreSettings
|
||||||
|
fields = ["id", "address", "latitude", "longitude", "updated_at"]
|
||||||
|
read_only_fields = ["id", "updated_at"]
|
||||||
157
tienda_ilusion/don_confiao/tests/test_store_settings.py
Normal file
157
tienda_ilusion/don_confiao/tests/test_store_settings.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APIClient, APITestCase
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
|
||||||
|
from ..models.store_settings import StoreSettings
|
||||||
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
|
URL = "/don_confiao/api/store_settings"
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoreSettingsModel(APITestCase, LoginMixin):
|
||||||
|
def test_singleton_returns_single_instance(self):
|
||||||
|
first = StoreSettings.get_singleton()
|
||||||
|
second = StoreSettings.get_singleton()
|
||||||
|
self.assertEqual(first.pk, second.pk)
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_singleton_created_without_address(self):
|
||||||
|
instance = StoreSettings.get_singleton()
|
||||||
|
self.assertEqual(instance.address, "")
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
instance = StoreSettings.get_singleton()
|
||||||
|
self.assertEqual(str(instance), f"StoreSettings (id={instance.id})")
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoreSettingsAPIPermissions(APITestCase, LoginMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.url = URL
|
||||||
|
|
||||||
|
def test_get_unauthenticated(self):
|
||||||
|
client = APIClient()
|
||||||
|
response = client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_patch_unauthenticated(self):
|
||||||
|
client = APIClient()
|
||||||
|
response = client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Calle falsa 123"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
def test_patch_non_admin(self):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username="regularuser",
|
||||||
|
email="regular@example.com",
|
||||||
|
password="regularpass",
|
||||||
|
)
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
client = APIClient()
|
||||||
|
client.credentials(
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||||
|
)
|
||||||
|
response = client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Calle falsa 123"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_patch_publico(self):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username="publico",
|
||||||
|
email="publico@example.com",
|
||||||
|
password="publicopass",
|
||||||
|
)
|
||||||
|
user.profile.user_type = "publico"
|
||||||
|
user.profile.save()
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
client = APIClient()
|
||||||
|
client.credentials(
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}"
|
||||||
|
)
|
||||||
|
response = client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Calle falsa 123"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoreSettingsAPI(APITestCase, LoginMixin):
|
||||||
|
def setUp(self):
|
||||||
|
self.login()
|
||||||
|
self.url = URL
|
||||||
|
|
||||||
|
def test_get_creates_singleton(self):
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 0)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 1)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("id", data)
|
||||||
|
self.assertIn("address", data)
|
||||||
|
self.assertIn("latitude", data)
|
||||||
|
self.assertIn("longitude", data)
|
||||||
|
self.assertIn("updated_at", data)
|
||||||
|
|
||||||
|
def test_patch_updates_settings(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{
|
||||||
|
"address": "Carrera 5 # 10-20, Bogotá",
|
||||||
|
"latitude": 4.6097,
|
||||||
|
"longitude": -74.0817,
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(data["address"], "Carrera 5 # 10-20, Bogotá")
|
||||||
|
self.assertEqual(data["latitude"], 4.6097)
|
||||||
|
self.assertEqual(data["longitude"], -74.0817)
|
||||||
|
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertEqual(settings.address, "Carrera 5 # 10-20, Bogotá")
|
||||||
|
self.assertEqual(settings.latitude, 4.6097)
|
||||||
|
self.assertEqual(settings.longitude, -74.0817)
|
||||||
|
|
||||||
|
def test_patch_partial(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Solo dirección"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
settings = StoreSettings.objects.get(pk=1)
|
||||||
|
self.assertEqual(settings.address, "Solo dirección")
|
||||||
|
self.assertIsNone(settings.latitude)
|
||||||
|
|
||||||
|
def test_patch_reuses_singleton(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Primera"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"address": "Segunda"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(StoreSettings.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_patch_invalid_coordinates(self):
|
||||||
|
self.client.get(self.url)
|
||||||
|
response = self.client.patch(
|
||||||
|
self.url,
|
||||||
|
{"latitude": "not-a-number"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -26,6 +26,8 @@ from .api import (
|
|||||||
SalesForReconciliationView,
|
SalesForReconciliationView,
|
||||||
# Admin
|
# Admin
|
||||||
AdminCodeValidateView,
|
AdminCodeValidateView,
|
||||||
|
# Store Settings
|
||||||
|
StoreSettingsView,
|
||||||
)
|
)
|
||||||
|
|
||||||
app_name = "don_confiao"
|
app_name = "don_confiao"
|
||||||
@@ -95,4 +97,5 @@ urlpatterns = [
|
|||||||
AdminCodeValidateView.as_view(),
|
AdminCodeValidateView.as_view(),
|
||||||
),
|
),
|
||||||
path("api/sales/for_tryton", SalesForTrytonView.as_view()),
|
path("api/sales/for_tryton", SalesForTrytonView.as_view()),
|
||||||
|
path("api/store_settings", StoreSettingsView.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user