diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index af3fb9d..6d77b07 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -6,8 +6,8 @@ from worldtravel.models import Country, Region, VisitedRegion class AdventureAdmin(admin.ModelAdmin): - list_display = ('name', 'type', 'user_id', 'date', 'image_display') - list_filter = ('type', 'user_id') + list_display = ('name', 'type', 'user_id', 'date', 'is_public', 'image_display') + list_filter = ('type', 'user_id', 'is_public') def image_display(self, obj): if obj.image: diff --git a/backend/server/adventures/permissions.py b/backend/server/adventures/permissions.py index f4dfbea..b4241c2 100644 --- a/backend/server/adventures/permissions.py +++ b/backend/server/adventures/permissions.py @@ -12,4 +12,19 @@ class IsOwnerOrReadOnly(permissions.BasePermission): return True # Write permissions are only allowed to the owner of the object. + return obj.user_id == request.user + + +class IsPublicReadOnly(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 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 \ No newline at end of file diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 2ccc5a6..b03d687 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -5,11 +5,11 @@ from .models import Adventure, Trip from .serializers import AdventureSerializer, TripSerializer from rest_framework.permissions import IsAuthenticated from django.db.models import Q, Prefetch -from .permissions import IsOwnerOrReadOnly +from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly class AdventureViewSet(viewsets.ModelViewSet): serializer_class = AdventureSerializer - permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] + permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly] def get_queryset(self): return Adventure.objects.filter( @@ -42,7 +42,7 @@ class AdventureViewSet(viewsets.ModelViewSet): class TripViewSet(viewsets.ModelViewSet): serializer_class = TripSerializer - permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] + permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly] def get_queryset(self): return Trip.objects.filter( diff --git a/frontend/src/lib/assets/undraw_lost.svg b/frontend/src/lib/assets/undraw_lost.svg new file mode 100644 index 0000000..2d05eb1 --- /dev/null +++ b/frontend/src/lib/assets/undraw_lost.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/lib/components/EditAdventure.svelte b/frontend/src/lib/components/EditAdventure.svelte index a7a1d56..7e1d3c6 100644 --- a/frontend/src/lib/components/EditAdventure.svelte +++ b/frontend/src/lib/components/EditAdventure.svelte @@ -21,6 +21,7 @@ import Star from '~icons/mdi/star'; import Attachment from '~icons/mdi/attachment'; import PointSelectionModal from './PointSelectionModal.svelte'; + import Earth from '~icons/mdi/earth'; onMount(async () => { modal = document.getElementById('my_modal_1') as HTMLDialogElement; @@ -245,6 +246,39 @@ Location +
+
+ +
+ + {#if adventureToEdit.is_public} +
+

Share this Adventure!

+
+

+ {window.location.origin}/adventures/{adventureToEdit.id} +

+ +
+
+ {/if} diff --git a/frontend/src/routes/adventures/+page.server.ts b/frontend/src/routes/adventures/+page.server.ts index 027b323..ddd3935 100644 --- a/frontend/src/routes/adventures/+page.server.ts +++ b/frontend/src/routes/adventures/+page.server.ts @@ -175,6 +175,13 @@ export const actions: Actions = { let link = formData.get('link') as string | null; let latitude = formData.get('latitude') as string | null; let longitude = formData.get('longitude') as string | null; + let is_public = formData.get('is_public') as string | null | boolean; + + if (is_public) { + is_public = true; + } else { + is_public = false; + } // check if latitude and longitude are valid if (latitude && longitude) { @@ -221,6 +228,7 @@ export const actions: Actions = { formDataToSend.append('description', description || ''); formDataToSend.append('latitude', latitude || ''); formDataToSend.append('longitude', longitude || ''); + formDataToSend.append('is_public', is_public.toString()); if (activity_types) { // Filter out empty and duplicate activity types, then trim each activity type const cleanedActivityTypes = Array.from( diff --git a/frontend/src/routes/adventures/[id]/+page.server.ts b/frontend/src/routes/adventures/[id]/+page.server.ts index d187d1a..1f3eb48 100644 --- a/frontend/src/routes/adventures/[id]/+page.server.ts +++ b/frontend/src/routes/adventures/[id]/+page.server.ts @@ -5,33 +5,27 @@ import type { Adventure } from '$lib/types'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { - if (!event.locals.user) { - return redirect(302, '/login'); - } else { - const id = event.params as { id: string }; - let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, { - headers: { - Cookie: `${event.cookies.get('auth')}` - } - }); - if (!request.ok) { - console.error('Failed to fetch adventure ' + id.id); - return { - props: { - adventure: null - } - }; - } else { - let adventure = (await request.json()) as Adventure; - if (!adventure.is_public && adventure.user_id !== event.locals.user.pk) { - return redirect(302, '/'); - } - return { - props: { - adventure - } - }; + const id = event.params as { id: string }; + let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, { + headers: { + Cookie: `${event.cookies.get('auth')}` } + }); + if (!request.ok) { + console.error('Failed to fetch adventure ' + id.id); + return { + props: { + adventure: null + } + }; + } else { + let adventure = (await request.json()) as Adventure; + + return { + props: { + adventure + } + }; } }) satisfies PageServerLoad; diff --git a/frontend/src/routes/adventures/[id]/+page.svelte b/frontend/src/routes/adventures/[id]/+page.svelte index a15db0c..ad938e9 100644 --- a/frontend/src/routes/adventures/[id]/+page.svelte +++ b/frontend/src/routes/adventures/[id]/+page.svelte @@ -18,25 +18,51 @@ import { onMount } from 'svelte'; import type { PageData } from './$types'; import { goto } from '$app/navigation'; + import Lost from '$lib/assets/undraw_lost.svg'; export let data: PageData; let adventure: Adventure; + let notFound: boolean = false; + onMount(() => { if (data.props.adventure) { adventure = data.props.adventure; } else { - goto('/404'); + notFound = true; } }); -{#if !adventure} +{#if notFound} +
+
+
+ Lost +
+

+ Adventure not Found +

+

+ The adventure you were looking for could not be found. Please try a different adventure or + check back later. +

+
+ +
+
+
+{/if} + +{#if !adventure && !notFound}
-{:else} +{/if} +{#if adventure} {#if adventure.name}

{adventure.name}

{/if}