diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index f079c8a..bbc2a74 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -11,17 +11,17 @@ 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): +class LocationAdmin(admin.ModelAdmin): list_display = ('name', 'get_category', 'get_visit_count', 'user', 'is_public') list_filter = ( 'user', 'is_public') search_fields = ('name',) @@ -96,7 +96,7 @@ class CustomUserAdmin(UserAdmin): else: return -class AdventureImageAdmin(admin.ModelAdmin): +class LocationImageAdmin(admin.ModelAdmin): list_display = ('user', 'image_display') 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(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(LocationImage, AdventureImageAdmin) +admin.site.register(LocationImage, LocationImageAdmin) admin.site.register(Category, CategoryAdmin) admin.site.register(City, CityAdmin) admin.site.register(VisitedCity) diff --git a/backend/server/adventures/managers.py b/backend/server/adventures/managers.py index 1c97212..fdbb151 100644 --- a/backend/server/adventures/managers.py +++ b/backend/server/adventures/managers.py @@ -2,7 +2,7 @@ from django.db import models from django.db.models import Q 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() if include_owned: diff --git a/backend/server/adventures/migrations/0051_rename_activity_types_location_tags_and_more.py b/backend/server/adventures/migrations/0051_rename_activity_types_location_tags_and_more.py new file mode 100644 index 0000000..03a8b65 --- /dev/null +++ b/backend/server/adventures/migrations/0051_rename_activity_types_location_tags_and_more.py @@ -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'), + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 49de0dc..cf71378 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -48,8 +48,6 @@ def background_geocode_and_assign(location_id: str): # Save updated location info, skip geocode threading location.save(update_fields=["region", "city", "country"], _skip_geocode=True) - # print(f"[Adventure Geocode Thread] Successfully processed {adventure_id}: {adventure.name} - {adventure.latitude}, {adventure.longitude}") - except Exception as e: # Optional: log or print the error print(f"[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) 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) @@ -159,7 +157,7 @@ class Location(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) @@ -256,13 +254,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 locations. 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 diff --git a/backend/server/adventures/permissions.py b/backend/server/adventures/permissions.py index ae88261..0e057ed 100644 --- a/backend/server/adventures/permissions.py +++ b/backend/server/adventures/permissions.py @@ -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) diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 175f320..757bafc 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -9,7 +9,7 @@ from geopy.distance import geodesic from integrations.models import ImmichIntegration -class AdventureImageSerializer(CustomModelSerializer): +class LocationImageSerializer(CustomModelSerializer): class Meta: model = LocationImage fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id'] @@ -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() @@ -81,7 +81,7 @@ class CategorySerializer(serializers.ModelSerializer): instance.save() return instance - def get_num_adventures(self, obj): + 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) @@ -110,14 +109,20 @@ class AdventureSerializer(CustomModelSerializer): class Meta: model = Location fields = [ - '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', '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] @@ -171,10 +176,6 @@ class AdventureSerializer(CustomModelSerializer): ) return category - def get_user(self, obj): - user = obj.user - 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 = Location.objects.create(**validated_data) + location = Location.objects.create(**validated_data) # Handle visits for visit_data in visits_data: - Visit.objects.create(location=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,7 +215,7 @@ 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 == user: # Only the owner can set categories @@ -247,7 +248,7 @@ class AdventureSerializer(CustomModelSerializer): visits_to_delete = current_visit_ids - updated_visit_ids instance.visits.filter(id__in=visits_to_delete).delete() - # call save on the adventure to update the updated_at field and trigger any geocoding + # call save on the location to update the updated_at field and trigger any geocoding instance.save() return instance @@ -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,7 +400,7 @@ class CollectionSerializer(CustomModelSerializer): class Meta: 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'] def to_representation(self, instance): diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index d1bf6cb..717446c 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -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') diff --git a/backend/server/adventures/utils/file_permissions.py b/backend/server/adventures/utils/file_permissions.py index fc0391c..494d7ea 100644 --- a/backend/server/adventures/utils/file_permissions.py +++ b/backend/server/adventures/utils/file_permissions.py @@ -10,14 +10,14 @@ 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 = LocationImage.objects.get(image=image_path).location - if adventure.is_public: + location = LocationImage.objects.get(image=image_path).location + if location.is_public: return True - elif adventure.user == 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 @@ -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.location - if adventure.is_public: + location = attachment.location + if location.is_public: return True - elif adventure.user == 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 diff --git a/backend/server/adventures/views/__init__.py b/backend/server/adventures/views/__init__.py index c9aedb0..4045b45 100644 --- a/backend/server/adventures/views/__init__.py +++ b/backend/server/adventures/views/__init__.py @@ -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 * diff --git a/backend/server/adventures/views/attachment_view.py b/backend/server/adventures/views/attachment_view.py index 4fcc2cd..a6c0642 100644 --- a/backend/server/adventures/views/attachment_view.py +++ b/backend/server/adventures/views/attachment_view.py @@ -11,10 +11,6 @@ class AttachmentViewSet(viewsets.ModelViewSet): def get_queryset(self): 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): if not request.user.is_authenticated: diff --git a/backend/server/adventures/views/category_view.py b/backend/server/adventures/views/category_view.py index f452fd1..6422b85 100644 --- a/backend/server/adventures/views/category_view.py +++ b/backend/server/adventures/views/category_view.py @@ -6,15 +6,13 @@ 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=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. """ diff --git a/backend/server/adventures/views/checklist_view.py b/backend/server/adventures/views/checklist_view.py index 705a333..280a21c 100644 --- a/backend/server/adventures/views/checklist_view.py +++ b/backend/server/adventures/views/checklist_view.py @@ -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=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') @@ -41,12 +31,12 @@ class ChecklistViewSet(viewsets.ModelViewSet): if self.action == 'retrieve': # For individual adventure retrieval, include public adventures return Checklist.objects.filter( - Q(is_public=True) | Q(user=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 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') def partial_update(self, request, *args, **kwargs): diff --git a/backend/server/adventures/views/collection_view.py b/backend/server/adventures/views/collection_view.py index eb5eb23..8c42525 100644 --- a/backend/server/adventures/views/collection_view.py +++ b/backend/server/adventures/views/collection_view.py @@ -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=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=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=request.user.id) + Q(user=request.user) ) 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) @@ -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}") return Response({"error": "User does not own the collection"}, status=400) - # Get all adventures in this collection - adventures_in_collection = Location.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 - Location.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[^/.]+)') 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() diff --git a/backend/server/adventures/views/global_search_view.py b/backend/server/adventures/views/global_search_view.py index b681556..adc0834 100644 --- a/backend/server/adventures/views/global_search_view.py +++ b/backend/server/adventures/views/global_search_view.py @@ -4,7 +4,7 @@ from rest_framework.permissions import IsAuthenticated from django.db.models import Q from django.contrib.postgres.search import SearchVector, SearchQuery 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.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer from users.models import CustomUser as User @@ -34,7 +34,7 @@ class GlobalSearchView(viewsets.ViewSet): adventures = Location.objects.annotate( search=SearchVector('name', 'description', 'location') ).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 = Collection.objects.filter( diff --git a/backend/server/adventures/views/ics_calendar_view.py b/backend/server/adventures/views/ics_calendar_view.py index 56242f2..2fd7f15 100644 --- a/backend/server/adventures/views/ics_calendar_view.py +++ b/backend/server/adventures/views/ics_calendar_view.py @@ -5,7 +5,7 @@ from rest_framework.permissions import IsAuthenticated from icalendar import Calendar, Event, vText, vCalAddress from datetime import datetime, timedelta from adventures.models import Location -from adventures.serializers import AdventureSerializer +from adventures.serializers import LocationSerializer class IcsCalendarGeneratorViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] @@ -13,7 +13,7 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet): @action(detail=False, methods=['get']) def generate(self, request): adventures = Location.objects.filter(user=request.user) - serializer = AdventureSerializer(adventures, many=True) + serializer = LocationSerializer(adventures, many=True) user = request.user name = f"{user.first_name} {user.last_name}" diff --git a/backend/server/adventures/views/adventure_image_view.py b/backend/server/adventures/views/location_image_view.py similarity index 68% rename from backend/server/adventures/views/adventure_image_view.py rename to backend/server/adventures/views/location_image_view.py index 6ce6493..063c7a8 100644 --- a/backend/server/adventures/views/adventure_image_view.py +++ b/backend/server/adventures/views/location_image_view.py @@ -5,13 +5,13 @@ from rest_framework.response import Response from django.db.models import Q from django.core.files.base import ContentFile from adventures.models import Location, LocationImage -from adventures.serializers import AdventureImageSerializer +from adventures.serializers import LocationImageSerializer from integrations.models import ImmichIntegration import uuid import requests class AdventureImageViewSet(viewsets.ModelViewSet): - serializer_class = AdventureImageSerializer + serializer_class = LocationImageSerializer permission_classes = [IsAuthenticated] @action(detail=True, methods=['post']) @@ -20,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 != 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 - 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 instance.is_primary = True @@ -44,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 = Location.objects.get(id=adventure_id) + location = Location.objects.get(id=location_id) 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: - # 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 and + if (request.user != location.user and 'immich_id' in request.data and request.data.get('immich_id')): @@ -121,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=adventure.user, 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) @@ -143,30 +143,28 @@ 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 = Location.objects.get(id=adventure_id) + location = Location.objects.get(id=location_id) 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: - 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) def perform_destroy(self, instance): - print("perform_destroy") 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 != 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 != 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[0-9a-f-]+)') - def adventure_images(self, request, adventure_id=None, *args, **kwargs): + @action(detail=False, methods=['GET'], url_path='(?P[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 + # Updated queryset to include images from locations the user owns OR has shared access to queryset = LocationImage.objects.filter( - Q(adventure__id=adventure_uuid) & ( - Q(adventure__user=request.user) | # User owns the adventure - Q(adventure__collections__shared_with=request.user) # User has shared access via collection + 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 + # Updated to include images from locations the user owns OR has shared access to return LocationImage.objects.filter( - Q(adventure__user=self.request.user) | # User owns the adventure - Q(adventure__collections__shared_with=self.request.user) # User has shared access via collection + 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=adventure.user) \ No newline at end of file + # Always set the image owner to the location owner, not the current user + location = serializer.validated_data['location'] + serializer.save(user=location.user) \ No newline at end of file diff --git a/backend/server/adventures/views/adventure_view.py b/backend/server/adventures/views/location_view.py similarity index 97% rename from backend/server/adventures/views/adventure_view.py rename to backend/server/adventures/views/location_view.py index 3e4f0c6..3f9797a 100644 --- a/backend/server/adventures/views/adventure_view.py +++ b/backend/server/adventures/views/location_view.py @@ -10,16 +10,16 @@ import requests from adventures.models import Location, Category, Transportation, Lodging from adventures.permissions import IsOwnerOrSharedWithFullAccess -from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer +from adventures.serializers import LocationSerializer, TransportationSerializer, LodgingSerializer 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 @@ -35,13 +35,13 @@ class AdventureViewSet(viewsets.ModelViewSet): if not user.is_authenticated: if self.action in public_allowed_actions: - return Location.objects.retrieve_adventures( + return Location.objects.retrieve_locations( user, include_public=True ).order_by('-updated_at') return Location.objects.none() include_public = self.action in public_allowed_actions - return Location.objects.retrieve_adventures( + return Location.objects.retrieve_locations( user, include_public=include_public, include_owned=True, diff --git a/backend/server/adventures/views/reverse_geocode_view.py b/backend/server/adventures/views/reverse_geocode_view.py index 67efd4e..1227a21 100644 --- a/backend/server/adventures/views/reverse_geocode_view.py +++ b/backend/server/adventures/views/reverse_geocode_view.py @@ -4,7 +4,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from worldtravel.models import Region, City, VisitedRegion, VisitedCity from adventures.models import Location -from adventures.serializers import AdventureSerializer +from adventures.serializers import LocationSerializer import requests from adventures.geocoding import reverse_geocode from adventures.geocoding import extractIsoCode @@ -53,7 +53,7 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): new_city_count = 0 new_cities = {} 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): if serialized_adventure['is_visited'] == True: lat = adventure.latitude diff --git a/backend/server/adventures/views/activity_types_view.py b/backend/server/adventures/views/tags_view.py similarity index 88% rename from backend/server/adventures/views/activity_types_view.py rename to backend/server/adventures/views/tags_view.py index b6d97b1..0700b9e 100644 --- a/backend/server/adventures/views/activity_types_view.py +++ b/backend/server/adventures/views/tags_view.py @@ -18,7 +18,7 @@ class ActivityTypesView(viewsets.ViewSet): Returns: 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 = [] diff --git a/backend/server/users/views.py b/backend/server/users/views.py index 682cabe..5fb7d2a 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -11,7 +11,7 @@ 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.serializers import LocationSerializer, CollectionSerializer from adventures.models import Location, Collection from allauth.socialaccount.models import SocialAccount @@ -101,7 +101,7 @@ class PublicUserDetailView(APIView): # Get the users adventures and collections to include in the response adventures = Location.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) return Response({ diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 46c473a..d29bc08 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -37,13 +37,13 @@ // Process activity types for display $: { - if (adventure.activity_types) { - if (adventure.activity_types.length <= 3) { - displayActivityTypes = adventure.activity_types; + if (adventure.tags) { + if (adventure.tags.length <= 3) { + displayActivityTypes = adventure.tags; remainingCount = 0; } else { - displayActivityTypes = adventure.activity_types.slice(0, 3); - remainingCount = adventure.activity_types.length - 3; + displayActivityTypes = adventure.tags.slice(0, 3); + remainingCount = adventure.tags.length - 3; } } } @@ -77,7 +77,7 @@ } async function deleteAdventure() { - let res = await fetch(`/api/adventures/${adventure.id}`, { + let res = await fetch(`/api/locations/${adventure.id}`, { method: 'DELETE' }); if (res.ok) { @@ -98,7 +98,7 @@ updatedCollections.push(collectionId); } - let res = await fetch(`/api/adventures/${adventure.id}`, { + let res = await fetch(`/api/locations/${adventure.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' @@ -128,7 +128,7 @@ (c) => String(c) !== String(collectionId) ); - let res = await fetch(`/api/adventures/${adventure.id}`, { + let res = await fetch(`/api/locations/${adventure.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' @@ -280,7 +280,7 @@ {$t('adventures.open_details')} - {#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))}