mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-02 19:55:18 +02:00
Merge branch 'development' into bool-case-sensitivity
This commit is contained in:
commit
3d9f4545a1
48 changed files with 3186 additions and 1748 deletions
|
@ -8,10 +8,25 @@ from allauth.account.decorators import secure_admin_login
|
|||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
||||
|
||||
@admin.action(description="Trigger geocoding")
|
||||
def trigger_geocoding(modeladmin, request, queryset):
|
||||
count = 0
|
||||
for adventure in queryset:
|
||||
try:
|
||||
adventure.save() # Triggers geocoding logic in your model
|
||||
count += 1
|
||||
except Exception as e:
|
||||
modeladmin.message_user(request, f"Error geocoding {adventure}: {e}", level='error')
|
||||
modeladmin.message_user(request, f"Geocoding triggered for {count} adventures.", level='success')
|
||||
|
||||
|
||||
|
||||
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')
|
||||
actions = [trigger_geocoding]
|
||||
|
||||
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)
|
|
@ -29,4 +29,12 @@ class XSessionTokenMiddleware(MiddlewareMixin):
|
|||
class DisableCSRFForSessionTokenMiddleware(MiddlewareMixin):
|
||||
def process_request(self, request):
|
||||
if 'X-Session-Token' in request.headers:
|
||||
setattr(request, '_dont_enforce_csrf_checks', True)
|
||||
setattr(request, '_dont_enforce_csrf_checks', True)
|
||||
|
||||
class DisableCSRFForMobileLoginSignup(MiddlewareMixin):
|
||||
def process_request(self, request):
|
||||
is_mobile = request.headers.get('X-Is-Mobile', '').lower() == 'true'
|
||||
is_login_or_signup = request.path in ['/auth/browser/v1/auth/login', '/auth/browser/v1/auth/signup']
|
||||
if is_mobile and is_login_or_signup:
|
||||
setattr(request, '_dont_enforce_csrf_checks', True)
|
||||
|
18
backend/server/adventures/migrations/0026_visit_timezone.py
Normal file
18
backend/server/adventures/migrations/0026_visit_timezone.py
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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,9 @@ 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, VisitedCity, VisitedRegion
|
||||
from adventures.geocoding import reverse_geocode
|
||||
from django.utils import timezone
|
||||
|
||||
def validate_file_extension(value):
|
||||
import os
|
||||
|
@ -43,6 +46,426 @@ ADVENTURE_TYPES = [
|
|||
('other', 'Other')
|
||||
]
|
||||
|
||||
TIMEZONES = [
|
||||
"Africa/Abidjan",
|
||||
"Africa/Accra",
|
||||
"Africa/Addis_Ababa",
|
||||
"Africa/Algiers",
|
||||
"Africa/Asmera",
|
||||
"Africa/Bamako",
|
||||
"Africa/Bangui",
|
||||
"Africa/Banjul",
|
||||
"Africa/Bissau",
|
||||
"Africa/Blantyre",
|
||||
"Africa/Brazzaville",
|
||||
"Africa/Bujumbura",
|
||||
"Africa/Cairo",
|
||||
"Africa/Casablanca",
|
||||
"Africa/Ceuta",
|
||||
"Africa/Conakry",
|
||||
"Africa/Dakar",
|
||||
"Africa/Dar_es_Salaam",
|
||||
"Africa/Djibouti",
|
||||
"Africa/Douala",
|
||||
"Africa/El_Aaiun",
|
||||
"Africa/Freetown",
|
||||
"Africa/Gaborone",
|
||||
"Africa/Harare",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Juba",
|
||||
"Africa/Kampala",
|
||||
"Africa/Khartoum",
|
||||
"Africa/Kigali",
|
||||
"Africa/Kinshasa",
|
||||
"Africa/Lagos",
|
||||
"Africa/Libreville",
|
||||
"Africa/Lome",
|
||||
"Africa/Luanda",
|
||||
"Africa/Lubumbashi",
|
||||
"Africa/Lusaka",
|
||||
"Africa/Malabo",
|
||||
"Africa/Maputo",
|
||||
"Africa/Maseru",
|
||||
"Africa/Mbabane",
|
||||
"Africa/Mogadishu",
|
||||
"Africa/Monrovia",
|
||||
"Africa/Nairobi",
|
||||
"Africa/Ndjamena",
|
||||
"Africa/Niamey",
|
||||
"Africa/Nouakchott",
|
||||
"Africa/Ouagadougou",
|
||||
"Africa/Porto-Novo",
|
||||
"Africa/Sao_Tome",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Tunis",
|
||||
"Africa/Windhoek",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Anguilla",
|
||||
"America/Antigua",
|
||||
"America/Araguaina",
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Salta",
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/Tucuman",
|
||||
"America/Argentina/Ushuaia",
|
||||
"America/Aruba",
|
||||
"America/Asuncion",
|
||||
"America/Bahia",
|
||||
"America/Bahia_Banderas",
|
||||
"America/Barbados",
|
||||
"America/Belem",
|
||||
"America/Belize",
|
||||
"America/Blanc-Sablon",
|
||||
"America/Boa_Vista",
|
||||
"America/Bogota",
|
||||
"America/Boise",
|
||||
"America/Buenos_Aires",
|
||||
"America/Cambridge_Bay",
|
||||
"America/Campo_Grande",
|
||||
"America/Cancun",
|
||||
"America/Caracas",
|
||||
"America/Catamarca",
|
||||
"America/Cayenne",
|
||||
"America/Cayman",
|
||||
"America/Chicago",
|
||||
"America/Chihuahua",
|
||||
"America/Ciudad_Juarez",
|
||||
"America/Coral_Harbour",
|
||||
"America/Cordoba",
|
||||
"America/Costa_Rica",
|
||||
"America/Creston",
|
||||
"America/Cuiaba",
|
||||
"America/Curacao",
|
||||
"America/Danmarkshavn",
|
||||
"America/Dawson",
|
||||
"America/Dawson_Creek",
|
||||
"America/Denver",
|
||||
"America/Detroit",
|
||||
"America/Dominica",
|
||||
"America/Edmonton",
|
||||
"America/Eirunepe",
|
||||
"America/El_Salvador",
|
||||
"America/Fort_Nelson",
|
||||
"America/Fortaleza",
|
||||
"America/Glace_Bay",
|
||||
"America/Godthab",
|
||||
"America/Goose_Bay",
|
||||
"America/Grand_Turk",
|
||||
"America/Grenada",
|
||||
"America/Guadeloupe",
|
||||
"America/Guatemala",
|
||||
"America/Guayaquil",
|
||||
"America/Guyana",
|
||||
"America/Halifax",
|
||||
"America/Havana",
|
||||
"America/Hermosillo",
|
||||
"America/Indiana/Knox",
|
||||
"America/Indiana/Marengo",
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Tell_City",
|
||||
"America/Indiana/Vevay",
|
||||
"America/Indiana/Vincennes",
|
||||
"America/Indiana/Winamac",
|
||||
"America/Indianapolis",
|
||||
"America/Inuvik",
|
||||
"America/Iqaluit",
|
||||
"America/Jamaica",
|
||||
"America/Jujuy",
|
||||
"America/Juneau",
|
||||
"America/Kentucky/Monticello",
|
||||
"America/Kralendijk",
|
||||
"America/La_Paz",
|
||||
"America/Lima",
|
||||
"America/Los_Angeles",
|
||||
"America/Louisville",
|
||||
"America/Lower_Princes",
|
||||
"America/Maceio",
|
||||
"America/Managua",
|
||||
"America/Manaus",
|
||||
"America/Marigot",
|
||||
"America/Martinique",
|
||||
"America/Matamoros",
|
||||
"America/Mazatlan",
|
||||
"America/Mendoza",
|
||||
"America/Menominee",
|
||||
"America/Merida",
|
||||
"America/Metlakatla",
|
||||
"America/Mexico_City",
|
||||
"America/Miquelon",
|
||||
"America/Moncton",
|
||||
"America/Monterrey",
|
||||
"America/Montevideo",
|
||||
"America/Montserrat",
|
||||
"America/Nassau",
|
||||
"America/New_York",
|
||||
"America/Nome",
|
||||
"America/Noronha",
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/Ojinaga",
|
||||
"America/Panama",
|
||||
"America/Paramaribo",
|
||||
"America/Phoenix",
|
||||
"America/Port-au-Prince",
|
||||
"America/Port_of_Spain",
|
||||
"America/Porto_Velho",
|
||||
"America/Puerto_Rico",
|
||||
"America/Punta_Arenas",
|
||||
"America/Rankin_Inlet",
|
||||
"America/Recife",
|
||||
"America/Regina",
|
||||
"America/Resolute",
|
||||
"America/Rio_Branco",
|
||||
"America/Santarem",
|
||||
"America/Santiago",
|
||||
"America/Santo_Domingo",
|
||||
"America/Sao_Paulo",
|
||||
"America/Scoresbysund",
|
||||
"America/Sitka",
|
||||
"America/St_Barthelemy",
|
||||
"America/St_Johns",
|
||||
"America/St_Kitts",
|
||||
"America/St_Lucia",
|
||||
"America/St_Thomas",
|
||||
"America/St_Vincent",
|
||||
"America/Swift_Current",
|
||||
"America/Tegucigalpa",
|
||||
"America/Thule",
|
||||
"America/Tijuana",
|
||||
"America/Toronto",
|
||||
"America/Tortola",
|
||||
"America/Vancouver",
|
||||
"America/Whitehorse",
|
||||
"America/Winnipeg",
|
||||
"America/Yakutat",
|
||||
"Antarctica/Casey",
|
||||
"Antarctica/Davis",
|
||||
"Antarctica/DumontDUrville",
|
||||
"Antarctica/Macquarie",
|
||||
"Antarctica/Mawson",
|
||||
"Antarctica/McMurdo",
|
||||
"Antarctica/Palmer",
|
||||
"Antarctica/Rothera",
|
||||
"Antarctica/Syowa",
|
||||
"Antarctica/Troll",
|
||||
"Antarctica/Vostok",
|
||||
"Arctic/Longyearbyen",
|
||||
"Asia/Aden",
|
||||
"Asia/Almaty",
|
||||
"Asia/Amman",
|
||||
"Asia/Anadyr",
|
||||
"Asia/Aqtau",
|
||||
"Asia/Aqtobe",
|
||||
"Asia/Ashgabat",
|
||||
"Asia/Atyrau",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Bahrain",
|
||||
"Asia/Baku",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Barnaul",
|
||||
"Asia/Beirut",
|
||||
"Asia/Bishkek",
|
||||
"Asia/Brunei",
|
||||
"Asia/Calcutta",
|
||||
"Asia/Chita",
|
||||
"Asia/Colombo",
|
||||
"Asia/Damascus",
|
||||
"Asia/Dhaka",
|
||||
"Asia/Dili",
|
||||
"Asia/Dubai",
|
||||
"Asia/Dushanbe",
|
||||
"Asia/Famagusta",
|
||||
"Asia/Gaza",
|
||||
"Asia/Hebron",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Hovd",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Jayapura",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Kabul",
|
||||
"Asia/Kamchatka",
|
||||
"Asia/Karachi",
|
||||
"Asia/Katmandu",
|
||||
"Asia/Khandyga",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Kuching",
|
||||
"Asia/Kuwait",
|
||||
"Asia/Macau",
|
||||
"Asia/Magadan",
|
||||
"Asia/Makassar",
|
||||
"Asia/Manila",
|
||||
"Asia/Muscat",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Novokuznetsk",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Omsk",
|
||||
"Asia/Oral",
|
||||
"Asia/Phnom_Penh",
|
||||
"Asia/Pontianak",
|
||||
"Asia/Pyongyang",
|
||||
"Asia/Qatar",
|
||||
"Asia/Qostanay",
|
||||
"Asia/Qyzylorda",
|
||||
"Asia/Rangoon",
|
||||
"Asia/Riyadh",
|
||||
"Asia/Saigon",
|
||||
"Asia/Sakhalin",
|
||||
"Asia/Samarkand",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Srednekolymsk",
|
||||
"Asia/Taipei",
|
||||
"Asia/Tashkent",
|
||||
"Asia/Tbilisi",
|
||||
"Asia/Tehran",
|
||||
"Asia/Thimphu",
|
||||
"Asia/Tokyo",
|
||||
"Asia/Tomsk",
|
||||
"Asia/Ulaanbaatar",
|
||||
"Asia/Urumqi",
|
||||
"Asia/Ust-Nera",
|
||||
"Asia/Vientiane",
|
||||
"Asia/Vladivostok",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Yerevan",
|
||||
"Atlantic/Azores",
|
||||
"Atlantic/Bermuda",
|
||||
"Atlantic/Canary",
|
||||
"Atlantic/Cape_Verde",
|
||||
"Atlantic/Faeroe",
|
||||
"Atlantic/Madeira",
|
||||
"Atlantic/Reykjavik",
|
||||
"Atlantic/South_Georgia",
|
||||
"Atlantic/St_Helena",
|
||||
"Atlantic/Stanley",
|
||||
"Australia/Adelaide",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Broken_Hill",
|
||||
"Australia/Darwin",
|
||||
"Australia/Eucla",
|
||||
"Australia/Hobart",
|
||||
"Australia/Lindeman",
|
||||
"Australia/Lord_Howe",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Perth",
|
||||
"Australia/Sydney",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Andorra",
|
||||
"Europe/Astrakhan",
|
||||
"Europe/Athens",
|
||||
"Europe/Belgrade",
|
||||
"Europe/Berlin",
|
||||
"Europe/Bratislava",
|
||||
"Europe/Brussels",
|
||||
"Europe/Bucharest",
|
||||
"Europe/Budapest",
|
||||
"Europe/Busingen",
|
||||
"Europe/Chisinau",
|
||||
"Europe/Copenhagen",
|
||||
"Europe/Dublin",
|
||||
"Europe/Gibraltar",
|
||||
"Europe/Guernsey",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Isle_of_Man",
|
||||
"Europe/Istanbul",
|
||||
"Europe/Jersey",
|
||||
"Europe/Kaliningrad",
|
||||
"Europe/Kiev",
|
||||
"Europe/Kirov",
|
||||
"Europe/Lisbon",
|
||||
"Europe/Ljubljana",
|
||||
"Europe/London",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Madrid",
|
||||
"Europe/Malta",
|
||||
"Europe/Mariehamn",
|
||||
"Europe/Minsk",
|
||||
"Europe/Monaco",
|
||||
"Europe/Moscow",
|
||||
"Europe/Oslo",
|
||||
"Europe/Paris",
|
||||
"Europe/Podgorica",
|
||||
"Europe/Prague",
|
||||
"Europe/Riga",
|
||||
"Europe/Rome",
|
||||
"Europe/Samara",
|
||||
"Europe/San_Marino",
|
||||
"Europe/Sarajevo",
|
||||
"Europe/Saratov",
|
||||
"Europe/Simferopol",
|
||||
"Europe/Skopje",
|
||||
"Europe/Sofia",
|
||||
"Europe/Stockholm",
|
||||
"Europe/Tallinn",
|
||||
"Europe/Tirane",
|
||||
"Europe/Ulyanovsk",
|
||||
"Europe/Vaduz",
|
||||
"Europe/Vatican",
|
||||
"Europe/Vienna",
|
||||
"Europe/Vilnius",
|
||||
"Europe/Volgograd",
|
||||
"Europe/Warsaw",
|
||||
"Europe/Zagreb",
|
||||
"Europe/Zurich",
|
||||
"Indian/Antananarivo",
|
||||
"Indian/Chagos",
|
||||
"Indian/Christmas",
|
||||
"Indian/Cocos",
|
||||
"Indian/Comoro",
|
||||
"Indian/Kerguelen",
|
||||
"Indian/Mahe",
|
||||
"Indian/Maldives",
|
||||
"Indian/Mauritius",
|
||||
"Indian/Mayotte",
|
||||
"Indian/Reunion",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
"Pacific/Chatham",
|
||||
"Pacific/Easter",
|
||||
"Pacific/Efate",
|
||||
"Pacific/Enderbury",
|
||||
"Pacific/Fakaofo",
|
||||
"Pacific/Fiji",
|
||||
"Pacific/Funafuti",
|
||||
"Pacific/Galapagos",
|
||||
"Pacific/Gambier",
|
||||
"Pacific/Guadalcanal",
|
||||
"Pacific/Guam",
|
||||
"Pacific/Honolulu",
|
||||
"Pacific/Kiritimati",
|
||||
"Pacific/Kosrae",
|
||||
"Pacific/Kwajalein",
|
||||
"Pacific/Majuro",
|
||||
"Pacific/Marquesas",
|
||||
"Pacific/Midway",
|
||||
"Pacific/Nauru",
|
||||
"Pacific/Niue",
|
||||
"Pacific/Norfolk",
|
||||
"Pacific/Noumea",
|
||||
"Pacific/Pago_Pago",
|
||||
"Pacific/Palau",
|
||||
"Pacific/Pitcairn",
|
||||
"Pacific/Ponape",
|
||||
"Pacific/Port_Moresby",
|
||||
"Pacific/Rarotonga",
|
||||
"Pacific/Saipan",
|
||||
"Pacific/Tahiti",
|
||||
"Pacific/Tarawa",
|
||||
"Pacific/Tongatapu",
|
||||
"Pacific/Truk",
|
||||
"Pacific/Wake",
|
||||
"Pacific/Wallis"
|
||||
]
|
||||
|
||||
LODGING_TYPES = [
|
||||
('hotel', 'Hotel'),
|
||||
('hostel', 'Hostel'),
|
||||
|
@ -78,6 +501,7 @@ class Visit(models.Model):
|
|||
adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits')
|
||||
start_date = models.DateTimeField(null=True, blank=True)
|
||||
end_date = models.DateTimeField(null=True, blank=True)
|
||||
timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
@ -104,9 +528,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)
|
||||
|
||||
|
@ -119,6 +550,17 @@ class Adventure(models.Model):
|
|||
# end_date = models.DateField(blank=True, null=True)
|
||||
# type = models.CharField(max_length=100, choices=ADVENTURE_TYPES, default='general')
|
||||
|
||||
def is_visited_status(self):
|
||||
current_date = timezone.now().date()
|
||||
for visit in self.visits.all():
|
||||
start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date
|
||||
end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date
|
||||
if start_date and end_date and (start_date <= current_date):
|
||||
return True
|
||||
elif start_date and not end_date and (start_date <= current_date):
|
||||
return True
|
||||
return False
|
||||
|
||||
def clean(self):
|
||||
if self.collection:
|
||||
if self.collection.is_public and not self.is_public:
|
||||
|
@ -146,6 +588,32 @@ class Adventure(models.Model):
|
|||
}
|
||||
)
|
||||
self.category = category
|
||||
|
||||
if self.latitude and self.longitude:
|
||||
is_visited = self.is_visited_status()
|
||||
reverse_geocode_result = reverse_geocode(self.latitude, self.longitude, self.user_id)
|
||||
if 'region_id' in reverse_geocode_result:
|
||||
region = Region.objects.filter(id=reverse_geocode_result['region_id']).first()
|
||||
if region:
|
||||
self.region = region
|
||||
if is_visited:
|
||||
visited_region, created = VisitedRegion.objects.get_or_create(
|
||||
user_id=self.user_id,
|
||||
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 is_visited:
|
||||
visited_city, created = VisitedCity.objects.get_or_create(
|
||||
user_id=self.user_id,
|
||||
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)
|
||||
|
||||
|
@ -191,6 +659,8 @@ class Transportation(models.Model):
|
|||
link = models.URLField(blank=True, null=True)
|
||||
date = models.DateTimeField(blank=True, null=True)
|
||||
end_date = models.DateTimeField(blank=True, null=True)
|
||||
start_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
|
||||
end_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
|
||||
flight_number = models.CharField(max_length=100, blank=True, null=True)
|
||||
from_location = models.CharField(max_length=200, blank=True, null=True)
|
||||
origin_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
|
@ -352,6 +822,7 @@ class Lodging(models.Model):
|
|||
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||
check_in = models.DateTimeField(blank=True, null=True)
|
||||
check_out = models.DateTimeField(blank=True, null=True)
|
||||
timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
|
||||
reservation_number = models.CharField(max_length=100, blank=True, null=True)
|
||||
price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True)
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
|
@ -363,8 +834,8 @@ class Lodging(models.Model):
|
|||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def clean(self):
|
||||
if self.date and self.end_date and self.date > self.end_date:
|
||||
raise ValidationError('The start date must be before the end date. Start date: ' + str(self.date) + ' End date: ' + str(self.end_date))
|
||||
if self.check_in and self.check_out and self.check_in > self.check_out:
|
||||
raise ValidationError('The start date must be before the end date. Start date: ' + str(self.check_in) + ' End date: ' + str(self.check_out))
|
||||
|
||||
if self.collection:
|
||||
if self.collection.is_public and not self.is_public:
|
||||
|
|
|
@ -72,7 +72,7 @@ class VisitSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Visit
|
||||
fields = ['id', 'start_date', 'end_date', 'notes']
|
||||
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes']
|
||||
read_only_fields = ['id']
|
||||
|
||||
class AdventureSerializer(CustomModelSerializer):
|
||||
|
@ -82,13 +82,14 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
category = CategorySerializer(read_only=False, required=False)
|
||||
is_visited = serializers.SerializerMethodField()
|
||||
user = serializers.SerializerMethodField()
|
||||
country = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Adventure
|
||||
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']
|
||||
|
||||
|
@ -103,6 +104,9 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
return existing_category
|
||||
category_data['name'] = name
|
||||
return category_data
|
||||
|
||||
def get_country(self, obj):
|
||||
return obj.country.country_code if obj.country else None
|
||||
|
||||
def get_or_create_category(self, category_data):
|
||||
user = self.context['request'].user
|
||||
|
@ -134,15 +138,7 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
return CustomUserDetailsSerializer(user).data
|
||||
|
||||
def get_is_visited(self, obj):
|
||||
current_date = timezone.now().date()
|
||||
for visit in obj.visits.all():
|
||||
start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date
|
||||
end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date
|
||||
if start_date and end_date and (start_date <= current_date):
|
||||
return True
|
||||
elif start_date and not end_date and (start_date <= current_date):
|
||||
return True
|
||||
return False
|
||||
return obj.is_visited_status()
|
||||
|
||||
def create(self, validated_data):
|
||||
visits_data = validated_data.pop('visits', None)
|
||||
|
@ -155,7 +151,8 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
if category_data:
|
||||
category = self.get_or_create_category(category_data)
|
||||
adventure.category = category
|
||||
adventure.save()
|
||||
|
||||
adventure.save()
|
||||
|
||||
return adventure
|
||||
|
||||
|
@ -192,6 +189,9 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
visits_to_delete = current_visit_ids - updated_visit_ids
|
||||
instance.visits.filter(id__in=visits_to_delete).delete()
|
||||
|
||||
# call save on the adventure to update the updated_at field and trigger any geocoding
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
class TransportationSerializer(CustomModelSerializer):
|
||||
|
@ -201,7 +201,7 @@ class TransportationSerializer(CustomModelSerializer):
|
|||
fields = [
|
||||
'id', 'user_id', 'type', 'name', 'description', 'rating',
|
||||
'link', 'date', 'flight_number', 'from_location', 'to_location',
|
||||
'is_public', 'collection', 'created_at', 'updated_at', 'end_date', 'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude'
|
||||
'is_public', 'collection', 'created_at', 'updated_at', 'end_date', 'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude', 'start_timezone', 'end_timezone'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||
|
||||
|
@ -212,7 +212,7 @@ class LodgingSerializer(CustomModelSerializer):
|
|||
fields = [
|
||||
'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out',
|
||||
'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public',
|
||||
'collection', 'created_at', 'updated_at', 'type'
|
||||
'collection', 'created_at', 'updated_at', 'type', 'timezone'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||
|
||||
|
|
|
@ -60,14 +60,15 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
user = self.request.user
|
||||
|
||||
# Actions that allow public access (include 'retrieve' and your custom action)
|
||||
public_allowed_actions = {'retrieve', 'additional_info'}
|
||||
|
||||
if not user.is_authenticated:
|
||||
# Unauthenticated users can only access public adventures for retrieval
|
||||
if self.action == 'retrieve':
|
||||
if self.action in public_allowed_actions:
|
||||
return Adventure.objects.retrieve_adventures(user, include_public=True).order_by('-updated_at')
|
||||
return Adventure.objects.none()
|
||||
|
||||
# Authenticated users: Handle retrieval separately
|
||||
include_public = self.action == 'retrieve'
|
||||
include_public = self.action in public_allowed_actions
|
||||
return Adventure.objects.retrieve_adventures(
|
||||
user,
|
||||
include_public=include_public,
|
||||
|
@ -75,6 +76,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
include_shared=True
|
||||
).order_by('-updated_at')
|
||||
|
||||
|
||||
def perform_update(self, serializer):
|
||||
adventure = serializer.save()
|
||||
if adventure.collection:
|
||||
|
@ -175,11 +177,15 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
def additional_info(self, request, pk=None):
|
||||
adventure = self.get_object()
|
||||
|
||||
# Permission check: owner or shared collection member
|
||||
if adventure.user_id != request.user:
|
||||
if not (adventure.collection and adventure.collection.shared_with.filter(id=request.user.id).exists()):
|
||||
return Response({"error": "User does not have permission to access this adventure"},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
user = request.user
|
||||
|
||||
# Allow if public
|
||||
if not adventure.is_public:
|
||||
# Only allow owner or shared collection members
|
||||
if not user.is_authenticated or adventure.user_id != user:
|
||||
if not (adventure.collection and adventure.collection.shared_with.filter(uuid=user.uuid).exists()):
|
||||
return Response({"error": "User does not have permission to access this adventure"},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
serializer = self.get_serializer(adventure)
|
||||
response_data = serializer.data
|
||||
|
@ -202,7 +208,6 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
"sunrise": results.get('sunrise'),
|
||||
"sunset": results.get('sunset')
|
||||
})
|
||||
|
||||
|
||||
response_data['sun_times'] = sun_times
|
||||
return Response(response_data)
|
|
@ -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):
|
||||
|
|
|
@ -71,6 +71,7 @@ MIDDLEWARE = (
|
|||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'adventures.middleware.XSessionTokenMiddleware',
|
||||
'adventures.middleware.DisableCSRFForSessionTokenMiddleware',
|
||||
'adventures.middleware.DisableCSRFForMobileLoginSignup',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
|
@ -312,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
|
||||
|
|
|
@ -165,7 +165,7 @@ class EnabledSocialProvidersView(APIView):
|
|||
providers = []
|
||||
for provider in social_providers:
|
||||
if provider.provider == 'openid_connect':
|
||||
new_provider = f'oidc/{provider.client_id}'
|
||||
new_provider = f'oidc/{provider.provider_id}'
|
||||
else:
|
||||
new_provider = provider.provider
|
||||
providers.append({
|
||||
|
@ -204,4 +204,4 @@ class DisablePasswordAuthenticationView(APIView):
|
|||
user.disable_password = False
|
||||
user.save()
|
||||
return Response({"detail": "Password authentication enabled."}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from adventures.models import Adventure
|
||||
import time
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Bulk geocode all adventures by triggering save on each one'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
adventures = Adventure.objects.all()
|
||||
total = adventures.count()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'Starting bulk geocoding of {total} adventures'))
|
||||
|
||||
for i, adventure in enumerate(adventures):
|
||||
try:
|
||||
self.stdout.write(f'Processing adventure {i+1}/{total}: {adventure}')
|
||||
adventure.save() # This should trigger any geocoding in the save method
|
||||
self.stdout.write(self.style.SUCCESS(f'Successfully processed adventure {i+1}/{total}'))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'Error processing adventure {i+1}/{total}: {adventure} - {e}'))
|
||||
|
||||
# Sleep for 2 seconds between each save
|
||||
if i < total - 1: # Don't sleep after the last one
|
||||
time.sleep(2)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Finished processing all adventures'))
|
Loading…
Add table
Add a link
Reference in a new issue