mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 15:29:36 +02:00
Rename Adventures to Locations (#696)
* 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. * Refactor adventure-related views and components to use "Location" terminology - Updated GlobalSearchView to replace AdventureSerializer with LocationSerializer. - Modified IcsCalendarGeneratorViewSet to use LocationSerializer instead of AdventureSerializer. - Created new LocationImageViewSet for managing location images, including primary image toggling and image deletion. - Introduced LocationViewSet for managing locations with enhanced filtering, sorting, and sharing capabilities. - Updated ReverseGeocodeViewSet to utilize LocationSerializer. - Added ActivityTypesView to retrieve distinct activity types from locations. - Refactored user views to replace AdventureSerializer with LocationSerializer. - Updated frontend components to reflect changes from "adventure" to "location", including AdventureCard, AdventureLink, AdventureModal, and others. - Adjusted API endpoints in frontend routes to align with new location-based structure. - Ensured all references to adventures are replaced with locations across the codebase. * refactor: rename adventures to locations across the application - Updated localization files to replace adventure-related terms with location-related terms. - Refactored TypeScript types and variables from Adventure to Location in various routes and components. - Adjusted UI elements and labels to reflect the change from adventures to locations. - Ensured all references to adventures in the codebase are consistent with the new location terminology. * Refactor code structure for improved readability and maintainability * feat: Implement location details page with server-side loading and deletion functionality - Added +page.server.ts to handle server-side loading of additional location info. - Created +page.svelte for displaying location details, including images, visits, and maps. - Integrated GPX file handling and rendering on the map. - Updated map route to link to locations instead of adventures. - Refactored profile and search routes to use LocationCard instead of AdventureCard. * docs: Update terminology from "Adventure" to "Location" and enhance project overview * docs: Clarify collection examples in usage documentation * feat: Enable credentials for GPX file fetch and add CORS_ALLOW_CREDENTIALS setting * Refactor adventure references to locations across the backend and frontend - Updated CategoryViewSet to reflect location context instead of adventures. - Modified ChecklistViewSet to include locations in retrieval logic. - Changed GlobalSearchView to search for locations instead of adventures. - Adjusted IcsCalendarGeneratorViewSet to handle locations instead of adventures. - Refactored LocationImageViewSet to remove unused import. - Updated LocationViewSet to clarify public access for locations. - Changed LodgingViewSet to reference locations instead of adventures. - Modified NoteViewSet to prevent listing all locations. - Updated RecommendationsViewSet to handle locations in parsing and response. - Adjusted ReverseGeocodeViewSet to search through user locations. - Updated StatsViewSet to count locations instead of adventures. - Changed TagsView to reflect activity types for locations. - Updated TransportationViewSet to reference locations instead of adventures. - Added new translations for search results related to locations in multiple languages. - Updated dashboard and profile pages to reflect location counts instead of adventure counts. - Adjusted search routes to handle locations instead of adventures. * Update banner image * style: Update stats component background and border for improved visibility * refactor: Rename AdventureCard and AdventureModal to LocationCard and LocationModal for consistency
This commit is contained in:
parent
5308ec21d6
commit
493a13995c
115 changed files with 3148 additions and 2759 deletions
|
@ -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
|
||||
|
||||
|
@ -11,19 +11,19 @@ admin.site.login = secure_admin_login(admin.site.login)
|
|||
@admin.action(description="Trigger geocoding")
|
||||
def trigger_geocoding(modeladmin, request, queryset):
|
||||
count = 0
|
||||
for adventure in queryset:
|
||||
for location in queryset:
|
||||
try:
|
||||
adventure.save() # Triggers geocoding logic in your model
|
||||
location.save() # Triggers geocoding logic in your model
|
||||
count += 1
|
||||
except Exception as e:
|
||||
modeladmin.message_user(request, f"Error geocoding {adventure}: {e}", level='error')
|
||||
modeladmin.message_user(request, f"Geocoding triggered for {count} adventures.", level='success')
|
||||
modeladmin.message_user(request, f"Error geocoding {location}: {e}", level='error')
|
||||
modeladmin.message_user(request, f"Geocoding triggered for {count} locations.", level='success')
|
||||
|
||||
|
||||
|
||||
class AdventureAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
|
||||
list_filter = ( 'user_id', 'is_public')
|
||||
class LocationAdmin(admin.ModelAdmin):
|
||||
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]
|
||||
|
@ -96,8 +96,8 @@ class CustomUserAdmin(UserAdmin):
|
|||
else:
|
||||
return
|
||||
|
||||
class AdventureImageAdmin(admin.ModelAdmin):
|
||||
list_display = ('user_id', 'image_display')
|
||||
class LocationImageAdmin(admin.ModelAdmin):
|
||||
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, LocationAdmin)
|
||||
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, LocationImageAdmin)
|
||||
admin.site.register(Category, CategoryAdmin)
|
||||
admin.site.register(City, CityAdmin)
|
||||
admin.site.register(VisitedCity)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
class AdventureManager(models.Manager):
|
||||
def retrieve_adventures(self, user, include_owned=False, include_shared=False, include_public=False):
|
||||
class LocationManager(models.Manager):
|
||||
def retrieve_locations(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)
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.2.1 on 2025-06-20 15:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('adventures', '0036_rename_adventure_location_squashed_0050_rename_user_id_lodging_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='location',
|
||||
old_name='activity_types',
|
||||
new_name='tags',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='collections',
|
||||
field=models.ManyToManyField(blank=True, related_name='locations', to='adventures.collection'),
|
||||
),
|
||||
]
|
|
@ -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,45 @@ 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)
|
||||
|
||||
# print(f"[Adventure Geocode Thread] Successfully processed {adventure_id}: {adventure.name} - {adventure.latitude}, {adventure.longitude}")
|
||||
location.save(update_fields=["region", "city", "country"], _skip_geocode=True)
|
||||
|
||||
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 +60,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 +86,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 +112,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,18 +131,18 @@ 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)
|
||||
location = models.CharField(max_length=200, blank=True, null=True)
|
||||
activity_types = ArrayField(models.CharField(
|
||||
tags = ArrayField(models.CharField(
|
||||
max_length=100), blank=True, null=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
rating = models.FloatField(blank=True, null=True)
|
||||
|
@ -578,12 +157,12 @@ class Adventure(models.Model):
|
|||
country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True)
|
||||
|
||||
# Changed from ForeignKey to ManyToManyField
|
||||
collections = models.ManyToManyField('Collection', blank=True, related_name='adventures')
|
||||
collections = models.ManyToManyField('Collection', blank=True, related_name='locations')
|
||||
|
||||
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 +188,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 +207,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': '🌍'}
|
||||
)
|
||||
|
@ -671,8 +250,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)
|
||||
|
@ -684,13 +263,13 @@ class Collection(models.Model):
|
|||
shared_with = models.ManyToManyField(User, related_name='shared_with', blank=True)
|
||||
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||
|
||||
# if connected adventures are private and collection is public, raise an error
|
||||
# if connected locations are private and collection is public, raise an error
|
||||
def clean(self):
|
||||
if self.is_public and self.pk: # Only check if the instance has a primary key
|
||||
# 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}')
|
||||
# Updated to use the new related_name 'locations'
|
||||
for location in self.locations.all():
|
||||
if not location.is_public:
|
||||
raise ValidationError(f'Public collections cannot be associated with private locations. Collection: {self.name} Location: {location.name}')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -698,8 +277,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)
|
||||
|
@ -729,8 +308,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
|
||||
|
@ -738,8 +317,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)
|
||||
|
@ -753,8 +332,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
|
||||
|
@ -762,8 +341,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)
|
||||
|
@ -775,8 +354,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
|
||||
|
@ -784,8 +363,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)
|
||||
|
@ -795,8 +374,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
|
||||
|
@ -812,9 +391,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,
|
||||
|
@ -823,7 +402,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):
|
||||
|
@ -858,10 +437,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 delete(self, *args, **kwargs):
|
||||
|
@ -874,15 +453,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()
|
||||
|
@ -895,8 +474,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)
|
||||
|
@ -921,9 +500,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
|
|
@ -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):
|
||||
|
@ -39,7 +39,7 @@ class CollectionShared(permissions.BasePermission):
|
|||
if obj.shared_with.filter(id=user.id).exists():
|
||||
return True
|
||||
|
||||
# If obj is an Adventure (has collections M2M)
|
||||
# If obj is a Location (has collections M2M)
|
||||
if hasattr(obj, 'collections'):
|
||||
# Check if user is in shared_with of any related collection
|
||||
shared_collections = obj.collections.filter(shared_with=user)
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -9,17 +9,17 @@ from geopy.distance import geodesic
|
|||
from integrations.models import ImmichIntegration
|
||||
|
||||
|
||||
class AdventureImageSerializer(CustomModelSerializer):
|
||||
class LocationImageSerializer(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]
|
||||
|
@ -59,11 +59,11 @@ class AttachmentSerializer(CustomModelSerializer):
|
|||
return representation
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
num_adventures = serializers.SerializerMethodField()
|
||||
num_locations = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['id', 'name', 'display_name', 'icon', 'num_adventures']
|
||||
read_only_fields = ['id', 'num_adventures']
|
||||
fields = ['id', 'name', 'display_name', 'icon', 'num_locations']
|
||||
read_only_fields = ['id', 'num_locations']
|
||||
|
||||
def validate_name(self, value):
|
||||
return value.lower()
|
||||
|
@ -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():
|
||||
|
@ -81,8 +81,8 @@ class CategorySerializer(serializers.ModelSerializer):
|
|||
instance.save()
|
||||
return instance
|
||||
|
||||
def get_num_adventures(self, obj):
|
||||
return Adventure.objects.filter(category=obj, user_id=obj.user_id).count()
|
||||
def get_num_locations(self, obj):
|
||||
return Location.objects.filter(category=obj, user=obj.user).count()
|
||||
|
||||
class VisitSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
@ -91,13 +91,12 @@ class VisitSerializer(serializers.ModelSerializer):
|
|||
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes']
|
||||
read_only_fields = ['id']
|
||||
|
||||
class AdventureSerializer(CustomModelSerializer):
|
||||
class LocationSerializer(CustomModelSerializer):
|
||||
images = serializers.SerializerMethodField()
|
||||
visits = VisitSerializer(many=True, read_only=False, required=False)
|
||||
attachments = AttachmentSerializer(many=True, read_only=True)
|
||||
category = CategorySerializer(read_only=False, required=False)
|
||||
is_visited = serializers.SerializerMethodField()
|
||||
user = serializers.SerializerMethodField()
|
||||
country = CountrySerializer(read_only=True)
|
||||
region = RegionSerializer(read_only=True)
|
||||
city = CitySerializer(read_only=True)
|
||||
|
@ -108,16 +107,22 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
)
|
||||
|
||||
class Meta:
|
||||
model = Adventure
|
||||
model = Location
|
||||
fields = [
|
||||
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
|
||||
'id', 'name', 'description', 'rating', 'tags', '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']
|
||||
|
||||
# Makes it so the whole user object is returned in the serializer instead of just the user uuid
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
representation['user'] = CustomUserDetailsSerializer(instance.user, context=self.context).data
|
||||
return representation
|
||||
|
||||
def get_images(self, obj):
|
||||
serializer = AdventureImageSerializer(obj.images.all(), many=True, context=self.context)
|
||||
serializer = LocationImageSerializer(obj.images.all(), many=True, context=self.context)
|
||||
# Filter out None values from the serialized data
|
||||
return [image for image in serializer.data if image is not None]
|
||||
|
||||
|
@ -128,7 +133,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 +145,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 +167,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,
|
||||
|
@ -171,10 +176,6 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
)
|
||||
return category
|
||||
|
||||
def get_user(self, obj):
|
||||
user = obj.user_id
|
||||
return CustomUserDetailsSerializer(user).data
|
||||
|
||||
def get_is_visited(self, obj):
|
||||
return obj.is_visited_status()
|
||||
|
||||
|
@ -184,24 +185,24 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
collections_data = validated_data.pop('collections', [])
|
||||
|
||||
print(category_data)
|
||||
adventure = Adventure.objects.create(**validated_data)
|
||||
location = Location.objects.create(**validated_data)
|
||||
|
||||
# Handle visits
|
||||
for visit_data in visits_data:
|
||||
Visit.objects.create(adventure=adventure, **visit_data)
|
||||
Visit.objects.create(location=location, **visit_data)
|
||||
|
||||
# Handle category
|
||||
if category_data:
|
||||
category = self.get_or_create_category(category_data)
|
||||
adventure.category = category
|
||||
location.category = category
|
||||
|
||||
# Handle collections - set after adventure is saved
|
||||
# Handle collections - set after location is saved
|
||||
if collections_data:
|
||||
adventure.collections.set(collections_data)
|
||||
location.collections.set(collections_data)
|
||||
|
||||
adventure.save()
|
||||
location.save()
|
||||
|
||||
return adventure
|
||||
return location
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
has_visits = 'visits' in validated_data
|
||||
|
@ -214,9 +215,9 @@ class AdventureSerializer(CustomModelSerializer):
|
|||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
|
||||
# Handle category - ONLY allow the adventure owner to change categories
|
||||
# Handle category - ONLY allow the location 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,13 +242,13 @@ 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
|
||||
instance.visits.filter(id__in=visits_to_delete).delete()
|
||||
|
||||
# call save on the adventure to update the updated_at field and trigger any geocoding
|
||||
# call save on the location to update the updated_at field and trigger any geocoding
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
@ -258,13 +259,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 +285,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 +315,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 +349,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 +364,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
|
||||
)
|
||||
|
||||
|
@ -391,7 +392,7 @@ class ChecklistSerializer(CustomModelSerializer):
|
|||
return data
|
||||
|
||||
class CollectionSerializer(CustomModelSerializer):
|
||||
adventures = AdventureSerializer(many=True, read_only=True)
|
||||
locations = LocationSerializer(many=True, read_only=True)
|
||||
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
||||
notes = NoteSerializer(many=True, read_only=True, source='note_set')
|
||||
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
|
||||
|
@ -399,8 +400,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', 'locations', '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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,11 +3,11 @@ from rest_framework.routers import DefaultRouter
|
|||
from adventures.views import *
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
||||
router.register(r'locations', LocationViewSet, basename='locations')
|
||||
router.register(r'collections', CollectionViewSet, basename='collections')
|
||||
router.register(r'stats', StatsViewSet, basename='stats')
|
||||
router.register(r'generate', GenerateDescription, basename='generate')
|
||||
router.register(r'activity-types', ActivityTypesView, basename='activity-types')
|
||||
router.register(r'tags', ActivityTypesView, basename='tags')
|
||||
router.register(r'transportations', TransportationViewSet, basename='transportations')
|
||||
router.register(r'notes', NoteViewSet, basename='notes')
|
||||
router.register(r'checklists', ChecklistViewSet, basename='checklists')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from adventures.models import AdventureImage, Attachment
|
||||
from adventures.models import LocationImage, Attachment
|
||||
|
||||
protected_paths = ['images/', 'attachments/']
|
||||
|
||||
|
@ -10,20 +10,20 @@ 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
|
||||
if adventure.is_public:
|
||||
location = LocationImage.objects.get(image=image_path).location
|
||||
if location.is_public:
|
||||
return True
|
||||
elif adventure.user_id == user:
|
||||
elif location.user == user:
|
||||
return True
|
||||
elif adventure.collections.exists():
|
||||
elif location.collections.exists():
|
||||
# Check if the user is in any collection's shared_with list
|
||||
for collection in adventure.collections.all():
|
||||
for collection in location.collections.all():
|
||||
if collection.shared_with.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
except AdventureImage.DoesNotExist:
|
||||
except LocationImage.DoesNotExist:
|
||||
return False
|
||||
elif mediaType == 'attachments/':
|
||||
try:
|
||||
|
@ -31,14 +31,14 @@ def checkFilePermission(fileId, user, mediaType):
|
|||
attachment_path = f"attachments/{fileId}"
|
||||
# Fetch the Attachment object
|
||||
attachment = Attachment.objects.get(file=attachment_path)
|
||||
adventure = attachment.adventure
|
||||
if adventure.is_public:
|
||||
location = attachment.location
|
||||
if location.is_public:
|
||||
return True
|
||||
elif adventure.user_id == user:
|
||||
elif location.user == user:
|
||||
return True
|
||||
elif adventure.collections.exists():
|
||||
elif location.collections.exists():
|
||||
# Check if the user is in any collection's shared_with list
|
||||
for collection in adventure.collections.all():
|
||||
for collection in location.collections.all():
|
||||
if collection.shared_with.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
|
|
419
backend/server/adventures/utils/timezones.py
Normal file
419
backend/server/adventures/utils/timezones.py
Normal 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"
|
||||
]
|
|
@ -1,6 +1,6 @@
|
|||
from .activity_types_view import *
|
||||
from .adventure_image_view import *
|
||||
from .adventure_view import *
|
||||
from .tags_view import *
|
||||
from .location_image_view import *
|
||||
from .location_view import *
|
||||
from .category_view import *
|
||||
from .checklist_view import *
|
||||
from .collection_view import *
|
||||
|
|
|
@ -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,47 +10,43 @@ class AttachmentViewSet(viewsets.ModelViewSet):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Attachment.objects.filter(user_id=self.request.user)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def attachment_delete(self, request, *args, **kwargs):
|
||||
return self.destroy(request, *args, **kwargs)
|
||||
return Attachment.objects.filter(user=self.request.user)
|
||||
|
||||
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)
|
|
@ -2,21 +2,19 @@ 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):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
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):
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve a list of distinct categories for adventures associated with the current user.
|
||||
Retrieve a list of distinct categories for locations associated with the current user.
|
||||
"""
|
||||
categories = self.get_queryset().distinct()
|
||||
serializer = self.get_serializer(categories, many=True)
|
||||
|
@ -24,19 +22,19 @@ 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)
|
||||
|
||||
if instance.name == 'general':
|
||||
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()
|
||||
# set any locations 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=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)
|
|
@ -6,32 +6,22 @@ from adventures.models import Checklist
|
|||
from adventures.serializers import ChecklistSerializer
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
class ChecklistViewSet(viewsets.ModelViewSet):
|
||||
queryset = Checklist.objects.all()
|
||||
serializer_class = ChecklistSerializer
|
||||
permission_classes = [IsOwnerOrSharedWithFullAccess]
|
||||
permission_classes = [IsAuthenticated, IsOwnerOrSharedWithFullAccess]
|
||||
filterset_fields = ['is_public', 'collection']
|
||||
|
||||
# return error message if user is not authenticated on the root endpoint
|
||||
def list(self, request, *args, **kwargs):
|
||||
# Prevent listing all adventures
|
||||
return Response({"detail": "Listing all checklists is not allowed."},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def all(self, request):
|
||||
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)
|
||||
)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
# if the user is not authenticated return only public transportations for retrieve action
|
||||
# if the user is not authenticated return only public checklists for retrieve action
|
||||
if not self.request.user.is_authenticated:
|
||||
if self.action == 'retrieve':
|
||||
return Checklist.objects.filter(is_public=True).distinct().order_by('-updated_at')
|
||||
|
@ -39,14 +29,14 @@ class ChecklistViewSet(viewsets.ModelViewSet):
|
|||
|
||||
|
||||
if self.action == 'retrieve':
|
||||
# For individual adventure retrieval, include public adventures
|
||||
# For individual adventure retrieval, include public locations
|
||||
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) | Q(collection__shared_with=self.request.user)
|
||||
).distinct().order_by('-updated_at')
|
||||
else:
|
||||
# For other actions, include user's own adventures and shared adventures
|
||||
# For other actions, include user's own locations and shared locations
|
||||
return Checklist.objects.filter(
|
||||
Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
|
||||
Q(user=self.request.user) | Q(collection__shared_with=self.request.user)
|
||||
).distinct().order_by('-updated_at')
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
|
@ -65,11 +55,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 +84,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 +109,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)
|
|
@ -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
|
||||
|
@ -15,8 +15,6 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
permission_classes = [CollectionShared]
|
||||
pagination_class = pagination.StandardResultsSetPagination
|
||||
|
||||
# def get_queryset(self):
|
||||
# return Collection.objects.filter(Q(user_id=self.request.user.id) & Q(is_archived=False))
|
||||
|
||||
def apply_sorting(self, queryset):
|
||||
order_by = self.request.query_params.get('order_by', 'name')
|
||||
|
@ -47,15 +45,13 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
if order_direction == 'asc':
|
||||
ordering = '-updated_at'
|
||||
|
||||
#print(f"Ordering by: {ordering}") # For debugging
|
||||
|
||||
return queryset.order_by(ordering)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
# 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, is_archived=False)
|
||||
queryset = self.apply_sorting(queryset)
|
||||
collections = self.paginate_and_respond(queryset, request)
|
||||
return collections
|
||||
|
@ -66,7 +62,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)
|
||||
)
|
||||
|
||||
queryset = self.apply_sorting(queryset)
|
||||
|
@ -80,7 +76,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)
|
||||
|
@ -88,7 +84,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
|
||||
return Response(serializer.data)
|
||||
|
||||
# this make the is_public field of the collection cascade to the adventures
|
||||
# this make the is_public field of the collection cascade to the locations
|
||||
@transaction.atomic
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
|
@ -99,7 +95,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,29 +103,29 @@ 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)
|
||||
# Get all locations in this collection
|
||||
locations_in_collection = Location.objects.filter(collections=instance)
|
||||
|
||||
if new_public_status:
|
||||
# If collection becomes public, make all adventures public
|
||||
adventures_in_collection.update(is_public=True)
|
||||
# If collection becomes public, make all locations public
|
||||
locations_in_collection.update(is_public=True)
|
||||
else:
|
||||
# If collection becomes private, check each adventure
|
||||
# Only set an adventure to private if ALL of its collections are private
|
||||
# Collect adventures that do NOT belong to any other public collection (excluding the current one)
|
||||
adventure_ids_to_set_private = []
|
||||
# If collection becomes private, check each location
|
||||
# Only set a location to private if ALL of its collections are private
|
||||
# Collect locations that do NOT belong to any other public collection (excluding the current one)
|
||||
location_ids_to_set_private = []
|
||||
|
||||
for adventure in adventures_in_collection:
|
||||
has_public_collection = adventure.collections.filter(is_public=True).exclude(id=instance.id).exists()
|
||||
for location in locations_in_collection:
|
||||
has_public_collection = location.collections.filter(is_public=True).exclude(id=instance.id).exists()
|
||||
if not has_public_collection:
|
||||
adventure_ids_to_set_private.append(adventure.id)
|
||||
location_ids_to_set_private.append(location.id)
|
||||
|
||||
# Bulk update those adventures
|
||||
Adventure.objects.filter(id__in=adventure_ids_to_set_private).update(is_public=False)
|
||||
# Bulk update those locations
|
||||
Location.objects.filter(id__in=location_ids_to_set_private).update(is_public=False)
|
||||
|
||||
# Update transportations, notes, and checklists related to this collection
|
||||
# These still use direct ForeignKey relationships
|
||||
|
@ -150,7 +146,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
|
||||
return Response(serializer.data)
|
||||
|
||||
# make an action to retreive all adventures that are shared with the user
|
||||
# make an action to retreive all locations that are shared with the user
|
||||
@action(detail=False, methods=['get'])
|
||||
def shared(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
|
@ -162,7 +158,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
# Adds a new user to the shared_with field of an adventure
|
||||
# Adds a new user to the shared_with field of a location
|
||||
@action(detail=True, methods=['post'], url_path='share/(?P<uuid>[^/.]+)')
|
||||
def share(self, request, pk=None, uuid=None):
|
||||
collection = self.get_object()
|
||||
|
@ -177,7 +173,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
return Response({"error": "Cannot share with yourself"}, status=400)
|
||||
|
||||
if collection.shared_with.filter(id=user.id).exists():
|
||||
return Response({"error": "Adventure is already shared with this user"}, status=400)
|
||||
return Response({"error": "Location is already shared with this user"}, status=400)
|
||||
|
||||
collection.shared_with.add(user)
|
||||
collection.save()
|
||||
|
@ -207,28 +203,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()
|
||||
|
|
|
@ -3,8 +3,8 @@ 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.serializers import AdventureSerializer, CollectionSerializer
|
||||
from adventures.models import Location, Collection
|
||||
from adventures.serializers import LocationSerializer, CollectionSerializer
|
||||
from worldtravel.models import Country, Region, City, VisitedCity, VisitedRegion
|
||||
from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer
|
||||
from users.models import CustomUser as User
|
||||
|
@ -20,7 +20,7 @@ class GlobalSearchView(viewsets.ViewSet):
|
|||
|
||||
# Initialize empty results
|
||||
results = {
|
||||
"adventures": [],
|
||||
"locations": [],
|
||||
"collections": [],
|
||||
"users": [],
|
||||
"countries": [],
|
||||
|
@ -30,15 +30,15 @@ class GlobalSearchView(viewsets.ViewSet):
|
|||
"visited_cities": []
|
||||
}
|
||||
|
||||
# Adventures: Full-Text Search
|
||||
adventures = Adventure.objects.annotate(
|
||||
# Locations: Full-Text Search
|
||||
locations = Location.objects.annotate(
|
||||
search=SearchVector('name', 'description', 'location')
|
||||
).filter(search=SearchQuery(search_term), user_id=request.user)
|
||||
results["adventures"] = AdventureSerializer(adventures, many=True).data
|
||||
).filter(search=SearchQuery(search_term), user=request.user)
|
||||
results["locations"] = LocationSerializer(locations, 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)
|
||||
|
|
|
@ -4,27 +4,26 @@ 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.serializers import AdventureSerializer
|
||||
from adventures.models import Location
|
||||
from adventures.serializers import LocationSerializer
|
||||
|
||||
class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def generate(self, request):
|
||||
adventures = Adventure.objects.filter(user_id=request.user)
|
||||
serializer = AdventureSerializer(adventures, many=True)
|
||||
locations = Location.objects.filter(user=request.user)
|
||||
serializer = LocationSerializer(locations, 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//')
|
||||
cal.add('version', '2.0')
|
||||
|
||||
for adventure in serializer.data:
|
||||
if adventure['visits']:
|
||||
for visit in adventure['visits']:
|
||||
for location in serializer.data:
|
||||
if location['visits']:
|
||||
for visit in location['visits']:
|
||||
# Skip if start_date is missing
|
||||
if not visit.get('start_date'):
|
||||
continue
|
||||
|
@ -42,7 +41,7 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
|||
|
||||
# Create event
|
||||
event = Event()
|
||||
event.add('summary', adventure['name'])
|
||||
event.add('summary', location['name'])
|
||||
event.add('dtstart', start_date)
|
||||
event.add('dtend', end_date)
|
||||
event.add('dtstamp', datetime.now())
|
||||
|
@ -50,11 +49,11 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
|||
event.add('class', 'PUBLIC')
|
||||
event.add('created', datetime.now())
|
||||
event.add('last-modified', datetime.now())
|
||||
event.add('description', adventure['description'])
|
||||
if adventure.get('location'):
|
||||
event.add('location', adventure['location'])
|
||||
if adventure.get('link'):
|
||||
event.add('url', adventure['link'])
|
||||
event.add('description', location['description'])
|
||||
if location.get('location'):
|
||||
event.add('location', location['location'])
|
||||
if location.get('link'):
|
||||
event.add('url', location['link'])
|
||||
|
||||
organizer = vCalAddress(f'MAILTO:{user.email}')
|
||||
organizer.params['cn'] = vText(name)
|
||||
|
|
|
@ -4,15 +4,14 @@ 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.serializers import AdventureImageSerializer
|
||||
from adventures.models import Location, LocationImage
|
||||
from adventures.serializers import LocationImageSerializer
|
||||
from integrations.models import ImmichIntegration
|
||||
import uuid
|
||||
import requests
|
||||
import os
|
||||
|
||||
class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = AdventureImageSerializer
|
||||
serializer_class = LocationImageSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
|
@ -21,21 +20,21 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
|
||||
@action(detail=True, methods=['post'])
|
||||
def toggle_primary(self, request, *args, **kwargs):
|
||||
# Makes the image the primary image for the adventure, if there is already a primary image linked to the adventure, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the adventure
|
||||
# Makes the image the primary image for the location, if there is already a primary image linked to the location, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the location
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
instance = self.get_object()
|
||||
adventure = instance.adventure
|
||||
if adventure.user_id != request.user:
|
||||
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
|
||||
location = instance.location
|
||||
if location.user != request.user:
|
||||
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Check if the image is already the primary image
|
||||
if instance.is_primary:
|
||||
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(location=location, is_primary=True).update(is_primary=False)
|
||||
|
||||
# Set the new image to true
|
||||
instance.is_primary = True
|
||||
|
@ -45,29 +44,29 @@ class AdventureImageViewSet(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)
|
||||
location = Location.objects.get(id=location_id)
|
||||
except Location.DoesNotExist:
|
||||
return Response({"error": "location 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
|
||||
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)
|
||||
|
||||
# Handle Immich ID for shared users by downloading the image
|
||||
if (request.user != adventure.user_id and
|
||||
if (request.user != location.user and
|
||||
'immich_id' in request.data and
|
||||
request.data.get('immich_id')):
|
||||
|
||||
|
@ -75,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,8 +121,8 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Save with the downloaded image
|
||||
adventure = serializer.validated_data['adventure']
|
||||
serializer.save(user_id=adventure.user_id, image=image_file)
|
||||
location = serializer.validated_data['location']
|
||||
serializer.save(user=location.user, image=image_file)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
@ -144,14 +143,14 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
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)
|
||||
location = Location.objects.get(id=location_id)
|
||||
except Location.DoesNotExist:
|
||||
return Response({"error": "location not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if adventure.user_id != request.user:
|
||||
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
|
||||
if location.user != request.user:
|
||||
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
@ -159,14 +158,13 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
return super().perform_destroy(instance)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
print("destroy")
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
instance = self.get_object()
|
||||
adventure = instance.adventure
|
||||
if adventure.user_id != request.user:
|
||||
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
|
||||
location = instance.location
|
||||
if location.user != request.user:
|
||||
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
@ -175,27 +173,27 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
instance = self.get_object()
|
||||
adventure = instance.adventure
|
||||
if adventure.user_id != request.user:
|
||||
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
|
||||
location = instance.location
|
||||
if location.user != request.user:
|
||||
return Response({"error": "User does not own this location"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@action(detail=False, methods=['GET'], url_path='(?P<adventure_id>[0-9a-f-]+)')
|
||||
def adventure_images(self, request, adventure_id=None, *args, **kwargs):
|
||||
@action(detail=False, methods=['GET'], url_path='(?P<location_id>[0-9a-f-]+)')
|
||||
def location_images(self, request, location_id=None, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
try:
|
||||
adventure_uuid = uuid.UUID(adventure_id)
|
||||
location_uuid = uuid.UUID(location_id)
|
||||
except ValueError:
|
||||
return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"error": "Invalid location 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(
|
||||
Q(adventure__id=adventure_uuid) & (
|
||||
Q(adventure__user_id=request.user) | # User owns the adventure
|
||||
Q(adventure__collections__shared_with=request.user) # User has shared access via collection
|
||||
# Updated queryset to include images from locations the user owns OR has shared access to
|
||||
queryset = LocationImage.objects.filter(
|
||||
Q(location__id=location_uuid) & (
|
||||
Q(location__user=request.user) | # User owns the location
|
||||
Q(location__collections__shared_with=request.user) # User has shared access via collection
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
@ -203,13 +201,13 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
|||
return Response(serializer.data)
|
||||
|
||||
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
|
||||
Q(adventure__collections__shared_with=self.request.user) # User has shared access via collection
|
||||
# Updated to include images from locations the user owns OR has shared access to
|
||||
return LocationImage.objects.filter(
|
||||
Q(location__user=self.request.user) | # User owns the location
|
||||
Q(location__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)
|
||||
# Always set the image owner to the location owner, not the current user
|
||||
location = serializer.validated_data['location']
|
||||
serializer.save(user=location.user)
|
|
@ -7,19 +7,18 @@ from rest_framework import viewsets, status
|
|||
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
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer
|
||||
from adventures.serializers import LocationSerializer
|
||||
from adventures.utils import pagination
|
||||
|
||||
|
||||
class AdventureViewSet(viewsets.ModelViewSet):
|
||||
class LocationViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing Adventure objects with support for filtering, sorting,
|
||||
and sharing functionality.
|
||||
"""
|
||||
serializer_class = AdventureSerializer
|
||||
serializer_class = LocationSerializer
|
||||
permission_classes = [IsOwnerOrSharedWithFullAccess]
|
||||
pagination_class = pagination.StandardResultsSetPagination
|
||||
|
||||
|
@ -28,20 +27,20 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
def get_queryset(self):
|
||||
"""
|
||||
Returns queryset based on user authentication and action type.
|
||||
Public actions allow unauthenticated access to public adventures.
|
||||
Public actions allow unauthenticated access to public locations.
|
||||
"""
|
||||
user = self.request.user
|
||||
public_allowed_actions = {'retrieve', 'additional_info'}
|
||||
|
||||
if not user.is_authenticated:
|
||||
if self.action in public_allowed_actions:
|
||||
return Adventure.objects.retrieve_adventures(
|
||||
return Location.objects.retrieve_locations(
|
||||
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_locations(
|
||||
user,
|
||||
include_public=include_public,
|
||||
include_owned=True,
|
||||
|
@ -67,7 +66,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
# Apply sorting logic
|
||||
queryset = self._apply_ordering(queryset, order_by, order_direction)
|
||||
|
||||
# Filter adventures without collections if requested
|
||||
# Filter locations without collections if requested
|
||||
if include_collections == 'false':
|
||||
queryset = queryset.filter(collections__isnull=True)
|
||||
|
||||
|
@ -116,7 +115,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."""
|
||||
|
@ -147,18 +146,18 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
|
||||
@action(detail=False, methods=['get'])
|
||||
def filtered(self, request):
|
||||
"""Filter adventures by category types and visit status."""
|
||||
"""Filter locations by category types and visit status."""
|
||||
types = request.query_params.get('types', '').split(',')
|
||||
|
||||
# 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 +166,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
|
||||
|
@ -180,19 +179,19 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
|
||||
@action(detail=False, methods=['get'])
|
||||
def all(self, request):
|
||||
"""Get all adventures (public and owned) with optional collection filtering."""
|
||||
"""Get all locations (public and owned) with optional collection filtering."""
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"error": "User is not authenticated"}, status=400)
|
||||
|
||||
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 +224,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 +234,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 +246,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 +283,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
|
|
@ -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)
|
||||
|
@ -25,13 +25,13 @@ class LodgingViewSet(viewsets.ModelViewSet):
|
|||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if self.action == 'retrieve':
|
||||
# For individual adventure retrieval, include public adventures, user's own adventures and shared adventures
|
||||
# For individual adventure retrieval, include public locations, user's own locations and shared locations
|
||||
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
|
||||
# For other actions, include user's own locations and shared locations
|
||||
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)
|
|
@ -15,7 +15,7 @@ class NoteViewSet(viewsets.ModelViewSet):
|
|||
|
||||
# return error message if user is not authenticated on the root endpoint
|
||||
def list(self, request, *args, **kwargs):
|
||||
# Prevent listing all adventures
|
||||
# Prevent listing all locations
|
||||
return Response({"detail": "Listing all notes is not allowed."},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -39,14 +39,14 @@ class NoteViewSet(viewsets.ModelViewSet):
|
|||
|
||||
|
||||
if self.action == 'retrieve':
|
||||
# For individual adventure retrieval, include public adventures
|
||||
# For individual adventure retrieval, include public locations
|
||||
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
|
||||
# For other actions, include user's own locations and shared locations
|
||||
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)
|
||||
|
|
|
@ -5,8 +5,6 @@ from rest_framework.response import Response
|
|||
from django.conf import settings
|
||||
import requests
|
||||
from geopy.distance import geodesic
|
||||
import time
|
||||
|
||||
|
||||
class RecommendationsViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
@ -14,7 +12,7 @@ class RecommendationsViewSet(viewsets.ViewSet):
|
|||
HEADERS = {'User-Agent': 'AdventureLog Server'}
|
||||
|
||||
def parse_google_places(self, places, origin):
|
||||
adventures = []
|
||||
locations = []
|
||||
|
||||
for place in places:
|
||||
location = place.get('location', {})
|
||||
|
@ -45,16 +43,16 @@ class RecommendationsViewSet(viewsets.ViewSet):
|
|||
"distance_km": round(distance_km, 2),
|
||||
}
|
||||
|
||||
adventures.append(adventure)
|
||||
locations.append(adventure)
|
||||
|
||||
# Sort by distance ascending
|
||||
adventures.sort(key=lambda x: x["distance_km"])
|
||||
locations.sort(key=lambda x: x["distance_km"])
|
||||
|
||||
return adventures
|
||||
return locations
|
||||
|
||||
def parse_overpass_response(self, data, request):
|
||||
nodes = data.get('elements', [])
|
||||
adventures = []
|
||||
locations = []
|
||||
all = request.query_params.get('all', False)
|
||||
|
||||
origin = None
|
||||
|
@ -102,13 +100,13 @@ class RecommendationsViewSet(viewsets.ViewSet):
|
|||
"powered_by": "osm"
|
||||
}
|
||||
|
||||
adventures.append(adventure)
|
||||
locations.append(adventure)
|
||||
|
||||
# Sort by distance if available
|
||||
if origin:
|
||||
adventures.sort(key=lambda x: x.get("distance_km") or float("inf"))
|
||||
locations.sort(key=lambda x: x.get("distance_km") or float("inf"))
|
||||
|
||||
return adventures
|
||||
return locations
|
||||
|
||||
|
||||
def query_overpass(self, lat, lon, radius, category, request):
|
||||
|
@ -172,8 +170,8 @@ class RecommendationsViewSet(viewsets.ViewSet):
|
|||
print("Overpass API error:", e)
|
||||
return Response({"error": "Failed to retrieve data from Overpass API."}, status=500)
|
||||
|
||||
adventures = self.parse_overpass_response(data, request)
|
||||
return Response(adventures)
|
||||
locations = self.parse_overpass_response(data, request)
|
||||
return Response(locations)
|
||||
|
||||
def query_google_nearby(self, lat, lon, radius, category, request):
|
||||
"""Query Google Places API (New) for nearby places"""
|
||||
|
@ -216,9 +214,9 @@ class RecommendationsViewSet(viewsets.ViewSet):
|
|||
|
||||
places = data.get('places', [])
|
||||
origin = (float(lat), float(lon))
|
||||
adventures = self.parse_google_places(places, origin)
|
||||
locations = self.parse_google_places(places, origin)
|
||||
|
||||
return Response(adventures)
|
||||
return Response(locations)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Google Places API error: {e}")
|
||||
|
|
|
@ -3,11 +3,9 @@ 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.serializers import AdventureSerializer
|
||||
import requests
|
||||
from adventures.models import Location
|
||||
from adventures.serializers import LocationSerializer
|
||||
from adventures.geocoding import reverse_geocode
|
||||
from adventures.geocoding import extractIsoCode
|
||||
from django.conf import settings
|
||||
from adventures.geocoding import search_google, search_osm
|
||||
|
||||
|
@ -47,14 +45,14 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
|||
|
||||
@action(detail=False, methods=['post'])
|
||||
def mark_visited_region(self, request):
|
||||
# searches through all of the users adventures, if the serialized data is_visited, is true, runs reverse geocode on the adventures and if a region is found, marks it as visited. Use the extractIsoCode function to get the region
|
||||
# searches through all of the users locations, if the serialized data is_visited, is true, runs reverse geocode on the locations and if a region is found, marks it as visited. Use the extractIsoCode function to get the region
|
||||
new_region_count = 0
|
||||
new_regions = {}
|
||||
new_city_count = 0
|
||||
new_cities = {}
|
||||
adventures = Adventure.objects.filter(user_id=self.request.user)
|
||||
serializer = AdventureSerializer(adventures, many=True)
|
||||
for adventure, serialized_adventure in zip(adventures, serializer.data):
|
||||
locations = Location.objects.filter(user=self.request.user)
|
||||
serializer = LocationSerializer(locations, many=True)
|
||||
for adventure, serialized_adventure in zip(locations, serializer.data):
|
||||
if serialized_adventure['is_visited'] == True:
|
||||
lat = adventure.latitude
|
||||
lon = adventure.longitude
|
||||
|
@ -69,18 +67,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
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
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 users.serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
||||
from adventures.models import Location, Collection
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -26,21 +24,21 @@ class StatsViewSet(viewsets.ViewSet):
|
|||
user.email = None
|
||||
|
||||
# get the counts for the user
|
||||
adventure_count = Adventure.objects.filter(
|
||||
user_id=user.id).count()
|
||||
location_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,
|
||||
'location_count': location_count,
|
||||
'trips_count': trips_count,
|
||||
'visited_city_count': visited_city_count,
|
||||
'total_cities': total_cities,
|
||||
|
|
|
@ -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]
|
||||
|
@ -10,7 +10,7 @@ class ActivityTypesView(viewsets.ViewSet):
|
|||
@action(detail=False, methods=['get'])
|
||||
def types(self, request):
|
||||
"""
|
||||
Retrieve a list of distinct activity types for adventures associated with the current user.
|
||||
Retrieve a list of distinct activity types for locations associated with the current user.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The HTTP request object.
|
||||
|
@ -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).values_list('tags', flat=True).distinct()
|
||||
|
||||
allTypes = []
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Q
|
||||
from adventures.models import Transportation
|
||||
from adventures.serializers import TransportationSerializer
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
class TransportationViewSet(viewsets.ModelViewSet):
|
||||
queryset = Transportation.objects.all()
|
||||
|
@ -17,7 +15,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)
|
||||
|
@ -25,13 +23,13 @@ class TransportationViewSet(viewsets.ModelViewSet):
|
|||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if self.action == 'retrieve':
|
||||
# For individual adventure retrieval, include public adventures, user's own adventures and shared adventures
|
||||
# For individual adventure retrieval, include public locations, user's own locations and shared locations
|
||||
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
|
||||
# For other actions, include user's own locations and shared locations
|
||||
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 +46,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 +71,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)
|
|
@ -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)
|
||||
|
|
|
@ -295,6 +295,8 @@ CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGIN
|
|||
|
||||
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
LOGGING = {
|
||||
|
|
|
@ -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
|
|
@ -2,111 +2,199 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="AdventureLog Server" />
|
||||
<meta name="description" content="AdventureLog API Server" />
|
||||
<meta name="author" content="Sean Morley" />
|
||||
|
||||
<title>AdventureLog API Server</title>
|
||||
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- Bootstrap Icons -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
|
||||
rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
|
||||
/>
|
||||
|
||||
<!-- Optional theme -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"
|
||||
/>
|
||||
|
||||
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
<style>
|
||||
body {
|
||||
background-color: #f9f9fb;
|
||||
color: #222;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
.navbar {
|
||||
background-color: #2c3e50;
|
||||
}
|
||||
.navbar-brand,
|
||||
.nav-link {
|
||||
color: #ecf0f1 !important;
|
||||
}
|
||||
.hero {
|
||||
padding: 4rem 1rem;
|
||||
background: linear-gradient(135deg, #2980b9, #6dd5fa);
|
||||
color: white;
|
||||
text-align: center;
|
||||
border-radius: 0 0 1rem 1rem;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.hero p {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.api-response {
|
||||
margin-top: 1rem;
|
||||
font-family: monospace;
|
||||
background-color: #eef2f7;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body role="document">
|
||||
<div class="navbar navbar-inverse" role="navigation">
|
||||
<body>
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
class="navbar-toggle collapsed"
|
||||
data-toggle="collapse"
|
||||
data-target=".navbar-collapse"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">AdventureLog API Server</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="/">Server Home</a></li>
|
||||
<li>
|
||||
<a target="_blank" href="http://adventurelog.app"
|
||||
>Documentation</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="navbar-brand" href="/">AdventureLog API</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="http://adventurelog.app"
|
||||
target="_blank"
|
||||
href="https://github.com/seanmorley15/AdventureLog"
|
||||
>Source Code</a
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="https://github.com/seanmorley15/AdventureLog"
|
||||
target="_blank"
|
||||
>
|
||||
Source Code
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/docs">API Docs</a>
|
||||
</li>
|
||||
<li><a href="/docs">API Docs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--/.nav-collapse -->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="hero">
|
||||
<div class="container">
|
||||
<h1><i class="bi bi-map"></i> AdventureLog API</h1>
|
||||
<p>
|
||||
The backend powering your travels — flexible, powerful, and open
|
||||
source.
|
||||
</p>
|
||||
<a href="/docs" class="btn btn-light btn-lg shadow-sm"
|
||||
><i class="bi bi-book"></i> Explore API Docs</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container theme-showcase" role="main">
|
||||
{% block content %}{% endblock %}
|
||||
<!-- Main Content -->
|
||||
<div class="container my-5">
|
||||
{% block content %}
|
||||
<div class="text-center">
|
||||
<h2>Try a Sample Request</h2>
|
||||
<p>Use the form below to test an API POST request.</p>
|
||||
<form
|
||||
class="ajax-post d-flex flex-column align-items-center"
|
||||
action="/api/test"
|
||||
method="post"
|
||||
style="max-width: 500px; margin: auto"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="example"
|
||||
placeholder="Enter example data"
|
||||
class="form-control mb-3"
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-send"></i> Send Request
|
||||
</button>
|
||||
</form>
|
||||
<div class="api-response"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<!-- Bootstrap core JavaScript
|
||||
================================================== -->
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var error_response = function (data) {
|
||||
|
||||
<footer class="text-center text-muted py-4">
|
||||
Open source with ❤️ by
|
||||
<a href="https://seanmorley.com" target="_blank">Sean Morley</a> • View on
|
||||
<a href="https://github.com/seanmorley15/AdventureLog" target="_blank"
|
||||
>GitHub</a
|
||||
>
|
||||
•
|
||||
<a href="https://adventurelog.app" target="_blank">adventurelog.app</a>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<!-- jQuery (optional, used here for legacy script) -->
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||
integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<script>
|
||||
const error_response = (data) => {
|
||||
$(".api-response").html(
|
||||
"API Response: " +
|
||||
data.status +
|
||||
" " +
|
||||
data.statusText +
|
||||
"<br/>Content: " +
|
||||
data.responseText
|
||||
`<strong>API Response:</strong> ${data.status} ${data.statusText}<br/><strong>Content:</strong> ${data.responseText}`
|
||||
);
|
||||
};
|
||||
var susccess_response = function (data) {
|
||||
const susccess_response = (data) => {
|
||||
$(".api-response").html(
|
||||
"API Response: OK<br/>Content: " + JSON.stringify(data)
|
||||
`<strong>API Response:</strong> OK<br/><strong>Content:</strong> ${JSON.stringify(
|
||||
data,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
};
|
||||
|
||||
$().ready(function () {
|
||||
$(document).ready(() => {
|
||||
$("form.ajax-post button[type=submit]").click(function () {
|
||||
var form = $("form.ajax-post");
|
||||
const form = $("form.ajax-post");
|
||||
$.post(form.attr("action"), form.serialize())
|
||||
.fail(function (data) {
|
||||
error_response(data);
|
||||
})
|
||||
.done(function (data) {
|
||||
susccess_response(data);
|
||||
});
|
||||
.fail(error_response)
|
||||
.done(susccess_response);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block script %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -11,8 +11,8 @@ from django.shortcuts import get_object_or_404
|
|||
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.serializers import LocationSerializer, CollectionSerializer
|
||||
from adventures.models import Location, Collection
|
||||
from allauth.socialaccount.models import SocialAccount
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -99,9 +99,9 @@ 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)
|
||||
adventure_serializer = AdventureSerializer(adventures, many=True)
|
||||
adventures = Location.objects.filter(user=user, is_public=True)
|
||||
collections = Collection.objects.filter(user=user, is_public=True)
|
||||
adventure_serializer = LocationSerializer(adventures, many=True)
|
||||
collection_serializer = CollectionSerializer(collections, many=True)
|
||||
|
||||
return Response({
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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']
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue