mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 12:59:36 +02:00
Allow for Sharing of Collections to other Public Users
This commit is contained in:
commit
4a293798eb
47 changed files with 1368 additions and 311 deletions
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
**Documentation can be found [here](https://docs.adventurelog.app).**
|
**Documentation can be found [here](https://docs.adventurelog.app).**
|
||||||
|
|
||||||
|
**Join the AdventureLog Community Discord Server [here](https://discord.gg/wRbQ9Egr8C).**
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
|
|
@ -46,8 +46,9 @@ from users.models import CustomUser
|
||||||
class CustomUserAdmin(UserAdmin):
|
class CustomUserAdmin(UserAdmin):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display']
|
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display']
|
||||||
|
readonly_fields = ('uuid',)
|
||||||
fieldsets = UserAdmin.fieldsets + (
|
fieldsets = UserAdmin.fieldsets + (
|
||||||
(None, {'fields': ('profile_pic',)}),
|
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
|
||||||
)
|
)
|
||||||
def image_display(self, obj):
|
def image_display(self, obj):
|
||||||
if obj.profile_pic:
|
if obj.profile_pic:
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -81,6 +81,7 @@ class Collection(models.Model):
|
||||||
end_date = models.DateField(blank=True, null=True)
|
end_date = models.DateField(blank=True, null=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
is_archived = models.BooleanField(default=False)
|
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
|
# if connected adventures are private and collection is public, raise an error
|
||||||
|
|
|
@ -27,4 +27,71 @@ class IsPublicReadOnly(permissions.BasePermission):
|
||||||
return obj.is_public or obj.user_id == request.user
|
return obj.is_public or obj.user_id == request.user
|
||||||
|
|
||||||
# Write permissions are only allowed to the owner of the object
|
# 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
|
return obj.user_id == request.user
|
|
@ -8,19 +8,6 @@ class AdventureImageSerializer(serializers.ModelSerializer):
|
||||||
fields = ['id', 'image', 'adventure']
|
fields = ['id', 'image', 'adventure']
|
||||||
read_only_fields = ['id']
|
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):
|
def to_representation(self, instance):
|
||||||
representation = super().to_representation(instance)
|
representation = super().to_representation(instance)
|
||||||
if instance.image:
|
if instance.image:
|
||||||
|
@ -55,29 +42,6 @@ class TransportationSerializer(serializers.ModelSerializer):
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
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 NoteSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -87,29 +51,6 @@ class NoteSerializer(serializers.ModelSerializer):
|
||||||
'is_public', 'collection', 'created_at', 'updated_at'
|
'is_public', 'collection', 'created_at', 'updated_at'
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
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 ChecklistItemSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -119,29 +60,15 @@ class ChecklistItemSerializer(serializers.ModelSerializer):
|
||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'checklist']
|
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'checklist']
|
||||||
|
|
||||||
def validate(self, data):
|
# def validate(self, data):
|
||||||
# Check if the checklist is public and the checklist item is not
|
# # Check if the checklist is public and the checklist item is not
|
||||||
checklist = data.get('checklist')
|
# checklist = data.get('checklist')
|
||||||
is_checked = data.get('is_checked', False)
|
# is_checked = data.get('is_checked', False)
|
||||||
if checklist and checklist.is_public and not is_checked:
|
# if checklist and checklist.is_public and not is_checked:
|
||||||
raise serializers.ValidationError(
|
# raise serializers.ValidationError(
|
||||||
'Checklist items associated with a public checklist must be checked.'
|
# '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)
|
|
||||||
|
|
||||||
|
|
||||||
class ChecklistSerializer(serializers.ModelSerializer):
|
class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
items = ChecklistItemSerializer(many=True, source='checklistitem_set')
|
items = ChecklistItemSerializer(many=True, source='checklistitem_set')
|
||||||
|
@ -204,13 +131,6 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
'Checklists associated with a public collection must be public.'
|
'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
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -225,5 +145,14 @@ class CollectionSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Collection
|
model = Collection
|
||||||
# fields are all plus the adventures field
|
# 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']
|
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
|
|
@ -6,14 +6,18 @@ from rest_framework import viewsets
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage
|
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from worldtravel.models import VisitedRegion, Region, Country
|
from worldtravel.models import VisitedRegion, Region, Country
|
||||||
from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
|
from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from django.db.models import Q, Prefetch
|
from django.db.models import Q
|
||||||
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
|
from .permissions import CollectionShared, IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
class StandardResultsSetPagination(PageNumberPagination):
|
class StandardResultsSetPagination(PageNumberPagination):
|
||||||
page_size = 25
|
page_size = 25
|
||||||
|
@ -28,7 +32,7 @@ from django.db.models import Q
|
||||||
|
|
||||||
class AdventureViewSet(viewsets.ModelViewSet):
|
class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = AdventureSerializer
|
serializer_class = AdventureSerializer
|
||||||
permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly]
|
permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess]
|
||||||
pagination_class = StandardResultsSetPagination
|
pagination_class = StandardResultsSetPagination
|
||||||
|
|
||||||
def apply_sorting(self, queryset):
|
def apply_sorting(self, queryset):
|
||||||
|
@ -68,41 +72,36 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
return queryset.order_by(ordering)
|
return queryset.order_by(ordering)
|
||||||
|
|
||||||
def get_queryset(self):
|
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':
|
if self.action == 'retrieve':
|
||||||
# For individual adventure retrieval, include public adventures
|
# For individual adventure retrieval, include public adventures
|
||||||
return Adventure.objects.filter(
|
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:
|
else:
|
||||||
# For other actions, only include user's own adventures
|
# For other actions, include user's own adventures and shared adventures
|
||||||
return Adventure.objects.filter(user_id=self.request.user.id)
|
return Adventure.objects.filter(
|
||||||
|
Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
|
||||||
def list(self, request, *args, **kwargs):
|
)
|
||||||
# Prevent listing all adventures
|
|
||||||
return Response({"detail": "Listing all adventures is not allowed."},
|
|
||||||
status=status.HTTP_403_FORBIDDEN)
|
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
adventure = get_object_or_404(queryset, pk=kwargs['pk'])
|
adventure = get_object_or_404(queryset, pk=kwargs['pk'])
|
||||||
serializer = self.get_serializer(adventure)
|
serializer = self.get_serializer(adventure)
|
||||||
return Response(serializer.data)
|
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):
|
def perform_update(self, serializer):
|
||||||
adventure = serializer.save()
|
adventure = serializer.save()
|
||||||
if adventure.collection:
|
if adventure.collection:
|
||||||
adventure.is_public = adventure.collection.is_public
|
adventure.is_public = adventure.collection.is_public
|
||||||
adventure.save()
|
adventure.save()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
serializer.save(user_id=self.request.user)
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def filtered(self, request):
|
def filtered(self, request):
|
||||||
types = request.query_params.get('types', '').split(',')
|
types = request.query_params.get('types', '').split(',')
|
||||||
|
@ -195,6 +194,100 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
queryset = self.apply_sorting(queryset)
|
queryset = self.apply_sorting(queryset)
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
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):
|
def paginate_and_respond(self, queryset, request):
|
||||||
paginator = self.pagination_class()
|
paginator = self.pagination_class()
|
||||||
|
@ -207,7 +300,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
class CollectionViewSet(viewsets.ModelViewSet):
|
class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = CollectionSerializer
|
serializer_class = CollectionSerializer
|
||||||
permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly]
|
permission_classes = [CollectionShared]
|
||||||
pagination_class = StandardResultsSetPagination
|
pagination_class = StandardResultsSetPagination
|
||||||
|
|
||||||
# def get_queryset(self):
|
# def get_queryset(self):
|
||||||
|
@ -244,7 +337,7 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
# make sure the user is authenticated
|
# make sure the user is authenticated
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=400)
|
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)
|
queryset = self.apply_sorting(queryset)
|
||||||
collections = self.paginate_and_respond(queryset, request)
|
collections = self.paginate_and_respond(queryset, request)
|
||||||
return collections
|
return collections
|
||||||
|
@ -285,10 +378,21 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||||
serializer.is_valid(raise_exception=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)
|
||||||
|
|
||||||
# Check if the 'is_public' field is present in the update data
|
# Check if the 'is_public' field is present in the update data
|
||||||
if 'is_public' in serializer.validated_data:
|
if 'is_public' in serializer.validated_data:
|
||||||
new_public_status = serializer.validated_data['is_public']
|
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
|
# Update associated adventures to match the collection's is_public status
|
||||||
Adventure.objects.filter(collection=instance).update(is_public=new_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 = {}
|
instance._prefetched_objects_cache = {}
|
||||||
|
|
||||||
return Response(serializer.data)
|
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):
|
def get_queryset(self):
|
||||||
if self.action == 'destroy':
|
if self.action == 'destroy':
|
||||||
return Collection.objects.filter(user_id=self.request.user.id)
|
return Collection.objects.filter(user_id=self.request.user.id)
|
||||||
|
|
||||||
if self.action in ['update', 'partial_update']:
|
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 self.action == 'retrieve':
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return Collection.objects.filter(is_public=True)
|
||||||
return Collection.objects.filter(
|
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(
|
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):
|
def perform_create(self, serializer):
|
||||||
|
# This is ok because you cannot share a collection when creating it
|
||||||
serializer.save(user_id=self.request.user)
|
serializer.save(user_id=self.request.user)
|
||||||
|
|
||||||
def paginate_and_respond(self, queryset, request):
|
def paginate_and_respond(self, queryset, request):
|
||||||
|
@ -430,7 +595,7 @@ class ActivityTypesView(viewsets.ViewSet):
|
||||||
class TransportationViewSet(viewsets.ModelViewSet):
|
class TransportationViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Transportation.objects.all()
|
queryset = Transportation.objects.all()
|
||||||
serializer_class = TransportationSerializer
|
serializer_class = TransportationSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess]
|
||||||
filterset_fields = ['type', 'is_public', 'collection']
|
filterset_fields = ['type', 'is_public', 'collection']
|
||||||
|
|
||||||
# return error message if user is not authenticated on the root endpoint
|
# return error message if user is not authenticated on the root endpoint
|
||||||
|
@ -451,21 +616,108 @@ class TransportationViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
# if the user is not authenticated return only public transportations for retrieve action
|
||||||
"""
|
if not self.request.user.is_authenticated:
|
||||||
This view should return a list of all transportations
|
if self.action == 'retrieve':
|
||||||
for the currently authenticated user.
|
return Transportation.objects.filter(is_public=True)
|
||||||
"""
|
return Transportation.objects.none()
|
||||||
user = self.request.user
|
|
||||||
return Transportation.objects.filter(user_id=user)
|
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
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)
|
serializer.save(user_id=self.request.user)
|
||||||
|
|
||||||
class NoteViewSet(viewsets.ModelViewSet):
|
class NoteViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Note.objects.all()
|
queryset = Note.objects.all()
|
||||||
serializer_class = NoteSerializer
|
serializer_class = NoteSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess]
|
||||||
filterset_fields = ['is_public', 'collection']
|
filterset_fields = ['is_public', 'collection']
|
||||||
|
|
||||||
# return error message if user is not authenticated on the root endpoint
|
# return error message if user is not authenticated on the root endpoint
|
||||||
|
@ -486,21 +738,108 @@ class NoteViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
# if the user is not authenticated return only public transportations for retrieve action
|
||||||
"""
|
if not self.request.user.is_authenticated:
|
||||||
This view should return a list of all notes
|
if self.action == 'retrieve':
|
||||||
for the currently authenticated user.
|
return Note.objects.filter(is_public=True)
|
||||||
"""
|
return Note.objects.none()
|
||||||
user = self.request.user
|
|
||||||
return Note.objects.filter(user_id=user)
|
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
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)
|
serializer.save(user_id=self.request.user)
|
||||||
|
|
||||||
class ChecklistViewSet(viewsets.ModelViewSet):
|
class ChecklistViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Checklist.objects.all()
|
queryset = Checklist.objects.all()
|
||||||
serializer_class = ChecklistSerializer
|
serializer_class = ChecklistSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsOwnerOrSharedWithFullAccess, IsPublicOrOwnerOrSharedWithFullAccess]
|
||||||
filterset_fields = ['is_public', 'collection']
|
filterset_fields = ['is_public', 'collection']
|
||||||
|
|
||||||
# return error message if user is not authenticated on the root endpoint
|
# return error message if user is not authenticated on the root endpoint
|
||||||
|
@ -521,15 +860,102 @@ class ChecklistViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
# if the user is not authenticated return only public transportations for retrieve action
|
||||||
"""
|
if not self.request.user.is_authenticated:
|
||||||
This view should return a list of all checklists
|
if self.action == 'retrieve':
|
||||||
for the currently authenticated user.
|
return Checklist.objects.filter(is_public=True)
|
||||||
"""
|
return Checklist.objects.none()
|
||||||
user = self.request.user
|
|
||||||
return Checklist.objects.filter(user_id=user)
|
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
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)
|
serializer.save(user_id=self.request.user)
|
||||||
|
|
||||||
class AdventureImageViewSet(viewsets.ModelViewSet):
|
class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
|
@ -555,7 +981,13 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
|
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
if adventure.user_id != request.user:
|
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)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.views.generic import RedirectView, TemplateView
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from adventures import urls as adventures
|
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 .views import get_csrf_token
|
||||||
from drf_yasg.views import get_schema_view
|
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/change-email/', ChangeEmailView.as_view(), name='change_email'),
|
||||||
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
|
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'),
|
path('csrf/', get_csrf_token, name='get_csrf_token'),
|
||||||
re_path(r'^$', TemplateView.as_view(
|
re_path(r'^$', TemplateView.as_view(
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,6 +6,7 @@ from django_resized import ResizedImageField
|
||||||
class CustomUser(AbstractUser):
|
class CustomUser(AbstractUser):
|
||||||
profile_pic = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='profile-pics/')
|
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)
|
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||||
|
public_profile = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.username
|
return self.username
|
|
@ -1,7 +1,7 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.contrib.auth import get_user_model
|
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 users.forms import CustomAllAuthPasswordResetForm
|
||||||
from dj_rest_auth.serializers import PasswordResetSerializer
|
from dj_rest_auth.serializers import PasswordResetSerializer
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
@ -133,8 +133,6 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_username(username):
|
def validate_username(username):
|
||||||
if 'allauth.account' not in settings.INSTALLED_APPS:
|
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
|
return username
|
||||||
|
|
||||||
from allauth.account.adapter import get_adapter
|
from allauth.account.adapter import get_adapter
|
||||||
|
@ -142,12 +140,9 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
||||||
return username
|
return username
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
extra_fields = ['profile_pic']
|
extra_fields = ['profile_pic', 'uuid', 'public_profile']
|
||||||
profile_pic = serializers.ImageField(required=False)
|
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'):
|
if hasattr(UserModel, 'USERNAME_FIELD'):
|
||||||
extra_fields.append(UserModel.USERNAME_FIELD)
|
extra_fields.append(UserModel.USERNAME_FIELD)
|
||||||
if hasattr(UserModel, 'EMAIL_FIELD'):
|
if hasattr(UserModel, 'EMAIL_FIELD'):
|
||||||
|
@ -160,21 +155,38 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
||||||
extra_fields.append('date_joined')
|
extra_fields.append('date_joined')
|
||||||
if hasattr(UserModel, 'is_staff'):
|
if hasattr(UserModel, 'is_staff'):
|
||||||
extra_fields.append('is_staff')
|
extra_fields.append('is_staff')
|
||||||
|
if hasattr(UserModel, 'public_profile'):
|
||||||
|
extra_fields.append('public_profile')
|
||||||
|
|
||||||
class Meta(UserDetailsSerializer.Meta):
|
class Meta(UserDetailsSerializer.Meta):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
fields = UserDetailsSerializer.Meta.fields + ('profile_pic',)
|
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
|
||||||
|
|
||||||
model = UserModel
|
model = UserModel
|
||||||
fields = ('pk', *extra_fields)
|
fields = ('pk', *extra_fields)
|
||||||
read_only_fields = ('email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
|
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 CustomUserDetailsSerializer(UserDetailsSerializer):
|
||||||
|
|
||||||
|
|
||||||
class Meta(UserDetailsSerializer.Meta):
|
class Meta(UserDetailsSerializer.Meta):
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
fields = UserDetailsSerializer.Meta.fields + ('profile_pic',)
|
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
representation = super().to_representation(instance)
|
representation = super().to_representation(instance)
|
||||||
|
|
|
@ -6,6 +6,11 @@ from .serializers import ChangeEmailSerializer
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from django.conf import settings
|
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):
|
class ChangeEmailView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
@ -41,4 +46,40 @@ class IsRegistrationDisabled(APIView):
|
||||||
)
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return Response({"is_disabled": settings.DISABLE_REGISTRATION, "message": settings.DISABLE_REGISTRATION_MESSAGE}, status=status.HTTP_200_OK)
|
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)
|
||||||
|
|
|
@ -4,4 +4,5 @@ echo "Deploying latest version of AdventureLog"
|
||||||
docker compose pull
|
docker compose pull
|
||||||
echo "Stating containers"
|
echo "Stating containers"
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
echo "All set!"
|
echo "All set!"
|
||||||
|
docker logs adventurelog-backend --follow
|
2
frontend/src/app.d.ts
vendored
2
frontend/src/app.d.ts
vendored
|
@ -13,6 +13,8 @@ declare global {
|
||||||
date_joined: string | null;
|
date_joined: string | null;
|
||||||
is_staff: boolean;
|
is_staff: boolean;
|
||||||
profile_pic: string | null;
|
profile_pic: string | null;
|
||||||
|
uuid: string;
|
||||||
|
public_profile: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import type { Adventure, User } from '$lib/types';
|
import type { Adventure, Collection, User } from '$lib/types';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import Launch from '~icons/mdi/launch';
|
import Launch from '~icons/mdi/launch';
|
||||||
|
@ -21,8 +21,8 @@
|
||||||
import ImageDisplayModal from './ImageDisplayModal.svelte';
|
import ImageDisplayModal from './ImageDisplayModal.svelte';
|
||||||
|
|
||||||
export let type: string;
|
export let type: string;
|
||||||
|
|
||||||
export let user: User | null;
|
export let user: User | null;
|
||||||
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
let isCollectionModalOpen: boolean = false;
|
let isCollectionModalOpen: boolean = false;
|
||||||
let isWarningModalOpen: boolean = false;
|
let isWarningModalOpen: boolean = false;
|
||||||
|
@ -161,7 +161,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl text-base-content"
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl"
|
||||||
>
|
>
|
||||||
<figure>
|
<figure>
|
||||||
{#if adventure.images && adventure.images.length > 0}
|
{#if adventure.images && adventure.images.length > 0}
|
||||||
|
@ -209,19 +209,17 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
|
{#if adventure.type == 'visited'}
|
||||||
<div class="badge badge-primary">Visited</div>
|
<div class="badge badge-primary">Visited</div>
|
||||||
{:else if user?.pk == adventure.user_id && adventure.type == 'planned'}
|
{:else if adventure.type == 'planned'}
|
||||||
<div class="badge badge-secondary">Planned</div>
|
<div class="badge badge-secondary">Planned</div>
|
||||||
{:else if (user?.pk !== adventure.user_id && adventure.type == 'planned') || adventure.type == 'visited'}
|
{:else if adventure.type == 'lodging'}
|
||||||
<div class="badge badge-secondary">Adventure</div>
|
|
||||||
{:else if user?.pk == adventure.user_id && adventure.type == 'lodging'}
|
|
||||||
<div class="badge badge-success">Lodging</div>
|
<div class="badge badge-success">Lodging</div>
|
||||||
{:else if adventure.type == 'dining'}
|
{:else if adventure.type == 'dining'}
|
||||||
<div class="badge badge-accent">Dining</div>
|
<div class="badge badge-accent">Dining</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="badge badge-neutral">{adventure.is_public ? 'Public' : 'Private'}</div>
|
<div class="badge badge-secondary">{adventure.is_public ? 'Public' : 'Private'}</div>
|
||||||
</div>
|
</div>
|
||||||
{#if adventure.location && adventure.location !== ''}
|
{#if adventure.location && adventure.location !== ''}
|
||||||
<div class="inline-flex items-center">
|
<div class="inline-flex items-center">
|
||||||
|
@ -254,9 +252,9 @@
|
||||||
<div class="card-actions justify-end mt-2">
|
<div class="card-actions justify-end mt-2">
|
||||||
<!-- action options dropdown -->
|
<!-- action options dropdown -->
|
||||||
{#if type != 'link'}
|
{#if type != 'link'}
|
||||||
{#if user?.pk == adventure.user_id}
|
{#if adventure.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-neutral">
|
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
||||||
<DotsHorizontal class="w-6 h-6" />
|
<DotsHorizontal class="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
@ -272,18 +270,18 @@
|
||||||
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
||||||
<FileDocumentEdit class="w-6 h-6" />Edit {keyword}
|
<FileDocumentEdit class="w-6 h-6" />Edit {keyword}
|
||||||
</button>
|
</button>
|
||||||
{#if adventure.type == 'visited'}
|
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
|
||||||
<button class="btn btn-neutral mb-2" on:click={changeType('planned')}
|
<button class="btn btn-neutral mb-2" on:click={changeType('planned')}
|
||||||
><FormatListBulletedSquare class="w-6 h-6" />Change to Plan</button
|
><FormatListBulletedSquare class="w-6 h-6" />Change to Plan</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if adventure.type == 'planned'}
|
{#if adventure.type == 'planned' && user?.pk == adventure.user_id}
|
||||||
<button class="btn btn-neutral mb-2" on:click={changeType('visited')}
|
<button class="btn btn-neutral mb-2" on:click={changeType('visited')}
|
||||||
><CheckBold class="w-6 h-6" />Mark Visited</button
|
><CheckBold class="w-6 h-6" />Mark Visited</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- remove from adventure -->
|
<!-- remove from adventure -->
|
||||||
{#if adventure.collection && (adventure.type == 'visited' || adventure.type == 'planned')}
|
{#if adventure.collection && (adventure.type == 'visited' || adventure.type == 'planned') && user?.pk == adventure.user_id}
|
||||||
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
|
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
|
||||||
><LinkVariantRemove class="w-6 h-6" />Remove from Collection</button
|
><LinkVariantRemove class="w-6 h-6" />Remove from Collection</button
|
||||||
>
|
>
|
||||||
|
@ -309,8 +307,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button class="btn btn-neutral mb-2" on:click={() => goto(`/adventures/${adventure.id}`)}
|
<button
|
||||||
><Launch class="w-6 h-6" /></button
|
class="btn btn-neutral-200 mb-2"
|
||||||
|
on:click={() => goto(`/adventures/${adventure.id}`)}><Launch class="w-6 h-6" /></button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -627,22 +627,24 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{#if !collection_id}
|
||||||
<div class="mt-2">
|
<div>
|
||||||
<div>
|
<div class="mt-2">
|
||||||
<label for="is_public"
|
<div>
|
||||||
>Public <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
<label for="is_public"
|
||||||
><br />
|
>Public <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||||
<input
|
><br />
|
||||||
type="checkbox"
|
<input
|
||||||
class="toggle toggle-primary"
|
type="checkbox"
|
||||||
id="is_public"
|
class="toggle toggle-primary"
|
||||||
name="is_public"
|
id="is_public"
|
||||||
bind:checked={adventure.is_public}
|
name="is_public"
|
||||||
/>
|
bind:checked={adventure.is_public}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h2 class="text-2xl font-semibold mb-2 mt-2">Location Information</h2>
|
<h2 class="text-2xl font-semibold mb-2 mt-2">Location Information</h2>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
<div class="dropdown dropdown-bottom dropdown-end" tabindex="0" role="button">
|
<div class="dropdown dropdown-bottom dropdown-end" tabindex="0" role="button">
|
||||||
<div class="avatar placeholder">
|
<div class="avatar placeholder">
|
||||||
<div class="bg-neutral text-neutral-content rounded-full w-10 ml-4">
|
<div class="bg-neutral rounded-full text-neutral-200 w-10 ml-4">
|
||||||
{#if user.profile_pic}
|
{#if user.profile_pic}
|
||||||
<img src={user.profile_pic} alt="User Profile" />
|
<img src={user.profile_pic} alt="User Profile" />
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content z-[1] menu p-2 shadow bg-primary-content mt-2 rounded-box w-52"
|
class="dropdown-content z-[1] text-neutral-200 menu p-2 shadow bg-neutral mt-2 rounded-box w-52"
|
||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y-missing-attribute -->
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
<!-- svelte-ignore a11y-missing-attribute -->
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
<li><button on:click={() => goto('/profile')}>Profile</button></li>
|
<li><button on:click={() => goto('/profile')}>Profile</button></li>
|
||||||
<li><button on:click={() => goto('/adventures')}>My Adventures</button></li>
|
<li><button on:click={() => goto('/adventures')}>My Adventures</button></li>
|
||||||
<li><button on:click={() => goto('/activities')}>My Activities</button></li>
|
<li><button on:click={() => goto('/activities')}>My Activities</button></li>
|
||||||
|
<li><button on:click={() => goto('/shared')}>Shared With Me</button></li>
|
||||||
<li><button on:click={() => goto('/settings')}>User Settings</button></li>
|
<li><button on:click={() => goto('/settings')}>User Settings</button></li>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<li><button formaction="/?/logout">Logout</button></li>
|
<li><button formaction="/?/logout">Logout</button></li>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import type { Checklist, User } from '$lib/types';
|
import type { Checklist, Collection, User } from '$lib/types';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
export let checklist: Checklist;
|
export let checklist: Checklist;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
function editChecklist() {
|
function editChecklist() {
|
||||||
dispatch('edit', checklist);
|
dispatch('edit', checklist);
|
||||||
|
@ -29,7 +30,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl overflow-hidden text-base-content"
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
|
@ -51,10 +52,10 @@
|
||||||
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
|
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
|
||||||
><Launch class="w-6 h-6" />Open Details</button
|
><Launch class="w-6 h-6" />Open Details</button
|
||||||
> -->
|
> -->
|
||||||
<button class="btn btn-neutral mb-2" on:click={editChecklist}>
|
<button class="btn btn-neutral-200 mb-2" on:click={editChecklist}>
|
||||||
<Launch class="w-6 h-6" />Open
|
<Launch class="w-6 h-6" />Open
|
||||||
</button>
|
</button>
|
||||||
{#if checklist.user_id == user?.pk}
|
{#if checklist.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
|
||||||
<button
|
<button
|
||||||
id="delete_adventure"
|
id="delete_adventure"
|
||||||
data-umami-event="Delete Checklist"
|
data-umami-event="Delete Checklist"
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
{
|
{
|
||||||
name: newItem,
|
name: newItem,
|
||||||
is_checked: newStatus,
|
is_checked: newStatus,
|
||||||
id: 0,
|
id: '',
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
checklist: 0,
|
checklist: 0,
|
||||||
created_at: '',
|
created_at: '',
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
<p class="font-semibold text-md mb-2">Editing checklist {initialName}</p>
|
<p class="font-semibold text-md mb-2">Editing checklist {initialName}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (checklist && user?.pk == checklist?.user_id) || !checklist}
|
{#if (checklist && user?.pk == checklist?.user_id) || (user && collection && collection.shared_with.includes(user.uuid)) || !checklist}
|
||||||
<form on:submit|preventDefault>
|
<form on:submit|preventDefault>
|
||||||
<div class="form-control mb-2">
|
<div class="form-control mb-2">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
import DotsHorizontal from '~icons/mdi/dots-horizontal';
|
import DotsHorizontal from '~icons/mdi/dots-horizontal';
|
||||||
import TrashCan from '~icons/mdi/trashcan';
|
import TrashCan from '~icons/mdi/trashcan';
|
||||||
import DeleteWarning from './DeleteWarning.svelte';
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
|
import ShareModal from './ShareModal.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let type: String | undefined | null;
|
export let type: String | undefined | null;
|
||||||
|
let isShareModalOpen: boolean = false;
|
||||||
|
|
||||||
// export let type: String;
|
// export let type: String;
|
||||||
|
|
||||||
|
@ -77,8 +79,12 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if isShareModalOpen}
|
||||||
|
<ShareModal {collection} on:close={() => (isShareModalOpen = false)} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-primary-content shadow-xl text-base-content"
|
class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-neutral text-neutral-content shadow-xl"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
|
@ -90,7 +96,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex gap-2 mb-2">
|
<div class="inline-flex gap-2 mb-2">
|
||||||
<div class="badge badge-neutral">{collection.is_public ? 'Public' : 'Private'}</div>
|
<div class="badge badge-secondary">{collection.is_public ? 'Public' : 'Private'}</div>
|
||||||
{#if collection.is_archived}
|
{#if collection.is_archived}
|
||||||
<div class="badge badge-warning">Archived</div>
|
<div class="badge badge-warning">Archived</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -117,7 +123,7 @@
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-neutral">
|
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
||||||
<DotsHorizontal class="w-6 h-6" />
|
<DotsHorizontal class="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
@ -125,7 +131,7 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
|
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
|
||||||
>
|
>
|
||||||
{#if type != 'link'}
|
{#if type != 'link' && type != 'viewonly'}
|
||||||
<button
|
<button
|
||||||
class="btn btn-neutral mb-2"
|
class="btn btn-neutral mb-2"
|
||||||
on:click={() => goto(`/collections/${collection.id}`)}
|
on:click={() => goto(`/collections/${collection.id}`)}
|
||||||
|
@ -135,6 +141,9 @@
|
||||||
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
||||||
<FileDocumentEdit class="w-6 h-6" />Edit Collection
|
<FileDocumentEdit class="w-6 h-6" />Edit Collection
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-neutral mb-2" on:click={() => (isShareModalOpen = true)}>
|
||||||
|
<FileDocumentEdit class="w-6 h-6" />Share
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if collection.is_archived}
|
{#if collection.is_archived}
|
||||||
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(false)}>
|
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(false)}>
|
||||||
|
@ -153,6 +162,13 @@
|
||||||
><TrashCan class="w-6 h-6" />Delete</button
|
><TrashCan class="w-6 h-6" />Delete</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if type == 'viewonly'}
|
||||||
|
<button
|
||||||
|
class="btn btn-neutral mb-2"
|
||||||
|
on:click={() => goto(`/collections/${collection.id}`)}
|
||||||
|
><Launch class="w-5 h-5 mr-1" />Open Details</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { getFlag } from '$lib';
|
import { continentCodeToString, getFlag } from '$lib';
|
||||||
import type { Country } from '$lib/types';
|
import type { Country } from '$lib/types';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl overflow-hidden text-base-content"
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
|
||||||
>
|
>
|
||||||
<figure>
|
<figure>
|
||||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title overflow-ellipsis">{country.name}</h2>
|
<h2 class="card-title overflow-ellipsis">{country.name}</h2>
|
||||||
|
<div class="badge badge-primary">{continentCodeToString(country.continent)}</div>
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
||||||
<button class="btn btn-primary" on:click={nav}>Open</button>
|
<button class="btn btn-primary" on:click={nav}>Open</button>
|
||||||
|
|
|
@ -11,7 +11,9 @@
|
||||||
import Flower from '~icons/mdi/flower';
|
import Flower from '~icons/mdi/flower';
|
||||||
import Water from '~icons/mdi/water';
|
import Water from '~icons/mdi/water';
|
||||||
import AboutModal from './AboutModal.svelte';
|
import AboutModal from './AboutModal.svelte';
|
||||||
|
import AccountMultiple from '~icons/mdi/account-multiple';
|
||||||
import Avatar from './Avatar.svelte';
|
import Avatar from './Avatar.svelte';
|
||||||
|
import PaletteOutline from '~icons/mdi/palette-outline';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
let query: string = '';
|
let query: string = '';
|
||||||
|
@ -81,6 +83,9 @@
|
||||||
<li>
|
<li>
|
||||||
<button on:click={() => goto('/map')}>Map</button>
|
<button on:click={() => goto('/map')}>Map</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button on:click={() => goto('/users')}>Users</button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !data.user}
|
{#if !data.user}
|
||||||
|
@ -133,6 +138,11 @@
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/map')}>Map</button>
|
<button class="btn btn-neutral" on:click={() => goto('/map')}>Map</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="btn btn-neutral" on:click={() => goto('/users')}
|
||||||
|
><AccountMultiple /></button
|
||||||
|
>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !data.user}
|
{#if !data.user}
|
||||||
|
@ -184,6 +194,10 @@
|
||||||
on:click={() => (window.location.href = 'https://docs.adventurelog.app/')}
|
on:click={() => (window.location.href = 'https://docs.adventurelog.app/')}
|
||||||
>Documentation</button
|
>Documentation</button
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm mt-2"
|
||||||
|
on:click={() => (window.location.href = 'https://discord.gg/wRbQ9Egr8C')}>Discord</button
|
||||||
|
>
|
||||||
<p class="font-bold m-4 text-lg">Theme Selection</p>
|
<p class="font-bold m-4 text-lg">Theme Selection</p>
|
||||||
<form method="POST" use:enhance={submitUpdateTheme}>
|
<form method="POST" use:enhance={submitUpdateTheme}>
|
||||||
<li>
|
<li>
|
||||||
|
@ -202,7 +216,12 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button formaction="/?/setTheme&theme=forest">Forest<Forest class="w-6 h-6" /></button>
|
<button formaction="/?/setTheme&theme=forest">Forest<Forest class="w-6 h-6" /></button>
|
||||||
<button formaction="/?/setTheme&theme=garden">Garden<Flower class="w-6 h-6" /></button>
|
<button formaction="/?/setTheme&theme=aestheticLight"
|
||||||
|
>Aesthetic Light<PaletteOutline class="w-6 h-6" /></button
|
||||||
|
>
|
||||||
|
<button formaction="/?/setTheme&theme=aestheticDark"
|
||||||
|
>Aesthetic Dark<PaletteOutline class="w-6 h-6" /></button
|
||||||
|
>
|
||||||
<button formaction="/?/setTheme&theme=aqua">Aqua<Water class="w-6 h-6" /></button>
|
<button formaction="/?/setTheme&theme=aqua">Aqua<Water class="w-6 h-6" /></button>
|
||||||
</li>
|
</li>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import type { Note, User } from '$lib/types';
|
import type { Collection, Note, User } from '$lib/types';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
export let note: Note;
|
export let note: Note;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
function editNote() {
|
function editNote() {
|
||||||
dispatch('edit', note);
|
dispatch('edit', note);
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl overflow-hidden text-base-content"
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md overflow-hidden bg-neutral text-neutral-content shadow-xl"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
|
@ -52,10 +53,10 @@
|
||||||
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
|
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
|
||||||
><Launch class="w-6 h-6" />Open Details</button
|
><Launch class="w-6 h-6" />Open Details</button
|
||||||
> -->
|
> -->
|
||||||
<button class="btn btn-neutral mb-2" on:click={editNote}>
|
<button class="btn btn-neutral-200 mb-2" on:click={editNote}>
|
||||||
<Launch class="w-6 h-6" />Open
|
<Launch class="w-6 h-6" />Open
|
||||||
</button>
|
</button>
|
||||||
{#if note.user_id == user?.pk}
|
{#if note.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
|
||||||
<button
|
<button
|
||||||
id="delete_adventure"
|
id="delete_adventure"
|
||||||
data-umami-event="Delete Adventure"
|
data-umami-event="Delete Adventure"
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import ShareModal from './ShareModal.svelte';
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
export let note: Note | null = null;
|
export let note: Note | null = null;
|
||||||
|
@ -113,7 +114,7 @@
|
||||||
<p class="font-semibold text-md mb-2">Editing note {initialName}</p>
|
<p class="font-semibold text-md mb-2">Editing note {initialName}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (note && user?.pk == note?.user_id) || !note}
|
{#if (note && user?.pk == note?.user_id) || (collection && user && collection.shared_with.includes(user.uuid)) || !note}
|
||||||
<form on:submit|preventDefault>
|
<form on:submit|preventDefault>
|
||||||
<div class="form-control mb-2">
|
<div class="form-control mb-2">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl overflow-hidden text-base-content"
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{#if region.name_en && region.name !== region.name_en}
|
{#if region.name_en && region.name !== region.name_en}
|
||||||
|
|
118
frontend/src/lib/components/ShareModal.svelte
Normal file
118
frontend/src/lib/components/ShareModal.svelte
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Collection, User } from '$lib/types';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import UserCard from './UserCard.svelte';
|
||||||
|
import { addToast } from '$lib/toasts';
|
||||||
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
|
export let collection: Collection;
|
||||||
|
|
||||||
|
let allUsers: User[] = [];
|
||||||
|
|
||||||
|
let sharedWithUsers: User[] = [];
|
||||||
|
let notSharedWithUsers: User[] = [];
|
||||||
|
|
||||||
|
async function share(user: User) {
|
||||||
|
let res = await fetch(`/api/collections/${collection.id}/share/${user.uuid}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
sharedWithUsers = sharedWithUsers.concat(user);
|
||||||
|
collection.shared_with.push(user.uuid);
|
||||||
|
notSharedWithUsers = notSharedWithUsers.filter((u) => u.uuid !== user.uuid);
|
||||||
|
addToast('success', `Shared ${collection.name} with ${user.first_name} ${user.last_name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unshare(user: User) {
|
||||||
|
let res = await fetch(`/api/collections/${collection.id}/unshare/${user.uuid}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
notSharedWithUsers = notSharedWithUsers.concat(user);
|
||||||
|
collection.shared_with = collection.shared_with.filter((u) => u !== user.uuid);
|
||||||
|
sharedWithUsers = sharedWithUsers.filter((u) => u.uuid !== user.uuid);
|
||||||
|
addToast('success', `Unshared ${collection.name} with ${user.first_name} ${user.last_name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
|
if (modal) {
|
||||||
|
modal.showModal();
|
||||||
|
}
|
||||||
|
let res = await fetch(`/auth/users/`);
|
||||||
|
if (res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
allUsers = data;
|
||||||
|
sharedWithUsers = allUsers.filter((user) => collection.shared_with.includes(user.uuid));
|
||||||
|
notSharedWithUsers = allUsers.filter((user) => !collection.shared_with.includes(user.uuid));
|
||||||
|
console.log(sharedWithUsers);
|
||||||
|
console.log(notSharedWithUsers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog id="my_modal_1" class="modal">
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
<div class="modal-box w-11/12 max-w-5xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||||
|
<h3 class="font-bold text-lg">Share {collection.name}</h3>
|
||||||
|
<p class="py-1">Share this collection with other users.</p>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<h3 class="font-bold text-md">Shared With</h3>
|
||||||
|
<ul>
|
||||||
|
{#each sharedWithUsers as user}
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
<UserCard
|
||||||
|
{user}
|
||||||
|
shared_with={collection.shared_with}
|
||||||
|
sharing={true}
|
||||||
|
on:share={(event) => share(event.detail)}
|
||||||
|
on:unshare={(event) => unshare(event.detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if sharedWithUsers.length === 0}
|
||||||
|
<p class="text-neutral-content">No users shared with</p>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<h3 class="font-bold text-md">Not Shared With</h3>
|
||||||
|
<ul>
|
||||||
|
{#each notSharedWithUsers as user}
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
<UserCard
|
||||||
|
{user}
|
||||||
|
shared_with={collection.shared_with}
|
||||||
|
sharing={true}
|
||||||
|
on:share={(event) => share(event.detail)}
|
||||||
|
on:unshare={(event) => unshare(event.detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if notSharedWithUsers.length === 0}
|
||||||
|
<p class="text-neutral-content">No users not shared with</p>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
<button class="btn btn-primary mt-4" on:click={close}>Close</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
|
@ -1,22 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import Launch from '~icons/mdi/launch';
|
|
||||||
import TrashCanOutline from '~icons/mdi/trash-can-outline';
|
import TrashCanOutline from '~icons/mdi/trash-can-outline';
|
||||||
|
|
||||||
import FileDocumentEdit from '~icons/mdi/file-document-edit';
|
import FileDocumentEdit from '~icons/mdi/file-document-edit';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import type { Collection, Transportation, User } from '$lib/types';
|
import type { Collection, Transportation, User } from '$lib/types';
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
|
|
||||||
import Plus from '~icons/mdi/plus';
|
|
||||||
import ArrowDownThick from '~icons/mdi/arrow-down-thick';
|
import ArrowDownThick from '~icons/mdi/arrow-down-thick';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let transportation: Transportation;
|
export let transportation: Transportation;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
|
export let collection: Collection | null = null;
|
||||||
|
|
||||||
function editTransportation() {
|
function editTransportation() {
|
||||||
dispatch('edit', transportation);
|
dispatch('edit', transportation);
|
||||||
|
@ -40,7 +35,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl text-base-content"
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title overflow-ellipsis">{transportation.name}</h2>
|
<h2 class="card-title overflow-ellipsis">{transportation.name}</h2>
|
||||||
|
@ -64,7 +59,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if user?.pk === transportation.user_id}
|
{#if transportation.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<button on:click={deleteTransportation} class="btn btn-secondary"
|
<button on:click={deleteTransportation} class="btn btn-secondary"
|
||||||
><TrashCanOutline class="w-5 h-5 mr-1" /></button
|
><TrashCanOutline class="w-5 h-5 mr-1" /></button
|
||||||
|
|
51
frontend/src/lib/components/UserCard.svelte
Normal file
51
frontend/src/lib/components/UserCard.svelte
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { continentCodeToString, getFlag } from '$lib';
|
||||||
|
import type { User } from '$lib/types';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
import Calendar from '~icons/mdi/calendar';
|
||||||
|
|
||||||
|
export let sharing: boolean = false;
|
||||||
|
export let shared_with: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
export let user: User;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<div>
|
||||||
|
{#if user.profile_pic}
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="w-24 rounded-full">
|
||||||
|
<img src={user.profile_pic} alt={user.username} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<h2 class="card-title overflow-ellipsis">{user.first_name} {user.last_name}</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-neutral-content">{user.username}</p>
|
||||||
|
{#if user.is_staff}
|
||||||
|
<div class="badge badge-primary">Admin</div>
|
||||||
|
{/if}
|
||||||
|
<!-- member since -->
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Calendar class="w-4 h-4 mr-1" />
|
||||||
|
<p class="text-sm text-neutral-content">
|
||||||
|
{user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
{#if !sharing}
|
||||||
|
<button class="btn btn-primary" on:click={() => goto(`/user/${user.uuid}`)}>View</button>
|
||||||
|
{:else if shared_with && !shared_with.includes(user.uuid)}
|
||||||
|
<button class="btn btn-primary" on:click={() => dispatch('share', user)}>Share</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn btn-primary" on:click={() => dispatch('unshare', user)}>Unshare</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,5 +1,4 @@
|
||||||
export let appVersion = 'Web v0.6.0';
|
export let appVersion = 'Web v0.6.0';
|
||||||
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.5.2';
|
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.6.0';
|
||||||
export let appTitle = 'AdventureLog';
|
export let appTitle = 'AdventureLog';
|
||||||
export let copyrightYear = '2024';
|
export let copyrightYear = '2024';
|
||||||
// config for the frontend
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import inspirationalQuotes from './json/quotes.json';
|
import inspirationalQuotes from './json/quotes.json';
|
||||||
import type { Adventure, Checklist, Collection, Note, Transportation } from './types';
|
import type { Adventure, Checklist, Collection, Note, Transportation, User } from './types';
|
||||||
|
|
||||||
export function getRandomQuote() {
|
export function getRandomQuote() {
|
||||||
const quotes = inspirationalQuotes.quotes;
|
const quotes = inspirationalQuotes.quotes;
|
||||||
|
@ -177,3 +177,24 @@ export function groupChecklistsByDate(
|
||||||
|
|
||||||
return groupedChecklists;
|
return groupedChecklists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function continentCodeToString(code: string) {
|
||||||
|
switch (code) {
|
||||||
|
case 'AF':
|
||||||
|
return 'Africa';
|
||||||
|
case 'AN':
|
||||||
|
return 'Antarctica';
|
||||||
|
case 'AS':
|
||||||
|
return 'Asia';
|
||||||
|
case 'EU':
|
||||||
|
return 'Europe';
|
||||||
|
case 'NA':
|
||||||
|
return 'North America';
|
||||||
|
case 'OC':
|
||||||
|
return 'Oceania';
|
||||||
|
case 'SA':
|
||||||
|
return 'South America';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ export type User = {
|
||||||
date_joined: string | null;
|
date_joined: string | null;
|
||||||
is_staff: boolean;
|
is_staff: boolean;
|
||||||
profile_pic: string | null;
|
profile_pic: string | null;
|
||||||
|
uuid: string;
|
||||||
|
public_profile: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Adventure = {
|
export type Adventure = {
|
||||||
|
@ -78,6 +80,7 @@ export type Collection = {
|
||||||
notes?: Note[];
|
notes?: Note[];
|
||||||
checklists?: Checklist[];
|
checklists?: Checklist[];
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
|
shared_with: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenStreetMapPlace = {
|
export type OpenStreetMapPlace = {
|
||||||
|
|
|
@ -4,21 +4,20 @@
|
||||||
import Lost from '$lib/assets/undraw_lost.svg';
|
import Lost from '$lib/assets/undraw_lost.svg';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>{$page.status}: {$page.error?.message}</h1>
|
|
||||||
|
|
||||||
{#if $page.status === 404}
|
{#if $page.status === 404}
|
||||||
<div
|
<div
|
||||||
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8"
|
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8"
|
||||||
>
|
>
|
||||||
<div class="mx-auto max-w-md text-center">
|
<div class="mx-auto max-w-md text-center">
|
||||||
<img src={Lost} alt="Lost in the forest" />
|
<img src={Lost} alt="Lost in the forest" />
|
||||||
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
<h1 class="text-center text-5xl font-extrabold mt-2">
|
||||||
|
{$page.status}: {$page.error?.message}
|
||||||
|
</h1>
|
||||||
|
<h1 class="mt-4 text-xl font-bold tracking-tight text-foreground">
|
||||||
Oops, looks like you've wandered off the beaten path.
|
Oops, looks like you've wandered off the beaten path.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-4 text-muted-foreground">
|
|
||||||
We couldn't find the page you were looking for. Don't worry, we can help you find your way
|
<p class="mt-4 text-muted-foreground">We couldn't find the page you were looking for.</p>
|
||||||
back.ry, we can
|
|
||||||
</p>
|
|
||||||
<div class="mt-6 flex flex-col items-center gap-4 sm:flex-row">
|
<div class="mt-6 flex flex-col items-center gap-4 sm:flex-row">
|
||||||
<button class="btn btn-neutral" on:click={() => goto('/')}>Go to Homepage</button>
|
<button class="btn btn-neutral" on:click={() => goto('/')}>Go to Homepage</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,9 +9,18 @@ export const actions: Actions = {
|
||||||
// change the theme only if it is one of the allowed themes
|
// change the theme only if it is one of the allowed themes
|
||||||
if (
|
if (
|
||||||
theme &&
|
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, {
|
cookies.set('colortheme', theme, {
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
|
@ -99,72 +99,5 @@ export const actions: Actions = {
|
||||||
status: 204
|
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -96,11 +96,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if adventure}
|
{#if adventure}
|
||||||
<div class="fixed bottom-4 right-4 z-[999]">
|
{#if data.user && data.user.pk == adventure.user_id}
|
||||||
<button class="btn m-1 size-16 btn-primary" on:click={() => (isEditModalOpen = true)}
|
<div class="fixed bottom-4 right-4 z-[999]">
|
||||||
><ClipboardList class="w-8 h-8" /></button
|
<button class="btn m-1 size-16 btn-primary" on:click={() => (isEditModalOpen = true)}
|
||||||
>
|
><ClipboardList class="w-8 h-8" /></button
|
||||||
</div>
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex flex-col min-h-dvh">
|
<div class="flex flex-col min-h-dvh">
|
||||||
<main class="flex-1">
|
<main class="flex-1">
|
||||||
<div class="max-w-5xl mx-auto p-4 md:p-6 lg:p-8">
|
<div class="max-w-5xl mx-auto p-4 md:p-6 lg:p-8">
|
||||||
|
|
83
frontend/src/routes/auth/[...path]/+server.ts
Normal file
83
frontend/src/routes/auth/[...path]/+server.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
/** @type {import('./$types').RequestHandler} */
|
||||||
|
export async function GET({ url, params, request, fetch, cookies }) {
|
||||||
|
// add the param format = json to the url or add additional if anothre param is already present
|
||||||
|
if (url.search) {
|
||||||
|
url.search = url.search + '&format=json';
|
||||||
|
} else {
|
||||||
|
url.search = '?format=json';
|
||||||
|
}
|
||||||
|
return handleRequest(url, params, request, fetch, cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('./$types').RequestHandler} */
|
||||||
|
export async function POST({ url, params, request, fetch, cookies }) {
|
||||||
|
return handleRequest(url, params, request, fetch, cookies, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH({ url, params, request, fetch, cookies }) {
|
||||||
|
return handleRequest(url, params, request, fetch, cookies, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT({ url, params, request, fetch, cookies }) {
|
||||||
|
return handleRequest(url, params, request, fetch, cookies, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE({ url, params, request, fetch, cookies }) {
|
||||||
|
return handleRequest(url, params, request, fetch, cookies, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement other HTTP methods as needed (PUT, DELETE, etc.)
|
||||||
|
|
||||||
|
async function handleRequest(
|
||||||
|
url: any,
|
||||||
|
params: any,
|
||||||
|
request: any,
|
||||||
|
fetch: any,
|
||||||
|
cookies: any,
|
||||||
|
requreTrailingSlash: boolean | undefined = false
|
||||||
|
) {
|
||||||
|
const path = params.path;
|
||||||
|
let targetUrl = `${endpoint}/auth/${path}${url.search}`;
|
||||||
|
|
||||||
|
if (requreTrailingSlash && !targetUrl.endsWith('/')) {
|
||||||
|
targetUrl += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
|
||||||
|
const authCookie = cookies.get('auth');
|
||||||
|
|
||||||
|
if (authCookie) {
|
||||||
|
headers.set('Cookie', `${authCookie}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(targetUrl, {
|
||||||
|
method: request.method,
|
||||||
|
headers: headers,
|
||||||
|
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
// For 204 No Content, return a response with no body
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: response.headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.text();
|
||||||
|
|
||||||
|
return new Response(responseData, {
|
||||||
|
status: response.status,
|
||||||
|
headers: response.headers
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error forwarding request:', error);
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,16 +88,18 @@
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
let adventure = event.detail;
|
let adventure = event.detail;
|
||||||
let formData = new FormData();
|
|
||||||
formData.append('collection_id', collection.id.toString());
|
|
||||||
|
|
||||||
let res = await fetch(`/adventures/${adventure.id}?/addToCollection`, {
|
let res = await fetch(`/api/adventures/${adventure.id}/`, {
|
||||||
method: 'POST',
|
method: 'PATCH',
|
||||||
body: formData // Remove the Content-Type header
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ collection: collection.id.toString() })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
console.log('Adventure added to collection');
|
console.log('Adventure added to collection');
|
||||||
|
adventure = await res.json();
|
||||||
adventures = [...adventures, adventure];
|
adventures = [...adventures, adventure];
|
||||||
} else {
|
} else {
|
||||||
console.log('Error adding adventure to collection');
|
console.log('Error adding adventure to collection');
|
||||||
|
@ -404,6 +406,7 @@
|
||||||
type={adventure.type}
|
type={adventure.type}
|
||||||
{adventure}
|
{adventure}
|
||||||
on:typeChange={changeType}
|
on:typeChange={changeType}
|
||||||
|
{collection}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -423,6 +426,7 @@
|
||||||
transportationToEdit = event.detail;
|
transportationToEdit = event.detail;
|
||||||
isTransportationEditModalOpen = true;
|
isTransportationEditModalOpen = true;
|
||||||
}}
|
}}
|
||||||
|
{collection}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -442,6 +446,7 @@
|
||||||
on:delete={(event) => {
|
on:delete={(event) => {
|
||||||
notes = notes.filter((n) => n.id != event.detail);
|
notes = notes.filter((n) => n.id != event.detail);
|
||||||
}}
|
}}
|
||||||
|
{collection}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -461,6 +466,7 @@
|
||||||
checklistToEdit = event.detail;
|
checklistToEdit = event.detail;
|
||||||
isShowingChecklistModal = true;
|
isShowingChecklistModal = true;
|
||||||
}}
|
}}
|
||||||
|
{collection}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -45,6 +45,7 @@ export const actions: Actions = {
|
||||||
let first_name = formData.get('first_name') as string | null | undefined;
|
let first_name = formData.get('first_name') as string | null | undefined;
|
||||||
let last_name = formData.get('last_name') as string | null | undefined;
|
let last_name = formData.get('last_name') as string | null | undefined;
|
||||||
let profile_pic = formData.get('profile_pic') as File | null | undefined;
|
let profile_pic = formData.get('profile_pic') as File | null | undefined;
|
||||||
|
let public_profile = formData.get('public_profile') as string | null | undefined | boolean;
|
||||||
|
|
||||||
const resCurrent = await fetch(`${endpoint}/auth/user/`, {
|
const resCurrent = await fetch(`${endpoint}/auth/user/`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -56,6 +57,13 @@ export const actions: Actions = {
|
||||||
return fail(resCurrent.status, await resCurrent.json());
|
return fail(resCurrent.status, await resCurrent.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (public_profile === 'on') {
|
||||||
|
public_profile = true;
|
||||||
|
} else {
|
||||||
|
public_profile = false;
|
||||||
|
}
|
||||||
|
console.log(public_profile);
|
||||||
|
|
||||||
let currentUser = (await resCurrent.json()) as User;
|
let currentUser = (await resCurrent.json()) as User;
|
||||||
|
|
||||||
if (username === currentUser.username || !username) {
|
if (username === currentUser.username || !username) {
|
||||||
|
@ -84,6 +92,7 @@ export const actions: Actions = {
|
||||||
if (profile_pic) {
|
if (profile_pic) {
|
||||||
formDataToSend.append('profile_pic', profile_pic);
|
formDataToSend.append('profile_pic', profile_pic);
|
||||||
}
|
}
|
||||||
|
formDataToSend.append('public_profile', public_profile.toString());
|
||||||
|
|
||||||
let res = await fetch(`${endpoint}/auth/user/`, {
|
let res = await fetch(`${endpoint}/auth/user/`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|
|
@ -111,6 +111,24 @@
|
||||||
id="profile_pic"
|
id="profile_pic"
|
||||||
class="file-input file-input-bordered w-full max-w-xs mb-2"
|
class="file-input file-input-bordered w-full max-w-xs mb-2"
|
||||||
/><br />
|
/><br />
|
||||||
|
<div class="form-control">
|
||||||
|
<div
|
||||||
|
class="tooltip tooltip-info"
|
||||||
|
data-tip="With a public profile, users can share collections with you and view your profile on the users page."
|
||||||
|
>
|
||||||
|
<label class="label cursor-pointer">
|
||||||
|
<span class="label-text">Public Profile</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="public_profile"
|
||||||
|
name="public_profile"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle"
|
||||||
|
checked={user.public_profile}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="py-2 mt-2 px-4 btn btn-primary">Update</button>
|
<button class="py-2 mt-2 px-4 btn btn-primary">Update</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
26
frontend/src/routes/shared/+page.server.ts
Normal file
26
frontend/src/routes/shared/+page.server.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
|
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
if (!event.locals.user) {
|
||||||
|
return redirect(302, '/login');
|
||||||
|
} else {
|
||||||
|
let res = await fetch(`${serverEndpoint}/api/collections/shared/`, {
|
||||||
|
headers: {
|
||||||
|
Cookie: `${event.cookies.get('auth')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
return redirect(302, '/login');
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
collections: await res.json()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) satisfies PageServerLoad;
|
27
frontend/src/routes/shared/+page.svelte
Normal file
27
frontend/src/routes/shared/+page.svelte
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import CollectionCard from '$lib/components/CollectionCard.svelte';
|
||||||
|
import type { Collection } from '$lib/types';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
console.log(data);
|
||||||
|
let collections: Collection[] = data.props.collections;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if collections.length > 0}
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each collections as collection}
|
||||||
|
<CollectionCard type="viewonly" {collection} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-center font-semibold text-xl mt-6">
|
||||||
|
No collections found that are shared with you.
|
||||||
|
{#if data.user && !data.user?.public_profile}
|
||||||
|
<p>In order to allow users to share with you, you need your profile set to public.</p>
|
||||||
|
<button class="btn btn-neutral mt-4" on:click={() => goto('/settings')}>Go to Settings</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
29
frontend/src/routes/user/[uuid]/+page.server.ts
Normal file
29
frontend/src/routes/user/[uuid]/+page.server.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
|
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
if (!event.cookies.get('auth')) {
|
||||||
|
return redirect(302, '/login');
|
||||||
|
}
|
||||||
|
const uuid = event.params.uuid;
|
||||||
|
if (!uuid) {
|
||||||
|
return redirect(302, '/users');
|
||||||
|
}
|
||||||
|
let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, {
|
||||||
|
headers: {
|
||||||
|
Cookie: `${event.cookies.get('auth')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
return redirect(302, '/users');
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
user: data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}) satisfies PageServerLoad;
|
35
frontend/src/routes/user/[uuid]/+page.svelte
Normal file
35
frontend/src/routes/user/[uuid]/+page.svelte
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
const user = data.props.user;
|
||||||
|
console.log(user);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if user.profile_pic}
|
||||||
|
<div class="avatar flex items-center justify-center mt-4">
|
||||||
|
<div class="w-48 rounded-md">
|
||||||
|
<img src={user.profile_pic} alt={user.username} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h1 class="text-center font-semibold text-4xl mt-4">{user.first_name} {user.last_name}</h1>
|
||||||
|
<h2 class="text-center font-semibold text-2xl">{user.username}</h2>
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-4">
|
||||||
|
{#if user.is_staff}
|
||||||
|
<div class="badge badge-primary">Admin</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-4">
|
||||||
|
<p class="text-sm text-neutral-content">
|
||||||
|
{user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{user.username} | AdventureLog</title>
|
||||||
|
<meta name="description" content="View your adventure collections." />
|
||||||
|
</svelte:head>
|
26
frontend/src/routes/users/+page.server.ts
Normal file
26
frontend/src/routes/users/+page.server.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
|
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
if (!event.cookies.get('auth')) {
|
||||||
|
return redirect(302, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${serverEndpoint}/auth/users/`, {
|
||||||
|
headers: {
|
||||||
|
Cookie: `${event.cookies.get('auth')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
return redirect(302, '/login');
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
users: data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}) satisfies PageServerLoad;
|
25
frontend/src/routes/users/+page.svelte
Normal file
25
frontend/src/routes/users/+page.svelte
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import UserCard from '$lib/components/UserCard.svelte';
|
||||||
|
import type { User } from '$lib/types';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
let users: User[] = data.props.users;
|
||||||
|
console.log(users);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-center font-bold text-4xl mb-4">AdventureLog Users</h1>
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each users as user (user.uuid)}
|
||||||
|
<UserCard {user} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if users.length === 0}
|
||||||
|
<p class="text-center">No users found with public profiles.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Users</title>
|
||||||
|
<meta name="description" content="View your adventure collections." />
|
||||||
|
</svelte:head>
|
|
@ -3,16 +3,44 @@
|
||||||
import type { Country } from '$lib/types';
|
import type { Country } from '$lib/types';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
let searchQuery: string = '';
|
||||||
|
|
||||||
|
let filteredCountries: Country[] = [];
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
const countries: Country[] = data.props?.countries || [];
|
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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-center font-bold text-4xl mb-4">Country List</h1>
|
<h1 class="text-center font-bold text-4xl">Country List</h1>
|
||||||
|
<!-- result count -->
|
||||||
|
<p class="text-center mb-4">
|
||||||
|
{filteredCountries.length} countries found
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
class="input input-bordered w-full max-w-xs"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||||
{#each countries as country}
|
{#each filteredCountries as country}
|
||||||
<CountryCard {country} />
|
<CountryCard {country} />
|
||||||
<!-- <p>Name: {item.name}, Continent: {item.continent}</p> -->
|
<!-- <p>Name: {item.name}, Continent: {item.continent}</p> -->
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -15,7 +15,52 @@ export default {
|
||||||
'aqua',
|
'aqua',
|
||||||
'emerald',
|
'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': '#272525',
|
||||||
|
'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: '#5a7c65',
|
||||||
'primary-focus': '#48604f',
|
'primary-focus': '#48604f',
|
||||||
'primary-content': '#dcd5d5',
|
'primary-content': '#dcd5d5',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue