1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-05 05:05:17 +02:00

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.
This commit is contained in:
Sean Morley 2025-06-19 16:50:11 -04:00
parent 7a17e0e1d8
commit 241d27a1a6
50 changed files with 918 additions and 792 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = []

View file

@ -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)
serializer.save(user=adventure.user)

View file

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

View file

@ -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)
serializer.save(user=self.request.user)

View file

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

View file

@ -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)
serializer.save(user=self.request.user)

View file

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

View file

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

View file

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

View file

@ -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)
serializer.save(user=self.request.user)

View file

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

View file

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

View file

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

View file

@ -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)
serializer.save(user=self.request.user)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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']
fields = ['id', 'user', 'city', 'longitude', 'latitude', 'name']
read_only_fields = ['user', 'id', 'longitude', 'latitude', 'name']

View file

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

View file

@ -280,7 +280,7 @@
{$t('adventures.open_details')}
</button>
{#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))}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-square btn-sm btn-base-300">
<DotsHorizontal class="w-5 h-5" />
@ -297,7 +297,7 @@
</button>
</li>
{#if user?.uuid == adventure.user_id}
{#if user?.uuid == adventure.user}
<li>
<button
on:click={() => (isCollectionModalOpen = true)}

View file

@ -110,13 +110,13 @@
longitude: NaN,
location: null,
images: [],
user_id: null,
user: null,
category: {
id: '',
name: '',
display_name: '',
icon: '',
user_id: ''
user: ''
},
attachments: []
};
@ -135,7 +135,7 @@
longitude: adventureToEdit?.longitude || NaN,
location: adventureToEdit?.location || null,
images: adventureToEdit?.images || [],
user_id: adventureToEdit?.user_id || null,
user: adventureToEdit?.user || null,
visits: adventureToEdit?.visits || [],
is_visited: adventureToEdit?.is_visited || false,
category: adventureToEdit?.category || {
@ -143,7 +143,7 @@
name: '',
display_name: '',
icon: '',
user_id: ''
user: ''
},
attachments: adventureToEdit?.attachments || []
@ -245,7 +245,7 @@
const formData = new FormData();
formData.append('file', file);
formData.append('adventure', adventure.id);
formData.append('location', adventure.id);
formData.append('name', attachmentName);
try {
@ -451,7 +451,7 @@
name: 'general',
display_name: 'General',
icon: '🌍',
user_id: ''
user: ''
};
}
}

View file

@ -10,7 +10,7 @@
display_name: '',
icon: '',
id: '',
user_id: '',
user: '',
num_adventures: 0
};

View file

