1
0
Fork 0
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:
Sean Morley 2025-05-22 20:05:13 -04:00
parent 14e71626f6
commit d52e302e9b
8 changed files with 141 additions and 65 deletions

View file

@ -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:

View 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)

View file

@ -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'),
),
]

View file

@ -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):

View file

@ -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']

View file

@ -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):

View file

@ -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'

View file

@ -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