mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 13:15:18 +02:00
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.
This commit is contained in:
parent
241d27a1a6
commit
84b01b9749
38 changed files with 215 additions and 216 deletions
|
@ -11,17 +11,17 @@ admin.site.login = secure_admin_login(admin.site.login)
|
||||||
@admin.action(description="Trigger geocoding")
|
@admin.action(description="Trigger geocoding")
|
||||||
def trigger_geocoding(modeladmin, request, queryset):
|
def trigger_geocoding(modeladmin, request, queryset):
|
||||||
count = 0
|
count = 0
|
||||||
for adventure in queryset:
|
for location in queryset:
|
||||||
try:
|
try:
|
||||||
adventure.save() # Triggers geocoding logic in your model
|
location.save() # Triggers geocoding logic in your model
|
||||||
count += 1
|
count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
modeladmin.message_user(request, f"Error geocoding {adventure}: {e}", level='error')
|
modeladmin.message_user(request, f"Error geocoding {location}: {e}", level='error')
|
||||||
modeladmin.message_user(request, f"Geocoding triggered for {count} adventures.", level='success')
|
modeladmin.message_user(request, f"Geocoding triggered for {count} locations.", level='success')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AdventureAdmin(admin.ModelAdmin):
|
class LocationAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'get_category', 'get_visit_count', 'user', 'is_public')
|
list_display = ('name', 'get_category', 'get_visit_count', 'user', 'is_public')
|
||||||
list_filter = ( 'user', 'is_public')
|
list_filter = ( 'user', 'is_public')
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
|
@ -96,7 +96,7 @@ class CustomUserAdmin(UserAdmin):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
class AdventureImageAdmin(admin.ModelAdmin):
|
class LocationImageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('user', 'image_display')
|
list_display = ('user', 'image_display')
|
||||||
|
|
||||||
def image_display(self, obj):
|
def image_display(self, obj):
|
||||||
|
@ -137,7 +137,7 @@ admin.site.register(CustomUser, CustomUserAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Location, AdventureAdmin)
|
admin.site.register(Location, LocationAdmin)
|
||||||
admin.site.register(Collection, CollectionAdmin)
|
admin.site.register(Collection, CollectionAdmin)
|
||||||
admin.site.register(Visit, VisitAdmin)
|
admin.site.register(Visit, VisitAdmin)
|
||||||
admin.site.register(Country, CountryAdmin)
|
admin.site.register(Country, CountryAdmin)
|
||||||
|
@ -147,7 +147,7 @@ admin.site.register(Transportation)
|
||||||
admin.site.register(Note)
|
admin.site.register(Note)
|
||||||
admin.site.register(Checklist)
|
admin.site.register(Checklist)
|
||||||
admin.site.register(ChecklistItem)
|
admin.site.register(ChecklistItem)
|
||||||
admin.site.register(LocationImage, AdventureImageAdmin)
|
admin.site.register(LocationImage, LocationImageAdmin)
|
||||||
admin.site.register(Category, CategoryAdmin)
|
admin.site.register(Category, CategoryAdmin)
|
||||||
admin.site.register(City, CityAdmin)
|
admin.site.register(City, CityAdmin)
|
||||||
admin.site.register(VisitedCity)
|
admin.site.register(VisitedCity)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
class LocationManager(models.Manager):
|
class LocationManager(models.Manager):
|
||||||
def retrieve_adventures(self, user, include_owned=False, include_shared=False, include_public=False):
|
def retrieve_locations(self, user, include_owned=False, include_shared=False, include_public=False):
|
||||||
query = Q()
|
query = Q()
|
||||||
|
|
||||||
if include_owned:
|
if include_owned:
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -48,8 +48,6 @@ def background_geocode_and_assign(location_id: str):
|
||||||
# Save updated location info, skip geocode threading
|
# Save updated location info, skip geocode threading
|
||||||
location.save(update_fields=["region", "city", "country"], _skip_geocode=True)
|
location.save(update_fields=["region", "city", "country"], _skip_geocode=True)
|
||||||
|
|
||||||
# print(f"[Adventure Geocode Thread] Successfully processed {adventure_id}: {adventure.name} - {adventure.latitude}, {adventure.longitude}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Optional: log or print the error
|
# Optional: log or print the error
|
||||||
print(f"[Location Geocode Thread] Error processing {location_id}: {e}")
|
print(f"[Location Geocode Thread] Error processing {location_id}: {e}")
|
||||||
|
@ -144,7 +142,7 @@ class Location(models.Model):
|
||||||
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
|
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
location = models.CharField(max_length=200, blank=True, null=True)
|
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)
|
max_length=100), blank=True, null=True)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
rating = models.FloatField(blank=True, null=True)
|
rating = models.FloatField(blank=True, null=True)
|
||||||
|
@ -159,7 +157,7 @@ class Location(models.Model):
|
||||||
country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True)
|
country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
|
||||||
# Changed from ForeignKey to ManyToManyField
|
# 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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
@ -256,13 +254,13 @@ class Collection(models.Model):
|
||||||
shared_with = models.ManyToManyField(User, related_name='shared_with', blank=True)
|
shared_with = models.ManyToManyField(User, related_name='shared_with', blank=True)
|
||||||
link = models.URLField(blank=True, null=True, max_length=2083)
|
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):
|
def clean(self):
|
||||||
if self.is_public and self.pk: # Only check if the instance has a primary key
|
if self.is_public and self.pk: # Only check if the instance has a primary key
|
||||||
# Updated to use the new related_name 'adventures'
|
# Updated to use the new related_name 'locations'
|
||||||
for adventure in self.adventures.all():
|
for location in self.locations.all():
|
||||||
if not adventure.is_public:
|
if not location.is_public:
|
||||||
raise ValidationError(f'Public collections cannot be associated with private locations. Collection: {self.name} Adventure: {adventure.name}')
|
raise ValidationError(f'Public collections cannot be associated with private locations. Collection: {self.name} Location: {location.name}')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
@ -39,7 +39,7 @@ class CollectionShared(permissions.BasePermission):
|
||||||
if obj.shared_with.filter(id=user.id).exists():
|
if obj.shared_with.filter(id=user.id).exists():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# If obj is an Adventure (has collections M2M)
|
# If obj is a Location (has collections M2M)
|
||||||
if hasattr(obj, 'collections'):
|
if hasattr(obj, 'collections'):
|
||||||
# Check if user is in shared_with of any related collection
|
# Check if user is in shared_with of any related collection
|
||||||
shared_collections = obj.collections.filter(shared_with=user)
|
shared_collections = obj.collections.filter(shared_with=user)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from geopy.distance import geodesic
|
||||||
from integrations.models import ImmichIntegration
|
from integrations.models import ImmichIntegration
|
||||||
|
|
||||||
|
|
||||||
class AdventureImageSerializer(CustomModelSerializer):
|
class LocationImageSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LocationImage
|
model = LocationImage
|
||||||
fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id']
|
fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id']
|
||||||
|
@ -59,11 +59,11 @@ class AttachmentSerializer(CustomModelSerializer):
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
class CategorySerializer(serializers.ModelSerializer):
|
class CategorySerializer(serializers.ModelSerializer):
|
||||||
num_adventures = serializers.SerializerMethodField()
|
num_locations = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
fields = ['id', 'name', 'display_name', 'icon', 'num_adventures']
|
fields = ['id', 'name', 'display_name', 'icon', 'num_locations']
|
||||||
read_only_fields = ['id', 'num_adventures']
|
read_only_fields = ['id', 'num_locations']
|
||||||
|
|
||||||
def validate_name(self, value):
|
def validate_name(self, value):
|
||||||
return value.lower()
|
return value.lower()
|
||||||
|
@ -81,7 +81,7 @@ class CategorySerializer(serializers.ModelSerializer):
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def get_num_adventures(self, obj):
|
def get_num_locations(self, obj):
|
||||||
return Location.objects.filter(category=obj, user=obj.user).count()
|
return Location.objects.filter(category=obj, user=obj.user).count()
|
||||||
|
|
||||||
class VisitSerializer(serializers.ModelSerializer):
|
class VisitSerializer(serializers.ModelSerializer):
|
||||||
|
@ -91,13 +91,12 @@ class VisitSerializer(serializers.ModelSerializer):
|
||||||
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes']
|
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes']
|
||||||
read_only_fields = ['id']
|
read_only_fields = ['id']
|
||||||
|
|
||||||
class AdventureSerializer(CustomModelSerializer):
|
class LocationSerializer(CustomModelSerializer):
|
||||||
images = serializers.SerializerMethodField()
|
images = serializers.SerializerMethodField()
|
||||||
visits = VisitSerializer(many=True, read_only=False, required=False)
|
visits = VisitSerializer(many=True, read_only=False, required=False)
|
||||||
attachments = AttachmentSerializer(many=True, read_only=True)
|
attachments = AttachmentSerializer(many=True, read_only=True)
|
||||||
category = CategorySerializer(read_only=False, required=False)
|
category = CategorySerializer(read_only=False, required=False)
|
||||||
is_visited = serializers.SerializerMethodField()
|
is_visited = serializers.SerializerMethodField()
|
||||||
user = serializers.SerializerMethodField()
|
|
||||||
country = CountrySerializer(read_only=True)
|
country = CountrySerializer(read_only=True)
|
||||||
region = RegionSerializer(read_only=True)
|
region = RegionSerializer(read_only=True)
|
||||||
city = CitySerializer(read_only=True)
|
city = CitySerializer(read_only=True)
|
||||||
|
@ -110,14 +109,20 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'description', 'rating', 'activity_types', 'location',
|
'id', 'name', 'description', 'rating', 'tags', 'location',
|
||||||
'is_public', 'collections', 'created_at', 'updated_at', 'images', 'link', 'longitude',
|
'is_public', 'collections', 'created_at', 'updated_at', 'images', 'link', 'longitude',
|
||||||
'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user', 'city', 'country', 'region'
|
'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user', 'city', 'country', 'region'
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'is_visited']
|
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):
|
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
|
# Filter out None values from the serialized data
|
||||||
return [image for image in serializer.data if image is not None]
|
return [image for image in serializer.data if image is not None]
|
||||||
|
|
||||||
|
@ -171,10 +176,6 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
)
|
)
|
||||||
return category
|
return category
|
||||||
|
|
||||||
def get_user(self, obj):
|
|
||||||
user = obj.user
|
|
||||||
return CustomUserDetailsSerializer(user).data
|
|
||||||
|
|
||||||
def get_is_visited(self, obj):
|
def get_is_visited(self, obj):
|
||||||
return obj.is_visited_status()
|
return obj.is_visited_status()
|
||||||
|
|
||||||
|
@ -184,24 +185,24 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
collections_data = validated_data.pop('collections', [])
|
collections_data = validated_data.pop('collections', [])
|
||||||
|
|
||||||
print(category_data)
|
print(category_data)
|
||||||
adventure = Location.objects.create(**validated_data)
|
location = Location.objects.create(**validated_data)
|
||||||
|
|
||||||
# Handle visits
|
# Handle visits
|
||||||
for visit_data in visits_data:
|
for visit_data in visits_data:
|
||||||
Visit.objects.create(location=adventure, **visit_data)
|
Visit.objects.create(location=location, **visit_data)
|
||||||
|
|
||||||
# Handle category
|
# Handle category
|
||||||
if category_data:
|
if category_data:
|
||||||
category = self.get_or_create_category(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:
|
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):
|
def update(self, instance, validated_data):
|
||||||
has_visits = 'visits' in validated_data
|
has_visits = 'visits' in validated_data
|
||||||
|
@ -214,7 +215,7 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
for attr, value in validated_data.items():
|
for attr, value in validated_data.items():
|
||||||
setattr(instance, attr, value)
|
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
|
user = self.context['request'].user
|
||||||
if category_data and instance.user == user:
|
if category_data and instance.user == user:
|
||||||
# Only the owner can set categories
|
# Only the owner can set categories
|
||||||
|
@ -247,7 +248,7 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
visits_to_delete = current_visit_ids - updated_visit_ids
|
visits_to_delete = current_visit_ids - updated_visit_ids
|
||||||
instance.visits.filter(id__in=visits_to_delete).delete()
|
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()
|
instance.save()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
@ -391,7 +392,7 @@ class ChecklistSerializer(CustomModelSerializer):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class CollectionSerializer(CustomModelSerializer):
|
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')
|
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
||||||
notes = NoteSerializer(many=True, read_only=True, source='note_set')
|
notes = NoteSerializer(many=True, read_only=True, source='note_set')
|
||||||
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
|
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
|
||||||
|
@ -399,7 +400,7 @@ class CollectionSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Collection
|
model = Collection
|
||||||
fields = ['id', 'description', 'user', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
|
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']
|
read_only_fields = ['id', 'created_at', 'updated_at', 'user']
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
|
|
|
@ -3,11 +3,11 @@ from rest_framework.routers import DefaultRouter
|
||||||
from adventures.views import *
|
from adventures.views import *
|
||||||
|
|
||||||
router = DefaultRouter()
|
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'collections', CollectionViewSet, basename='collections')
|
||||||
router.register(r'stats', StatsViewSet, basename='stats')
|
router.register(r'stats', StatsViewSet, basename='stats')
|
||||||
router.register(r'generate', GenerateDescription, basename='generate')
|
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'transportations', TransportationViewSet, basename='transportations')
|
||||||
router.register(r'notes', NoteViewSet, basename='notes')
|
router.register(r'notes', NoteViewSet, basename='notes')
|
||||||
router.register(r'checklists', ChecklistViewSet, basename='checklists')
|
router.register(r'checklists', ChecklistViewSet, basename='checklists')
|
||||||
|
|
|
@ -10,14 +10,14 @@ def checkFilePermission(fileId, user, mediaType):
|
||||||
# Construct the full relative path to match the database field
|
# Construct the full relative path to match the database field
|
||||||
image_path = f"images/{fileId}"
|
image_path = f"images/{fileId}"
|
||||||
# Fetch the AdventureImage object
|
# Fetch the AdventureImage object
|
||||||
adventure = LocationImage.objects.get(image=image_path).location
|
location = LocationImage.objects.get(image=image_path).location
|
||||||
if adventure.is_public:
|
if location.is_public:
|
||||||
return True
|
return True
|
||||||
elif adventure.user == user:
|
elif location.user == user:
|
||||||
return True
|
return True
|
||||||
elif adventure.collections.exists():
|
elif location.collections.exists():
|
||||||
# Check if the user is in any collection's shared_with list
|
# 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():
|
if collection.shared_with.filter(id=user.id).exists():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -31,14 +31,14 @@ def checkFilePermission(fileId, user, mediaType):
|
||||||
attachment_path = f"attachments/{fileId}"
|
attachment_path = f"attachments/{fileId}"
|
||||||
# Fetch the Attachment object
|
# Fetch the Attachment object
|
||||||
attachment = Attachment.objects.get(file=attachment_path)
|
attachment = Attachment.objects.get(file=attachment_path)
|
||||||
adventure = attachment.location
|
location = attachment.location
|
||||||
if adventure.is_public:
|
if location.is_public:
|
||||||
return True
|
return True
|
||||||
elif adventure.user == user:
|
elif location.user == user:
|
||||||
return True
|
return True
|
||||||
elif adventure.collections.exists():
|
elif location.collections.exists():
|
||||||
# Check if the user is in any collection's shared_with list
|
# 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():
|
if collection.shared_with.filter(id=user.id).exists():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from .activity_types_view import *
|
from .tags_view import *
|
||||||
from .adventure_image_view import *
|
from .location_image_view import *
|
||||||
from .adventure_view import *
|
from .location_view import *
|
||||||
from .category_view import *
|
from .category_view import *
|
||||||
from .checklist_view import *
|
from .checklist_view import *
|
||||||
from .collection_view import *
|
from .collection_view import *
|
||||||
|
|
|
@ -11,10 +11,6 @@ class AttachmentViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Attachment.objects.filter(user=self.request.user)
|
return Attachment.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
|
||||||
def attachment_delete(self, request, *args, **kwargs):
|
|
||||||
return self.destroy(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
|
|
|
@ -6,15 +6,13 @@ from adventures.models import Category, Location
|
||||||
from adventures.serializers import CategorySerializer
|
from adventures.serializers import CategorySerializer
|
||||||
|
|
||||||
class CategoryViewSet(viewsets.ModelViewSet):
|
class CategoryViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Category.objects.all()
|
|
||||||
serializer_class = CategorySerializer
|
serializer_class = CategorySerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Category.objects.filter(user=self.request.user)
|
return Category.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
def list(self, request, *args, **kwargs):
|
||||||
def categories(self, request):
|
|
||||||
"""
|
"""
|
||||||
Retrieve a list of distinct categories for adventures associated with the current user.
|
Retrieve a list of distinct categories for adventures associated with the current user.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -6,32 +6,22 @@ from adventures.models import Checklist
|
||||||
from adventures.serializers import ChecklistSerializer
|
from adventures.serializers import ChecklistSerializer
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
class ChecklistViewSet(viewsets.ModelViewSet):
|
class ChecklistViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Checklist.objects.all()
|
|
||||||
serializer_class = ChecklistSerializer
|
serializer_class = ChecklistSerializer
|
||||||
permission_classes = [IsOwnerOrSharedWithFullAccess]
|
permission_classes = [IsAuthenticated, IsOwnerOrSharedWithFullAccess]
|
||||||
filterset_fields = ['is_public', 'collection']
|
filterset_fields = ['is_public', 'collection']
|
||||||
|
|
||||||
# return error message if user is not authenticated on the root endpoint
|
|
||||||
def list(self, request, *args, **kwargs):
|
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(
|
queryset = Checklist.objects.filter(
|
||||||
Q(user=request.user.id)
|
Q(user=request.user)
|
||||||
)
|
)
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
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 not self.request.user.is_authenticated:
|
||||||
if self.action == 'retrieve':
|
if self.action == 'retrieve':
|
||||||
return Checklist.objects.filter(is_public=True).distinct().order_by('-updated_at')
|
return Checklist.objects.filter(is_public=True).distinct().order_by('-updated_at')
|
||||||
|
@ -41,12 +31,12 @@ class ChecklistViewSet(viewsets.ModelViewSet):
|
||||||
if self.action == 'retrieve':
|
if self.action == 'retrieve':
|
||||||
# For individual adventure retrieval, include public adventures
|
# For individual adventure retrieval, include public adventures
|
||||||
return Checklist.objects.filter(
|
return Checklist.objects.filter(
|
||||||
Q(is_public=True) | Q(user=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')
|
).distinct().order_by('-updated_at')
|
||||||
else:
|
else:
|
||||||
# For other actions, include user's own adventures and shared adventures
|
# For other actions, include user's own adventures and shared adventures
|
||||||
return Checklist.objects.filter(
|
return Checklist.objects.filter(
|
||||||
Q(user=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')
|
).distinct().order_by('-updated_at')
|
||||||
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -15,8 +15,6 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [CollectionShared]
|
permission_classes = [CollectionShared]
|
||||||
pagination_class = pagination.StandardResultsSetPagination
|
pagination_class = pagination.StandardResultsSetPagination
|
||||||
|
|
||||||
# def get_queryset(self):
|
|
||||||
# return Collection.objects.filter(Q(user=self.request.user.id) & Q(is_archived=False))
|
|
||||||
|
|
||||||
def apply_sorting(self, queryset):
|
def apply_sorting(self, queryset):
|
||||||
order_by = self.request.query_params.get('order_by', 'name')
|
order_by = self.request.query_params.get('order_by', 'name')
|
||||||
|
@ -47,15 +45,13 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
if order_direction == 'asc':
|
if order_direction == 'asc':
|
||||||
ordering = '-updated_at'
|
ordering = '-updated_at'
|
||||||
|
|
||||||
#print(f"Ordering by: {ordering}") # For debugging
|
|
||||||
|
|
||||||
return queryset.order_by(ordering)
|
return queryset.order_by(ordering)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
# make sure the user is authenticated
|
# make sure the user is authenticated
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=400)
|
return Response({"error": "User is not authenticated"}, status=400)
|
||||||
queryset = Collection.objects.filter(user=request.user.id, is_archived=False)
|
queryset = Collection.objects.filter(user=request.user, is_archived=False)
|
||||||
queryset = self.apply_sorting(queryset)
|
queryset = self.apply_sorting(queryset)
|
||||||
collections = self.paginate_and_respond(queryset, request)
|
collections = self.paginate_and_respond(queryset, request)
|
||||||
return collections
|
return collections
|
||||||
|
@ -66,7 +62,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
return Response({"error": "User is not authenticated"}, status=400)
|
return Response({"error": "User is not authenticated"}, status=400)
|
||||||
|
|
||||||
queryset = Collection.objects.filter(
|
queryset = Collection.objects.filter(
|
||||||
Q(user=request.user.id)
|
Q(user=request.user)
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = self.apply_sorting(queryset)
|
queryset = self.apply_sorting(queryset)
|
||||||
|
@ -88,7 +84,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
return Response(serializer.data)
|
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
|
@transaction.atomic
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
partial = kwargs.pop('partial', False)
|
partial = kwargs.pop('partial', False)
|
||||||
|
@ -111,25 +107,25 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.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)
|
return Response({"error": "User does not own the collection"}, status=400)
|
||||||
|
|
||||||
# Get all adventures in this collection
|
# Get all locations in this collection
|
||||||
adventures_in_collection = Location.objects.filter(collections=instance)
|
locations_in_collection = Location.objects.filter(collections=instance)
|
||||||
|
|
||||||
if new_public_status:
|
if new_public_status:
|
||||||
# If collection becomes public, make all adventures public
|
# If collection becomes public, make all locations public
|
||||||
adventures_in_collection.update(is_public=True)
|
locations_in_collection.update(is_public=True)
|
||||||
else:
|
else:
|
||||||
# If collection becomes private, check each adventure
|
# If collection becomes private, check each location
|
||||||
# Only set an adventure to private if ALL of its collections are private
|
# Only set a location to private if ALL of its collections are private
|
||||||
# Collect adventures that do NOT belong to any other public collection (excluding the current one)
|
# Collect locations that do NOT belong to any other public collection (excluding the current one)
|
||||||
adventure_ids_to_set_private = []
|
location_ids_to_set_private = []
|
||||||
|
|
||||||
for adventure in adventures_in_collection:
|
for location in locations_in_collection:
|
||||||
has_public_collection = adventure.collections.filter(is_public=True).exclude(id=instance.id).exists()
|
has_public_collection = location.collections.filter(is_public=True).exclude(id=instance.id).exists()
|
||||||
if not has_public_collection:
|
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
|
# Bulk update those locations
|
||||||
Location.objects.filter(id__in=adventure_ids_to_set_private).update(is_public=False)
|
Location.objects.filter(id__in=location_ids_to_set_private).update(is_public=False)
|
||||||
|
|
||||||
# Update transportations, notes, and checklists related to this collection
|
# Update transportations, notes, and checklists related to this collection
|
||||||
# These still use direct ForeignKey relationships
|
# These still use direct ForeignKey relationships
|
||||||
|
@ -150,7 +146,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
return Response(serializer.data)
|
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'])
|
@action(detail=False, methods=['get'])
|
||||||
def shared(self, request):
|
def shared(self, request):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
|
@ -162,7 +158,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
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>[^/.]+)')
|
@action(detail=True, methods=['post'], url_path='share/(?P<uuid>[^/.]+)')
|
||||||
def share(self, request, pk=None, uuid=None):
|
def share(self, request, pk=None, uuid=None):
|
||||||
collection = self.get_object()
|
collection = self.get_object()
|
||||||
|
@ -177,7 +173,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
return Response({"error": "Cannot share with yourself"}, status=400)
|
return Response({"error": "Cannot share with yourself"}, status=400)
|
||||||
|
|
||||||
if collection.shared_with.filter(id=user.id).exists():
|
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.shared_with.add(user)
|
||||||
collection.save()
|
collection.save()
|
||||||
|
|
|
@ -4,7 +4,7 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.contrib.postgres.search import SearchVector, SearchQuery
|
from django.contrib.postgres.search import SearchVector, SearchQuery
|
||||||
from adventures.models import Location, Collection
|
from adventures.models import Location, Collection
|
||||||
from adventures.serializers import AdventureSerializer, CollectionSerializer
|
from adventures.serializers import LocationSerializer, CollectionSerializer
|
||||||
from worldtravel.models import Country, Region, City, VisitedCity, VisitedRegion
|
from worldtravel.models import Country, Region, City, VisitedCity, VisitedRegion
|
||||||
from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer
|
from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer
|
||||||
from users.models import CustomUser as User
|
from users.models import CustomUser as User
|
||||||
|
@ -34,7 +34,7 @@ class GlobalSearchView(viewsets.ViewSet):
|
||||||
adventures = Location.objects.annotate(
|
adventures = Location.objects.annotate(
|
||||||
search=SearchVector('name', 'description', 'location')
|
search=SearchVector('name', 'description', 'location')
|
||||||
).filter(search=SearchQuery(search_term), user=request.user)
|
).filter(search=SearchQuery(search_term), user=request.user)
|
||||||
results["adventures"] = AdventureSerializer(adventures, many=True).data
|
results["adventures"] = LocationSerializer(adventures, many=True).data
|
||||||
|
|
||||||
# Collections: Partial Match Search
|
# Collections: Partial Match Search
|
||||||
collections = Collection.objects.filter(
|
collections = Collection.objects.filter(
|
||||||
|
|
|
@ -5,7 +5,7 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from icalendar import Calendar, Event, vText, vCalAddress
|
from icalendar import Calendar, Event, vText, vCalAddress
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from adventures.models import Location
|
from adventures.models import Location
|
||||||
from adventures.serializers import AdventureSerializer
|
from adventures.serializers import LocationSerializer
|
||||||
|
|
||||||
class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
@ -13,7 +13,7 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def generate(self, request):
|
def generate(self, request):
|
||||||
adventures = Location.objects.filter(user=request.user)
|
adventures = Location.objects.filter(user=request.user)
|
||||||
serializer = AdventureSerializer(adventures, many=True)
|
serializer = LocationSerializer(adventures, many=True)
|
||||||
user = request.user
|
user = request.user
|
||||||
name = f"{user.first_name} {user.last_name}"
|
name = f"{user.first_name} {user.last_name}"
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,13 @@ from rest_framework.response import Response
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from adventures.models import Location, LocationImage
|
from adventures.models import Location, LocationImage
|
||||||
from adventures.serializers import AdventureImageSerializer
|
from adventures.serializers import LocationImageSerializer
|
||||||
from integrations.models import ImmichIntegration
|
from integrations.models import ImmichIntegration
|
||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
class AdventureImageViewSet(viewsets.ModelViewSet):
|
class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = AdventureImageSerializer
|
serializer_class = LocationImageSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
|
@ -20,21 +20,21 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def toggle_primary(self, request, *args, **kwargs):
|
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:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
adventure = instance.adventure
|
location = instance.location
|
||||||
if adventure.user != request.user:
|
if location.user != request.user:
|
||||||
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)
|
||||||
|
|
||||||
# Check if the image is already the primary image
|
# Check if the image is already the primary image
|
||||||
if instance.is_primary:
|
if instance.is_primary:
|
||||||
return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Set the current primary image to false
|
# Set the current primary image to false
|
||||||
LocationImage.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
|
# Set the new image to true
|
||||||
instance.is_primary = True
|
instance.is_primary = True
|
||||||
|
@ -44,29 +44,29 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
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:
|
try:
|
||||||
adventure = Location.objects.get(id=adventure_id)
|
location = Location.objects.get(id=location_id)
|
||||||
except Location.DoesNotExist:
|
except Location.DoesNotExist:
|
||||||
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
|
return Response({"error": "location not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if adventure.user != request.user:
|
if location.user != request.user:
|
||||||
# Check if the adventure has any collections
|
# Check if the location has any collections
|
||||||
if adventure.collections.exists():
|
if location.collections.exists():
|
||||||
# Check if the user is in the shared_with list of any of the adventure's collections
|
# Check if the user is in the shared_with list of any of the location's collections
|
||||||
user_has_access = False
|
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():
|
if collection.shared_with.filter(id=request.user.id).exists():
|
||||||
user_has_access = True
|
user_has_access = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not user_has_access:
|
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:
|
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
|
# Handle Immich ID for shared users by downloading the image
|
||||||
if (request.user != adventure.user and
|
if (request.user != location.user and
|
||||||
'immich_id' in request.data and
|
'immich_id' in request.data and
|
||||||
request.data.get('immich_id')):
|
request.data.get('immich_id')):
|
||||||
|
|
||||||
|
@ -121,8 +121,8 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
# Save with the downloaded image
|
# Save with the downloaded image
|
||||||
adventure = serializer.validated_data['adventure']
|
location = serializer.validated_data['location']
|
||||||
serializer.save(user=adventure.user, image=image_file)
|
serializer.save(user=location.user, image=image_file)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@ -143,30 +143,28 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
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:
|
try:
|
||||||
adventure = Location.objects.get(id=adventure_id)
|
location = Location.objects.get(id=location_id)
|
||||||
except Location.DoesNotExist:
|
except Location.DoesNotExist:
|
||||||
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
|
return Response({"error": "location not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if adventure.user != request.user:
|
if location.user != request.user:
|
||||||
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().update(request, *args, **kwargs)
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
print("perform_destroy")
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
print("destroy")
|
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
adventure = instance.adventure
|
location = instance.location
|
||||||
if adventure.user != request.user:
|
if location.user != request.user:
|
||||||
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().destroy(request, *args, **kwargs)
|
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)
|
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
adventure = instance.adventure
|
location = instance.location
|
||||||
if adventure.user != request.user:
|
if location.user != request.user:
|
||||||
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().partial_update(request, *args, **kwargs)
|
return super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
@action(detail=False, methods=['GET'], url_path='(?P<adventure_id>[0-9a-f-]+)')
|
@action(detail=False, methods=['GET'], url_path='(?P<location_id>[0-9a-f-]+)')
|
||||||
def adventure_images(self, request, adventure_id=None, *args, **kwargs):
|
def location_images(self, request, location_id=None, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
adventure_uuid = uuid.UUID(adventure_id)
|
location_uuid = uuid.UUID(location_id)
|
||||||
except ValueError:
|
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
|
# Updated queryset to include images from locations the user owns OR has shared access to
|
||||||
queryset = LocationImage.objects.filter(
|
queryset = LocationImage.objects.filter(
|
||||||
Q(adventure__id=adventure_uuid) & (
|
Q(location__id=location_uuid) & (
|
||||||
Q(adventure__user=request.user) | # User owns the adventure
|
Q(location__user=request.user) | # User owns the location
|
||||||
Q(adventure__collections__shared_with=request.user) # User has shared access via collection
|
Q(location__collections__shared_with=request.user) # User has shared access via collection
|
||||||
)
|
)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
@ -203,13 +201,13 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Updated to include images from adventures the user owns OR has shared access to
|
# Updated to include images from locations the user owns OR has shared access to
|
||||||
return LocationImage.objects.filter(
|
return LocationImage.objects.filter(
|
||||||
Q(adventure__user=self.request.user) | # User owns the adventure
|
Q(location__user=self.request.user) | # User owns the location
|
||||||
Q(adventure__collections__shared_with=self.request.user) # User has shared access via collection
|
Q(location__collections__shared_with=self.request.user) # User has shared access via collection
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
# Always set the image owner to the adventure owner, not the current user
|
# Always set the image owner to the location owner, not the current user
|
||||||
adventure = serializer.validated_data['adventure']
|
location = serializer.validated_data['location']
|
||||||
serializer.save(user=adventure.user)
|
serializer.save(user=location.user)
|
|
@ -10,16 +10,16 @@ import requests
|
||||||
|
|
||||||
from adventures.models import Location, Category, Transportation, Lodging
|
from adventures.models import Location, Category, Transportation, Lodging
|
||||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||||
from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer
|
from adventures.serializers import LocationSerializer, TransportationSerializer, LodgingSerializer
|
||||||
from adventures.utils import pagination
|
from adventures.utils import pagination
|
||||||
|
|
||||||
|
|
||||||
class AdventureViewSet(viewsets.ModelViewSet):
|
class LocationViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
ViewSet for managing Adventure objects with support for filtering, sorting,
|
ViewSet for managing Adventure objects with support for filtering, sorting,
|
||||||
and sharing functionality.
|
and sharing functionality.
|
||||||
"""
|
"""
|
||||||
serializer_class = AdventureSerializer
|
serializer_class = LocationSerializer
|
||||||
permission_classes = [IsOwnerOrSharedWithFullAccess]
|
permission_classes = [IsOwnerOrSharedWithFullAccess]
|
||||||
pagination_class = pagination.StandardResultsSetPagination
|
pagination_class = pagination.StandardResultsSetPagination
|
||||||
|
|
||||||
|
@ -35,13 +35,13 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
if self.action in public_allowed_actions:
|
if self.action in public_allowed_actions:
|
||||||
return Location.objects.retrieve_adventures(
|
return Location.objects.retrieve_locations(
|
||||||
user, include_public=True
|
user, include_public=True
|
||||||
).order_by('-updated_at')
|
).order_by('-updated_at')
|
||||||
return Location.objects.none()
|
return Location.objects.none()
|
||||||
|
|
||||||
include_public = self.action in public_allowed_actions
|
include_public = self.action in public_allowed_actions
|
||||||
return Location.objects.retrieve_adventures(
|
return Location.objects.retrieve_locations(
|
||||||
user,
|
user,
|
||||||
include_public=include_public,
|
include_public=include_public,
|
||||||
include_owned=True,
|
include_owned=True,
|
|
@ -4,7 +4,7 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from worldtravel.models import Region, City, VisitedRegion, VisitedCity
|
from worldtravel.models import Region, City, VisitedRegion, VisitedCity
|
||||||
from adventures.models import Location
|
from adventures.models import Location
|
||||||
from adventures.serializers import AdventureSerializer
|
from adventures.serializers import LocationSerializer
|
||||||
import requests
|
import requests
|
||||||
from adventures.geocoding import reverse_geocode
|
from adventures.geocoding import reverse_geocode
|
||||||
from adventures.geocoding import extractIsoCode
|
from adventures.geocoding import extractIsoCode
|
||||||
|
@ -53,7 +53,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
||||||
new_city_count = 0
|
new_city_count = 0
|
||||||
new_cities = {}
|
new_cities = {}
|
||||||
adventures = Location.objects.filter(user=self.request.user)
|
adventures = Location.objects.filter(user=self.request.user)
|
||||||
serializer = AdventureSerializer(adventures, many=True)
|
serializer = LocationSerializer(adventures, many=True)
|
||||||
for adventure, serialized_adventure in zip(adventures, serializer.data):
|
for adventure, serialized_adventure in zip(adventures, serializer.data):
|
||||||
if serialized_adventure['is_visited'] == True:
|
if serialized_adventure['is_visited'] == True:
|
||||||
lat = adventure.latitude
|
lat = adventure.latitude
|
||||||
|
|
|
@ -18,7 +18,7 @@ class ActivityTypesView(viewsets.ViewSet):
|
||||||
Returns:
|
Returns:
|
||||||
Response: A response containing a list of distinct activity types.
|
Response: A response containing a list of distinct activity types.
|
||||||
"""
|
"""
|
||||||
types = Location.objects.filter(user=request.user.id).values_list('activity_types', flat=True).distinct()
|
types = Location.objects.filter(user=request.user).values_list('tags', flat=True).distinct()
|
||||||
|
|
||||||
allTypes = []
|
allTypes = []
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.shortcuts import get_object_or_404
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
|
||||||
from allauth.socialaccount.models import SocialApp
|
from allauth.socialaccount.models import SocialApp
|
||||||
from adventures.serializers import AdventureSerializer, CollectionSerializer
|
from adventures.serializers import LocationSerializer, CollectionSerializer
|
||||||
from adventures.models import Location, Collection
|
from adventures.models import Location, Collection
|
||||||
from allauth.socialaccount.models import SocialAccount
|
from allauth.socialaccount.models import SocialAccount
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ class PublicUserDetailView(APIView):
|
||||||
# Get the users adventures and collections to include in the response
|
# Get the users adventures and collections to include in the response
|
||||||
adventures = Location.objects.filter(user=user, is_public=True)
|
adventures = Location.objects.filter(user=user, is_public=True)
|
||||||
collections = Collection.objects.filter(user=user, is_public=True)
|
collections = Collection.objects.filter(user=user, is_public=True)
|
||||||
adventure_serializer = AdventureSerializer(adventures, many=True)
|
adventure_serializer = LocationSerializer(adventures, many=True)
|
||||||
collection_serializer = CollectionSerializer(collections, many=True)
|
collection_serializer = CollectionSerializer(collections, many=True)
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
|
|
|
@ -37,13 +37,13 @@
|
||||||
|
|
||||||
// Process activity types for display
|
// Process activity types for display
|
||||||
$: {
|
$: {
|
||||||
if (adventure.activity_types) {
|
if (adventure.tags) {
|
||||||
if (adventure.activity_types.length <= 3) {
|
if (adventure.tags.length <= 3) {
|
||||||
displayActivityTypes = adventure.activity_types;
|
displayActivityTypes = adventure.tags;
|
||||||
remainingCount = 0;
|
remainingCount = 0;
|
||||||
} else {
|
} else {
|
||||||
displayActivityTypes = adventure.activity_types.slice(0, 3);
|
displayActivityTypes = adventure.tags.slice(0, 3);
|
||||||
remainingCount = adventure.activity_types.length - 3;
|
remainingCount = adventure.tags.length - 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAdventure() {
|
async function deleteAdventure() {
|
||||||
let res = await fetch(`/api/adventures/${adventure.id}`, {
|
let res = await fetch(`/api/locations/${adventure.id}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
updatedCollections.push(collectionId);
|
updatedCollections.push(collectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await fetch(`/api/adventures/${adventure.id}`, {
|
let res = await fetch(`/api/locations/${adventure.id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
(c) => String(c) !== String(collectionId)
|
(c) => String(c) !== String(collectionId)
|
||||||
);
|
);
|
||||||
|
|
||||||
let res = await fetch(`/api/adventures/${adventure.id}`, {
|
let res = await fetch(`/api/locations/${adventure.id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
@ -280,7 +280,7 @@
|
||||||
{$t('adventures.open_details')}
|
{$t('adventures.open_details')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if adventure.user == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
{#if (adventure.user && adventure.user.uuid == user?.uuid) || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-square btn-sm btn-base-300">
|
<div tabindex="0" role="button" class="btn btn-square btn-sm btn-base-300">
|
||||||
<DotsHorizontal class="w-5 h-5" />
|
<DotsHorizontal class="w-5 h-5" />
|
||||||
|
@ -297,7 +297,7 @@
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{#if user?.uuid == adventure.user}
|
{#if user?.uuid == adventure.user?.uuid}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
on:click={() => (isCollectionModalOpen = true)}
|
on:click={() => (isCollectionModalOpen = true)}
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
modal.showModal();
|
modal.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await fetch(`/api/adventures/all/?include_collections=true`, {
|
let res = await fetch(`/api/locations/all/?include_collections=true`, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@
|
||||||
visits: [],
|
visits: [],
|
||||||
link: null,
|
link: null,
|
||||||
description: null,
|
description: null,
|
||||||
activity_types: [],
|
tags: [],
|
||||||
rating: NaN,
|
rating: NaN,
|
||||||
is_public: false,
|
is_public: false,
|
||||||
latitude: NaN,
|
latitude: NaN,
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
name: adventureToEdit?.name || '',
|
name: adventureToEdit?.name || '',
|
||||||
link: adventureToEdit?.link || null,
|
link: adventureToEdit?.link || null,
|
||||||
description: adventureToEdit?.description || null,
|
description: adventureToEdit?.description || null,
|
||||||
activity_types: adventureToEdit?.activity_types || [],
|
tags: adventureToEdit?.tags || [],
|
||||||
rating: adventureToEdit?.rating || NaN,
|
rating: adventureToEdit?.rating || NaN,
|
||||||
is_public: adventureToEdit?.is_public || false,
|
is_public: adventureToEdit?.is_public || false,
|
||||||
latitude: adventureToEdit?.latitude || NaN,
|
latitude: adventureToEdit?.latitude || NaN,
|
||||||
|
@ -152,7 +152,7 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
modal.showModal();
|
modal.showModal();
|
||||||
let categoryFetch = await fetch('/api/categories/categories');
|
let categoryFetch = await fetch('/api/categories');
|
||||||
if (categoryFetch.ok) {
|
if (categoryFetch.ok) {
|
||||||
categories = await categoryFetch.json();
|
categories = await categoryFetch.json();
|
||||||
} else {
|
} else {
|
||||||
|
@ -461,7 +461,7 @@
|
||||||
adventure.collections = [collection.id];
|
adventure.collections = [collection.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await fetch('/api/adventures', {
|
let res = await fetch('/api/locations', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
@ -480,7 +480,7 @@
|
||||||
addToast('error', $t('adventures.adventure_create_error'));
|
addToast('error', $t('adventures.adventure_create_error'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let res = await fetch(`/api/adventures/${adventure.id}`, {
|
let res = await fetch(`/api/locations/${adventure.id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
@ -654,18 +654,18 @@
|
||||||
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
{$t('adventures.tags')} ({adventure.activity_types?.length || 0})
|
{$t('adventures.tags')} ({adventure.tags?.length || 0})
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="activity_types"
|
id="tags"
|
||||||
name="activity_types"
|
name="tags"
|
||||||
hidden
|
hidden
|
||||||
bind:value={adventure.activity_types}
|
bind:value={adventure.tags}
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered w-full"
|
||||||
/>
|
/>
|
||||||
<ActivityComplete bind:activities={adventure.activity_types} />
|
<ActivityComplete bind:activities={adventure.tags} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
icon: '',
|
icon: '',
|
||||||
id: '',
|
id: '',
|
||||||
user: '',
|
user: '',
|
||||||
num_adventures: 0
|
num_locations: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let isOpen: boolean = false;
|
let isOpen: boolean = false;
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
let dropdownRef: HTMLDivElement;
|
let dropdownRef: HTMLDivElement;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
categories = categories.sort((a, b) => (b.num_adventures || 0) - (a.num_adventures || 0));
|
categories = categories.sort((a, b) => (b.num_locations || 0) - (a.num_locations || 0));
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
<!-- Sort the categories dynamically before rendering -->
|
<!-- Sort the categories dynamically before rendering -->
|
||||||
{#each categories
|
{#each categories
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (b.num_adventures || 0) - (a.num_adventures || 0)) as category}
|
.sort((a, b) => (b.num_locations || 0) - (a.num_locations || 0)) as category}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-neutral flex items-center space-x-2"
|
class="btn btn-neutral flex items-center space-x-2"
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={selected_category && selected_category.id === category.id}
|
aria-selected={selected_category && selected_category.id === category.id}
|
||||||
>
|
>
|
||||||
<span>{category.display_name} {category.icon} ({category.num_adventures})</span>
|
<span>{category.display_name} {category.icon} ({category.num_locations})</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
let adventure_types: Category[] = [];
|
let adventure_types: Category[] = [];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let categoryFetch = await fetch('/api/categories/categories');
|
let categoryFetch = await fetch('/api/categories');
|
||||||
let categoryData = await categoryFetch.json();
|
let categoryData = await categoryFetch.json();
|
||||||
adventure_types = categoryData;
|
adventure_types = categoryData;
|
||||||
console.log(categoryData);
|
console.log(categoryData);
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{type.display_name}
|
{type.display_name}
|
||||||
{type.icon} ({type.num_adventures})
|
{type.icon} ({type.num_locations})
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/categories/categories');
|
const res = await fetch('/api/categories');
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
categories = await res.json();
|
categories = await res.json();
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
>
|
>
|
||||||
<!-- Image Carousel -->
|
<!-- Image Carousel -->
|
||||||
<div class="relative overflow-hidden rounded-t-2xl">
|
<div class="relative overflow-hidden rounded-t-2xl">
|
||||||
<CardCarousel adventures={collection.adventures} />
|
<CardCarousel adventures={collection.locations} />
|
||||||
|
|
||||||
<!-- Badge Overlay -->
|
<!-- Badge Overlay -->
|
||||||
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
||||||
|
@ -119,7 +119,7 @@
|
||||||
|
|
||||||
<!-- Adventure Count -->
|
<!-- Adventure Count -->
|
||||||
<p class="text-sm text-base-content/70">
|
<p class="text-sm text-base-content/70">
|
||||||
{collection.adventures.length}
|
{collection.locations.length}
|
||||||
{$t('navbar.adventures')}
|
{$t('navbar.adventures')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
adventure.name = markers[0].name;
|
adventure.name = markers[0].name;
|
||||||
}
|
}
|
||||||
if (adventure.type == 'visited' || adventure.type == 'planned') {
|
if (adventure.type == 'visited' || adventure.type == 'planned') {
|
||||||
adventure.activity_types = [...adventure.activity_types, markers[0].activity_type];
|
adventure.tags = [...adventure.tags, markers[0].activity_type];
|
||||||
}
|
}
|
||||||
dispatch('submit', adventure);
|
dispatch('submit', adventure);
|
||||||
close();
|
close();
|
||||||
|
|
|
@ -35,8 +35,8 @@ export function checkLink(link: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportData() {
|
export async function exportData() {
|
||||||
let res = await fetch('/api/adventures/all');
|
let res = await fetch('/api/locations/all');
|
||||||
let adventures = (await res.json()) as Adventure[];
|
let adventures = (await res.json()) as Location[];
|
||||||
|
|
||||||
res = await fetch('/api/collections/all');
|
res = await fetch('/api/collections/all');
|
||||||
let collections = (await res.json()) as Collection[];
|
let collections = (await res.json()) as Collection[];
|
||||||
|
@ -78,7 +78,7 @@ export function groupAdventuresByDate(
|
||||||
}
|
}
|
||||||
|
|
||||||
adventures.forEach((adventure) => {
|
adventures.forEach((adventure) => {
|
||||||
adventure.visits.forEach((visit) => {
|
adventure.visits.forEach((visit: { start_date: string; end_date: string; timezone: any }) => {
|
||||||
if (visit.start_date) {
|
if (visit.start_date) {
|
||||||
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
|
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
|
||||||
const startHasZeros = isAllDay(visit.start_date);
|
const startHasZeros = isAllDay(visit.start_date);
|
||||||
|
|
|
@ -16,10 +16,9 @@ export type User = {
|
||||||
|
|
||||||
export type Adventure = {
|
export type Adventure = {
|
||||||
id: string;
|
id: string;
|
||||||
user: string | null;
|
|
||||||
name: string;
|
name: string;
|
||||||
location?: string | null;
|
location?: string | null;
|
||||||
activity_types?: string[] | null;
|
tags?: string[] | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
rating?: number | null;
|
rating?: number | null;
|
||||||
link?: string | null;
|
link?: string | null;
|
||||||
|
@ -45,7 +44,7 @@ export type Adventure = {
|
||||||
is_visited?: boolean;
|
is_visited?: boolean;
|
||||||
category: Category | null;
|
category: Category | null;
|
||||||
attachments: Attachment[];
|
attachments: Attachment[];
|
||||||
user?: User | null;
|
user: User | null;
|
||||||
city?: City | null;
|
city?: City | null;
|
||||||
region?: Region | null;
|
region?: Region | null;
|
||||||
country?: Country | null;
|
country?: Country | null;
|
||||||
|
@ -127,7 +126,7 @@ export type Collection = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
adventures: Adventure[];
|
locations: Adventure[];
|
||||||
created_at?: string | null;
|
created_at?: string | null;
|
||||||
start_date: string | null;
|
start_date: string | null;
|
||||||
end_date: string | null;
|
end_date: string | null;
|
||||||
|
@ -236,7 +235,7 @@ export type Category = {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
user: string;
|
user: string;
|
||||||
num_adventures?: number | null;
|
num_locations?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ImmichIntegration = {
|
export type ImmichIntegration = {
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const POST: RequestHandler = async (event) => {
|
||||||
let allActivities: string[] = [];
|
let allActivities: string[] = [];
|
||||||
let csrfToken = await fetchCSRFToken();
|
let csrfToken = await fetchCSRFToken();
|
||||||
let sessionId = event.cookies.get('sessionid');
|
let sessionId = event.cookies.get('sessionid');
|
||||||
let res = await event.fetch(`${endpoint}/api/activity-types/types/`, {
|
let res = await event.fetch(`${endpoint}/api/tags/types/`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': csrfToken,
|
'X-CSRFToken': csrfToken,
|
||||||
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`
|
Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const load = (async (event) => {
|
||||||
const is_visited = event.url.searchParams.get('is_visited') || 'all';
|
const is_visited = event.url.searchParams.get('is_visited') || 'all';
|
||||||
|
|
||||||
let initialFetch = await event.fetch(
|
let initialFetch = await event.fetch(
|
||||||
`${serverEndpoint}/api/adventures/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`,
|
`${serverEndpoint}/api/locations/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||||
|
|
|
@ -5,7 +5,7 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
const id = event.params as { id: string };
|
const id = event.params as { id: string };
|
||||||
let request = await fetch(`${endpoint}/api/adventures/${id.id}/additional-info/`, {
|
let request = await fetch(`${endpoint}/api/locations/${id.id}/additional-info/`, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||||
},
|
},
|
||||||
|
@ -51,7 +51,7 @@ export const actions: Actions = {
|
||||||
|
|
||||||
let csrfToken = await fetchCSRFToken();
|
let csrfToken = await fetchCSRFToken();
|
||||||
|
|
||||||
let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}`, {
|
let res = await fetch(`${serverEndpoint}/api/locations/${event.params.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
Referer: event.url.origin, // Include Referer header
|
Referer: event.url.origin, // Include Referer header
|
||||||
|
|
|
@ -150,7 +150,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if adventure}
|
{#if adventure}
|
||||||
{#if data.user && data.user.uuid == adventure.user}
|
{#if data.user?.uuid && adventure.user?.uuid && data.user.uuid === adventure.user.uuid}
|
||||||
<div class="fixed bottom-6 right-6 z-50">
|
<div class="fixed bottom-6 right-6 z-50">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-circle w-16 h-16 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-110"
|
class="btn btn-primary btn-circle w-16 h-16 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-110"
|
||||||
|
@ -649,11 +649,11 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title text-lg mb-4">ℹ️ {$t('adventures.basic_information')}</h3>
|
<h3 class="card-title text-lg mb-4">ℹ️ {$t('adventures.basic_information')}</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#if adventure.activity_types && adventure.activity_types?.length > 0}
|
{#if adventure.tags && adventure.tags?.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm opacity-70 mb-1">{$t('adventures.tags')}</div>
|
<div class="text-sm opacity-70 mb-1">{$t('adventures.tags')}</div>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each adventure.activity_types as activity}
|
{#each adventure.tags as activity}
|
||||||
<span class="badge badge-sm badge-outline">{activity}</span>
|
<span class="badge badge-sm badge-outline">{activity}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
let sessionId = event.cookies.get('sessionid');
|
let sessionId = event.cookies.get('sessionid');
|
||||||
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, {
|
let visitedFetch = await fetch(`${endpoint}/api/locations/all/?include_collections=true`, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${sessionId}`
|
Cookie: `sessionid=${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -417,7 +417,7 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.props.adventure) {
|
if (data.props.adventure) {
|
||||||
collection = data.props.adventure;
|
collection = data.props.adventure;
|
||||||
adventures = collection.adventures as Adventure[];
|
adventures = collection.locations as Adventure[];
|
||||||
} else {
|
} else {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
}
|
}
|
||||||
|
@ -477,7 +477,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await fetch(`/api/adventures/${adventure.id}/`, {
|
let res = await fetch(`/api/locations/${adventure.id}/`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const load = (async (event) => {
|
||||||
} else {
|
} else {
|
||||||
let adventures: Adventure[] = [];
|
let adventures: Adventure[] = [];
|
||||||
|
|
||||||
let initialFetch = await event.fetch(`${serverEndpoint}/api/adventures/`, {
|
let initialFetch = await event.fetch(`${serverEndpoint}/api/locations/`, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
Cookie: `sessionid=${event.cookies.get('sessionid')}`
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const load = (async (event) => {
|
||||||
return redirect(302, '/login');
|
return redirect(302, '/login');
|
||||||
} else {
|
} else {
|
||||||
let sessionId = event.cookies.get('sessionid');
|
let sessionId = event.cookies.get('sessionid');
|
||||||
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, {
|
let visitedFetch = await fetch(`${endpoint}/api/locations/all/?include_collections=true`, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${sessionId}`
|
Cookie: `sessionid=${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue