From 241d27a1a6cb8a75db9c76fb16dbc5e7e11d1e95 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 19 Jun 2025 16:50:11 -0400 Subject: [PATCH] Refactor user_id to user in adventures and related models, views, and components - Updated all instances of user_id to user in the adventures app, including models, serializers, views, and frontend components. - Adjusted queries and filters to reflect the new user field naming convention. - Ensured consistency across the codebase for user identification in adventures, collections, notes, and transportation entities. - Modified frontend components to align with the updated data structure, ensuring proper access control and rendering based on user ownership. --- backend/server/adventures/admin.py | 18 +- backend/server/adventures/geocoding.py | 4 +- .../management/commands/travel-seed.py | 6 +- backend/server/adventures/managers.py | 4 +- ...uashed_0050_rename_user_id_lodging_user.py | 91 +++ backend/server/adventures/models.py | 547 ++---------------- backend/server/adventures/permissions.py | 16 +- backend/server/adventures/serializers.py | 82 +-- backend/server/adventures/signals.py | 4 +- .../adventures/utils/file_permissions.py | 12 +- backend/server/adventures/utils/timezones.py | 419 ++++++++++++++ .../adventures/views/activity_types_view.py | 4 +- .../adventures/views/adventure_image_view.py | 38 +- .../server/adventures/views/adventure_view.py | 34 +- .../adventures/views/attachment_view.py | 44 +- .../server/adventures/views/category_view.py | 12 +- .../server/adventures/views/checklist_view.py | 20 +- .../adventures/views/collection_view.py | 30 +- .../adventures/views/global_search_view.py | 12 +- .../adventures/views/ics_calendar_view.py | 5 +- .../server/adventures/views/lodging_view.py | 16 +- backend/server/adventures/views/note_view.py | 20 +- .../adventures/views/reverse_geocode_view.py | 12 +- backend/server/adventures/views/stats_view.py | 14 +- .../adventures/views/transportation_view.py | 16 +- backend/server/integrations/views.py | 46 +- backend/server/main/utils.py | 2 +- backend/server/users/views.py | 6 +- .../commands/bulk-adventure-geocode.py | 4 +- .../0017_rename_user_id_visitedregion_user.py | 18 + .../0018_rename_user_id_visitedcity_user.py | 18 + backend/server/worldtravel/models.py | 18 +- backend/server/worldtravel/serializers.py | 10 +- backend/server/worldtravel/views.py | 34 +- .../src/lib/components/AdventureCard.svelte | 4 +- .../src/lib/components/AdventureModal.svelte | 12 +- .../lib/components/CategoryDropdown.svelte | 2 +- .../src/lib/components/ChecklistCard.svelte | 2 +- .../src/lib/components/ChecklistModal.svelte | 4 +- .../src/lib/components/CollectionCard.svelte | 2 +- .../src/lib/components/CollectionModal.svelte | 2 +- .../src/lib/components/LodgingCard.svelte | 4 +- .../src/lib/components/LodgingModal.svelte | 2 +- frontend/src/lib/components/NoteCard.svelte | 2 +- frontend/src/lib/components/NoteModal.svelte | 2 +- .../lib/components/TransportationCard.svelte | 2 +- .../lib/components/TransportationModal.svelte | 2 +- frontend/src/lib/types.ts | 22 +- .../src/routes/adventures/[id]/+page.svelte | 2 +- .../src/routes/collections/[id]/+page.svelte | 8 +- 50 files changed, 918 insertions(+), 792 deletions(-) create mode 100644 backend/server/adventures/migrations/0036_rename_adventure_location_squashed_0050_rename_user_id_lodging_user.py create mode 100644 backend/server/adventures/utils/timezones.py create mode 100644 backend/server/worldtravel/migrations/0017_rename_user_id_visitedregion_user.py create mode 100644 backend/server/worldtravel/migrations/0018_rename_user_id_visitedcity_user.py diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index e23fa15..f079c8a 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -1,7 +1,7 @@ import os from django.contrib import admin from django.utils.html import mark_safe -from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Lodging +from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, LocationImage, Visit, Category, Attachment, Lodging from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity from allauth.account.decorators import secure_admin_login @@ -22,8 +22,8 @@ def trigger_geocoding(modeladmin, request, queryset): class AdventureAdmin(admin.ModelAdmin): - list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public') - list_filter = ( 'user_id', 'is_public') + list_display = ('name', 'get_category', 'get_visit_count', 'user', 'is_public') + list_filter = ( 'user', 'is_public') search_fields = ('name',) readonly_fields = ('city', 'region', 'country') actions = [trigger_geocoding] @@ -97,7 +97,7 @@ class CustomUserAdmin(UserAdmin): return class AdventureImageAdmin(admin.ModelAdmin): - list_display = ('user_id', 'image_display') + list_display = ('user', 'image_display') def image_display(self, obj): if obj.image: @@ -109,7 +109,7 @@ class AdventureImageAdmin(admin.ModelAdmin): class VisitAdmin(admin.ModelAdmin): - list_display = ('adventure', 'start_date', 'end_date', 'notes') + list_display = ('location', 'start_date', 'end_date', 'notes') list_filter = ('start_date', 'end_date') search_fields = ('notes',) @@ -125,19 +125,19 @@ class VisitAdmin(admin.ModelAdmin): image_display.short_description = 'Image Preview' class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'user_id', 'display_name', 'icon') + list_display = ('name', 'user', 'display_name', 'icon') search_fields = ('name', 'display_name') class CollectionAdmin(admin.ModelAdmin): - list_display = ('name', 'user_id', 'is_public') + list_display = ('name', 'user', 'is_public') admin.site.register(CustomUser, CustomUserAdmin) -admin.site.register(Adventure, AdventureAdmin) +admin.site.register(Location, AdventureAdmin) admin.site.register(Collection, CollectionAdmin) admin.site.register(Visit, VisitAdmin) admin.site.register(Country, CountryAdmin) @@ -147,7 +147,7 @@ admin.site.register(Transportation) admin.site.register(Note) admin.site.register(Checklist) admin.site.register(ChecklistItem) -admin.site.register(AdventureImage, AdventureImageAdmin) +admin.site.register(LocationImage, AdventureImageAdmin) admin.site.register(Category, CategoryAdmin) admin.site.register(City, CityAdmin) admin.site.register(VisitedCity) diff --git a/backend/server/adventures/geocoding.py b/backend/server/adventures/geocoding.py index f10c97a..25437f4 100644 --- a/backend/server/adventures/geocoding.py +++ b/backend/server/adventures/geocoding.py @@ -167,7 +167,7 @@ def extractIsoCode(user, data): return {"error": "No region found"} region = Region.objects.filter(id=iso_code).first() - visited_region = VisitedRegion.objects.filter(region=region, user_id=user).first() + visited_region = VisitedRegion.objects.filter(region=region, user=user).first() region_visited = False city_visited = False @@ -177,7 +177,7 @@ def extractIsoCode(user, data): 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() + visited_city = VisitedCity.objects.filter(city=city, user=user).first() if visited_region: region_visited = True diff --git a/backend/server/adventures/management/commands/travel-seed.py b/backend/server/adventures/management/commands/travel-seed.py index 96700c9..eec9e35 100644 --- a/backend/server/adventures/management/commands/travel-seed.py +++ b/backend/server/adventures/management/commands/travel-seed.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model -from adventures.models import Adventure +from adventures.models import Location class Command(BaseCommand): @@ -38,8 +38,8 @@ class Command(BaseCommand): ] for name, location, type_ in adventures: - Adventure.objects.create( - user_id=user, + Location.objects.create( + user=user, name=name, location=location, type=type_, diff --git a/backend/server/adventures/managers.py b/backend/server/adventures/managers.py index 4a12194..1c97212 100644 --- a/backend/server/adventures/managers.py +++ b/backend/server/adventures/managers.py @@ -1,12 +1,12 @@ from django.db import models from django.db.models import Q -class AdventureManager(models.Manager): +class LocationManager(models.Manager): def retrieve_adventures(self, user, include_owned=False, include_shared=False, include_public=False): query = Q() if include_owned: - query |= Q(user_id=user) + query |= Q(user=user) if include_shared: query |= Q(collections__shared_with=user) diff --git a/backend/server/adventures/migrations/0036_rename_adventure_location_squashed_0050_rename_user_id_lodging_user.py b/backend/server/adventures/migrations/0036_rename_adventure_location_squashed_0050_rename_user_id_lodging_user.py new file mode 100644 index 0000000..52f989a --- /dev/null +++ b/backend/server/adventures/migrations/0036_rename_adventure_location_squashed_0050_rename_user_id_lodging_user.py @@ -0,0 +1,91 @@ +# Generated by Django 5.2.1 on 2025-06-19 20:29 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + replaces = [('adventures', '0036_rename_adventure_location'), ('adventures', '0037_rename_adventure_visit_location'), ('adventures', '0038_rename_adventureimage_locationimage'), ('adventures', '0039_rename_adventure_locationimage_location'), ('adventures', '0040_rename_adventure_attachment_location'), ('adventures', '0041_rename_user_id_location_user'), ('adventures', '0042_rename_user_id_locationimage_user'), ('adventures', '0043_rename_user_id_attachment_user'), ('adventures', '0044_rename_user_id_collection_user'), ('adventures', '0045_rename_user_id_transportation_user'), ('adventures', '0046_rename_user_id_note_user'), ('adventures', '0047_rename_user_id_checklist_user'), ('adventures', '0048_rename_user_id_checklistitem_user'), ('adventures', '0049_rename_user_id_category_user'), ('adventures', '0050_rename_user_id_lodging_user')] + + dependencies = [ + ('adventures', '0035_remove_adventure_collection_adventure_collections'), + ('worldtravel', '0016_remove_city_insert_id_remove_country_insert_id_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RenameModel( + old_name='Adventure', + new_name='Location', + ), + migrations.RenameField( + model_name='visit', + old_name='adventure', + new_name='location', + ), + migrations.RenameModel( + old_name='AdventureImage', + new_name='LocationImage', + ), + migrations.RenameField( + model_name='locationimage', + old_name='adventure', + new_name='location', + ), + migrations.RenameField( + model_name='attachment', + old_name='adventure', + new_name='location', + ), + migrations.RenameField( + model_name='location', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='locationimage', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='attachment', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='collection', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='transportation', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='note', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='checklist', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='checklistitem', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='category', + old_name='user_id', + new_name='user', + ), + migrations.RenameField( + model_name='lodging', + old_name='user_id', + new_name='user', + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 40bb680..49de0dc 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -1,10 +1,9 @@ from django.core.exceptions import ValidationError import os -from typing import Iterable import uuid from django.db import models from django.utils.deconstruct import deconstructible -from adventures.managers import AdventureManager +from adventures.managers import LocationManager import threading from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField @@ -13,46 +12,47 @@ from django_resized import ResizedImageField from worldtravel.models import City, Country, Region, VisitedCity, VisitedRegion from django.core.exceptions import ValidationError from django.utils import timezone +from adventures.utils.timezones import TIMEZONES -def background_geocode_and_assign(adventure_id: str): - print(f"[Adventure Geocode Thread] Starting geocode for adventure {adventure_id}") +def background_geocode_and_assign(location_id: str): + print(f"[Location Geocode Thread] Starting geocode for location {location_id}") try: - adventure = Adventure.objects.get(id=adventure_id) - if not (adventure.latitude and adventure.longitude): + location = Location.objects.get(id=location_id) + if not (location.latitude and location.longitude): return from adventures.geocoding import reverse_geocode # or wherever you defined it - is_visited = adventure.is_visited_status() - result = reverse_geocode(adventure.latitude, adventure.longitude, adventure.user_id) + is_visited = location.is_visited_status() + result = reverse_geocode(location.latitude, location.longitude, location.user) if 'region_id' in result: region = Region.objects.filter(id=result['region_id']).first() if region: - adventure.region = region + location.region = region if is_visited: - VisitedRegion.objects.get_or_create(user_id=adventure.user_id, region=region) + VisitedRegion.objects.get_or_create(user=location.user, region=region) if 'city_id' in result: city = City.objects.filter(id=result['city_id']).first() if city: - adventure.city = city + location.city = city if is_visited: - VisitedCity.objects.get_or_create(user_id=adventure.user_id, city=city) + VisitedCity.objects.get_or_create(user=location.user, city=city) if 'country_id' in result: country = Country.objects.filter(country_code=result['country_id']).first() if country: - adventure.country = country + location.country = country # Save updated location info # Save updated location info, skip geocode threading - adventure.save(update_fields=["region", "city", "country"], _skip_geocode=True) + location.save(update_fields=["region", "city", "country"], _skip_geocode=True) # print(f"[Adventure Geocode Thread] Successfully processed {adventure_id}: {adventure.name} - {adventure.latitude}, {adventure.longitude}") except Exception as e: # Optional: log or print the error - print(f"[Adventure Geocode Thread] Error processing {adventure_id}: {e}") + print(f"[Location Geocode Thread] Error processing {location_id}: {e}") def validate_file_extension(value): import os @@ -62,6 +62,7 @@ def validate_file_extension(value): if not ext.lower() in valid_extensions: raise ValidationError('Unsupported file extension.') +# Legacy support for old adventure types, not used in newer versions since custom categories are now used ADVENTURE_TYPES = [ ('general', 'General 🌍'), ('outdoor', 'Outdoor 🏞️'), @@ -87,426 +88,6 @@ 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'), @@ -533,13 +114,13 @@ TRANSPORTATION_TYPES = [ ] # Assuming you have a default user ID you want to use -default_user_id = 1 # Replace with an actual user ID +default_user = 1 # Replace with an actual user ID User = get_user_model() class Visit(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits') + location = models.ForeignKey('Location', 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) @@ -552,13 +133,13 @@ class Visit(models.Model): raise ValidationError('The start date must be before or equal to the end date.') def __str__(self): - return f"{self.adventure.name} - {self.start_date} to {self.end_date}" + return f"{self.location.name} - {self.start_date} to {self.end_date}" -class Adventure(models.Model): +class Location(models.Model): #id = models.AutoField(primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True) name = models.CharField(max_length=200) @@ -583,7 +164,7 @@ class Adventure(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - objects = AdventureManager() + objects = LocationManager() def is_visited_status(self): current_date = timezone.now().date() @@ -609,18 +190,18 @@ class Adventure(models.Model): if self.pk: # Only check if the instance has been saved for collection in self.collections.all(): if collection.is_public and not self.is_public: - raise ValidationError(f'Adventures associated with a public collection must be public. Collection: {collection.name} Adventure: {self.name}') + raise ValidationError(f'Locations associated with a public collection must be public. Collection: {collection.name} Location: {self.name}') # Only enforce same-user constraint for non-shared collections - if self.user_id != collection.user_id: + if self.user != collection.user: # Check if this is a shared collection scenario - # Allow if the adventure owner has access to the collection through sharing - if not collection.shared_with.filter(uuid=self.user_id.uuid).exists(): - raise ValidationError(f'Adventures must be associated with collections owned by the same user or shared collections. Collection owner: {collection.user_id.username} Adventure owner: {self.user_id.username}') + # Allow if the location owner has access to the collection through sharing + if not collection.shared_with.filter(uuid=self.user.uuid).exists(): + raise ValidationError(f'Locations must be associated with collections owned by the same user or shared collections. Collection owner: {collection.user.username} Location owner: {self.user.username}') if self.category: - if self.user_id != self.category.user_id: - raise ValidationError(f'Adventures must be associated with categories owned by the same user. Category owner: {self.category.user_id.username} Adventure owner: {self.user_id.username}') + if self.user != self.category.user: + raise ValidationError(f'Locations must be associated with categories owned by the same user. Category owner: {self.category.user.username} Location owner: {self.user.username}') def save(self, force_insert=False, force_update=False, using=None, update_fields=None, _skip_geocode=False, _skip_shared_validation=False): if force_insert and force_update: @@ -628,7 +209,7 @@ class Adventure(models.Model): if not self.category: category, _ = Category.objects.get_or_create( - user_id=self.user_id, + user=self.user, name='general', defaults={'display_name': 'General', 'icon': '🌍'} ) @@ -662,8 +243,8 @@ class Adventure(models.Model): class Collection(models.Model): #id = models.AutoField(primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) name = models.CharField(max_length=200) description = models.TextField(blank=True, null=True) is_public = models.BooleanField(default=False) @@ -681,7 +262,7 @@ class Collection(models.Model): # Updated to use the new related_name 'adventures' for adventure in self.adventures.all(): if not adventure.is_public: - raise ValidationError(f'Public collections cannot be associated with private adventures. Collection: {self.name} Adventure: {adventure.name}') + raise ValidationError(f'Public collections cannot be associated with private locations. Collection: {self.name} Adventure: {adventure.name}') def __str__(self): return self.name @@ -689,8 +270,8 @@ class Collection(models.Model): class Transportation(models.Model): #id = models.AutoField(primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES) name = models.CharField(max_length=200) description = models.TextField(blank=True, null=True) @@ -720,8 +301,8 @@ class Transportation(models.Model): if self.collection: if self.collection.is_public and not self.is_public: raise ValidationError('Transportations associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name) - if self.user_id != self.collection.user_id: - raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username) + if self.user != self.collection.user: + raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Transportation owner: ' + self.user.username) def __str__(self): return self.name @@ -729,8 +310,8 @@ class Transportation(models.Model): class Note(models.Model): #id = models.AutoField(primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) name = models.CharField(max_length=200) content = models.TextField(blank=True, null=True) links = ArrayField(models.URLField(), blank=True, null=True) @@ -744,8 +325,8 @@ class Note(models.Model): if self.collection: if self.collection.is_public and not self.is_public: raise ValidationError('Notes associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name) - if self.user_id != self.collection.user_id: - raise ValidationError('Notes must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username) + if self.user != self.collection.user: + raise ValidationError('Notes must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Transportation owner: ' + self.user.username) def __str__(self): return self.name @@ -753,8 +334,8 @@ class Note(models.Model): class Checklist(models.Model): # id = models.AutoField(primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) name = models.CharField(max_length=200) date = models.DateField(blank=True, null=True) is_public = models.BooleanField(default=False) @@ -766,8 +347,8 @@ class Checklist(models.Model): if self.collection: if self.collection.is_public and not self.is_public: raise ValidationError('Checklists associated with a public collection must be public. Collection: ' + self.collection.name + ' Checklist: ' + self.name) - if self.user_id != self.collection.user_id: - raise ValidationError('Checklists must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Checklist owner: ' + self.user_id.username) + if self.user != self.collection.user: + raise ValidationError('Checklists must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Checklist owner: ' + self.user.username) def __str__(self): return self.name @@ -775,8 +356,8 @@ class Checklist(models.Model): class ChecklistItem(models.Model): #id = models.AutoField(primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) name = models.CharField(max_length=200) is_checked = models.BooleanField(default=False) checklist = models.ForeignKey('Checklist', on_delete=models.CASCADE) @@ -786,8 +367,8 @@ class ChecklistItem(models.Model): def clean(self): if self.checklist.is_public and not self.checklist.is_public: raise ValidationError('Checklist items associated with a public checklist must be public. Checklist: ' + self.checklist.name + ' Checklist item: ' + self.name) - if self.user_id != self.checklist.user_id: - raise ValidationError('Checklist items must be associated with checklists owned by the same user. Checklist owner: ' + self.checklist.user_id.username + ' Checklist item owner: ' + self.user_id.username) + if self.user != self.checklist.user: + raise ValidationError('Checklist items must be associated with checklists owned by the same user. Checklist owner: ' + self.checklist.user.username + ' Checklist item owner: ' + self.user.username) def __str__(self): return self.name @@ -803,9 +384,9 @@ class PathAndRename: filename = f"{uuid.uuid4()}.{ext}" return os.path.join(self.path, filename) -class AdventureImage(models.Model): +class LocationImage(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user) image = ResizedImageField( force_format="WEBP", quality=75, @@ -814,7 +395,7 @@ class AdventureImage(models.Model): null=True, ) immich_id = models.CharField(max_length=200, null=True, blank=True) - adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE) + location = models.ForeignKey(Location, related_name='images', on_delete=models.CASCADE) is_primary = models.BooleanField(default=False) def clean(self): @@ -843,10 +424,10 @@ class AdventureImage(models.Model): class Attachment(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) file = models.FileField(upload_to=PathAndRename('attachments/'),validators=[validate_file_extension]) - adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE) + location = models.ForeignKey(Location, related_name='attachments', on_delete=models.CASCADE) name = models.CharField(max_length=200, null=True, blank=True) def __str__(self): @@ -854,15 +435,15 @@ class Attachment(models.Model): class Category(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) name = models.CharField(max_length=200) display_name = models.CharField(max_length=200) icon = models.CharField(max_length=200, default='🌍') class Meta: verbose_name_plural = 'Categories' - unique_together = ['name', 'user_id'] + unique_together = ['name', 'user'] def clean(self) -> None: self.name = self.name.lower().strip() @@ -875,8 +456,8 @@ class Category(models.Model): class Lodging(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) name = models.CharField(max_length=200) type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other') description = models.TextField(blank=True, null=True) @@ -901,9 +482,9 @@ class Lodging(models.Model): if self.collection: if self.collection.is_public and not self.is_public: - raise ValidationError('Lodging associated with a public collection must be public. Collection: ' + self.collection.name + ' Loging: ' + self.name) - if self.user_id != self.collection.user_id: - raise ValidationError('Lodging must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Lodging owner: ' + self.user_id.username) + raise ValidationError('Lodging associated with a public collection must be public. Collection: ' + self.collection.name + ' Lodging: ' + self.name) + if self.user != self.collection.user: + raise ValidationError('Lodging must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Lodging owner: ' + self.user.username) def __str__(self): return self.name \ No newline at end of file diff --git a/backend/server/adventures/permissions.py b/backend/server/adventures/permissions.py index ce38f74..ae88261 100644 --- a/backend/server/adventures/permissions.py +++ b/backend/server/adventures/permissions.py @@ -7,8 +7,8 @@ class IsOwnerOrReadOnly(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True - # obj.user_id is FK to User, compare with request.user - return obj.user_id == request.user + # obj.user is FK to User, compare with request.user + return obj.user == request.user class IsPublicReadOnly(permissions.BasePermission): @@ -17,8 +17,8 @@ class IsPublicReadOnly(permissions.BasePermission): """ def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: - return obj.is_public or obj.user_id == request.user - return obj.user_id == request.user + return obj.is_public or obj.user == request.user + return obj.user == request.user class CollectionShared(permissions.BasePermission): @@ -48,10 +48,10 @@ class CollectionShared(permissions.BasePermission): # Read permission if public or owner if request.method in permissions.SAFE_METHODS: - return obj.is_public or obj.user_id == user + return obj.is_public or obj.user == user # Write permission only if owner or shared user via collections - if obj.user_id == user: + if obj.user == user: return True if hasattr(obj, 'collections'): @@ -76,7 +76,7 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission): if request.method in permissions.SAFE_METHODS: if obj.is_public: return True - if obj.user_id == user: + if obj.user == user: return True # If user in shared_with of any collection related to obj if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists(): @@ -88,7 +88,7 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission): return False # For write methods, allow if owner or shared user - if obj.user_id == user: + if obj.user == user: return True if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists(): return True diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 3023e04..175f320 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -1,6 +1,6 @@ from django.utils import timezone import os -from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging +from .models import Location, LocationImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging from rest_framework import serializers from main.utils import CustomModelSerializer from users.serializers import CustomUserDetailsSerializer @@ -11,15 +11,15 @@ from integrations.models import ImmichIntegration class AdventureImageSerializer(CustomModelSerializer): class Meta: - model = AdventureImage - fields = ['id', 'image', 'adventure', 'is_primary', 'user_id', 'immich_id'] - read_only_fields = ['id', 'user_id'] + model = LocationImage + fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id'] + read_only_fields = ['id', 'user'] def to_representation(self, instance): # If immich_id is set, check for user integration once integration = None if instance.immich_id: - integration = ImmichIntegration.objects.filter(user=instance.user_id).first() + integration = ImmichIntegration.objects.filter(user=instance.user).first() if not integration: return None # Skip if Immich image but no integration @@ -42,8 +42,8 @@ class AttachmentSerializer(CustomModelSerializer): extension = serializers.SerializerMethodField() class Meta: model = Attachment - fields = ['id', 'file', 'adventure', 'extension', 'name', 'user_id'] - read_only_fields = ['id', 'user_id'] + fields = ['id', 'file', 'location', 'extension', 'name', 'user'] + read_only_fields = ['id', 'user'] def get_extension(self, obj): return obj.file.name.split('.')[-1] @@ -71,7 +71,7 @@ class CategorySerializer(serializers.ModelSerializer): def create(self, validated_data): user = self.context['request'].user validated_data['name'] = validated_data['name'].lower() - return Category.objects.create(user_id=user, **validated_data) + return Category.objects.create(user=user, **validated_data) def update(self, instance, validated_data): for attr, value in validated_data.items(): @@ -82,7 +82,7 @@ class CategorySerializer(serializers.ModelSerializer): return instance def get_num_adventures(self, obj): - return Adventure.objects.filter(category=obj, user_id=obj.user_id).count() + return Location.objects.filter(category=obj, user=obj.user).count() class VisitSerializer(serializers.ModelSerializer): @@ -108,13 +108,13 @@ class AdventureSerializer(CustomModelSerializer): ) class Meta: - model = Adventure + model = Location fields = [ - 'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', + 'id', 'name', 'description', 'rating', 'activity_types', 'location', 'is_public', 'collections', 'created_at', 'updated_at', 'images', 'link', 'longitude', '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', 'is_visited'] def get_images(self, obj): serializer = AdventureImageSerializer(obj.images.all(), many=True, context=self.context) @@ -128,7 +128,7 @@ class AdventureSerializer(CustomModelSerializer): user = self.context['request'].user for collection in collections: - if collection.user_id != user: + if collection.user != user: raise serializers.ValidationError( f"Collection '{collection.name}' does not belong to the current user." ) @@ -140,7 +140,7 @@ class AdventureSerializer(CustomModelSerializer): if category_data: user = self.context['request'].user name = category_data.get('name', '').lower() - existing_category = Category.objects.filter(user_id=user, name=name).first() + existing_category = Category.objects.filter(user=user, name=name).first() if existing_category: return existing_category category_data['name'] = name @@ -162,7 +162,7 @@ class AdventureSerializer(CustomModelSerializer): icon = category_data.icon category, created = Category.objects.get_or_create( - user_id=user, + user=user, name=name, defaults={ 'display_name': display_name, @@ -172,7 +172,7 @@ class AdventureSerializer(CustomModelSerializer): return category def get_user(self, obj): - user = obj.user_id + user = obj.user return CustomUserDetailsSerializer(user).data def get_is_visited(self, obj): @@ -184,11 +184,11 @@ class AdventureSerializer(CustomModelSerializer): collections_data = validated_data.pop('collections', []) print(category_data) - adventure = Adventure.objects.create(**validated_data) + adventure = Location.objects.create(**validated_data) # Handle visits for visit_data in visits_data: - Visit.objects.create(adventure=adventure, **visit_data) + Visit.objects.create(location=adventure, **visit_data) # Handle category if category_data: @@ -216,7 +216,7 @@ class AdventureSerializer(CustomModelSerializer): # Handle category - ONLY allow the adventure owner to change categories user = self.context['request'].user - if category_data and instance.user_id == user: + if category_data and instance.user == user: # Only the owner can set categories category = self.get_or_create_category(category_data) instance.category = category @@ -241,7 +241,7 @@ class AdventureSerializer(CustomModelSerializer): visit.save() updated_visit_ids.add(visit_id) else: - new_visit = Visit.objects.create(adventure=instance, **visit_data) + new_visit = Visit.objects.create(location=instance, **visit_data) updated_visit_ids.add(new_visit.id) visits_to_delete = current_visit_ids - updated_visit_ids @@ -258,13 +258,13 @@ class TransportationSerializer(CustomModelSerializer): class Meta: model = Transportation fields = [ - 'id', 'user_id', 'type', 'name', 'description', 'rating', + 'id', 'user', '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', - 'start_timezone', 'end_timezone', 'distance' # ✅ Add distance here + 'start_timezone', 'end_timezone', 'distance' ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'distance'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'distance'] def get_distance(self, obj): if ( @@ -284,29 +284,29 @@ class LodgingSerializer(CustomModelSerializer): class Meta: model = Lodging fields = [ - 'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', + 'id', 'user', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', 'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public', 'collection', 'created_at', 'updated_at', 'type', 'timezone' ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user'] class NoteSerializer(CustomModelSerializer): class Meta: model = Note fields = [ - 'id', 'user_id', 'name', 'content', 'date', 'links', + 'id', 'user', 'name', 'content', 'date', 'links', 'is_public', 'collection', 'created_at', 'updated_at' ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user'] class ChecklistItemSerializer(CustomModelSerializer): class Meta: model = ChecklistItem fields = [ - 'id', 'user_id', 'name', 'is_checked', 'checklist', 'created_at', 'updated_at' + 'id', 'user', 'name', 'is_checked', 'checklist', 'created_at', 'updated_at' ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'checklist'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'checklist'] class ChecklistSerializer(CustomModelSerializer): items = ChecklistItemSerializer(many=True, source='checklistitem_set') @@ -314,21 +314,21 @@ class ChecklistSerializer(CustomModelSerializer): class Meta: model = Checklist fields = [ - 'id', 'user_id', 'name', 'date', 'is_public', 'collection', 'created_at', 'updated_at', 'items' + 'id', 'user', 'name', 'date', 'is_public', 'collection', 'created_at', 'updated_at', 'items' ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user'] def create(self, validated_data): items_data = validated_data.pop('checklistitem_set') checklist = Checklist.objects.create(**validated_data) for item_data in items_data: - # Remove user_id from item_data to avoid constraint issues - item_data.pop('user_id', None) - # Set user_id from the parent checklist + # Remove user from item_data to avoid constraint issues + item_data.pop('user', None) + # Set user from the parent checklist ChecklistItem.objects.create( checklist=checklist, - user_id=checklist.user_id, + user=checklist.user, **item_data ) return checklist @@ -348,8 +348,8 @@ class ChecklistSerializer(CustomModelSerializer): # Update or create items updated_item_ids = set() for item_data in items_data: - # Remove user_id from item_data to avoid constraint issues - item_data.pop('user_id', None) + # Remove user from item_data to avoid constraint issues + item_data.pop('user', None) item_id = item_data.get('id') if item_id: @@ -363,14 +363,14 @@ class ChecklistSerializer(CustomModelSerializer): # If ID is provided but doesn't exist, create new item ChecklistItem.objects.create( checklist=instance, - user_id=instance.user_id, + user=instance.user, **item_data ) else: # If no ID is provided, create new item ChecklistItem.objects.create( checklist=instance, - user_id=instance.user_id, + user=instance.user, **item_data ) @@ -399,8 +399,8 @@ class CollectionSerializer(CustomModelSerializer): class Meta: model = Collection - fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging'] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + fields = ['id', 'description', 'user', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user'] def to_representation(self, instance): representation = super().to_representation(instance) diff --git a/backend/server/adventures/signals.py b/backend/server/adventures/signals.py index b8501c8..bf309c9 100644 --- a/backend/server/adventures/signals.py +++ b/backend/server/adventures/signals.py @@ -1,8 +1,8 @@ from django.db.models.signals import m2m_changed from django.dispatch import receiver -from adventures.models import Adventure +from adventures.models import Location -@receiver(m2m_changed, sender=Adventure.collections.through) +@receiver(m2m_changed, sender=Location.collections.through) def update_adventure_publicity(sender, instance, action, **kwargs): """ Signal handler to update adventure publicity when collections are added/removed diff --git a/backend/server/adventures/utils/file_permissions.py b/backend/server/adventures/utils/file_permissions.py index b9a63f0..fc0391c 100644 --- a/backend/server/adventures/utils/file_permissions.py +++ b/backend/server/adventures/utils/file_permissions.py @@ -1,4 +1,4 @@ -from adventures.models import AdventureImage, Attachment +from adventures.models import LocationImage, Attachment protected_paths = ['images/', 'attachments/'] @@ -10,10 +10,10 @@ def checkFilePermission(fileId, user, mediaType): # Construct the full relative path to match the database field image_path = f"images/{fileId}" # Fetch the AdventureImage object - adventure = AdventureImage.objects.get(image=image_path).adventure + adventure = LocationImage.objects.get(image=image_path).location if adventure.is_public: return True - elif adventure.user_id == user: + elif adventure.user == user: return True elif adventure.collections.exists(): # Check if the user is in any collection's shared_with list @@ -23,7 +23,7 @@ def checkFilePermission(fileId, user, mediaType): return False else: return False - except AdventureImage.DoesNotExist: + except LocationImage.DoesNotExist: return False elif mediaType == 'attachments/': try: @@ -31,10 +31,10 @@ def checkFilePermission(fileId, user, mediaType): attachment_path = f"attachments/{fileId}" # Fetch the Attachment object attachment = Attachment.objects.get(file=attachment_path) - adventure = attachment.adventure + adventure = attachment.location if adventure.is_public: return True - elif adventure.user_id == user: + elif adventure.user == user: return True elif adventure.collections.exists(): # Check if the user is in any collection's shared_with list diff --git a/backend/server/adventures/utils/timezones.py b/backend/server/adventures/utils/timezones.py new file mode 100644 index 0000000..58575d3 --- /dev/null +++ b/backend/server/adventures/utils/timezones.py @@ -0,0 +1,419 @@ +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" +] diff --git a/backend/server/adventures/views/activity_types_view.py b/backend/server/adventures/views/activity_types_view.py index 438c0d0..b6d97b1 100644 --- a/backend/server/adventures/views/activity_types_view.py +++ b/backend/server/adventures/views/activity_types_view.py @@ -2,7 +2,7 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from adventures.models import Adventure +from adventures.models import Location class ActivityTypesView(viewsets.ViewSet): permission_classes = [IsAuthenticated] @@ -18,7 +18,7 @@ class ActivityTypesView(viewsets.ViewSet): Returns: Response: A response containing a list of distinct activity types. """ - types = Adventure.objects.filter(user_id=request.user.id).values_list('activity_types', flat=True).distinct() + types = Location.objects.filter(user=request.user.id).values_list('activity_types', flat=True).distinct() allTypes = [] diff --git a/backend/server/adventures/views/adventure_image_view.py b/backend/server/adventures/views/adventure_image_view.py index ab7f8e1..6ce6493 100644 --- a/backend/server/adventures/views/adventure_image_view.py +++ b/backend/server/adventures/views/adventure_image_view.py @@ -4,7 +4,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db.models import Q from django.core.files.base import ContentFile -from adventures.models import Adventure, AdventureImage +from adventures.models import Location, LocationImage from adventures.serializers import AdventureImageSerializer from integrations.models import ImmichIntegration import uuid @@ -26,7 +26,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet): instance = self.get_object() adventure = instance.adventure - if adventure.user_id != request.user: + if adventure.user != request.user: return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) # Check if the image is already the primary image @@ -34,7 +34,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet): return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST) # Set the current primary image to false - AdventureImage.objects.filter(adventure=adventure, is_primary=True).update(is_primary=False) + LocationImage.objects.filter(adventure=adventure, is_primary=True).update(is_primary=False) # Set the new image to true instance.is_primary = True @@ -46,11 +46,11 @@ class AdventureImageViewSet(viewsets.ModelViewSet): return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED) adventure_id = request.data.get('adventure') try: - adventure = Adventure.objects.get(id=adventure_id) - except Adventure.DoesNotExist: + adventure = Location.objects.get(id=adventure_id) + except Location.DoesNotExist: return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND) - if adventure.user_id != request.user: + if adventure.user != request.user: # Check if the adventure has any collections if adventure.collections.exists(): # Check if the user is in the shared_with list of any of the adventure's collections @@ -66,7 +66,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet): return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) # Handle Immich ID for shared users by downloading the image - if (request.user != adventure.user_id and + if (request.user != adventure.user and 'immich_id' in request.data and request.data.get('immich_id')): @@ -74,7 +74,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet): # Get the shared user's Immich integration try: - user_integration = ImmichIntegration.objects.get(user_id=request.user) + user_integration = ImmichIntegration.objects.get(user=request.user) except ImmichIntegration.DoesNotExist: return Response({ "error": "No Immich integration found for your account. Please set up Immich integration first.", @@ -122,7 +122,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet): # Save with the downloaded image adventure = serializer.validated_data['adventure'] - serializer.save(user_id=adventure.user_id, image=image_file) + serializer.save(user=adventure.user, image=image_file) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -145,11 +145,11 @@ class AdventureImageViewSet(viewsets.ModelViewSet): adventure_id = request.data.get('adventure') try: - adventure = Adventure.objects.get(id=adventure_id) - except Adventure.DoesNotExist: + adventure = Location.objects.get(id=adventure_id) + except Location.DoesNotExist: return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND) - if adventure.user_id != request.user: + if adventure.user != request.user: return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) return super().update(request, *args, **kwargs) @@ -165,7 +165,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet): instance = self.get_object() adventure = instance.adventure - if adventure.user_id != request.user: + if adventure.user != request.user: return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) return super().destroy(request, *args, **kwargs) @@ -176,7 +176,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet): instance = self.get_object() adventure = instance.adventure - if adventure.user_id != request.user: + if adventure.user != request.user: return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) return super().partial_update(request, *args, **kwargs) @@ -192,9 +192,9 @@ class AdventureImageViewSet(viewsets.ModelViewSet): return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST) # Updated queryset to include images from adventures the user owns OR has shared access to - queryset = AdventureImage.objects.filter( + queryset = LocationImage.objects.filter( Q(adventure__id=adventure_uuid) & ( - Q(adventure__user_id=request.user) | # User owns the adventure + Q(adventure__user=request.user) | # User owns the adventure Q(adventure__collections__shared_with=request.user) # User has shared access via collection ) ).distinct() @@ -204,12 +204,12 @@ class AdventureImageViewSet(viewsets.ModelViewSet): def get_queryset(self): # Updated to include images from adventures the user owns OR has shared access to - return AdventureImage.objects.filter( - Q(adventure__user_id=self.request.user) | # User owns the adventure + return LocationImage.objects.filter( + Q(adventure__user=self.request.user) | # User owns the adventure Q(adventure__collections__shared_with=self.request.user) # User has shared access via collection ).distinct() def perform_create(self, serializer): # Always set the image owner to the adventure owner, not the current user adventure = serializer.validated_data['adventure'] - serializer.save(user_id=adventure.user_id) \ No newline at end of file + serializer.save(user=adventure.user) \ No newline at end of file diff --git a/backend/server/adventures/views/adventure_view.py b/backend/server/adventures/views/adventure_view.py index b4bbfb9..3e4f0c6 100644 --- a/backend/server/adventures/views/adventure_view.py +++ b/backend/server/adventures/views/adventure_view.py @@ -8,7 +8,7 @@ from rest_framework.decorators import action from rest_framework.response import Response import requests -from adventures.models import Adventure, Category, Transportation, Lodging +from adventures.models import Location, Category, Transportation, Lodging from adventures.permissions import IsOwnerOrSharedWithFullAccess from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer from adventures.utils import pagination @@ -35,13 +35,13 @@ class AdventureViewSet(viewsets.ModelViewSet): if not user.is_authenticated: if self.action in public_allowed_actions: - return Adventure.objects.retrieve_adventures( + return Location.objects.retrieve_adventures( user, include_public=True ).order_by('-updated_at') - return Adventure.objects.none() + return Location.objects.none() include_public = self.action in public_allowed_actions - return Adventure.objects.retrieve_adventures( + return Location.objects.retrieve_adventures( user, include_public=include_public, include_owned=True, @@ -116,7 +116,7 @@ class AdventureViewSet(viewsets.ModelViewSet): # Use the current user as owner since ManyToMany allows multiple collection owners user_to_assign = self.request.user - serializer.save(user_id=user_to_assign) + serializer.save(user=user_to_assign) def perform_update(self, serializer): """Update adventure.""" @@ -153,12 +153,12 @@ class AdventureViewSet(viewsets.ModelViewSet): # Handle 'all' types if 'all' in types: types = Category.objects.filter( - user_id=request.user + user=request.user ).values_list('name', flat=True) else: # Validate provided types if not types or not all( - Category.objects.filter(user_id=request.user, name=type_name).exists() + Category.objects.filter(user=request.user, name=type_name).exists() for type_name in types ): return Response( @@ -167,9 +167,9 @@ class AdventureViewSet(viewsets.ModelViewSet): ) # Build base queryset - queryset = Adventure.objects.filter( - category__in=Category.objects.filter(name__in=types, user_id=request.user), - user_id=request.user.id + queryset = Location.objects.filter( + category__in=Category.objects.filter(name__in=types, user=request.user), + user=request.user.id ) # Apply visit status filtering @@ -187,12 +187,12 @@ class AdventureViewSet(viewsets.ModelViewSet): include_collections = request.query_params.get('include_collections', 'false') == 'true' # Build queryset with collection filtering - base_filter = Q(user_id=request.user.id) + base_filter = Q(user=request.user.id) if include_collections: - queryset = Adventure.objects.filter(base_filter) + queryset = Location.objects.filter(base_filter) else: - queryset = Adventure.objects.filter(base_filter, collections__isnull=True) + queryset = Location.objects.filter(base_filter, collections__isnull=True) queryset = self.apply_sorting(queryset) serializer = self.get_serializer(queryset, many=True) @@ -225,7 +225,7 @@ class AdventureViewSet(viewsets.ModelViewSet): def _validate_collection_permissions(self, collections): """Validate user has permission to use all provided collections. Only the owner or shared users can use collections.""" for collection in collections: - if not (collection.user_id == self.request.user or + if not (collection.user == self.request.user or collection.shared_with.filter(uuid=self.request.user.uuid).exists()): raise PermissionDenied( f"You do not have permission to use collection '{collection.name}'." @@ -235,7 +235,7 @@ class AdventureViewSet(viewsets.ModelViewSet): """Validate permissions for collection updates (add/remove).""" # Check permissions for new collections being added for collection in new_collections: - if (collection.user_id != self.request.user and + if (collection.user != self.request.user and not collection.shared_with.filter(uuid=self.request.user.uuid).exists()): raise PermissionDenied( f"You do not have permission to use collection '{collection.name}'." @@ -247,7 +247,7 @@ class AdventureViewSet(viewsets.ModelViewSet): collections_to_remove = current_collections - new_collections_set for collection in collections_to_remove: - if (collection.user_id != self.request.user and + if (collection.user != self.request.user and not collection.shared_with.filter(uuid=self.request.user.uuid).exists()): raise PermissionDenied( f"You cannot remove the adventure from collection '{collection.name}' " @@ -284,7 +284,7 @@ class AdventureViewSet(viewsets.ModelViewSet): return True # Check ownership - if user.is_authenticated and adventure.user_id == user: + if user.is_authenticated and adventure.user == user: return True # Check shared collection access diff --git a/backend/server/adventures/views/attachment_view.py b/backend/server/adventures/views/attachment_view.py index 2ca4770..4fcc2cd 100644 --- a/backend/server/adventures/views/attachment_view.py +++ b/backend/server/adventures/views/attachment_view.py @@ -2,7 +2,7 @@ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from adventures.models import Adventure, Attachment +from adventures.models import Location, Attachment from adventures.serializers import AttachmentSerializer class AttachmentViewSet(viewsets.ModelViewSet): @@ -10,7 +10,7 @@ class AttachmentViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return Attachment.objects.filter(user_id=self.request.user) + return Attachment.objects.filter(user=self.request.user) @action(detail=True, methods=['post']) def attachment_delete(self, request, *args, **kwargs): @@ -19,38 +19,38 @@ class AttachmentViewSet(viewsets.ModelViewSet): def create(self, request, *args, **kwargs): if not request.user.is_authenticated: return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED) - adventure_id = request.data.get('adventure') + location_id = request.data.get('location') try: - adventure = Adventure.objects.get(id=adventure_id) - except Adventure.DoesNotExist: - return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND) - - if adventure.user_id != request.user: - # Check if the adventure has any collections - if adventure.collections.exists(): - # Check if the user is in the shared_with list of any of the adventure's collections + location = Location.objects.get(id=location_id) + except Location.DoesNotExist: + return Response({"error": "Location not found"}, status=status.HTTP_404_NOT_FOUND) + + if location.user != request.user: + # Check if the location has any collections + if location.collections.exists(): + # Check if the user is in the shared_with list of any of the location's collections user_has_access = False - for collection in adventure.collections.all(): + for collection in location.collections.all(): if collection.shared_with.filter(id=request.user.id).exists(): user_has_access = True break if not user_has_access: - return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN) + return Response({"error": "User does not have permission to access this location"}, status=status.HTTP_403_FORBIDDEN) else: - return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) + return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN) return super().create(request, *args, **kwargs) def perform_create(self, serializer): - adventure_id = self.request.data.get('adventure') - adventure = Adventure.objects.get(id=adventure_id) - - # If the adventure belongs to collections, set the owner to the collection owner - if adventure.collections.exists(): + location_id = self.request.data.get('location') + location = Location.objects.get(id=location_id) + + # If the location belongs to collections, set the owner to the collection owner + if location.collections.exists(): # Get the first collection's owner (assuming all collections have the same owner) - collection = adventure.collections.first() - serializer.save(user_id=collection.user_id) + collection = location.collections.first() + serializer.save(user=collection.user) else: # Otherwise, set the owner to the request user - serializer.save(user_id=self.request.user) \ No newline at end of file + serializer.save(user=self.request.user) \ No newline at end of file diff --git a/backend/server/adventures/views/category_view.py b/backend/server/adventures/views/category_view.py index 4bde278..f452fd1 100644 --- a/backend/server/adventures/views/category_view.py +++ b/backend/server/adventures/views/category_view.py @@ -2,7 +2,7 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from adventures.models import Category, Adventure +from adventures.models import Category, Location from adventures.serializers import CategorySerializer class CategoryViewSet(viewsets.ModelViewSet): @@ -11,7 +11,7 @@ class CategoryViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return Category.objects.filter(user_id=self.request.user) + return Category.objects.filter(user=self.request.user) @action(detail=False, methods=['get']) def categories(self, request): @@ -24,7 +24,7 @@ class CategoryViewSet(viewsets.ModelViewSet): def destroy(self, request, *args, **kwargs): instance = self.get_object() - if instance.user_id != request.user: + if instance.user != request.user: return Response({"error": "User does not own this category"}, status =400) @@ -32,11 +32,11 @@ class CategoryViewSet(viewsets.ModelViewSet): return Response({"error": "Cannot delete the general category"}, status=400) # set any adventures with this category to a default category called general before deleting the category, if general does not exist create it for the user - general_category = Category.objects.filter(user_id=request.user, name='general').first() + general_category = Category.objects.filter(user=request.user, name='general').first() if not general_category: - general_category = Category.objects.create(user_id=request.user, name='general', icon='🌍', display_name='General') + general_category = Category.objects.create(user=request.user, name='general', icon='🌍', display_name='General') - Adventure.objects.filter(category=instance).update(category=general_category) + Location.objects.filter(category=instance).update(category=general_category) return super().destroy(request, *args, **kwargs) \ No newline at end of file diff --git a/backend/server/adventures/views/checklist_view.py b/backend/server/adventures/views/checklist_view.py index 6824f03..705a333 100644 --- a/backend/server/adventures/views/checklist_view.py +++ b/backend/server/adventures/views/checklist_view.py @@ -24,7 +24,7 @@ class ChecklistViewSet(viewsets.ModelViewSet): if not request.user.is_authenticated: return Response({"error": "User is not authenticated"}, status=400) queryset = Checklist.objects.filter( - Q(user_id=request.user.id) + Q(user=request.user.id) ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @@ -41,12 +41,12 @@ class ChecklistViewSet(viewsets.ModelViewSet): if self.action == 'retrieve': # For individual adventure retrieval, include public adventures return Checklist.objects.filter( - Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + Q(is_public=True) | Q(user=self.request.user.id) | Q(collection__shared_with=self.request.user) ).distinct().order_by('-updated_at') else: # For other actions, include user's own adventures and shared adventures return Checklist.objects.filter( - Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + Q(user=self.request.user.id) | Q(collection__shared_with=self.request.user) ).distinct().order_by('-updated_at') def partial_update(self, request, *args, **kwargs): @@ -65,11 +65,11 @@ class ChecklistViewSet(viewsets.ModelViewSet): if new_collection is not None and new_collection!=instance.collection: # Check if the user is the owner of the new collection - if new_collection.user_id != user or instance.user_id != user: + if new_collection.user != user or instance.user != user: raise PermissionDenied("You do not have permission to use this collection.") elif new_collection is None: # Handle the case where the user is trying to set the collection to None - if instance.collection is not None and instance.collection.user_id != user: + if instance.collection is not None and instance.collection.user != user: raise PermissionDenied("You cannot remove the collection as you are not the owner.") # Perform the update @@ -94,11 +94,11 @@ class ChecklistViewSet(viewsets.ModelViewSet): if new_collection is not None and new_collection!=instance.collection: # Check if the user is the owner of the new collection - if new_collection.user_id != user or instance.user_id != user: + if new_collection.user != user or instance.user != user: raise PermissionDenied("You do not have permission to use this collection.") elif new_collection is None: # Handle the case where the user is trying to set the collection to None - if instance.collection is not None and instance.collection.user_id != user: + if instance.collection is not None and instance.collection.user != user: raise PermissionDenied("You cannot remove the collection as you are not the owner.") # Perform the update @@ -119,12 +119,12 @@ class ChecklistViewSet(viewsets.ModelViewSet): if collection: user = self.request.user # Check if the user is the owner or is in the shared_with list - if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists(): + if collection.user != user and not collection.shared_with.filter(id=user.id).exists(): # Return an error response if the user does not have permission raise PermissionDenied("You do not have permission to use this collection.") # if collection the owner of the adventure is the owner of the collection - serializer.save(user_id=collection.user_id) + serializer.save(user=collection.user) return # Save the adventure with the current user as the owner - serializer.save(user_id=self.request.user) \ No newline at end of file + serializer.save(user=self.request.user) \ No newline at end of file diff --git a/backend/server/adventures/views/collection_view.py b/backend/server/adventures/views/collection_view.py index 40ebbd6..eb5eb23 100644 --- a/backend/server/adventures/views/collection_view.py +++ b/backend/server/adventures/views/collection_view.py @@ -4,7 +4,7 @@ from django.db import transaction from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response -from adventures.models import Collection, Adventure, Transportation, Note, Checklist +from adventures.models import Collection, Location, Transportation, Note, Checklist from adventures.permissions import CollectionShared from adventures.serializers import CollectionSerializer from users.models import CustomUser as User @@ -16,7 +16,7 @@ class CollectionViewSet(viewsets.ModelViewSet): pagination_class = pagination.StandardResultsSetPagination # def get_queryset(self): - # return Collection.objects.filter(Q(user_id=self.request.user.id) & Q(is_archived=False)) + # return Collection.objects.filter(Q(user=self.request.user.id) & Q(is_archived=False)) def apply_sorting(self, queryset): order_by = self.request.query_params.get('order_by', 'name') @@ -55,7 +55,7 @@ class CollectionViewSet(viewsets.ModelViewSet): # make sure the user is authenticated if not request.user.is_authenticated: return Response({"error": "User is not authenticated"}, status=400) - queryset = Collection.objects.filter(user_id=request.user.id, is_archived=False) + queryset = Collection.objects.filter(user=request.user.id, is_archived=False) queryset = self.apply_sorting(queryset) collections = self.paginate_and_respond(queryset, request) return collections @@ -66,7 +66,7 @@ class CollectionViewSet(viewsets.ModelViewSet): return Response({"error": "User is not authenticated"}, status=400) queryset = Collection.objects.filter( - Q(user_id=request.user.id) + Q(user=request.user.id) ) queryset = self.apply_sorting(queryset) @@ -80,7 +80,7 @@ class CollectionViewSet(viewsets.ModelViewSet): return Response({"error": "User is not authenticated"}, status=400) queryset = Collection.objects.filter( - Q(user_id=request.user.id) & Q(is_archived=True) + Q(user=request.user.id) & Q(is_archived=True) ) queryset = self.apply_sorting(queryset) @@ -99,7 +99,7 @@ class CollectionViewSet(viewsets.ModelViewSet): if 'collection' in serializer.validated_data: new_collection = serializer.validated_data['collection'] # if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error - if new_collection != instance.collection and new_collection.user_id != request.user: + if new_collection != instance.collection and new_collection.user != request.user: return Response({"error": "User does not own the new collection"}, status=400) # Check if the 'is_public' field is present in the update data @@ -107,12 +107,12 @@ class CollectionViewSet(viewsets.ModelViewSet): new_public_status = serializer.validated_data['is_public'] # if is_public has changed and the user is not the owner of the collection return an error - if new_public_status != instance.is_public and instance.user_id != request.user: - print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.user_id}") + if new_public_status != instance.is_public and instance.user != request.user: + print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.user}") return Response({"error": "User does not own the collection"}, status=400) # Get all adventures in this collection - adventures_in_collection = Adventure.objects.filter(collections=instance) + adventures_in_collection = Location.objects.filter(collections=instance) if new_public_status: # If collection becomes public, make all adventures public @@ -129,7 +129,7 @@ class CollectionViewSet(viewsets.ModelViewSet): adventure_ids_to_set_private.append(adventure.id) # Bulk update those adventures - Adventure.objects.filter(id__in=adventure_ids_to_set_private).update(is_public=False) + Location.objects.filter(id__in=adventure_ids_to_set_private).update(is_public=False) # Update transportations, notes, and checklists related to this collection # These still use direct ForeignKey relationships @@ -207,28 +207,28 @@ class CollectionViewSet(viewsets.ModelViewSet): def get_queryset(self): if self.action == 'destroy': - return Collection.objects.filter(user_id=self.request.user.id) + return Collection.objects.filter(user=self.request.user.id) if self.action in ['update', 'partial_update']: return Collection.objects.filter( - Q(user_id=self.request.user.id) | Q(shared_with=self.request.user) + Q(user=self.request.user.id) | Q(shared_with=self.request.user) ).distinct() if self.action == 'retrieve': if not self.request.user.is_authenticated: return Collection.objects.filter(is_public=True) return Collection.objects.filter( - Q(is_public=True) | Q(user_id=self.request.user.id) | Q(shared_with=self.request.user) + Q(is_public=True) | Q(user=self.request.user.id) | Q(shared_with=self.request.user) ).distinct() # For list action, include collections owned by the user or shared with the user, that are not archived return Collection.objects.filter( - (Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False) + (Q(user=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False) ).distinct() def perform_create(self, serializer): # This is ok because you cannot share a collection when creating it - serializer.save(user_id=self.request.user) + serializer.save(user=self.request.user) def paginate_and_respond(self, queryset, request): paginator = self.pagination_class() diff --git a/backend/server/adventures/views/global_search_view.py b/backend/server/adventures/views/global_search_view.py index d2fa5d3..b681556 100644 --- a/backend/server/adventures/views/global_search_view.py +++ b/backend/server/adventures/views/global_search_view.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.db.models import Q from django.contrib.postgres.search import SearchVector, SearchQuery -from adventures.models import Adventure, Collection +from adventures.models import Location, Collection from adventures.serializers import AdventureSerializer, CollectionSerializer from worldtravel.models import Country, Region, City, VisitedCity, VisitedRegion from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer @@ -31,14 +31,14 @@ class GlobalSearchView(viewsets.ViewSet): } # Adventures: Full-Text Search - adventures = Adventure.objects.annotate( + adventures = Location.objects.annotate( search=SearchVector('name', 'description', 'location') - ).filter(search=SearchQuery(search_term), user_id=request.user) + ).filter(search=SearchQuery(search_term), user=request.user) results["adventures"] = AdventureSerializer(adventures, many=True).data # Collections: Partial Match Search collections = Collection.objects.filter( - Q(name__icontains=search_term) & Q(user_id=request.user) + Q(name__icontains=search_term) & Q(user=request.user) ) results["collections"] = CollectionSerializer(collections, many=True).data @@ -64,10 +64,10 @@ class GlobalSearchView(viewsets.ViewSet): results["cities"] = CitySerializer(cities, many=True).data # Visited Regions and Cities - visited_regions = VisitedRegion.objects.filter(user_id=request.user) + visited_regions = VisitedRegion.objects.filter(user=request.user) results["visited_regions"] = VisitedRegionSerializer(visited_regions, many=True).data - visited_cities = VisitedCity.objects.filter(user_id=request.user) + visited_cities = VisitedCity.objects.filter(user=request.user) results["visited_cities"] = VisitedCitySerializer(visited_cities, many=True).data return Response(results) diff --git a/backend/server/adventures/views/ics_calendar_view.py b/backend/server/adventures/views/ics_calendar_view.py index 9d120e4..56242f2 100644 --- a/backend/server/adventures/views/ics_calendar_view.py +++ b/backend/server/adventures/views/ics_calendar_view.py @@ -4,7 +4,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from icalendar import Calendar, Event, vText, vCalAddress from datetime import datetime, timedelta -from adventures.models import Adventure +from adventures.models import Location from adventures.serializers import AdventureSerializer class IcsCalendarGeneratorViewSet(viewsets.ViewSet): @@ -12,11 +12,10 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet): @action(detail=False, methods=['get']) def generate(self, request): - adventures = Adventure.objects.filter(user_id=request.user) + adventures = Location.objects.filter(user=request.user) serializer = AdventureSerializer(adventures, many=True) user = request.user name = f"{user.first_name} {user.last_name}" - print(serializer.data) cal = Calendar() cal.add('prodid', '-//My Adventure Calendar//example.com//') diff --git a/backend/server/adventures/views/lodging_view.py b/backend/server/adventures/views/lodging_view.py index 16114ba..d526c30 100644 --- a/backend/server/adventures/views/lodging_view.py +++ b/backend/server/adventures/views/lodging_view.py @@ -17,7 +17,7 @@ class LodgingViewSet(viewsets.ModelViewSet): if not request.user.is_authenticated: return Response(status=status.HTTP_403_FORBIDDEN) queryset = Lodging.objects.filter( - Q(user_id=request.user.id) + Q(user=request.user.id) ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @@ -27,11 +27,11 @@ class LodgingViewSet(viewsets.ModelViewSet): if self.action == 'retrieve': # For individual adventure retrieval, include public adventures, user's own adventures and shared adventures return Lodging.objects.filter( - Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id) + Q(is_public=True) | Q(user=user.id) | Q(collection__shared_with=user.id) ).distinct().order_by('-updated_at') # For other actions, include user's own adventures and shared adventures return Lodging.objects.filter( - Q(user_id=user.id) | Q(collection__shared_with=user.id) + Q(user=user.id) | Q(collection__shared_with=user.id) ).distinct().order_by('-updated_at') def partial_update(self, request, *args, **kwargs): @@ -48,11 +48,11 @@ class LodgingViewSet(viewsets.ModelViewSet): if new_collection is not None and new_collection != instance.collection: # Check if the user is the owner of the new collection - if new_collection.user_id != user or instance.user_id != user: + if new_collection.user != user or instance.user != user: raise PermissionDenied("You do not have permission to use this collection.") elif new_collection is None: # Handle the case where the user is trying to set the collection to None - if instance.collection is not None and instance.collection.user_id != user: + if instance.collection is not None and instance.collection.user != user: raise PermissionDenied("You cannot remove the collection as you are not the owner.") # Perform the update @@ -73,12 +73,12 @@ class LodgingViewSet(viewsets.ModelViewSet): if collection: user = self.request.user # Check if the user is the owner or is in the shared_with list - if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists(): + if collection.user != user and not collection.shared_with.filter(id=user.id).exists(): # Return an error response if the user does not have permission raise PermissionDenied("You do not have permission to use this collection.") # if collection the owner of the adventure is the owner of the collection - serializer.save(user_id=collection.user_id) + serializer.save(user=collection.user) return # Save the adventure with the current user as the owner - serializer.save(user_id=self.request.user) \ No newline at end of file + serializer.save(user=self.request.user) \ No newline at end of file diff --git a/backend/server/adventures/views/note_view.py b/backend/server/adventures/views/note_view.py index 5f5f314..55da7b9 100644 --- a/backend/server/adventures/views/note_view.py +++ b/backend/server/adventures/views/note_view.py @@ -24,7 +24,7 @@ class NoteViewSet(viewsets.ModelViewSet): if not request.user.is_authenticated: return Response({"error": "User is not authenticated"}, status=400) queryset = Note.objects.filter( - Q(user_id=request.user.id) + Q(user=request.user.id) ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @@ -41,12 +41,12 @@ class NoteViewSet(viewsets.ModelViewSet): if self.action == 'retrieve': # For individual adventure retrieval, include public adventures return Note.objects.filter( - Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + Q(is_public=True) | Q(user=self.request.user.id) | Q(collection__shared_with=self.request.user) ).distinct().order_by('-updated_at') else: # For other actions, include user's own adventures and shared adventures return Note.objects.filter( - Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + Q(user=self.request.user.id) | Q(collection__shared_with=self.request.user) ).distinct().order_by('-updated_at') def partial_update(self, request, *args, **kwargs): @@ -65,11 +65,11 @@ class NoteViewSet(viewsets.ModelViewSet): if new_collection is not None and new_collection!=instance.collection: # Check if the user is the owner of the new collection - if new_collection.user_id != user or instance.user_id != user: + if new_collection.user != user or instance.user != user: raise PermissionDenied("You do not have permission to use this collection.") elif new_collection is None: # Handle the case where the user is trying to set the collection to None - if instance.collection is not None and instance.collection.user_id != user: + if instance.collection is not None and instance.collection.user != user: raise PermissionDenied("You cannot remove the collection as you are not the owner.") # Perform the update @@ -94,11 +94,11 @@ class NoteViewSet(viewsets.ModelViewSet): if new_collection is not None and new_collection!=instance.collection: # Check if the user is the owner of the new collection - if new_collection.user_id != user or instance.user_id != user: + if new_collection.user != user or instance.user != user: raise PermissionDenied("You do not have permission to use this collection.") elif new_collection is None: # Handle the case where the user is trying to set the collection to None - if instance.collection is not None and instance.collection.user_id != user: + if instance.collection is not None and instance.collection.user != user: raise PermissionDenied("You cannot remove the collection as you are not the owner.") # Perform the update @@ -119,12 +119,12 @@ class NoteViewSet(viewsets.ModelViewSet): if collection: user = self.request.user # Check if the user is the owner or is in the shared_with list - if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists(): + if collection.user != user and not collection.shared_with.filter(id=user.id).exists(): # Return an error response if the user does not have permission raise PermissionDenied("You do not have permission to use this collection.") # if collection the owner of the adventure is the owner of the collection - serializer.save(user_id=collection.user_id) + serializer.save(user=collection.user) return # Save the adventure with the current user as the owner - serializer.save(user_id=self.request.user) + serializer.save(user=self.request.user) diff --git a/backend/server/adventures/views/reverse_geocode_view.py b/backend/server/adventures/views/reverse_geocode_view.py index df45f13..67efd4e 100644 --- a/backend/server/adventures/views/reverse_geocode_view.py +++ b/backend/server/adventures/views/reverse_geocode_view.py @@ -3,7 +3,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from worldtravel.models import Region, City, VisitedRegion, VisitedCity -from adventures.models import Adventure +from adventures.models import Location from adventures.serializers import AdventureSerializer import requests from adventures.geocoding import reverse_geocode @@ -52,7 +52,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): new_regions = {} new_city_count = 0 new_cities = {} - adventures = Adventure.objects.filter(user_id=self.request.user) + adventures = Location.objects.filter(user=self.request.user) serializer = AdventureSerializer(adventures, many=True) for adventure, serialized_adventure in zip(adventures, serializer.data): if serialized_adventure['is_visited'] == True: @@ -69,18 +69,18 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): # data already contains region_id and city_id if 'region_id' in data and data['region_id'] is not None: region = Region.objects.filter(id=data['region_id']).first() - visited_region = VisitedRegion.objects.filter(region=region, user_id=self.request.user).first() + visited_region = VisitedRegion.objects.filter(region=region, user=self.request.user).first() if not visited_region: - visited_region = VisitedRegion(region=region, user_id=self.request.user) + visited_region = VisitedRegion(region=region, user=self.request.user) visited_region.save() new_region_count += 1 new_regions[region.id] = region.name if 'city_id' in data and data['city_id'] is not None: city = City.objects.filter(id=data['city_id']).first() - visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first() + visited_city = VisitedCity.objects.filter(city=city, user=self.request.user).first() if not visited_city: - visited_city = VisitedCity(city=city, user_id=self.request.user) + visited_city = VisitedCity(city=city, user=self.request.user) visited_city.save() new_city_count += 1 new_cities[city.id] = city.name diff --git a/backend/server/adventures/views/stats_view.py b/backend/server/adventures/views/stats_view.py index 8b56f26..3f279e2 100644 --- a/backend/server/adventures/views/stats_view.py +++ b/backend/server/adventures/views/stats_view.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from rest_framework.decorators import action from django.shortcuts import get_object_or_404 from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion -from adventures.models import Adventure, Collection +from adventures.models import Location, Collection from users.serializers import CustomUserDetailsSerializer as PublicUserSerializer from django.contrib.auth import get_user_model @@ -26,18 +26,18 @@ class StatsViewSet(viewsets.ViewSet): user.email = None # get the counts for the user - adventure_count = Adventure.objects.filter( - user_id=user.id).count() + adventure_count = Location.objects.filter( + user=user.id).count() trips_count = Collection.objects.filter( - user_id=user.id).count() + user=user.id).count() visited_city_count = VisitedCity.objects.filter( - user_id=user.id).count() + user=user.id).count() total_cities = City.objects.count() visited_region_count = VisitedRegion.objects.filter( - user_id=user.id).count() + user=user.id).count() total_regions = Region.objects.count() visited_country_count = VisitedRegion.objects.filter( - user_id=user.id).values('region__country').distinct().count() + user=user.id).values('region__country').distinct().count() total_countries = Country.objects.count() return Response({ 'adventure_count': adventure_count, diff --git a/backend/server/adventures/views/transportation_view.py b/backend/server/adventures/views/transportation_view.py index 2bd1e8c..00a6bd6 100644 --- a/backend/server/adventures/views/transportation_view.py +++ b/backend/server/adventures/views/transportation_view.py @@ -17,7 +17,7 @@ class TransportationViewSet(viewsets.ModelViewSet): if not request.user.is_authenticated: return Response(status=status.HTTP_403_FORBIDDEN) queryset = Transportation.objects.filter( - Q(user_id=request.user.id) + Q(user=request.user.id) ) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) @@ -27,11 +27,11 @@ class TransportationViewSet(viewsets.ModelViewSet): if self.action == 'retrieve': # For individual adventure retrieval, include public adventures, user's own adventures and shared adventures return Transportation.objects.filter( - Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id) + Q(is_public=True) | Q(user=user.id) | Q(collection__shared_with=user.id) ).distinct().order_by('-updated_at') # For other actions, include user's own adventures and shared adventures return Transportation.objects.filter( - Q(user_id=user.id) | Q(collection__shared_with=user.id) + Q(user=user.id) | Q(collection__shared_with=user.id) ).distinct().order_by('-updated_at') def partial_update(self, request, *args, **kwargs): @@ -48,11 +48,11 @@ class TransportationViewSet(viewsets.ModelViewSet): if new_collection is not None and new_collection != instance.collection: # Check if the user is the owner of the new collection - if new_collection.user_id != user or instance.user_id != user: + if new_collection.user != user or instance.user != user: raise PermissionDenied("You do not have permission to use this collection.") elif new_collection is None: # Handle the case where the user is trying to set the collection to None - if instance.collection is not None and instance.collection.user_id != user: + if instance.collection is not None and instance.collection.user != user: raise PermissionDenied("You cannot remove the collection as you are not the owner.") # Perform the update @@ -73,12 +73,12 @@ class TransportationViewSet(viewsets.ModelViewSet): if collection: user = self.request.user # Check if the user is the owner or is in the shared_with list - if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists(): + if collection.user != user and not collection.shared_with.filter(id=user.id).exists(): # Return an error response if the user does not have permission raise PermissionDenied("You do not have permission to use this collection.") # if collection the owner of the adventure is the owner of the collection - serializer.save(user_id=collection.user_id) + serializer.save(user=collection.user) return # Save the adventure with the current user as the owner - serializer.save(user_id=self.request.user) \ No newline at end of file + serializer.save(user=self.request.user) \ No newline at end of file diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py index e5540b9..35e5551 100644 --- a/backend/server/integrations/views.py +++ b/backend/server/integrations/views.py @@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated import requests from rest_framework.pagination import PageNumberPagination from django.conf import settings -from adventures.models import AdventureImage +from adventures.models import LocationImage from django.http import HttpResponse from django.shortcuts import get_object_or_404 import logging @@ -253,11 +253,11 @@ class ImmichIntegrationView(viewsets.ViewSet): """ GET an Immich image using the integration and asset ID. Access levels (in order of priority): - 1. Public adventures: accessible by anyone - 2. Private adventures in public collections: accessible by anyone - 3. Private adventures in private collections shared with user: accessible by shared users - 4. Private adventures: accessible only to the owner - 5. No AdventureImage: owner can still view via integration + 1. Public locations: accessible by anyone + 2. Private locations in public collections: accessible by anyone + 3. Private locations in private collections shared with user: accessible by shared users + 4. Private locations: accessible only to the owner + 5. No LocationImage: owner can still view via integration """ if not imageid or not integration_id: return Response({ @@ -268,31 +268,31 @@ class ImmichIntegrationView(viewsets.ViewSet): # Lookup integration and user integration = get_object_or_404(ImmichIntegration, id=integration_id) - owner_id = integration.user_id + owner_id = integration.user # Try to find the image entry with collections and sharing information image_entry = ( - AdventureImage.objects - .filter(immich_id=imageid, user_id=owner_id) - .select_related('adventure') - .prefetch_related('adventure__collections', 'adventure__collections__shared_with') - .order_by('-adventure__is_public') # Public adventures first + LocationImage.objects + .filter(immich_id=imageid, user=owner_id) + .select_related('location') + .prefetch_related('location__collections', 'location__collections__shared_with') + .order_by('-location__is_public') # Public locations first .first() ) # Access control if image_entry: - adventure = image_entry.adventure - collections = adventure.collections.all() - + location = image_entry.location + collections = location.collections.all() + # Determine access level is_authorized = False - - # Level 1: Public adventure (highest priority) - if adventure.is_public: + + # Level 1: Public location (highest priority) + if location.is_public: is_authorized = True - - # Level 2: Private adventure in any public collection + + # Level 2: Private location in any public collection elif any(collection.is_public for collection in collections): is_authorized = True @@ -308,15 +308,15 @@ class ImmichIntegrationView(viewsets.ViewSet): if not is_authorized: return Response({ - 'message': 'This image belongs to a private adventure and you are not authorized.', + 'message': 'This image belongs to a private location and you are not authorized.', 'error': True, 'code': 'immich.permission_denied' }, status=status.HTTP_403_FORBIDDEN) else: - # No AdventureImage exists; allow only the integration owner + # No LocationImage exists; allow only the integration owner if not request.user.is_authenticated or request.user.id != owner_id: return Response({ - 'message': 'Image is not linked to any adventure and you are not the owner.', + 'message': 'Image is not linked to any location and you are not the owner.', 'error': True, 'code': 'immich.not_found' }, status=status.HTTP_404_NOT_FOUND) diff --git a/backend/server/main/utils.py b/backend/server/main/utils.py index 0963ba6..067e888 100644 --- a/backend/server/main/utils.py +++ b/backend/server/main/utils.py @@ -6,5 +6,5 @@ def get_user_uuid(user): class CustomModelSerializer(serializers.ModelSerializer): def to_representation(self, instance): representation = super().to_representation(instance) - representation['user_id'] = get_user_uuid(instance.user_id) + representation['user'] = get_user_uuid(instance.user) return representation \ No newline at end of file diff --git a/backend/server/users/views.py b/backend/server/users/views.py index d5a5a41..682cabe 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -12,7 +12,7 @@ from django.contrib.auth import get_user_model from .serializers import CustomUserDetailsSerializer as PublicUserSerializer from allauth.socialaccount.models import SocialApp from adventures.serializers import AdventureSerializer, CollectionSerializer -from adventures.models import Adventure, Collection +from adventures.models import Location, Collection from allauth.socialaccount.models import SocialAccount User = get_user_model() @@ -99,8 +99,8 @@ class PublicUserDetailView(APIView): user.email = None # Get the users adventures and collections to include in the response - adventures = Adventure.objects.filter(user_id=user, is_public=True) - collections = Collection.objects.filter(user_id=user, is_public=True) + adventures = Location.objects.filter(user=user, is_public=True) + collections = Collection.objects.filter(user=user, is_public=True) adventure_serializer = AdventureSerializer(adventures, many=True) collection_serializer = CollectionSerializer(collections, many=True) diff --git a/backend/server/worldtravel/management/commands/bulk-adventure-geocode.py b/backend/server/worldtravel/management/commands/bulk-adventure-geocode.py index 1a5d7a5..fff255e 100644 --- a/backend/server/worldtravel/management/commands/bulk-adventure-geocode.py +++ b/backend/server/worldtravel/management/commands/bulk-adventure-geocode.py @@ -1,12 +1,12 @@ from django.core.management.base import BaseCommand -from adventures.models import Adventure +from adventures.models import Location 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() + adventures = Location.objects.all() total = adventures.count() self.stdout.write(self.style.SUCCESS(f'Starting bulk geocoding of {total} adventures')) diff --git a/backend/server/worldtravel/migrations/0017_rename_user_id_visitedregion_user.py b/backend/server/worldtravel/migrations/0017_rename_user_id_visitedregion_user.py new file mode 100644 index 0000000..6614c77 --- /dev/null +++ b/backend/server/worldtravel/migrations/0017_rename_user_id_visitedregion_user.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.1 on 2025-06-19 20:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0016_remove_city_insert_id_remove_country_insert_id_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='visitedregion', + old_name='user_id', + new_name='user', + ), + ] diff --git a/backend/server/worldtravel/migrations/0018_rename_user_id_visitedcity_user.py b/backend/server/worldtravel/migrations/0018_rename_user_id_visitedcity_user.py new file mode 100644 index 0000000..43cac9d --- /dev/null +++ b/backend/server/worldtravel/migrations/0018_rename_user_id_visitedcity_user.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.1 on 2025-06-19 20:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0017_rename_user_id_visitedregion_user'), + ] + + operations = [ + migrations.RenameField( + model_name='visitedcity', + old_name='user_id', + new_name='user', + ), + ] diff --git a/backend/server/worldtravel/models.py b/backend/server/worldtravel/models.py index 9e83f59..e4f69e6 100644 --- a/backend/server/worldtravel/models.py +++ b/backend/server/worldtravel/models.py @@ -6,7 +6,7 @@ from django.contrib.gis.db import models as gis_models User = get_user_model() -default_user_id = 1 # Replace with an actual user ID +default_user = 1 # Replace with an actual user ID class Country(models.Model): @@ -50,29 +50,29 @@ class City(models.Model): class VisitedRegion(models.Model): id = models.AutoField(primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) region = models.ForeignKey(Region, on_delete=models.CASCADE) def __str__(self): - return f'{self.region.name} ({self.region.country.country_code}) visited by: {self.user_id.username}' + return f'{self.region.name} ({self.region.country.country_code}) visited by: {self.user.username}' def save(self, *args, **kwargs): - if VisitedRegion.objects.filter(user_id=self.user_id, region=self.region).exists(): + if VisitedRegion.objects.filter(user=self.user, region=self.region).exists(): raise ValidationError("Region already visited by user.") super().save(*args, **kwargs) class VisitedCity(models.Model): id = models.AutoField(primary_key=True) - user_id = models.ForeignKey( - User, on_delete=models.CASCADE, default=default_user_id) + user = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user) city = models.ForeignKey(City, on_delete=models.CASCADE) def __str__(self): - return f'{self.city.name} ({self.city.region.name}) visited by: {self.user_id.username}' + return f'{self.city.name} ({self.city.region.name}) visited by: {self.user.username}' def save(self, *args, **kwargs): - if VisitedCity.objects.filter(user_id=self.user_id, city=self.city).exists(): + if VisitedCity.objects.filter(user=self.user, city=self.city).exists(): raise ValidationError("City already visited by user.") super().save(*args, **kwargs) diff --git a/backend/server/worldtravel/serializers.py b/backend/server/worldtravel/serializers.py index 962914b..a325abd 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -25,7 +25,7 @@ class CountrySerializer(serializers.ModelSerializer): user = getattr(request, 'user', None) if user and user.is_authenticated: - return VisitedRegion.objects.filter(region__country=obj, user_id=user).count() + return VisitedRegion.objects.filter(region__country=obj, user=user).count() return 0 @@ -62,8 +62,8 @@ class VisitedRegionSerializer(CustomModelSerializer): class Meta: model = VisitedRegion - fields = ['id', 'user_id', 'region', 'longitude', 'latitude', 'name'] - read_only_fields = ['user_id', 'id', 'longitude', 'latitude', 'name'] + fields = ['id', 'user', 'region', 'longitude', 'latitude', 'name'] + read_only_fields = ['user', 'id', 'longitude', 'latitude', 'name'] class VisitedCitySerializer(CustomModelSerializer): longitude = serializers.DecimalField(source='city.longitude', max_digits=9, decimal_places=6, read_only=True) @@ -72,5 +72,5 @@ class VisitedCitySerializer(CustomModelSerializer): class Meta: model = VisitedCity - fields = ['id', 'user_id', 'city', 'longitude', 'latitude', 'name'] - read_only_fields = ['user_id', 'id', 'longitude', 'latitude', 'name'] \ No newline at end of file + fields = ['id', 'user', 'city', 'longitude', 'latitude', 'name'] + read_only_fields = ['user', 'id', 'longitude', 'latitude', 'name'] \ No newline at end of file diff --git a/backend/server/worldtravel/views.py b/backend/server/worldtravel/views.py index c77309d..c418bf3 100644 --- a/backend/server/worldtravel/views.py +++ b/backend/server/worldtravel/views.py @@ -13,7 +13,7 @@ from django.contrib.gis.geos import Point from django.conf import settings from rest_framework.decorators import action from django.contrib.staticfiles import finders -from adventures.models import Adventure +from adventures.models import Location @api_view(['GET']) @permission_classes([IsAuthenticated]) @@ -28,7 +28,7 @@ def regions_by_country(request, country_code): @permission_classes([IsAuthenticated]) def visits_by_country(request, country_code): country = get_object_or_404(Country, country_code=country_code) - visits = VisitedRegion.objects.filter(region__country=country, user_id=request.user.id) + visits = VisitedRegion.objects.filter(region__country=country, user=request.user.id) serializer = VisitedRegionSerializer(visits, many=True) return Response(serializer.data) @@ -45,7 +45,7 @@ def cities_by_region(request, region_id): @permission_classes([IsAuthenticated]) def visits_by_region(request, region_id): region = get_object_or_404(Region, id=region_id) - visits = VisitedCity.objects.filter(city__region=region, user_id=request.user.id) + visits = VisitedCity.objects.filter(city__region=region, user=request.user.id) serializer = VisitedCitySerializer(visits, many=True) return Response(serializer.data) @@ -71,7 +71,7 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet): # make a post action that will get all of the users adventures and check if the point is in any of the regions if so make a visited region object for that user if it does not already exist @action(detail=False, methods=['post']) def region_check_all_adventures(self, request): - adventures = Adventure.objects.filter(user_id=request.user.id, type='visited') + adventures = Location.objects.filter(user=request.user.id, type='visited') count = 0 for adventure in adventures: if adventure.latitude is not None and adventure.longitude is not None: @@ -79,8 +79,8 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet): point = Point(float(adventure.longitude), float(adventure.latitude), srid=4326) region = Region.objects.filter(geometry__contains=point).first() if region: - if not VisitedRegion.objects.filter(user_id=request.user.id, region=region).exists(): - VisitedRegion.objects.create(user_id=request.user, region=region) + if not VisitedRegion.objects.filter(user=request.user.id, region=region).exists(): + VisitedRegion.objects.create(user=request.user, region=region) count += 1 except Exception as e: print(f"Error processing adventure {adventure.id}: {e}") @@ -97,14 +97,14 @@ class VisitedRegionViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return VisitedRegion.objects.filter(user_id=self.request.user.id) + return VisitedRegion.objects.filter(user=self.request.user.id) def perform_create(self, serializer): - serializer.save(user_id=self.request.user) + serializer.save(user=self.request.user) def create(self, request, *args, **kwargs): - request.data['user_id'] = request.user - if VisitedRegion.objects.filter(user_id=request.user.id, region=request.data['region']).exists(): + request.data['user'] = request.user + if VisitedRegion.objects.filter(user=request.user.id, region=request.data['region']).exists(): return Response({"error": "Region already visited by user."}, status=400) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -115,7 +115,7 @@ class VisitedRegionViewSet(viewsets.ModelViewSet): def destroy(self, request, **kwargs): # delete by region id region = get_object_or_404(Region, id=kwargs['pk']) - visited_region = VisitedRegion.objects.filter(user_id=request.user.id, region=region) + visited_region = VisitedRegion.objects.filter(user=request.user.id, region=region) if visited_region.exists(): visited_region.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -127,27 +127,27 @@ class VisitedCityViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return VisitedCity.objects.filter(user_id=self.request.user.id) + return VisitedCity.objects.filter(user=self.request.user.id) def perform_create(self, serializer): - serializer.save(user_id=self.request.user) + serializer.save(user=self.request.user) def create(self, request, *args, **kwargs): - request.data['user_id'] = request.user + request.data['user'] = request.user serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) # if the region is not visited, visit it region = serializer.validated_data['city'].region - if not VisitedRegion.objects.filter(user_id=request.user.id, region=region).exists(): - VisitedRegion.objects.create(user_id=request.user, region=region) + if not VisitedRegion.objects.filter(user=request.user.id, region=region).exists(): + VisitedRegion.objects.create(user=request.user, region=region) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def destroy(self, request, **kwargs): # delete by city id city = get_object_or_404(City, id=kwargs['pk']) - visited_city = VisitedCity.objects.filter(user_id=request.user.id, city=city) + visited_city = VisitedCity.objects.filter(user=request.user.id, city=city) if visited_city.exists(): visited_city.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index d0f6b98..46c473a 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -280,7 +280,7 @@ {$t('adventures.open_details')} - {#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))} + {#if adventure.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}