diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 6452922..df0f9ac 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -1,6 +1,6 @@ from django.utils import timezone import os -from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment +from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Hotel from rest_framework import serializers from main.utils import CustomModelSerializer from users.serializers import CustomUserDetailsSerializer @@ -303,4 +303,14 @@ class CollectionSerializer(CustomModelSerializer): shared_uuids.append(str(user.uuid)) representation['shared_with'] = shared_uuids return representation - \ No newline at end of file + +class HotelSerializer(CustomModelSerializer): + + class Meta: + model = Hotel + fields = [ + 'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', + 'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public', + 'collection', 'created_at', 'updated_at' + ] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] \ No newline at end of file diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 16ab3b5..3c7eff8 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -18,6 +18,7 @@ router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-cale router.register(r'overpass', OverpassViewSet, basename='overpass') router.register(r'search', GlobalSearchView, basename='search') router.register(r'attachments', AttachmentViewSet, basename='attachments') +router.register(r'hotels', HotelViewSet, basename='hotels') urlpatterns = [ diff --git a/backend/server/adventures/views/__init__.py b/backend/server/adventures/views/__init__.py index 171df52..957af52 100644 --- a/backend/server/adventures/views/__init__.py +++ b/backend/server/adventures/views/__init__.py @@ -12,4 +12,5 @@ from .reverse_geocode_view import * from .stats_view import * from .transportation_view import * from .global_search_view import * -from .attachment_view import * \ No newline at end of file +from .attachment_view import * +from .hotel_view import * \ No newline at end of file diff --git a/backend/server/adventures/views/hotel_view.py b/backend/server/adventures/views/hotel_view.py new file mode 100644 index 0000000..4f5b0eb --- /dev/null +++ b/backend/server/adventures/views/hotel_view.py @@ -0,0 +1,84 @@ +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from django.db.models import Q +from adventures.models import Hotel +from adventures.serializers import HotelSerializer +from rest_framework.exceptions import PermissionDenied +from adventures.permissions import IsOwnerOrSharedWithFullAccess +from rest_framework.permissions import IsAuthenticated + +class HotelViewSet(viewsets.ModelViewSet): + queryset = Hotel.objects.all() + serializer_class = HotelSerializer + permission_classes = [IsOwnerOrSharedWithFullAccess] + + def list(self, request, *args, **kwargs): + if not request.user.is_authenticated: + return Response(status=status.HTTP_403_FORBIDDEN) + queryset = Hotel.objects.filter( + Q(user_id=request.user.id) + ) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + def get_queryset(self): + user = self.request.user + if self.action == 'retrieve': + # For individual adventure retrieval, include public adventures, user's own adventures and shared adventures + return Hotel.objects.filter( + Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id) + ).distinct().order_by('-updated_at') + # For other actions, include user's own adventures and shared adventures + return Hotel.objects.filter( + Q(user_id=user.id) | Q(collection__shared_with=user.id) + ).distinct().order_by('-updated_at') + + def partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + user = request.user + + # Partially update the instance with the request data + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + + # Retrieve the collection from the validated data + new_collection = serializer.validated_data.get('collection') + + if new_collection is not None and new_collection != instance.collection: + # Check if the user is the owner of the new collection + if new_collection.user_id != user or instance.user_id != user: + raise PermissionDenied("You do not have permission to use this collection.") + elif new_collection is None: + # Handle the case where the user is trying to set the collection to None + if instance.collection is not None and instance.collection.user_id != user: + raise PermissionDenied("You cannot remove the collection as you are not the owner.") + + # Perform the update + self.perform_update(serializer) + + # Return the updated instance + return Response(serializer.data) + + def perform_update(self, serializer): + serializer.save() + + # when creating an adventure, make sure the user is the owner of the collection or shared with the collection + def perform_create(self, serializer): + # Retrieve the collection from the validated data + collection = serializer.validated_data.get('collection') + + # Check if a collection is provided + if collection: + user = self.request.user + # Check if the user is the owner or is in the shared_with list + if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists(): + # Return an error response if the user does not have permission + raise PermissionDenied("You do not have permission to use this collection.") + # if collection the owner of the adventure is the owner of the collection + serializer.save(user_id=collection.user_id) + return + + # Save the adventure with the current user as the owner + serializer.save(user_id=self.request.user) \ No newline at end of file diff --git a/documentation/docs/configuration/social_auth/github.md b/documentation/docs/configuration/social_auth/github.md index 677d519..51efb74 100644 --- a/documentation/docs/configuration/social_auth/github.md +++ b/documentation/docs/configuration/social_auth/github.md @@ -35,6 +35,10 @@ This configuration is done in the [Admin Panel](../../guides/admin_panel.md). Yo - Settings: can be left blank - Sites: move over the sites you want to enable Authentik on, usually `example.com` and `www.example.com` unless you renamed your sites. +4. Save the configuration. + +Users should now be able to log in to AdventureLog using their GitHub account, and link it to existing accounts. + ## Linking to Existing Account If a user has an existing AdventureLog account and wants to link it to their Github account, they can do so by logging in to their AdventureLog account and navigating to the `Settings` page. There is a button that says `Launch Account Connections`, click that and then choose the provider to link to the existing account. @@ -43,6 +47,3 @@ If a user has an existing AdventureLog account and wants to link it to their Git ![Authentik Social Auth Configuration](/github_settings.png) -4. Save the configuration. - -Users should now be able to log in to AdventureLog using their GitHub account, and link it to existing accounts. diff --git a/frontend/src/lib/components/HotelModal.svelte b/frontend/src/lib/components/HotelModal.svelte new file mode 100644 index 0000000..128f0ad --- /dev/null +++ b/frontend/src/lib/components/HotelModal.svelte @@ -0,0 +1,328 @@ + + + + + + + diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index ab512a4..e339ee0 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -262,3 +262,23 @@ export type Attachment = { user_id: string; name: string; }; + +export type Hotel = { + id: string; + user_id: string; + name: string; + description: string | null; + rating: number | null; + link: string | null; + check_in: string | null; // ISO 8601 date string + check_out: string | null; // ISO 8601 date string + reservation_number: string | null; + price: number | null; + latitude: number | null; + longitude: number | null; + location: string | null; + is_public: boolean; + collection: string | null; + created_at: string; // ISO 8601 date string + updated_at: string; // ISO 8601 date string +};