mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 12:59: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_filter = ( 'user_id', 'is_public')
|
||||
search_fields = ('name',)
|
||||
readonly_fields = ('city', 'region', 'country')
|
||||
|
||||
def get_category(self, obj):
|
||||
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.forms import ValidationError
|
||||
from django_resized import ResizedImageField
|
||||
from worldtravel.models import City, Country, Region
|
||||
from adventures.geocoding import reverse_geocode
|
||||
|
||||
def validate_file_extension(value):
|
||||
import os
|
||||
|
@ -525,9 +527,16 @@ class Adventure(models.Model):
|
|||
rating = models.FloatField(blank=True, null=True)
|
||||
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||
is_public = models.BooleanField(default=False)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
@ -568,6 +577,22 @@ class Adventure(models.Model):
|
|||
)
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -88,7 +88,7 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
fields = [
|
||||
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
|
||||
'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']
|
||||
|
||||
|
|
|
@ -6,77 +6,26 @@ from worldtravel.models import Region, City, VisitedRegion, VisitedCity
|
|||
from adventures.models import Adventure
|
||||
from adventures.serializers import AdventureSerializer
|
||||
import requests
|
||||
from adventures.geocoding import reverse_geocode
|
||||
|
||||
class ReverseGeocodeViewSet(viewsets.ViewSet):
|
||||
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'])
|
||||
def reverse_geocode(self, request):
|
||||
lat = request.query_params.get('lat', '')
|
||||
lon = request.query_params.get('lon', '')
|
||||
url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}"
|
||||
headers = {'User-Agent': 'AdventureLog Server'}
|
||||
response = requests.get(url, headers=headers)
|
||||
if not lat or not lon:
|
||||
return Response({"error": "Latitude and longitude are required"}, status=400)
|
||||
try:
|
||||
data = response.json()
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
return Response({"error": "Invalid response from geocoding service"}, status=400)
|
||||
return Response(self.extractIsoCode(data))
|
||||
lat = float(lat)
|
||||
lon = float(lon)
|
||||
except ValueError:
|
||||
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'])
|
||||
def mark_visited_region(self, request):
|
||||
|
|
|
@ -313,4 +313,4 @@ LOGGING = {
|
|||
# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app')
|
||||
|
||||
# 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
|
||||
django-allauth==0.63.3
|
||||
drf-yasg==1.21.4
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue