1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-24 15:29:36 +02:00

Allow for Sharing of Collections to other Public Users

This commit is contained in:
Sean Morley 2024-09-10 17:16:05 -04:00 committed by GitHub
commit 4a293798eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1368 additions and 311 deletions

View file

@ -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:

View file

@ -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),
),
]

View file

@ -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

View file

@ -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 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 for shared users
if request.user in obj.collection.shared_with.all():
return True
# 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 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 for shared users
if request.user in obj.collection.shared_with.all():
return True
# Allow all actions for the owner
return obj.user_id == request.user

View file

@ -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
@ -225,5 +145,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

View file

@ -6,14 +6,18 @@ 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 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
@ -28,7 +32,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):
@ -68,41 +72,36 @@ class AdventureViewSet(viewsets.ModelViewSet):
return queryset.order_by(ordering)
def get_queryset(self):
# 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)
return Adventure.objects.none()
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()
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()
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):
types = request.query_params.get('types', '').split(',')
@ -195,6 +194,100 @@ 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):
# 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 the adventure is trying to have is_public changed and its part of a collection return an error
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')
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 the adventure is trying to have is_public changed and its part of a collection return an error
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
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
# 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
serializer.save(user_id=self.request.user)
def paginate_and_respond(self, queryset, request):
paginator = self.pagination_class()
@ -207,7 +300,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):
@ -244,7 +337,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
@ -285,10 +378,21 @@ 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']
# 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)
@ -310,25 +414,86 @@ class CollectionViewSet(viewsets.ModelViewSet):
instance._prefetched_objects_cache = {}
return Response(serializer.data)
# make an action to retreive all adventures that are shared with the user
@action(detail=False, methods=['get'])
def shared(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(
shared_with=request.user
)
queryset = self.apply_sorting(queryset)
serializer = self.get_serializer(queryset, many=True)
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<uuid>[^/.]+)')
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<uuid>[^/.]+)')
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':
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':
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(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):
@ -430,7 +595,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
@ -451,21 +616,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
@ -486,21 +738,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
@ -521,15 +860,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):
@ -555,7 +981,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)

View file

@ -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/<uuid:user_id>/', PublicUserDetailView.as_view(), name='public-user-detail'),
path('csrf/', get_csrf_token, name='get_csrf_token'),
re_path(r'^$', TemplateView.as_view(

View file

@ -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),
),
]

View file

@ -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

View file

@ -1,7 +1,7 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from adventures.models import Adventure
from adventures.models import Adventure, Collection
from users.forms import CustomAllAuthPasswordResetForm
from dj_rest_auth.serializers import PasswordResetSerializer
from rest_framework.exceptions import PermissionDenied
@ -133,8 +133,6 @@ class UserDetailsSerializer(serializers.ModelSerializer):
@staticmethod
def validate_username(username):
if 'allauth.account' not in settings.INSTALLED_APPS:
# We don't need to call the all-auth
# username validator unless it's installed
return username
from allauth.account.adapter import get_adapter
@ -142,12 +140,9 @@ class UserDetailsSerializer(serializers.ModelSerializer):
return username
class Meta:
extra_fields = ['profile_pic']
extra_fields = ['profile_pic', 'uuid', 'public_profile']
profile_pic = serializers.ImageField(required=False)
# see https://github.com/iMerica/dj-rest-auth/issues/181
# UserModel.XYZ causing attribute error while importing other
# classes from `serializers.py`. So, we need to check whether the auth model has
# the attribute or not
if hasattr(UserModel, 'USERNAME_FIELD'):
extra_fields.append(UserModel.USERNAME_FIELD)
if hasattr(UserModel, 'EMAIL_FIELD'):
@ -160,21 +155,38 @@ class UserDetailsSerializer(serializers.ModelSerializer):
extra_fields.append('date_joined')
if hasattr(UserModel, 'is_staff'):
extra_fields.append('is_staff')
if hasattr(UserModel, 'public_profile'):
extra_fields.append('public_profile')
class Meta(UserDetailsSerializer.Meta):
model = CustomUser
fields = UserDetailsSerializer.Meta.fields + ('profile_pic',)
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
model = UserModel
fields = ('pk', *extra_fields)
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
def handle_public_profile_change(self, instance, validated_data):
"""Remove user from `shared_with` if public profile is set to False."""
if 'public_profile' in validated_data and not validated_data['public_profile']:
for collection in Collection.objects.filter(shared_with=instance):
collection.shared_with.remove(instance)
def update(self, instance, validated_data):
self.handle_public_profile_change(instance, validated_data)
return super().update(instance, validated_data)
def partial_update(self, instance, validated_data):
self.handle_public_profile_change(instance, validated_data)
return super().partial_update(instance, validated_data)
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)

View file

@ -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,40 @@ class IsRegistrationDisabled(APIView):
)
def get(self, request):
return Response({"is_disabled": settings.DISABLE_REGISTRATION, "message": settings.DISABLE_REGISTRATION_MESSAGE}, status=status.HTTP_200_OK)
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)
# 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)
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)
# remove the email address from the response
user.email = None
serializer = PublicUserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK)