1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 12:59:36 +02:00

More sharing feartures and permissions

This commit is contained in:
Sean Morley 2024-09-02 10:29:51 -04:00
parent f2ba479c8e
commit 0664d9434c
5 changed files with 160 additions and 21 deletions

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

@ -28,3 +28,70 @@ class IsPublicReadOnly(permissions.BasePermission):
# 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

View file

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

View file

@ -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()
@ -100,8 +98,6 @@ class AdventureViewSet(viewsets.ModelViewSet):
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):
@ -196,6 +192,46 @@ class AdventureViewSet(viewsets.ModelViewSet):
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()
page = paginator.paginate_queryset(queryset, request)
@ -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):