update pencarian lokasi dan init migration
This commit is contained in:
parent
82e2a23972
commit
0c1550ba5c
66
microsite_api/location/migrations/0001_initial.py
Normal file
66
microsite_api/location/migrations/0001_initial.py
Normal file
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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()]
|
||||
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
|
||||
)
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user