mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-22 14:29:36 +02:00
Add geocoding functionality and enhance Adventure model with location fields
This commit is contained in:
parent
14e71626f6
commit
d52e302e9b
8 changed files with 141 additions and 65 deletions
|
@ -12,6 +12,7 @@ class AdventureAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
|
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
|
||||||
list_filter = ( 'user_id', 'is_public')
|
list_filter = ( 'user_id', 'is_public')
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
|
readonly_fields = ('city', 'region', 'country')
|
||||||
|
|
||||||
def get_category(self, obj):
|
def get_category(self, obj):
|
||||||
if obj.category and obj.category.display_name and obj.category.icon:
|
if obj.category and obj.category.display_name and obj.category.icon:
|
||||||
|
|
71
backend/server/adventures/geocoding.py
Normal file
71
backend/server/adventures/geocoding.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import requests
|
||||||
|
from worldtravel.models import Region, City, VisitedRegion, VisitedCity
|
||||||
|
|
||||||
|
def extractIsoCode(user, data):
|
||||||
|
"""
|
||||||
|
Extract the ISO code from the response data.
|
||||||
|
Returns a dictionary containing the region name, country name, and ISO code if found.
|
||||||
|
"""
|
||||||
|
iso_code = None
|
||||||
|
town_city_or_county = None
|
||||||
|
display_name = None
|
||||||
|
country_code = None
|
||||||
|
city = None
|
||||||
|
visited_city = None
|
||||||
|
location_name = None
|
||||||
|
|
||||||
|
# town = None
|
||||||
|
# city = None
|
||||||
|
# county = None
|
||||||
|
|
||||||
|
if 'name' in data.keys():
|
||||||
|
location_name = data['name']
|
||||||
|
|
||||||
|
if 'address' in data.keys():
|
||||||
|
keys = data['address'].keys()
|
||||||
|
for key in keys:
|
||||||
|
if key.find("ISO") != -1:
|
||||||
|
iso_code = data['address'][key]
|
||||||
|
if 'town' in keys:
|
||||||
|
town_city_or_county = data['address']['town']
|
||||||
|
if 'county' in keys:
|
||||||
|
town_city_or_county = data['address']['county']
|
||||||
|
if 'city' in keys:
|
||||||
|
town_city_or_county = data['address']['city']
|
||||||
|
if not iso_code:
|
||||||
|
return {"error": "No region found"}
|
||||||
|
|
||||||
|
region = Region.objects.filter(id=iso_code).first()
|
||||||
|
visited_region = VisitedRegion.objects.filter(region=region, user_id=user).first()
|
||||||
|
|
||||||
|
region_visited = False
|
||||||
|
city_visited = False
|
||||||
|
country_code = iso_code[:2]
|
||||||
|
|
||||||
|
if region:
|
||||||
|
if town_city_or_county:
|
||||||
|
display_name = f"{town_city_or_county}, {region.name}, {country_code}"
|
||||||
|
city = City.objects.filter(name__contains=town_city_or_county, region=region).first()
|
||||||
|
visited_city = VisitedCity.objects.filter(city=city, user_id=user).first()
|
||||||
|
|
||||||
|
if visited_region:
|
||||||
|
region_visited = True
|
||||||
|
if visited_city:
|
||||||
|
city_visited = True
|
||||||
|
if region:
|
||||||
|
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "country_id": region.country.country_code, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name}
|
||||||
|
return {"error": "No region found"}
|
||||||
|
|
||||||
|
def reverse_geocode(lat, lon, user):
|
||||||
|
"""
|
||||||
|
Reverse geocode the given latitude and longitude using Nominatim API.
|
||||||
|
Returns a dictionary containing the region name, country name, and ISO code if found.
|
||||||
|
"""
|
||||||
|
url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}"
|
||||||
|
headers = {'User-Agent': 'AdventureLog Server'}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except requests.exceptions.JSONDecodeError:
|
||||||
|
return {"error": "Invalid response from geocoding service"}
|
||||||
|
return extractIsoCode(user, data)
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 5.0.11 on 2025-05-22 22:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0028_lodging_timezone'),
|
||||||
|
('worldtravel', '0015_city_insert_id_country_insert_id_region_insert_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='city',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.city'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='country',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.country'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='region',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.region'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,6 +9,8 @@ from django.contrib.auth import get_user_model
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django_resized import ResizedImageField
|
from django_resized import ResizedImageField
|
||||||
|
from worldtravel.models import City, Country, Region
|
||||||
|
from adventures.geocoding import reverse_geocode
|
||||||
|
|
||||||
def validate_file_extension(value):
|
def validate_file_extension(value):
|
||||||
import os
|
import os
|
||||||
|
@ -525,9 +527,16 @@ class Adventure(models.Model):
|
||||||
rating = models.FloatField(blank=True, null=True)
|
rating = models.FloatField(blank=True, null=True)
|
||||||
link = models.URLField(blank=True, null=True, max_length=2083)
|
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||||
is_public = models.BooleanField(default=False)
|
is_public = models.BooleanField(default=False)
|
||||||
|
|
||||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||||
|
|
||||||
|
city = models.ForeignKey(City, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
region = models.ForeignKey(Region, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
|
||||||
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
|
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
@ -568,6 +577,22 @@ class Adventure(models.Model):
|
||||||
)
|
)
|
||||||
self.category = category
|
self.category = category
|
||||||
|
|
||||||
|
if self.latitude and self.longitude:
|
||||||
|
reverse_geocode_result = reverse_geocode(self.latitude, self.longitude, self.user_id)
|
||||||
|
print(reverse_geocode_result)
|
||||||
|
if 'region_id' in reverse_geocode_result:
|
||||||
|
region = Region.objects.filter(id=reverse_geocode_result['region_id']).first()
|
||||||
|
if region:
|
||||||
|
self.region = region
|
||||||
|
if 'city_id' in reverse_geocode_result:
|
||||||
|
city = City.objects.filter(id=reverse_geocode_result['city_id']).first()
|
||||||
|
if city:
|
||||||
|
self.city = city
|
||||||
|
if 'country_id' in reverse_geocode_result:
|
||||||
|
country = Country.objects.filter(country_code=reverse_geocode_result['country_id']).first()
|
||||||
|
if country:
|
||||||
|
self.country = country
|
||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -88,7 +88,7 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
|
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
|
||||||
'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude',
|
'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude',
|
||||||
'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user'
|
'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user', 'city', 'country', 'region'
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited', 'user']
|
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited', 'user']
|
||||||
|
|
||||||
|
|
|
@ -6,77 +6,26 @@ from worldtravel.models import Region, City, VisitedRegion, VisitedCity
|
||||||
from adventures.models import Adventure
|
from adventures.models import Adventure
|
||||||
from adventures.serializers import AdventureSerializer
|
from adventures.serializers import AdventureSerializer
|
||||||
import requests
|
import requests
|
||||||
|
from adventures.geocoding import reverse_geocode
|
||||||
|
|
||||||
class ReverseGeocodeViewSet(viewsets.ViewSet):
|
class ReverseGeocodeViewSet(viewsets.ViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def extractIsoCode(self, data):
|
|
||||||
"""
|
|
||||||
Extract the ISO code from the response data.
|
|
||||||
Returns a dictionary containing the region name, country name, and ISO code if found.
|
|
||||||
"""
|
|
||||||
iso_code = None
|
|
||||||
town_city_or_county = None
|
|
||||||
display_name = None
|
|
||||||
country_code = None
|
|
||||||
city = None
|
|
||||||
visited_city = None
|
|
||||||
location_name = None
|
|
||||||
|
|
||||||
# town = None
|
|
||||||
# city = None
|
|
||||||
# county = None
|
|
||||||
|
|
||||||
if 'name' in data.keys():
|
|
||||||
location_name = data['name']
|
|
||||||
|
|
||||||
if 'address' in data.keys():
|
|
||||||
keys = data['address'].keys()
|
|
||||||
for key in keys:
|
|
||||||
if key.find("ISO") != -1:
|
|
||||||
iso_code = data['address'][key]
|
|
||||||
if 'town' in keys:
|
|
||||||
town_city_or_county = data['address']['town']
|
|
||||||
if 'county' in keys:
|
|
||||||
town_city_or_county = data['address']['county']
|
|
||||||
if 'city' in keys:
|
|
||||||
town_city_or_county = data['address']['city']
|
|
||||||
if not iso_code:
|
|
||||||
return {"error": "No region found"}
|
|
||||||
|
|
||||||
region = Region.objects.filter(id=iso_code).first()
|
|
||||||
visited_region = VisitedRegion.objects.filter(region=region, user_id=self.request.user).first()
|
|
||||||
|
|
||||||
region_visited = False
|
|
||||||
city_visited = False
|
|
||||||
country_code = iso_code[:2]
|
|
||||||
|
|
||||||
if region:
|
|
||||||
if town_city_or_county:
|
|
||||||
display_name = f"{town_city_or_county}, {region.name}, {country_code}"
|
|
||||||
city = City.objects.filter(name__contains=town_city_or_county, region=region).first()
|
|
||||||
visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first()
|
|
||||||
|
|
||||||
if visited_region:
|
|
||||||
region_visited = True
|
|
||||||
if visited_city:
|
|
||||||
city_visited = True
|
|
||||||
if region:
|
|
||||||
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name}
|
|
||||||
return {"error": "No region found"}
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def reverse_geocode(self, request):
|
def reverse_geocode(self, request):
|
||||||
lat = request.query_params.get('lat', '')
|
lat = request.query_params.get('lat', '')
|
||||||
lon = request.query_params.get('lon', '')
|
lon = request.query_params.get('lon', '')
|
||||||
url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}"
|
if not lat or not lon:
|
||||||
headers = {'User-Agent': 'AdventureLog Server'}
|
return Response({"error": "Latitude and longitude are required"}, status=400)
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
lat = float(lat)
|
||||||
except requests.exceptions.JSONDecodeError:
|
lon = float(lon)
|
||||||
return Response({"error": "Invalid response from geocoding service"}, status=400)
|
except ValueError:
|
||||||
return Response(self.extractIsoCode(data))
|
return Response({"error": "Invalid latitude or longitude"}, status=400)
|
||||||
|
data = reverse_geocode(lat, lon, self.request.user)
|
||||||
|
if 'error' in data:
|
||||||
|
return Response(data, status=400)
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
@action(detail=False, methods=['post'])
|
||||||
def mark_visited_region(self, request):
|
def mark_visited_region(self, request):
|
||||||
|
|
|
@ -313,4 +313,4 @@ LOGGING = {
|
||||||
# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app')
|
# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app')
|
||||||
|
|
||||||
# https://github.com/dr5hn/countries-states-cities-database/tags
|
# https://github.com/dr5hn/countries-states-cities-database/tags
|
||||||
COUNTRY_REGION_JSON_VERSION = 'v2.5'
|
COUNTRY_REGION_JSON_VERSION = 'v2.6'
|
|
@ -1,4 +1,4 @@
|
||||||
Django==5.0.11
|
Django==5.2.1
|
||||||
djangorestframework>=3.15.2
|
djangorestframework>=3.15.2
|
||||||
django-allauth==0.63.3
|
django-allauth==0.63.3
|
||||||
drf-yasg==1.21.4
|
drf-yasg==1.21.4
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue