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

-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
+};