From 0664d9434cc1c6e31c7f924ad4983a95bb25c5ea Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 2 Sep 2024 10:29:51 -0400 Subject: [PATCH 01/25] More sharing feartures and permissions --- .../migrations/0005_collection_shared_with.py | 20 +++++ backend/server/adventures/models.py | 1 + backend/server/adventures/permissions.py | 67 +++++++++++++++ backend/server/adventures/serializers.py | 11 ++- backend/server/adventures/views.py | 82 ++++++++++++++----- 5 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 backend/server/adventures/migrations/0005_collection_shared_with.py diff --git a/backend/server/adventures/migrations/0005_collection_shared_with.py b/backend/server/adventures/migrations/0005_collection_shared_with.py new file mode 100644 index 0000000..c3ee3ac --- /dev/null +++ b/backend/server/adventures/migrations/0005_collection_shared_with.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.8 on 2024-09-02 13:21 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0004_transportation_end_date'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='shared_with', + field=models.ManyToManyField(blank=True, related_name='shared_with', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index a7f3522..79033be 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -81,6 +81,7 @@ class Collection(models.Model): end_date = models.DateField(blank=True, null=True) updated_at = models.DateTimeField(auto_now=True) is_archived = models.BooleanField(default=False) + shared_with = models.ManyToManyField(User, related_name='shared_with', blank=True) # if connected adventures are private and collection is public, raise an error diff --git a/backend/server/adventures/permissions.py b/backend/server/adventures/permissions.py index b4241c2..5a690ef 100644 --- a/backend/server/adventures/permissions.py +++ b/backend/server/adventures/permissions.py @@ -27,4 +27,71 @@ class IsPublicReadOnly(permissions.BasePermission): return obj.is_public or obj.user_id == request.user # Write permissions are only allowed to the owner of the object + return obj.user_id == request.user + +class CollectionShared(permissions.BasePermission): + """ + Custom permission to only allow read-only access to public objects, + and write access to the owner of the object. + """ + + def has_object_permission(self, request, view, obj): + + # Read permissions are allowed if the object is shared with the user + if obj.shared_with and obj.shared_with.filter(id=request.user.id).exists(): + return True + + # Write permissions are allowed if the object is shared with the user + if request.method not in permissions.SAFE_METHODS and obj.shared_with.filter(id=request.user.id).exists(): + return True + + # Read permissions are allowed if the object is public + if request.method in permissions.SAFE_METHODS: + return obj.is_public or obj.user_id == request.user + + # Write permissions are only allowed to the owner of the object + return obj.user_id == request.user + +class IsOwnerOrSharedWithFullAccess(permissions.BasePermission): + """ + Custom permission to allow: + - Full access (except delete) for shared users + - Full access for owners + - Read-only access for others on safe methods + """ + + def has_object_permission(self, request, view, obj): + # Check if the object has a collection + if hasattr(obj, 'collection') and obj.collection: + # Allow all actions (except DELETE) for shared users + if request.user in obj.collection.shared_with.all(): + return request.method != 'DELETE' + + # Always allow GET, HEAD, or OPTIONS requests (safe methods) + if request.method in permissions.SAFE_METHODS: + return True + + # Allow all actions for the owner + return obj.user_id == request.user + +class IsPublicOrOwnerOrSharedWithFullAccess(permissions.BasePermission): + """ + Custom permission to allow: + - Read-only access for public objects + - Full access (except delete) for shared users + - Full access for owners + """ + + def has_object_permission(self, request, view, obj): + # Allow read-only access for public objects + if obj.is_public and request.method in permissions.SAFE_METHODS: + return True + + # Check if the object has a collection + if hasattr(obj, 'collection') and obj.collection: + # Allow all actions (except DELETE) for shared users + if request.user in obj.collection.shared_with.all(): + return request.method != 'DELETE' + + # Allow all actions for the owner return obj.user_id == request.user \ No newline at end of file diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 9acd0b2..3b7c2e2 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -225,5 +225,14 @@ class CollectionSerializer(serializers.ModelSerializer): class Meta: model = Collection # fields are all plus the adventures field - fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived'] + fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with'] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + + def to_representation(self, instance): + representation = super().to_representation(instance) + # Make it display the user uuid for the shared users instead of the PK + shared_uuids = [] + for user in instance.shared_with.all(): + shared_uuids.append(str(user.uuid)) + representation['shared_with'] = shared_uuids + return representation \ No newline at end of file diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index ba69054..13bb66c 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -6,11 +6,12 @@ from rest_framework import viewsets from django.db.models.functions import Lower from rest_framework.response import Response from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage +from django.core.exceptions import PermissionDenied from worldtravel.models import VisitedRegion, Region, Country from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer from rest_framework.permissions import IsAuthenticated from django.db.models import Q, Prefetch -from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly +from .permissions import CollectionShared, IsOwnerOrReadOnly, IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess, IsPublicReadOnly from rest_framework.pagination import PageNumberPagination from django.shortcuts import get_object_or_404 from rest_framework import status @@ -28,7 +29,7 @@ from django.db.models import Q class AdventureViewSet(viewsets.ModelViewSet): serializer_class = AdventureSerializer - permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly] + permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess] pagination_class = StandardResultsSetPagination def apply_sorting(self, queryset): @@ -71,16 +72,13 @@ class AdventureViewSet(viewsets.ModelViewSet): if self.action == 'retrieve': # For individual adventure retrieval, include public adventures return Adventure.objects.filter( - Q(is_public=True) | Q(user_id=self.request.user.id) + Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) ) else: - # For other actions, only include user's own adventures - return Adventure.objects.filter(user_id=self.request.user.id) - - def list(self, request, *args, **kwargs): - # Prevent listing all adventures - return Response({"detail": "Listing all adventures is not allowed."}, - status=status.HTTP_403_FORBIDDEN) + # For other actions, include user's own adventures and shared adventures + return Adventure.objects.filter( + Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + ) def retrieve(self, request, *args, **kwargs): queryset = self.get_queryset() @@ -99,9 +97,7 @@ class AdventureViewSet(viewsets.ModelViewSet): if adventure.collection: adventure.is_public = adventure.collection.is_public adventure.save() - - def perform_create(self, serializer): - serializer.save(user_id=self.request.user) + @action(detail=False, methods=['get']) def filtered(self, request): @@ -195,6 +191,46 @@ class AdventureViewSet(viewsets.ModelViewSet): queryset = self.apply_sorting(queryset) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + print(serializer.validated_data) + if 'collection' in serializer.validated_data: + new_collection = serializer.validated_data['collection'] + # if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error + if new_collection != instance.collection and new_collection.user_id != request.user: + return Response({"error": "User does not own the new collection"}, status=400) + self.perform_update(serializer) + return Response(serializer.data) + + def partial_update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + if 'collection' in serializer.validated_data: + new_collection = serializer.validated_data['collection'] + # if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error + if new_collection != instance.collection and new_collection.user_id != request.user: + return Response({"error": "User does not own the new collection"}, status=400) + self.perform_update(serializer) + return Response(serializer.data) + + # 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: + # Check if the user is the owner or is in the shared_with list + if collection.user_id != self.request.user.id and not collection.shared_with.filter(id=self.request.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.") + + # Save the adventure with the current user as the owner + serializer.save(user_id=self.request.user.id) def paginate_and_respond(self, queryset, request): paginator = self.pagination_class() @@ -207,7 +243,7 @@ class AdventureViewSet(viewsets.ModelViewSet): class CollectionViewSet(viewsets.ModelViewSet): serializer_class = CollectionSerializer - permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly] + permission_classes = [CollectionShared] pagination_class = StandardResultsSetPagination # def get_queryset(self): @@ -302,6 +338,8 @@ class CollectionViewSet(viewsets.ModelViewSet): action = "public" if new_public_status else "private" print(f"Collection {instance.id} and its adventures were set to {action}") + + self.perform_update(serializer) if getattr(instance, '_prefetched_objects_cache', None): @@ -316,19 +354,23 @@ class CollectionViewSet(viewsets.ModelViewSet): return Collection.objects.filter(user_id=self.request.user.id) if self.action in ['update', 'partial_update']: - return Collection.objects.filter(user_id=self.request.user.id) + return Collection.objects.filter( + Q(user_id=self.request.user.id) | Q(shared_with=self.request.user) + ).distinct() if self.action == 'retrieve': return Collection.objects.filter( - Q(is_public=True) | Q(user_id=self.request.user.id) - ) + Q(is_public=True) | Q(user_id=self.request.user.id) | Q(shared_with=self.request.user) + ).distinct() - # For other actions (like list), only include user's non-archived collections + # For list action, include collections owned by the user or shared with the user, that are not archived return Collection.objects.filter( - Q(user_id=self.request.user.id) & Q(is_archived=False) - ) + (Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False) + ).distinct() + def perform_create(self, serializer): + # This is ok because you cannot share a collection when creating it serializer.save(user_id=self.request.user) def paginate_and_respond(self, queryset, request): From d340934376df33880a2475adadbb5415791d83a0 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 2 Sep 2024 17:18:18 -0400 Subject: [PATCH 02/25] Fix adding and editing adventures in collections when shared --- backend/server/adventures/views.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 13bb66c..7359173 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -224,13 +224,17 @@ class AdventureViewSet(viewsets.ModelViewSet): # 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 != self.request.user.id and not collection.shared_with.filter(id=self.request.user.id).exists(): + 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.id) + serializer.save(user_id=self.request.user) def paginate_and_respond(self, queryset, request): paginator = self.pagination_class() @@ -321,6 +325,12 @@ class CollectionViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) + if 'collection' in serializer.validated_data: + new_collection = serializer.validated_data['collection'] + # if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error + if new_collection != instance.collection and new_collection.user_id != request.user: + return Response({"error": "User does not own the new collection"}, status=400) + # Check if the 'is_public' field is present in the update data if 'is_public' in serializer.validated_data: new_public_status = serializer.validated_data['is_public'] @@ -338,8 +348,6 @@ class CollectionViewSet(viewsets.ModelViewSet): action = "public" if new_public_status else "private" print(f"Collection {instance.id} and its adventures were set to {action}") - - self.perform_update(serializer) if getattr(instance, '_prefetched_objects_cache', None): From 3915afbc22f5eaeaf21f4ccdf7dc1947fdf56210 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 2 Sep 2024 22:27:07 -0400 Subject: [PATCH 03/25] more permission fixing --- backend/server/adventures/permissions.py | 12 ++-- backend/server/adventures/views.py | 74 +++++++++++++++++++----- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/backend/server/adventures/permissions.py b/backend/server/adventures/permissions.py index 5a690ef..5f9a911 100644 --- a/backend/server/adventures/permissions.py +++ b/backend/server/adventures/permissions.py @@ -55,7 +55,7 @@ class CollectionShared(permissions.BasePermission): class IsOwnerOrSharedWithFullAccess(permissions.BasePermission): """ Custom permission to allow: - - Full access (except delete) for shared users + - Full access for shared users - Full access for owners - Read-only access for others on safe methods """ @@ -63,9 +63,9 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission): def has_object_permission(self, request, view, obj): # Check if the object has a collection if hasattr(obj, 'collection') and obj.collection: - # Allow all actions (except DELETE) for shared users + # Allow all actions for shared users if request.user in obj.collection.shared_with.all(): - return request.method != 'DELETE' + return True # Always allow GET, HEAD, or OPTIONS requests (safe methods) if request.method in permissions.SAFE_METHODS: @@ -78,7 +78,7 @@ class IsPublicOrOwnerOrSharedWithFullAccess(permissions.BasePermission): """ Custom permission to allow: - Read-only access for public objects - - Full access (except delete) for shared users + - Full access for shared users - Full access for owners """ @@ -89,9 +89,9 @@ class IsPublicOrOwnerOrSharedWithFullAccess(permissions.BasePermission): # Check if the object has a collection if hasattr(obj, 'collection') and obj.collection: - # Allow all actions (except DELETE) for shared users + # Allow all actions for shared users if request.user in obj.collection.shared_with.all(): - return request.method != 'DELETE' + return True # Allow all actions for the owner return obj.user_id == request.user \ No newline at end of file diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 7359173..1dc45ca 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -69,6 +69,14 @@ class AdventureViewSet(viewsets.ModelViewSet): return queryset.order_by(ordering) def get_queryset(self): + + # if suer is not authenticated return only public adventures for retrieve action + if not self.request.user.is_authenticated: + if self.action == 'retrieve': + return Adventure.objects.filter(is_public=True) + return Adventure.objects.none() + + if self.action == 'retrieve': # For individual adventure retrieval, include public adventures return Adventure.objects.filter( @@ -192,30 +200,70 @@ class AdventureViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - def update(self, request, *args, **kwargs): + def partial_update(self, request, *args, **kwargs): + # Retrieve the current object instance = self.get_object() + + # Partially update the instance with the request data serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) - print(serializer.validated_data) - if 'collection' in serializer.validated_data: - new_collection = serializer.validated_data['collection'] - # if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error - if new_collection != instance.collection and new_collection.user_id != request.user: - return Response({"error": "User does not own the new collection"}, status=400) + + # Retrieve the collection from the validated data + new_collection = serializer.validated_data.get('collection') + + user = request.user + print(new_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() + def partial_update(self, request, *args, **kwargs): + # Retrieve the current object instance = self.get_object() + + # Partially update the instance with the request data serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) - if 'collection' in serializer.validated_data: - new_collection = serializer.validated_data['collection'] - # if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error - if new_collection != instance.collection and new_collection.user_id != request.user: - return Response({"error": "User does not own the new collection"}, status=400) + + # Retrieve the collection from the validated data + new_collection = serializer.validated_data.get('collection') + + user = request.user + print(new_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): @@ -284,7 +332,7 @@ class CollectionViewSet(viewsets.ModelViewSet): # make sure the user is authenticated if not request.user.is_authenticated: return Response({"error": "User is not authenticated"}, status=400) - queryset = self.get_queryset() + queryset = Collection.objects.filter(user_id=request.user.id) queryset = self.apply_sorting(queryset) collections = self.paginate_and_respond(queryset, request) return collections From c83ef1e904bb67f36226b1cbc1639f8acf429bb9 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 3 Sep 2024 10:24:11 -0400 Subject: [PATCH 04/25] Add trnasportations, checklists, and notes --- backend/server/adventures/serializers.py | 98 +------ backend/server/adventures/views.py | 321 ++++++++++++++++++++--- 2 files changed, 297 insertions(+), 122 deletions(-) diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 3b7c2e2..c012976 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -8,19 +8,6 @@ class AdventureImageSerializer(serializers.ModelSerializer): fields = ['id', 'image', 'adventure'] read_only_fields = ['id'] - # def to_representation(self, instance): - # representation = super().to_representation(instance) - - # # Build the full URL for the image - # request = self.context.get('request') - # if request and instance.image: - # public_url = request.build_absolute_uri(instance.image.url) - # else: - # public_url = f"{os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')}/media/{instance.image.name}" - - # representation['image'] = public_url - # return representation - def to_representation(self, instance): representation = super().to_representation(instance) if instance.image: @@ -55,29 +42,6 @@ class TransportationSerializer(serializers.ModelSerializer): ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] - def validate(self, data): - # Check if the collection is public and the transportation is not - collection = data.get('collection') - is_public = data.get('is_public', False) - if collection and collection.is_public and not is_public: - raise serializers.ValidationError( - 'Transportations associated with a public collection must be public.' - ) - - # Check if the user owns the collection - request = self.context.get('request') - if request and collection and collection.user_id != request.user: - raise serializers.ValidationError( - 'Transportations must be associated with collections owned by the same user.' - ) - - return data - - def create(self, validated_data): - # Set the user_id to the current user - validated_data['user_id'] = self.context['request'].user - return super().create(validated_data) - class NoteSerializer(serializers.ModelSerializer): class Meta: @@ -87,29 +51,6 @@ class NoteSerializer(serializers.ModelSerializer): 'is_public', 'collection', 'created_at', 'updated_at' ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] - - def validate(self, data): - # Check if the collection is public and the transportation is not - collection = data.get('collection') - is_public = data.get('is_public', False) - if collection and collection.is_public and not is_public: - raise serializers.ValidationError( - 'Notes associated with a public collection must be public.' - ) - - # Check if the user owns the collection - request = self.context.get('request') - if request and collection and collection.user_id != request.user: - raise serializers.ValidationError( - 'Notes must be associated with collections owned by the same user.' - ) - - return data - - def create(self, validated_data): - # Set the user_id to the current user - validated_data['user_id'] = self.context['request'].user - return super().create(validated_data) class ChecklistItemSerializer(serializers.ModelSerializer): class Meta: @@ -119,29 +60,15 @@ class ChecklistItemSerializer(serializers.ModelSerializer): ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'checklist'] - def validate(self, data): - # Check if the checklist is public and the checklist item is not - checklist = data.get('checklist') - is_checked = data.get('is_checked', False) - if checklist and checklist.is_public and not is_checked: - raise serializers.ValidationError( - 'Checklist items associated with a public checklist must be checked.' - ) - - # Check if the user owns the checklist - request = self.context.get('request') - if request and checklist and checklist.user_id != request.user: - raise serializers.ValidationError( - 'Checklist items must be associated with checklists owned by the same user.' - ) - - return data - - def create(self, validated_data): - # Set the user_id to the current user - validated_data['user_id'] = self.context['request'].user - return super().create(validated_data) - + # def validate(self, data): + # # Check if the checklist is public and the checklist item is not + # checklist = data.get('checklist') + # is_checked = data.get('is_checked', False) + # if checklist and checklist.is_public and not is_checked: + # raise serializers.ValidationError( + # 'Checklist items associated with a public checklist must be checked.' + # ) + class ChecklistSerializer(serializers.ModelSerializer): items = ChecklistItemSerializer(many=True, source='checklistitem_set') @@ -204,13 +131,6 @@ class ChecklistSerializer(serializers.ModelSerializer): 'Checklists associated with a public collection must be public.' ) - # Check if the user owns the checklist - request = self.context.get('request') - if request and collection and collection.user_id != request.user: - raise serializers.ValidationError( - 'Checklists must be associated with collections owned by the same user.' - ) - return data diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 1dc45ca..73a66ad 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -69,8 +69,7 @@ class AdventureViewSet(viewsets.ModelViewSet): return queryset.order_by(ordering) def get_queryset(self): - - # if suer is not authenticated return only public adventures for retrieve action + # if the user is not authenticated return only public adventures for retrieve action if not self.request.user.is_authenticated: if self.action == 'retrieve': return Adventure.objects.filter(is_public=True) @@ -106,7 +105,6 @@ class AdventureViewSet(viewsets.ModelViewSet): adventure.is_public = adventure.collection.is_public adventure.save() - @action(detail=False, methods=['get']) def filtered(self, request): types = request.query_params.get('types', '').split(',') @@ -228,10 +226,6 @@ class AdventureViewSet(viewsets.ModelViewSet): # Return the updated instance return Response(serializer.data) - - def perform_update(self, serializer): - serializer.save() - def partial_update(self, request, *args, **kwargs): # Retrieve the current object @@ -262,8 +256,8 @@ class AdventureViewSet(viewsets.ModelViewSet): # Return the updated instance return Response(serializer.data) -def perform_update(self, serializer): - serializer.save() + 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): @@ -528,7 +522,7 @@ class ActivityTypesView(viewsets.ViewSet): class TransportationViewSet(viewsets.ModelViewSet): queryset = Transportation.objects.all() serializer_class = TransportationSerializer - permission_classes = [IsAuthenticated] + permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess] filterset_fields = ['type', 'is_public', 'collection'] # return error message if user is not authenticated on the root endpoint @@ -549,21 +543,108 @@ class TransportationViewSet(viewsets.ModelViewSet): def get_queryset(self): - - """ - This view should return a list of all transportations - for the currently authenticated user. - """ - user = self.request.user - return Transportation.objects.filter(user_id=user) + # if the user is not authenticated return only public transportations for retrieve action + if not self.request.user.is_authenticated: + if self.action == 'retrieve': + return Transportation.objects.filter(is_public=True) + return Transportation.objects.none() + + if self.action == 'retrieve': + # For individual adventure retrieval, include public adventures + return Transportation.objects.filter( + Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + ) + else: + # For other actions, include user's own adventures and shared adventures + return Transportation.objects.filter( + Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + ) + + def partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + + # 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') + + user = request.user + print(new_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 partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + + # 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') + + user = request.user + print(new_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) class NoteViewSet(viewsets.ModelViewSet): queryset = Note.objects.all() serializer_class = NoteSerializer - permission_classes = [IsAuthenticated] + permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess] filterset_fields = ['is_public', 'collection'] # return error message if user is not authenticated on the root endpoint @@ -584,21 +665,108 @@ class NoteViewSet(viewsets.ModelViewSet): def get_queryset(self): - - """ - This view should return a list of all notes - for the currently authenticated user. - """ - user = self.request.user - return Note.objects.filter(user_id=user) + # if the user is not authenticated return only public transportations for retrieve action + if not self.request.user.is_authenticated: + if self.action == 'retrieve': + return Note.objects.filter(is_public=True) + return Note.objects.none() + + if self.action == 'retrieve': + # For individual adventure retrieval, include public adventures + return Note.objects.filter( + Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + ) + else: + # For other actions, include user's own adventures and shared adventures + return Note.objects.filter( + Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + ) + + def partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + + # 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') + + user = request.user + print(new_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 partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + + # 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') + + user = request.user + print(new_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) class ChecklistViewSet(viewsets.ModelViewSet): queryset = Checklist.objects.all() serializer_class = ChecklistSerializer - permission_classes = [IsAuthenticated] + permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess] filterset_fields = ['is_public', 'collection'] # return error message if user is not authenticated on the root endpoint @@ -619,15 +787,102 @@ class ChecklistViewSet(viewsets.ModelViewSet): def get_queryset(self): - - """ - This view should return a list of all checklists - for the currently authenticated user. - """ - user = self.request.user - return Checklist.objects.filter(user_id=user) + # if the user is not authenticated return only public transportations for retrieve action + if not self.request.user.is_authenticated: + if self.action == 'retrieve': + return Checklist.objects.filter(is_public=True) + return Checklist.objects.none() + + if self.action == 'retrieve': + # For individual adventure retrieval, include public adventures + return Checklist.objects.filter( + Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + ) + else: + # For other actions, include user's own adventures and shared adventures + return Checklist.objects.filter( + Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user) + ) + + def partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + + # 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') + + user = request.user + print(new_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 partial_update(self, request, *args, **kwargs): + # Retrieve the current object + instance = self.get_object() + + # 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') + + user = request.user + print(new_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) class AdventureImageViewSet(viewsets.ModelViewSet): From 711b974add9a061402b6f59f49c4c61293c419cb Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 6 Sep 2024 19:50:19 -0400 Subject: [PATCH 05/25] Add public profile to user model --- backend/server/adventures/admin.py | 3 +- backend/server/adventures/views.py | 50 ++++++++++++++++++- .../0002_customuser_public_profile.py | 18 +++++++ backend/server/users/models.py | 1 + 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 backend/server/users/migrations/0002_customuser_public_profile.py diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 7ac6842..ab8bfc6 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -46,8 +46,9 @@ from users.models import CustomUser class CustomUserAdmin(UserAdmin): model = CustomUser list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display'] + readonly_fields = ('uuid',) fieldsets = UserAdmin.fieldsets + ( - (None, {'fields': ('profile_pic',)}), + (None, {'fields': ('profile_pic', 'uuid', 'public_profile')}), ) def image_display(self, obj): if obj.profile_pic: diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 73a66ad..31c2d8e 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -10,11 +10,14 @@ from django.core.exceptions import PermissionDenied from worldtravel.models import VisitedRegion, Region, Country from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer from rest_framework.permissions import IsAuthenticated -from django.db.models import Q, Prefetch -from .permissions import CollectionShared, IsOwnerOrReadOnly, IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess, IsPublicReadOnly +from django.db.models import Q +from .permissions import CollectionShared, IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess from rest_framework.pagination import PageNumberPagination from django.shortcuts import get_object_or_404 from rest_framework import status +from django.contrib.auth import get_user_model + +User = get_user_model() class StandardResultsSetPagination(PageNumberPagination): page_size = 25 @@ -398,6 +401,49 @@ class CollectionViewSet(viewsets.ModelViewSet): instance._prefetched_objects_cache = {} return Response(serializer.data) + + # Adds a new user to the shared_with field of an adventure + @action(detail=True, methods=['post'], url_path='share/(?P[^/.]+)') + def share(self, request, pk=None, uuid=None): + collection = self.get_object() + if not uuid: + return Response({"error": "User UUID is required"}, status=400) + try: + user = User.objects.get(uuid=uuid, public_profile=True) + except User.DoesNotExist: + return Response({"error": "User not found"}, status=404) + + if user == request.user: + 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) + + collection.shared_with.add(user) + collection.save() + return Response({"success": f"Shared with {user.username}"}) + + @action(detail=True, methods=['post'], url_path='unshare/(?P[^/.]+)') + def unshare(self, request, pk=None, uuid=None): + if not request.user.is_authenticated: + return Response({"error": "User is not authenticated"}, status=400) + collection = self.get_object() + if not uuid: + return Response({"error": "User UUID is required"}, status=400) + try: + user = User.objects.get(uuid=uuid, public_profile=True) + except User.DoesNotExist: + return Response({"error": "User not found"}, status=404) + + if user == request.user: + return Response({"error": "Cannot unshare with yourself"}, status=400) + + if not collection.shared_with.filter(id=user.id).exists(): + return Response({"error": "Collection is not shared with this user"}, status=400) + + collection.shared_with.remove(user) + collection.save() + return Response({"success": f"Unshared with {user.username}"}) def get_queryset(self): if self.action == 'destroy': diff --git a/backend/server/users/migrations/0002_customuser_public_profile.py b/backend/server/users/migrations/0002_customuser_public_profile.py new file mode 100644 index 0000000..e8ba313 --- /dev/null +++ b/backend/server/users/migrations/0002_customuser_public_profile.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2024-09-06 23:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='public_profile', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/server/users/models.py b/backend/server/users/models.py index d1fd990..fa28f1c 100644 --- a/backend/server/users/models.py +++ b/backend/server/users/models.py @@ -6,6 +6,7 @@ from django_resized import ResizedImageField class CustomUser(AbstractUser): profile_pic = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='profile-pics/') uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + public_profile = models.BooleanField(default=False) def __str__(self): return self.username \ No newline at end of file From ee249fd6829f7a7bba35850d7facc06c7ec3371b Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 6 Sep 2024 23:19:44 -0400 Subject: [PATCH 06/25] Add users get and list views --- backend/server/main/urls.py | 4 ++- backend/server/users/serializers.py | 2 +- backend/server/users/views.py | 38 ++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index e9eb1f7..f13410d 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -4,7 +4,7 @@ from django.views.generic import RedirectView, TemplateView from django.conf import settings from django.conf.urls.static import static from adventures import urls as adventures -from users.views import ChangeEmailView, IsRegistrationDisabled +from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView from .views import get_csrf_token from drf_yasg.views import get_schema_view @@ -22,6 +22,8 @@ urlpatterns = [ path('auth/change-email/', ChangeEmailView.as_view(), name='change_email'), path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'), + path('auth/users', PublicUserListView.as_view(), name='public-user-list'), + path('auth/user/', PublicUserDetailView.as_view(), name='public-user-detail'), path('csrf/', get_csrf_token, name='get_csrf_token'), re_path(r'^$', TemplateView.as_view( diff --git a/backend/server/users/serializers.py b/backend/server/users/serializers.py index d9628ba..5d3e3b4 100644 --- a/backend/server/users/serializers.py +++ b/backend/server/users/serializers.py @@ -174,7 +174,7 @@ class CustomUserDetailsSerializer(UserDetailsSerializer): class Meta(UserDetailsSerializer.Meta): model = CustomUser - fields = UserDetailsSerializer.Meta.fields + ('profile_pic',) + fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile') def to_representation(self, instance): representation = super().to_representation(instance) diff --git a/backend/server/users/views.py b/backend/server/users/views.py index 2c8021d..f01e4da 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -6,6 +6,11 @@ from .serializers import ChangeEmailSerializer from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from django.conf import settings +from django.shortcuts import get_object_or_404 +from django.contrib.auth import get_user_model +from .serializers import CustomUserDetailsSerializer as PublicUserSerializer + +User = get_user_model() class ChangeEmailView(APIView): permission_classes = [IsAuthenticated] @@ -41,4 +46,35 @@ class IsRegistrationDisabled(APIView): ) def get(self, request): return Response({"is_disabled": settings.DISABLE_REGISTRATION, "message": settings.DISABLE_REGISTRATION_MESSAGE}, status=status.HTTP_200_OK) - \ No newline at end of file + +class PublicUserListView(APIView): + # Allow the listing of all public users + permission_classes = [] + + @swagger_auto_schema( + responses={ + 200: openapi.Response('List of public users'), + 400: 'Bad Request' + }, + operation_description="List public users." + ) + def get(self, request): + users = User.objects.filter(public_profile=True).exclude(id=request.user.id) + serializer = PublicUserSerializer(users, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + +class PublicUserDetailView(APIView): + # Allow the retrieval of a single public user + permission_classes = [] + + @swagger_auto_schema( + responses={ + 200: openapi.Response('Public user information'), + 400: 'Bad Request' + }, + operation_description="Get public user information." + ) + def get(self, request, user_id): + user = get_object_or_404(User, uuid=user_id, public_profile=True) + serializer = PublicUserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) From 5ca5e1f69ccb9129f094fac4e2a7a326d7532ca3 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 6 Sep 2024 23:35:48 -0400 Subject: [PATCH 07/25] Change card colors and add aestheticDark theme. --- .../src/lib/components/AdventureCard.svelte | 2 +- .../src/lib/components/ChecklistCard.svelte | 2 +- .../src/lib/components/CollectionCard.svelte | 2 +- .../src/lib/components/CountryCard.svelte | 2 +- frontend/src/lib/components/Navbar.svelte | 7 ++- frontend/src/lib/components/NoteCard.svelte | 2 +- frontend/src/lib/components/RegionCard.svelte | 2 +- .../lib/components/TransportationCard.svelte | 10 +--- frontend/src/routes/+page.server.ts | 15 ++++-- frontend/tailwind.config.js | 47 ++++++++++++++++++- 10 files changed, 72 insertions(+), 19 deletions(-) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 3264ae1..9dfad0a 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -161,7 +161,7 @@ {/if}
{#if adventure.images && adventure.images.length > 0} diff --git a/frontend/src/lib/components/ChecklistCard.svelte b/frontend/src/lib/components/ChecklistCard.svelte index b0f05d4..19a3e79 100644 --- a/frontend/src/lib/components/ChecklistCard.svelte +++ b/frontend/src/lib/components/ChecklistCard.svelte @@ -29,7 +29,7 @@
diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte index 53d76f2..11e86d1 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -78,7 +78,7 @@ {/if}
diff --git a/frontend/src/lib/components/CountryCard.svelte b/frontend/src/lib/components/CountryCard.svelte index e03dba3..bd45868 100644 --- a/frontend/src/lib/components/CountryCard.svelte +++ b/frontend/src/lib/components/CountryCard.svelte @@ -13,7 +13,7 @@
diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index e3aa004..152da3b 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -202,7 +202,12 @@
  • - + +
  • diff --git a/frontend/src/lib/components/NoteCard.svelte b/frontend/src/lib/components/NoteCard.svelte index 863ea4d..61fed53 100644 --- a/frontend/src/lib/components/NoteCard.svelte +++ b/frontend/src/lib/components/NoteCard.svelte @@ -30,7 +30,7 @@
    diff --git a/frontend/src/lib/components/RegionCard.svelte b/frontend/src/lib/components/RegionCard.svelte index fb2bc51..44b8878 100644 --- a/frontend/src/lib/components/RegionCard.svelte +++ b/frontend/src/lib/components/RegionCard.svelte @@ -54,7 +54,7 @@
    {#if region.name_en && region.name !== region.name_en} diff --git a/frontend/src/lib/components/TransportationCard.svelte b/frontend/src/lib/components/TransportationCard.svelte index efd5001..68bcfe9 100644 --- a/frontend/src/lib/components/TransportationCard.svelte +++ b/frontend/src/lib/components/TransportationCard.svelte @@ -1,16 +1,10 @@

    {transportation.name}

    diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 8cb9577..29cd18f 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -9,9 +9,18 @@ export const actions: Actions = { // change the theme only if it is one of the allowed themes if ( theme && - ['light', 'dark', 'night', 'retro', 'forest', 'aqua', 'forest', 'garden', 'emerald'].includes( - theme - ) + [ + 'light', + 'dark', + 'night', + 'retro', + 'forest', + 'aqua', + 'forest', + 'aestheticLight', + 'aestheticDark', + 'emerald' + ].includes(theme) ) { cookies.set('colortheme', theme, { path: '/', diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index eeb76df..0365fd8 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -15,7 +15,52 @@ export default { 'aqua', 'emerald', { - garden: { + aestheticDark: { + primary: '#3e5747', + 'primary-focus': '#2f4236', + 'primary-content': '#e9e7e7', + + secondary: '#547b82', + 'secondary-focus': '#3d5960', + 'secondary-content': '#c1dfe5', + + accent: '#8b6161', + 'accent-focus': '#6e4545', + 'accent-content': '#f2eaea', + + neutral: '#2b2a2a', + 'neutral-focus': '#1c1b1b', + 'neutral-content': '#e9e7e7', + + 'base-100': '#121212', // Dark background + 'base-200': '#1d1d1d', + 'base-300': '#292929', + 'base-content': '#e9e7e7', // Light content on dark background + + // set bg-primary-content + 'bg-base': '#121212', + 'bg-base-content': '#e9e7e7', + + info: '#3b7ecb', + success: '#007766', + warning: '#d4c111', + error: '#e64a19', + + '--rounded-box': '1rem', + '--rounded-btn': '.5rem', + '--rounded-badge': '1.9rem', + + '--animation-btn': '.25s', + '--animation-input': '.2s', + + '--btn-text-case': 'uppercase', + '--navbar-padding': '.5rem', + '--border-btn': '1px', + + fontFamily: + 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' + }, + aestheticLight: { primary: '#5a7c65', 'primary-focus': '#48604f', 'primary-content': '#dcd5d5', From 6b778dea794427e39eb0a0b9799ee241bf2b1764 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 6 Sep 2024 23:39:37 -0400 Subject: [PATCH 08/25] Fix card colors in light modes --- frontend/src/lib/components/AdventureCard.svelte | 2 +- frontend/src/lib/components/CollectionCard.svelte | 2 +- frontend/src/lib/components/Navbar.svelte | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 9dfad0a..899d3d2 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -221,7 +221,7 @@
    Dining
    {/if} -
    {adventure.is_public ? 'Public' : 'Private'}
    +
    {adventure.is_public ? 'Public' : 'Private'}
    {#if adventure.location && adventure.location !== ''}
    diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte index 11e86d1..d0bd379 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -90,7 +90,7 @@
    -
    {collection.is_public ? 'Public' : 'Private'}
    +
    {collection.is_public ? 'Public' : 'Private'}
    {#if collection.is_archived}
    Archived
    {/if} diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 152da3b..c07ffb7 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -12,6 +12,7 @@ import Water from '~icons/mdi/water'; import AboutModal from './AboutModal.svelte'; import Avatar from './Avatar.svelte'; + import PaletteOutline from '~icons/mdi/palette-outline'; import { page } from '$app/stores'; let query: string = ''; @@ -203,10 +204,10 @@
  • Aesthetic Light Aesthetic Dark
  • From bdb37c5ca1b1fb325feb46cdad7b0ee497d4be78 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 6 Sep 2024 23:47:40 -0400 Subject: [PATCH 09/25] Fix the 3 dot button --- frontend/src/lib/components/AdventureCard.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 899d3d2..e2aac26 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -256,7 +256,7 @@ {#if type != 'link'} {#if user?.pk == adventure.user_id}
    diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a0f219f..5e5060a 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -80,6 +80,7 @@ export type Collection = { notes?: Note[]; checklists?: Checklist[]; is_archived?: boolean; + shared_with: string[]; }; export type OpenStreetMapPlace = { From f150423c1edf5de46af0620641d90bab8a89a898 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sun, 8 Sep 2024 15:13:57 -0400 Subject: [PATCH 17/25] Add follow command to the deploy script --- deploy.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy.sh b/deploy.sh index 9548c7c..e2372e7 100644 --- a/deploy.sh +++ b/deploy.sh @@ -4,4 +4,5 @@ echo "Deploying latest version of AdventureLog" docker compose pull echo "Stating containers" docker compose up -d -echo "All set!" \ No newline at end of file +echo "All set!" +docker logs adventurelog-backend --follow \ No newline at end of file From ba89f90e5397c8c4c32c395c4117940cf562e7e2 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 9 Sep 2024 13:31:00 -0400 Subject: [PATCH 18/25] Shared with collaborative editing --- README.md | 2 ++ .../src/lib/components/AdventureCard.svelte | 20 +++++++++---------- .../src/lib/components/ChecklistCard.svelte | 7 ++++--- .../src/lib/components/ChecklistModal.svelte | 4 ++-- frontend/src/lib/components/NoteCard.svelte | 7 ++++--- frontend/src/lib/components/NoteModal.svelte | 3 ++- .../lib/components/TransportationCard.svelte | 5 +++-- frontend/src/lib/index.ts | 2 +- .../src/routes/adventures/[id]/+page.svelte | 12 ++++++----- .../src/routes/collections/[id]/+page.svelte | 4 ++++ 10 files changed, 38 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 8dc37c1..5976417 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ **Documentation can be found [here](https://docs.adventurelog.app).** +**Join the AdventureLog Community Discord Server [here](https://discord.gg/wRbQ9Egr8C).** + # Table of Contents - [Installation](#installation) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index e2aac26..8ad5668 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -1,7 +1,7 @@ + +{#if collections.length > 0} +
    + {#each collections as collection} + + {/each} +
    +{:else} +

    + No collections found that are shared with you. + {#if data.user && !data.user?.public_profile} +

    In order to allow users to share with you, you need your profile set to public.

    + + {/if} +

    +{/if} From dd17e24f4412ff2d0efa189360f64b3350b40c47 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 9 Sep 2024 14:29:50 -0400 Subject: [PATCH 20/25] Fix is_public bugs --- backend/server/adventures/views.py | 36 +++++++++++++------ .../src/lib/components/AdventureCard.svelte | 5 +-- .../src/lib/components/AdventureModal.svelte | 30 ++++++++-------- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 2018e99..fb5d220 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -95,12 +95,6 @@ class AdventureViewSet(viewsets.ModelViewSet): adventure = get_object_or_404(queryset, pk=kwargs['pk']) serializer = self.get_serializer(adventure) return Response(serializer.data) - - def perform_create(self, serializer): - adventure = serializer.save(user_id=self.request.user) - if adventure.collection: - adventure.is_public = adventure.collection.is_public - adventure.save() def perform_update(self, serializer): adventure = serializer.save() @@ -201,7 +195,7 @@ class AdventureViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - def partial_update(self, request, *args, **kwargs): + def update(self, request, *args, **kwargs): # Retrieve the current object instance = self.get_object() @@ -209,6 +203,10 @@ class AdventureViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) + # if the adventure is trying to have is_public changed and its part of a collection return an error + if 'is_public' in serializer.validated_data and instance.collection: + return Response({"error": "Cannot change is_public for adventures in a collection"}, status=400) + # Retrieve the collection from the validated data new_collection = serializer.validated_data.get('collection') @@ -244,6 +242,10 @@ class AdventureViewSet(viewsets.ModelViewSet): user = request.user print(new_collection) + # if the adventure is trying to have is_public changed and its part of a collection return an error + if 'is_public' in serializer.validated_data and instance.collection: + return Response({"error": "Cannot change is_public for adventures in a collection"}, status=400) + 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: @@ -266,7 +268,7 @@ class AdventureViewSet(viewsets.ModelViewSet): 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 @@ -275,7 +277,8 @@ class AdventureViewSet(viewsets.ModelViewSet): # 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) + # set the is_public field of the adventure to the is_public field of the collection + serializer.save(user_id=collection.user_id, is_public=collection.is_public) return # Save the adventure with the current user as the owner @@ -380,6 +383,11 @@ class CollectionViewSet(viewsets.ModelViewSet): if 'is_public' in serializer.validated_data: new_public_status = serializer.validated_data['is_public'] + # if is_publuc has changed and the user is not the owner of the collection return an error + if new_public_status != instance.is_public and instance.user_id != request.user: + print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.user_id}") + return Response({"error": "User does not own the collection"}, status=400) + # Update associated adventures to match the collection's is_public status Adventure.objects.filter(collection=instance).update(is_public=new_public_status) @@ -467,6 +475,8 @@ class CollectionViewSet(viewsets.ModelViewSet): ).distinct() if self.action == 'retrieve': + if not self.request.user.is_authenticated: + return Collection.objects.filter(is_public=True) return Collection.objects.filter( Q(is_public=True) | Q(user_id=self.request.user.id) | Q(shared_with=self.request.user) ).distinct() @@ -966,7 +976,13 @@ class AdventureImageViewSet(viewsets.ModelViewSet): return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND) if adventure.user_id != request.user: - return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) + # Check if the adventure has a collection + if adventure.collection: + # Check if the user is in the collection's shared_with list + if not adventure.collection.shared_with.filter(id=request.user.id).exists(): + return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN) + else: + return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) return super().create(request, *args, **kwargs) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 8ad5668..d72fdfa 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -307,8 +307,9 @@
    {:else} - goto(`/adventures/${adventure.id}`)}> {/if} {/if} diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index c2e6e03..e225c1f 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -627,22 +627,24 @@ {/if}
    -
    -
    -
    -
    - + {#if !collection_id} +
    +
    +
    +
    + +
    -
    + {/if}

    Location Information

    From 45cc925451bd8444e84d29200e9d8833168bf967 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 9 Sep 2024 16:18:06 -0400 Subject: [PATCH 21/25] Country search --- frontend/src/routes/worldtravel/+page.svelte | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/worldtravel/+page.svelte b/frontend/src/routes/worldtravel/+page.svelte index d52f228..8748a67 100644 --- a/frontend/src/routes/worldtravel/+page.svelte +++ b/frontend/src/routes/worldtravel/+page.svelte @@ -3,16 +3,40 @@ import type { Country } from '$lib/types'; import type { PageData } from './$types'; + let searchQuery: string = ''; + + let filteredCountries: Country[] = []; + export let data: PageData; console.log(data); const countries: Country[] = data.props?.countries || []; + + $: { + // if query is empty, show all countries + if (searchQuery === '') { + filteredCountries = countries; + } else { + // otherwise, filter countries by name + filteredCountries = countries.filter((country) => + country.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + } + }

    Country List

    +
    + +
    - {#each countries as country} + {#each filteredCountries as country} {/each} From eab236935269d9d7a042261cbbed79655fc0b7bf Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 9 Sep 2024 23:47:09 -0400 Subject: [PATCH 22/25] Fix is_public mismatching --- backend/server/adventures/views.py | 13 +++++++++---- frontend/src/routes/worldtravel/+page.svelte | 6 +++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index fb5d220..7d2a136 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -204,8 +204,11 @@ class AdventureViewSet(viewsets.ModelViewSet): serializer.is_valid(raise_exception=True) # if the adventure is trying to have is_public changed and its part of a collection return an error - if 'is_public' in serializer.validated_data and instance.collection: - return Response({"error": "Cannot change is_public for adventures in a collection"}, status=400) + if new_collection is not None: + serializer.validated_data['is_public'] = new_collection.is_public + elif instance.collection: + serializer.validated_data['is_public'] = instance.collection.is_public + # Retrieve the collection from the validated data new_collection = serializer.validated_data.get('collection') @@ -243,8 +246,10 @@ class AdventureViewSet(viewsets.ModelViewSet): print(new_collection) # if the adventure is trying to have is_public changed and its part of a collection return an error - if 'is_public' in serializer.validated_data and instance.collection: - return Response({"error": "Cannot change is_public for adventures in a collection"}, status=400) + if new_collection is not None: + serializer.validated_data['is_public'] = new_collection.is_public + elif instance.collection: + serializer.validated_data['is_public'] = instance.collection.is_public if new_collection is not None and new_collection!=instance.collection: # Check if the user is the owner of the new collection diff --git a/frontend/src/routes/worldtravel/+page.svelte b/frontend/src/routes/worldtravel/+page.svelte index 8748a67..48a1b54 100644 --- a/frontend/src/routes/worldtravel/+page.svelte +++ b/frontend/src/routes/worldtravel/+page.svelte @@ -25,7 +25,11 @@ } -

    Country List

    +

    Country List

    + +

    + {filteredCountries.length} countries found +

    Date: Mon, 9 Sep 2024 23:59:45 -0400 Subject: [PATCH 23/25] Add more to user page --- frontend/src/lib/components/UserCard.svelte | 6 +----- frontend/src/routes/user/[uuid]/+page.svelte | 21 +++++++++++++++++--- frontend/src/routes/users/+page.svelte | 5 +++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/UserCard.svelte b/frontend/src/lib/components/UserCard.svelte index 6f95774..3deb52d 100644 --- a/frontend/src/lib/components/UserCard.svelte +++ b/frontend/src/lib/components/UserCard.svelte @@ -11,10 +11,6 @@ export let shared_with: string[] | undefined = undefined; export let user: User; - - async function nav() { - goto(`/user/${user.uuid}`); - }
    {#if !sharing} - + {:else if shared_with && !shared_with.includes(user.uuid)} {:else} diff --git a/frontend/src/routes/user/[uuid]/+page.svelte b/frontend/src/routes/user/[uuid]/+page.svelte index ba42503..162b6f1 100644 --- a/frontend/src/routes/user/[uuid]/+page.svelte +++ b/frontend/src/routes/user/[uuid]/+page.svelte @@ -6,7 +6,22 @@ console.log(user); -

    {user.first_name} {user.last_name}

    -

    {user.username}

    +
    +
    + {user.username} +
    +
    -

    {user.is_staff ? 'Admin' : 'User'}

    +

    {user.first_name} {user.last_name}

    +

    {user.username}

    + +
    + {#if user.is_staff} +
    Admin
    + {/if} +
    + + + {user.username} | AdventureLog + + diff --git a/frontend/src/routes/users/+page.svelte b/frontend/src/routes/users/+page.svelte index 828d05f..f639776 100644 --- a/frontend/src/routes/users/+page.svelte +++ b/frontend/src/routes/users/+page.svelte @@ -18,3 +18,8 @@ {#if users.length === 0}

    No users found with public profiles.

    {/if} + + + Users + + From 4c858ab8b554b574df596258deabb5151f4ce7d7 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 10 Sep 2024 09:30:09 -0400 Subject: [PATCH 24/25] Fix adding to collection to change value of is_public first try --- frontend/src/lib/components/Navbar.svelte | 4 ++ .../routes/adventures/[id]/+page.server.ts | 67 ------------------- .../src/routes/collections/[id]/+page.svelte | 12 ++-- frontend/src/routes/user/[uuid]/+page.svelte | 16 +++-- 4 files changed, 23 insertions(+), 76 deletions(-) diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 3011b9d..8e52f89 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -194,6 +194,10 @@ on:click={() => (window.location.href = 'https://docs.adventurelog.app/')} >Documentation +

    Theme Selection

  • diff --git a/frontend/src/routes/adventures/[id]/+page.server.ts b/frontend/src/routes/adventures/[id]/+page.server.ts index aa253fb..aa01d2e 100644 --- a/frontend/src/routes/adventures/[id]/+page.server.ts +++ b/frontend/src/routes/adventures/[id]/+page.server.ts @@ -99,72 +99,5 @@ export const actions: Actions = { status: 204 }; } - }, - addToCollection: async (event) => { - const id = event.params as { id: string }; - const adventureId = id.id; - - const formData = await event.request.formData(); - const trip_id = formData.get('collection_id'); - - if (!trip_id) { - return { - status: 400, - error: { message: 'Missing collection id' } - }; - } - - if (!event.locals.user) { - const refresh = event.cookies.get('refresh'); - let auth = event.cookies.get('auth'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - if (!adventureId) { - return { - status: 400, - error: new Error('Bad request') - }; - } - - let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}/`, { - method: 'PATCH', - headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ collection: trip_id }) - }); - let res2 = await res.json(); - console.log(res2); - if (!res.ok) { - return { - status: res.status, - error: new Error('Failed to delete adventure') - }; - } else { - return { - status: 204 - }; - } } }; diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 10b996f..94b9283 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -88,16 +88,18 @@ return; } else { let adventure = event.detail; - let formData = new FormData(); - formData.append('collection_id', collection.id.toString()); - let res = await fetch(`/adventures/${adventure.id}?/addToCollection`, { - method: 'POST', - body: formData // Remove the Content-Type header + let res = await fetch(`/api/adventures/${adventure.id}/`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ collection: collection.id.toString() }) }); if (res.ok) { console.log('Adventure added to collection'); + adventure = await res.json(); adventures = [...adventures, adventure]; } else { console.log('Error adding adventure to collection'); diff --git a/frontend/src/routes/user/[uuid]/+page.svelte b/frontend/src/routes/user/[uuid]/+page.svelte index 162b6f1..071be0f 100644 --- a/frontend/src/routes/user/[uuid]/+page.svelte +++ b/frontend/src/routes/user/[uuid]/+page.svelte @@ -6,11 +6,13 @@ console.log(user); -
    -
    - {user.username} +{#if user.profile_pic} +
    +
    + {user.username} +
    -
    +{/if}

    {user.first_name} {user.last_name}

    {user.username}

    @@ -21,6 +23,12 @@ {/if}
    +
    +

    + {user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''} +

    +
    + {user.username} | AdventureLog From 3a2f095ab638a1f0fb8d11bbecc7f396ab2d2bd5 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 10 Sep 2024 09:59:59 -0400 Subject: [PATCH 25/25] Sharing modal fixes and redact email for privacy --- backend/server/users/views.py | 5 +++++ frontend/src/lib/components/ShareModal.svelte | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/backend/server/users/views.py b/backend/server/users/views.py index f01e4da..5300d8c 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -60,6 +60,9 @@ class PublicUserListView(APIView): ) def get(self, request): users = User.objects.filter(public_profile=True).exclude(id=request.user.id) + # remove the email addresses from the response + for user in users: + user.email = None serializer = PublicUserSerializer(users, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -76,5 +79,7 @@ class PublicUserDetailView(APIView): ) def get(self, request, user_id): user = get_object_or_404(User, uuid=user_id, public_profile=True) + # remove the email address from the response + user.email = None serializer = PublicUserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/frontend/src/lib/components/ShareModal.svelte b/frontend/src/lib/components/ShareModal.svelte index 5f688a9..0ad5cc3 100644 --- a/frontend/src/lib/components/ShareModal.svelte +++ b/frontend/src/lib/components/ShareModal.svelte @@ -91,6 +91,9 @@ />
  • {/each} + {#if sharedWithUsers.length === 0} +

    No users shared with

    + {/if}

    Not Shared With

    @@ -106,6 +109,9 @@ />
    {/each} + {#if notSharedWithUsers.length === 0} +

    No users not shared with

    + {/if}