@ -99,7 +99,7 @@
<Launch class="w-5 h-5" />
{$t('notes.open')}
</button>
{#if checklist.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if checklist.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Checklist"

View file

@ -19,7 +19,7 @@
let warning: string | null = '';
let isReadOnly =
!(checklist && user?.uuid == checklist?.user_id) &&
!(checklist && user?.uuid == checklist?.user) &&
!(user && collection && collection.shared_with && collection.shared_with.includes(user.uuid)) &&
!!checklist;
let newStatus: boolean = false;
@ -40,7 +40,7 @@
name: newItem,
is_checked: newStatus,
id: '',
user_id: '',
user: '',
checklist: 0,
created_at: '',
updated_at: ''

View file

@ -170,7 +170,7 @@
<Launch class="w-4 h-4" />
{$t('adventures.open_details')}
</button>
{#if user && user.uuid == collection.user_id}
{#if user && user.uuid == collection.user}
<div class="dropdown dropdown-end">
<button type="button" class="btn btn-square btn-sm btn-base-300">
<DotsHorizontal class="w-5 h-5" />

View file

@ -17,7 +17,7 @@
description: collectionToEdit?.description || '',
start_date: collectionToEdit?.start_date || null,
end_date: collectionToEdit?.end_date || null,
user_id: collectionToEdit?.user_id || '',
user: collectionToEdit?.user || '',
is_public: collectionToEdit?.is_public || false,
adventures: collectionToEdit?.adventures || [],
link: collectionToEdit?.link || '',

View file

@ -156,7 +156,7 @@
</div>
<!-- Reservation Info -->
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if lodging.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="space-y-2">
{#if lodging.reservation_number}
<div class="flex items-center gap-2">
@ -174,7 +174,7 @@
{/if}
<!-- Actions -->
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if lodging.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
<button
class="btn btn-neutral btn-sm flex items-center gap-1"

View file

@ -31,7 +31,7 @@
function initializeLodging(lodgingToEdit: Lodging | null): Lodging {
return {
id: lodgingToEdit?.id || '',
user_id: lodgingToEdit?.user_id || '',
user: lodgingToEdit?.user || '',
name: lodgingToEdit?.name || '',
type: lodgingToEdit?.type || 'other',
description: lodgingToEdit?.description || '',

View file

@ -124,7 +124,7 @@
<Launch class="w-5 h-5" />
{$t('notes.open')}
</button>
{#if note.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if note.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Adventure"

View file

@ -20,7 +20,7 @@
let constrainDates: boolean = false;
let isReadOnly =
!(note && user?.uuid == note?.user_id) &&
!(note && user?.uuid == note?.user) &&
!(user && collection && collection.shared_with && collection.shared_with.includes(user.uuid)) &&
!!note;

View file

@ -192,7 +192,7 @@
</div>
<!-- Actions -->
{#if transportation.user_id === user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
{#if transportation.user === user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
<button
class="btn btn-neutral btn-sm flex items-center gap-1"

View file

@ -29,7 +29,7 @@
flight_number: transportationToEdit?.flight_number || '',
from_location: transportationToEdit?.from_location || '',
to_location: transportationToEdit?.to_location || '',
user_id: transportationToEdit?.user_id || '',
user: transportationToEdit?.user || '',
is_public: transportationToEdit?.is_public || false,
collection: transportationToEdit?.collection || collection.id,
created_at: transportationToEdit?.created_at || '',

View file

@ -16,7 +16,7 @@ export type User = {
export type Adventure = {
id: string;
user_id: string | null;
user: string | null;
name: string;
location?: string | null;
activity_types?: string[] | null;
@ -96,7 +96,7 @@ export type City = {
export type VisitedRegion = {
id: number;
region: string;
user_id: string;
user: string;
longitude: number;
latitude: number;
name: string;
@ -105,7 +105,7 @@ export type VisitedRegion = {
export type VisitedCity = {
id: number;
city: string;
user_id: string;
user: string;
longitude: number;
latitude: number;
name: string;
@ -123,7 +123,7 @@ export type Point = {
export type Collection = {
id: string;
user_id: string;
user: string;
name: string;
description: string;
is_public: boolean;
@ -153,7 +153,7 @@ export type GeocodeSearchResult = {
export type Transportation = {
id: string;
user_id: string;
user: string;
type: string;
name: string;
description: string | null;
@ -179,7 +179,7 @@ export type Transportation = {
export type Note = {
id: string;
user_id: string;
user: string;
name: string;
content: string | null;
links: string[] | null;
@ -192,7 +192,7 @@ export type Note = {
export type Checklist = {
id: string;
user_id: string;
user: string;
name: string;
items: ChecklistItem[];
date: string | null; // ISO 8601 date string
@ -204,7 +204,7 @@ export type Checklist = {
export type ChecklistItem = {
id: string;
user_id: string;
user: string;
name: string;
is_checked: boolean;
checklist: number;
@ -235,7 +235,7 @@ export type Category = {
name: string;
display_name: string;
icon: string;
user_id: string;
user: string;
num_adventures?: number | null;
};
@ -279,13 +279,13 @@ export type Attachment = {
file: string;
adventure: string;
extension: string;
user_id: string;
user: string;
name: string;
};
export type Lodging = {
id: string;
user_id: string;
user: string;
name: string;
type: string;
description: string | null;

View file

@ -150,7 +150,7 @@
{/if}
{#if adventure}
{#if data.user && data.user.uuid == adventure.user_id}
{#if data.user && data.user.uuid == adventure.user}
<div class="fixed bottom-6 right-6 z-50">
<button
class="btn btn-primary btn-circle w-16 h-16 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-110"

View file

@ -498,7 +498,7 @@
function recomendationToAdventure(recomendation: any) {
adventureToEdit = {
id: '',
user_id: null,
user: null,
name: recomendation.name,
latitude: recomendation.latitude,
longitude: recomendation.longitude,
@ -513,7 +513,7 @@
icon: osmTagToEmoji(recomendation.tag),
id: '',
name: recomendation.tag,
user_id: ''
user: ''
},
attachments: []
};
@ -748,7 +748,7 @@
</div>
{/if}
{#if collection && collection.id}
{#if data.user && data.user.uuid && (data.user.uuid == collection.user_id || (collection.shared_with && collection.shared_with.includes(data.user.uuid))) && !collection.is_archived}
{#if data.user && data.user.uuid && (data.user.uuid == collection.user || (collection.shared_with && collection.shared_with.includes(data.user.uuid))) && !collection.is_archived}
<div class="fixed bottom-4 right-4 z-[999]">
<div class="flex flex-row items-center justify-center gap-4">
<div class="dropdown dropdown-top dropdown-end z-[999]">
@ -760,7 +760,7 @@
tabindex="0"
class="dropdown-content z-[1] menu p-4 shadow bg-base-300 text-base-content rounded-box w-52 gap-4"
>
{#if collection.user_id === data.user.uuid}
{#if collection.user === data.user.uuid}
<p class="text-center font-bold text-lg">{$t('adventures.link_new')}</p>
<button
class="btn btn-primary"