From 0c1550ba5cb49fb75cf37cb49cfe4349d9c1cb39 Mon Sep 17 00:00:00 2001 From: Irwan Cahyono Date: Fri, 27 Feb 2026 12:45:23 +0700 Subject: [PATCH] update pencarian lokasi dan init migration --- .../location/migrations/0001_initial.py | 66 ++++++++++++++++ microsite_api/location/views.py | 79 ++++++++++++++++++- microsite_api/microsite_api/settings.py | 14 ++++ 3 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 microsite_api/location/migrations/0001_initial.py diff --git a/microsite_api/location/migrations/0001_initial.py b/microsite_api/location/migrations/0001_initial.py new file mode 100644 index 0000000..134b2f7 --- /dev/null +++ b/microsite_api/location/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 6.0.2 on 2026-02-26 02:34 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Province', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('restored_at', models.DateTimeField(blank=True, null=True)), + ('transaction_id', models.UUIDField(blank=True, null=True)), + ('code', models.CharField(max_length=2, unique=True)), + ('name', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RegencyCity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('restored_at', models.DateTimeField(blank=True, null=True)), + ('transaction_id', models.UUIDField(blank=True, null=True)), + ('code', models.CharField(max_length=4, unique=True)), + ('name', models.CharField(max_length=255)), + ('province', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.province')), + ], + options={ + 'unique_together': {('code', 'province')}, + }, + ), + migrations.CreateModel( + name='Location', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('restored_at', models.DateTimeField(blank=True, null=True)), + ('transaction_id', models.UUIDField(blank=True, null=True)), + ('name', models.CharField(max_length=255)), + ('path', models.TextField(blank=True, null=True)), + ('totem_latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('totem_longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('totem_code', models.CharField(blank=True, max_length=50, null=True)), + ('active', models.BooleanField(default=True)), + ('province', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.province')), + ('regency_city', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.regencycity')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/microsite_api/location/views.py b/microsite_api/location/views.py index ee8f26b..abbccff 100644 --- a/microsite_api/location/views.py +++ b/microsite_api/location/views.py @@ -1,9 +1,16 @@ +import math + from django.shortcuts import render +from django.db.models import F, ExpressionWrapper, FloatField +from django.db.models.functions import Sqrt, Power, Sin, Cos, Radians, ATan2 from rest_framework import generics, filters, permissions from django_filters.rest_framework import DjangoFilterBackend +import logging from location import models, serializers +logger = logging.getLogger(__name__) + # PROVINCES class ProvinceList(generics.ListCreateAPIView): queryset = models.Province.objects.all() @@ -72,10 +79,63 @@ class LocationList(generics.ListCreateAPIView): queryset = models.Location.objects.all() serializer_class = serializers.LocationSerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] - search_fields = ['name', 'path'] - filterset_fields = ['name', 'path', 'totem_code', 'province', 'regency_city', 'active'] + search_fields = ['name'] + filterset_fields = ['name', 'totem_code', 'province', 'regency_city', 'active'] ordering_fields = '__all__' + def get_queryset(self): + queryset = models.Location.objects.all() + + lat = self.request.query_params.get('lat') + lng = self.request.query_params.get('lng') + radius = self.request.query_params.get('radius') + + if lat and lng: + lat = float(lat) + lng = float(lng) + radius = float(radius) if radius else 50 # default 50m + + min_lat, max_lat, min_lng, max_lng = get_bounding_box(lat, lng, radius) + + logger.info(f"Filtering locations within radius {radius}m of point ({lat}, {lng})") + + # Step 1: bounding box filter + queryset = queryset.filter( + totem_latitude__range=(min_lat, max_lat), + totem_longitude__range=(min_lng, max_lng) + ) + + # Step 2: haversine formula + distance_expr = ExpressionWrapper( + 6371000 * 2 * ATan2( + Sqrt( + Power(Sin((Radians(F('totem_latitude')) - Radians(lat)) / 2), 2) + + Cos(Radians(lat)) * + Cos(Radians(F('totem_latitude'))) * + Power(Sin((Radians(F('totem_longitude')) - Radians(lng)) / 2), 2) + ), + Sqrt(1 - ( + Power(Sin((Radians(F('totem_latitude')) - Radians(lat)) / 2), 2) + + Cos(Radians(lat)) * + Cos(Radians(F('totem_latitude'))) * + Power(Sin((Radians(F('totem_longitude')) - Radians(lng)) / 2), 2) + )) + ), + output_field=FloatField() + ) + + queryset = queryset.annotate( + distance=distance_expr + ).filter( + distance__lte=radius + ).order_by('distance') + + logger.info(f"Queryset after applying bounding box and distance filter: {queryset.query}") + logger.info(f"Applied bounding box filter: lat between {min_lat} and {max_lat}, lng between {min_lng} and {max_lng}") + logger.info(f"Found {queryset.count()} locations within radius {radius}m of point ({lat}, {lng})") + + return queryset + def get_serializer_class(self): serializer_class = self.serializer_class @@ -104,4 +164,17 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView): def get_permissions(self): if self.request.method == 'PUT' or self.request.method == 'DELETE': return [permissions.IsAdminUser()] - return [permissions.IsAuthenticated()] \ No newline at end of file + return [permissions.IsAuthenticated()] + +def get_bounding_box(lat, lng, radius_in_meters): + earth_radius = 6371000 # meter + + lat_delta = (radius_in_meters / earth_radius) * (180 / math.pi) + lng_delta = (radius_in_meters / earth_radius) * (180 / math.pi) / math.cos(lat * math.pi/180) + + return ( + lat - lat_delta, + lat + lat_delta, + lng - lng_delta, + lng + lng_delta + ) \ No newline at end of file diff --git a/microsite_api/microsite_api/settings.py b/microsite_api/microsite_api/settings.py index 03e2012..9d047d0 100644 --- a/microsite_api/microsite_api/settings.py +++ b/microsite_api/microsite_api/settings.py @@ -133,6 +133,20 @@ STATIC_URL = 'static/' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "root": { + "handlers": ["console"], + "level": "INFO", # penting! + }, +} + REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'microsite_api.pagination.CustomPagination',