feat(#49): add geolocation (latitude/longitude) to municipalities
- Municipality model with optional latitude/longitude decimal fields - Municipality and brief serializers expose lat/lng (also in product_provenance of public/authenticated summaries for the Leaflet map) - seed_geography endpoint reads lat/lng and updates existing municipalities on reseed (idempotent) - TDD tests for model, serializers, summaries and seed
This commit is contained in:
@@ -92,7 +92,7 @@ class SeedGeographyView(APIView):
|
|||||||
if created:
|
if created:
|
||||||
departments_created += 1
|
departments_created += 1
|
||||||
|
|
||||||
_, created = Municipality.objects.get_or_create(
|
municipality, created = Municipality.objects.get_or_create(
|
||||||
name=municipality_data["name"],
|
name=municipality_data["name"],
|
||||||
department=department,
|
department=department,
|
||||||
defaults={"country": country},
|
defaults={"country": country},
|
||||||
@@ -100,6 +100,16 @@ class SeedGeographyView(APIView):
|
|||||||
if created:
|
if created:
|
||||||
municipalities_created += 1
|
municipalities_created += 1
|
||||||
|
|
||||||
|
latitude = municipality_data.get("latitude")
|
||||||
|
longitude = municipality_data.get("longitude")
|
||||||
|
if latitude is not None or longitude is not None:
|
||||||
|
municipality.country = country
|
||||||
|
if latitude is not None:
|
||||||
|
municipality.latitude = latitude
|
||||||
|
if longitude is not None:
|
||||||
|
municipality.longitude = longitude
|
||||||
|
municipality.save()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"country": country.name,
|
"country": country.name,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-08-16 08:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('don_confiao', '0053_country_organization_department_municipality_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='municipality',
|
||||||
|
name='latitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='municipality',
|
||||||
|
name='longitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=7, max_digits=10, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -39,6 +39,12 @@ class Municipality(models.Model):
|
|||||||
country = models.ForeignKey(
|
country = models.ForeignKey(
|
||||||
Country, on_delete=models.PROTECT, related_name="municipalities"
|
Country, on_delete=models.PROTECT, related_name="municipalities"
|
||||||
)
|
)
|
||||||
|
latitude = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=7, null=True, blank=True
|
||||||
|
)
|
||||||
|
longitude = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=7, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Municipality"
|
verbose_name = "Municipality"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DepartmentBriefSerializer(serializers.ModelSerializer):
|
|||||||
class MunicipalityBriefSerializer(serializers.ModelSerializer):
|
class MunicipalityBriefSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Municipality
|
model = Municipality
|
||||||
fields = ["id", "name"]
|
fields = ["id", "name", "latitude", "longitude"]
|
||||||
|
|
||||||
|
|
||||||
class DepartmentSerializer(serializers.ModelSerializer):
|
class DepartmentSerializer(serializers.ModelSerializer):
|
||||||
@@ -43,4 +43,13 @@ class MunicipalitySerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Municipality
|
model = Municipality
|
||||||
fields = ["id", "name", "department", "department_detail", "country", "country_detail"]
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"department",
|
||||||
|
"department_detail",
|
||||||
|
"country",
|
||||||
|
"country_detail",
|
||||||
|
"latitude",
|
||||||
|
"longitude",
|
||||||
|
]
|
||||||
|
|||||||
@@ -68,6 +68,26 @@ class TestMunicipalityModel(TestCase):
|
|||||||
self.assertEqual(municipality.department.country.name, "Colombia")
|
self.assertEqual(municipality.department.country.name, "Colombia")
|
||||||
self.assertEqual(str(municipality), "La Mesa")
|
self.assertEqual(str(municipality), "La Mesa")
|
||||||
|
|
||||||
|
def test_municipality_geolocation_fields(self):
|
||||||
|
municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
latitude=4.63092,
|
||||||
|
longitude=-74.39152,
|
||||||
|
)
|
||||||
|
self.assertEqual(municipality.latitude, 4.63092)
|
||||||
|
self.assertEqual(municipality.longitude, -74.39152)
|
||||||
|
|
||||||
|
def test_municipality_geolocation_optional(self):
|
||||||
|
municipality = Municipality.objects.create(
|
||||||
|
name="La Mesa",
|
||||||
|
department=self.department,
|
||||||
|
country=self.country,
|
||||||
|
)
|
||||||
|
self.assertIsNone(municipality.latitude)
|
||||||
|
self.assertIsNone(municipality.longitude)
|
||||||
|
|
||||||
def test_municipality_requires_department_and_country(self):
|
def test_municipality_requires_department_and_country(self):
|
||||||
with self.assertRaises(IntegrityError):
|
with self.assertRaises(IntegrityError):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|||||||
@@ -241,6 +241,27 @@ class TestProvenanceCRUD(APITestCase, LoginMixin):
|
|||||||
self.assertEqual(data["country_detail"]["name"], "Colombia")
|
self.assertEqual(data["country_detail"]["name"], "Colombia")
|
||||||
self.assertEqual(data["country_detail"]["code"], "CO")
|
self.assertEqual(data["country_detail"]["code"], "CO")
|
||||||
|
|
||||||
|
def test_municipality_serializer_includes_geolocation(self):
|
||||||
|
self.municipality.latitude = 4.63092
|
||||||
|
self.municipality.longitude = -74.39152
|
||||||
|
self.municipality.save()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/api/municipalities/{self.municipality.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertEqual(float(data["latitude"]), 4.63092)
|
||||||
|
self.assertEqual(float(data["longitude"]), -74.39152)
|
||||||
|
|
||||||
|
def test_municipality_serializer_geolocation_optional(self):
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/api/municipalities/{self.municipality.id}/"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIsNone(data["latitude"])
|
||||||
|
self.assertIsNone(data["longitude"])
|
||||||
|
|
||||||
def test_link_product_to_suppliers_via_product_api(self):
|
def test_link_product_to_suppliers_via_product_api(self):
|
||||||
product = Product.objects.create(name="Panela", price=5000)
|
product = Product.objects.create(name="Panela", price=5000)
|
||||||
supplier2 = Supplier.objects.create(name="Proveedor 2")
|
supplier2 = Supplier.objects.create(name="Proveedor 2")
|
||||||
@@ -315,6 +336,37 @@ class TestSeedGeography(APITestCase, LoginMixin):
|
|||||||
self.assertEqual(Department.objects.count(), 1)
|
self.assertEqual(Department.objects.count(), 1)
|
||||||
self.assertEqual(Municipality.objects.count(), 2)
|
self.assertEqual(Municipality.objects.count(), 2)
|
||||||
|
|
||||||
|
def test_seed_creates_municipality_with_geolocation(self):
|
||||||
|
self.login()
|
||||||
|
payload = self._payload()
|
||||||
|
payload["municipalities"][0] = {
|
||||||
|
"name": "La Mesa",
|
||||||
|
"department": "Cundinamarca",
|
||||||
|
"latitude": 4.63092,
|
||||||
|
"longitude": -74.39152,
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, payload, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
municipality = Municipality.objects.get(name="La Mesa")
|
||||||
|
self.assertEqual(float(municipality.latitude), 4.63092)
|
||||||
|
self.assertEqual(float(municipality.longitude), -74.39152)
|
||||||
|
|
||||||
|
def test_seed_updates_geolocation_on_reseed(self):
|
||||||
|
self.login()
|
||||||
|
self.client.post(self.url, self._payload(), format="json")
|
||||||
|
payload = self._payload()
|
||||||
|
payload["municipalities"][0] = {
|
||||||
|
"name": "La Mesa",
|
||||||
|
"department": "Cundinamarca",
|
||||||
|
"latitude": 4.63092,
|
||||||
|
"longitude": -74.39152,
|
||||||
|
}
|
||||||
|
response = self.client.post(self.url, payload, format="json")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
municipality = Municipality.objects.get(name="La Mesa")
|
||||||
|
self.assertEqual(float(municipality.latitude), 4.63092)
|
||||||
|
self.assertEqual(float(municipality.longitude), -74.39152)
|
||||||
|
|
||||||
def test_seed_requires_country_name(self):
|
def test_seed_requires_country_name(self):
|
||||||
self.login()
|
self.login()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
|
|||||||
@@ -75,6 +75,23 @@ class TestProvenanceInSummaries(APITestCase, LoginMixin):
|
|||||||
self.assertEqual(supplier["department"]["name"], "Cundinamarca")
|
self.assertEqual(supplier["department"]["name"], "Cundinamarca")
|
||||||
self.assertEqual(supplier["country"]["name"], "Colombia")
|
self.assertEqual(supplier["country"]["name"], "Colombia")
|
||||||
|
|
||||||
|
def test_public_summary_municipality_includes_geolocation(self):
|
||||||
|
self.municipality.latitude = 4.63092
|
||||||
|
self.municipality.longitude = -74.39152
|
||||||
|
self.municipality.save()
|
||||||
|
sale = self._create_sale()
|
||||||
|
response = self.client.get(
|
||||||
|
f"/don_confiao/resumen_publico/{sale.code}"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
supplier = response.json()["product_provenance"][0]["suppliers"][0]
|
||||||
|
self.assertEqual(
|
||||||
|
float(supplier["municipality"]["latitude"]), 4.63092
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
float(supplier["municipality"]["longitude"]), -74.39152
|
||||||
|
)
|
||||||
|
|
||||||
def test_public_catalog_summary_includes_product_provenance(self):
|
def test_public_catalog_summary_includes_product_provenance(self):
|
||||||
catalog_sale = self._create_catalog_sale()
|
catalog_sale = self._create_catalog_sale()
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
|||||||
Reference in New Issue
Block a user