diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index cb4729bb..cd647257 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -11,6 +11,8 @@ from django.db.models import Q, Prefetch from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly from rest_framework.pagination import PageNumberPagination +from django.shortcuts import get_object_or_404 +from rest_framework import status class StandardResultsSetPagination(PageNumberPagination): page_size = 10 @@ -58,11 +60,37 @@ def apply_sorting(self, queryset): return queryset.order_by(ordering) def get_queryset(self): - queryset = Adventure.objects.annotate( - ).filter( - Q(is_public=True) | Q(user_id=self.request.user.id) - ) - return self.apply_sorting(queryset) + if self.action == 'retrieve': + # For individual adventure retrieval, include public adventures + return Adventure.objects.filter( + Q(is_public=True) | Q(user_id=self.request.user.id) + ) + else: + # For other actions, only include user's own adventures + return Adventure.objects.filter(user_id=self.request.user.id) + + def list(self, request, *args, **kwargs): + # Prevent listing all adventures + return Response({"detail": "Listing all adventures is not allowed."}, + status=status.HTTP_403_FORBIDDEN) + + def retrieve(self, request, *args, **kwargs): + queryset = self.get_queryset() + adventure = get_object_or_404(queryset, pk=kwargs['pk']) + serializer = self.get_serializer(adventure) + return Response(serializer.data) + + def perform_create(self, serializer): + adventure = serializer.save(user_id=self.request.user) + if adventure.collection: + adventure.is_public = adventure.collection.is_public + adventure.save() + + def perform_update(self, serializer): + adventure = serializer.save() + if adventure.collection: + adventure.is_public = adventure.collection.is_public + adventure.save() def perform_create(self, serializer): serializer.save(user_id=self.request.user) @@ -104,7 +132,7 @@ def all(self, request): # Q(is_public=True) | Q(user_id=request.user.id), collection=None # ) queryset = Adventure.objects.filter( - Q(is_public=True) | Q(user_id=request.user.id) + Q(user_id=request.user.id) ) queryset = self.apply_sorting(queryset) @@ -151,6 +179,29 @@ def apply_sorting(self, queryset): return queryset.order_by(ordering) + def list(self, request, *args, **kwargs): + # make sure the user is authenticated + if not request.user.is_authenticated: + return Response({"error": "User is not authenticated"}, status=400) + queryset = self.get_queryset() + queryset = self.apply_sorting(queryset) + collections = self.paginate_and_respond(queryset, request) + return collections + + @action(detail=False, methods=['get']) + def all(self, request): + if not request.user.is_authenticated: + return Response({"error": "User is not authenticated"}, status=400) + + queryset = Collection.objects.filter( + Q(user_id=request.user.id) + ) + + queryset = self.apply_sorting(queryset) + serializer = self.get_serializer(queryset, many=True) + + return Response(serializer.data) + # this make the is_public field of the collection cascade to the adventures @transaction.atomic def update(self, request, *args, **kwargs): diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 6510832e..b48b7cf0 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -11,9 +11,16 @@ import MapMarker from '~icons/mdi/map-marker'; import { addToast } from '$lib/toasts'; import Link from '~icons/mdi/link-variant'; + import CheckBold from '~icons/mdi/check-bold'; + import FormatListBulletedSquare from '~icons/mdi/format-list-bulleted-square'; + import LinkVariantRemove from '~icons/mdi/link-variant-remove'; + import Plus from '~icons/mdi/plus'; + import CollectionLink from './CollectionLink.svelte'; export let type: string; + let isCollectionModalOpen: boolean = false; + export let adventure: Adventure; async function deleteAdventure() { @@ -32,6 +39,61 @@ } } + async function removeFromCollection() { + let res = await fetch(`/api/adventures/${adventure.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ collection: null }) + }); + if (res.ok) { + console.log('Adventure removed from collection'); + addToast('info', 'Adventure removed from collection successfully!'); + dispatch('delete', adventure.id); + } else { + console.log('Error removing adventure from collection'); + } + } + + function changeType(newType: string) { + return async () => { + let res = await fetch(`/api/adventures/${adventure.id}/`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ type: newType }) + }); + if (res.ok) { + console.log('Adventure type changed'); + addToast('info', 'Adventure type changed successfully!'); + adventure.type = newType; + } else { + console.log('Error changing adventure type'); + } + }; + } + + async function linkCollection(event: CustomEvent) { + let collectionId = event.detail; + let res = await fetch(`/api/adventures/${adventure.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ collection: collectionId }) + }); + if (res.ok) { + console.log('Adventure linked to collection'); + addToast('info', 'Adventure linked to collection successfully!'); + isCollectionModalOpen = false; + dispatch('delete', adventure.id); + } else { + console.log('Error linking adventure to collection'); + } + } + function editAdventure() { dispatch('edit', adventure); } @@ -41,6 +103,10 @@ } +{#if isCollectionModalOpen} + (isCollectionModalOpen = false)} /> +{/if} +
@@ -61,6 +127,14 @@

{adventure.name}

+
+ {#if adventure.type == 'visited'} +
Visited
+ {:else} +
Planned
+ {/if} +
{adventure.is_public ? 'Public' : 'Private'}
+
{#if adventure.location && adventure.location !== ''}
@@ -90,7 +164,7 @@ - {/if} @@ -101,13 +175,34 @@ - {/if} {#if type == 'link'} {/if} + {#if adventure.type == 'visited'} + + {/if} + + {#if adventure.type == 'planned'} + + {/if} + {#if adventure.collection} + + {/if} + {#if !adventure.collection} + + {/if}
diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte index 3bc7d059..cd6d3145 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -9,8 +9,13 @@ import { goto } from '$app/navigation'; import type { Collection } from '$lib/types'; import { addToast } from '$lib/toasts'; + + import Plus from '~icons/mdi/plus'; + const dispatch = createEventDispatcher(); + export let type: String | undefined | null; + // export let type: String; function editAdventure() { @@ -43,15 +48,22 @@

{collection.name}

{collection.adventures.length} Adventures

- - - + {#if type != 'link'} + + + + {/if} + {#if type == 'link'} + + {/if}
diff --git a/frontend/src/lib/components/CollectionLink.svelte b/frontend/src/lib/components/CollectionLink.svelte new file mode 100644 index 00000000..aa99e7ad --- /dev/null +++ b/frontend/src/lib/components/CollectionLink.svelte @@ -0,0 +1,58 @@ + + + + + + + diff --git a/frontend/src/lib/components/EditAdventure.svelte b/frontend/src/lib/components/EditAdventure.svelte index 8a697565..099d2697 100644 --- a/frontend/src/lib/components/EditAdventure.svelte +++ b/frontend/src/lib/components/EditAdventure.svelte @@ -22,6 +22,7 @@ import Attachment from '~icons/mdi/attachment'; import PointSelectionModal from './PointSelectionModal.svelte'; import Earth from '~icons/mdi/earth'; + import Wikipedia from '~icons/mdi/wikipedia'; onMount(async () => { modal = document.getElementById('my_modal_1') as HTMLDialogElement; @@ -42,6 +43,14 @@ } } + async function generateDesc() { + let res = await fetch(`/api/generate/desc/?name=${adventureToEdit.name}`); + let data = await res.json(); + if (data.extract) { + adventureToEdit.description = data.extract; + } + } + async function handleSubmit(event: Event) { event.preventDefault(); const form = event.target as HTMLFormElement; @@ -166,13 +175,9 @@ bind:value={adventureToEdit.description} class="input input-bordered w-full max-w-xs mt-1 mb-2" /> - +
diff --git a/frontend/src/lib/components/NewAdventure.svelte b/frontend/src/lib/components/NewAdventure.svelte index f53d8cb1..edf23cb9 100644 --- a/frontend/src/lib/components/NewAdventure.svelte +++ b/frontend/src/lib/components/NewAdventure.svelte @@ -8,6 +8,8 @@ export let type: string = 'visited'; + import Wikipedia from '~icons/mdi/wikipedia'; + let newAdventure: Adventure = { id: NaN, type: type, @@ -49,6 +51,14 @@ } } + async function generateDesc() { + let res = await fetch(`/api/generate/desc/?name=${newAdventure.name}`); + let data = await res.json(); + if (data.extract) { + newAdventure.description = data.extract; + } + } + async function handleSubmit(event: Event) { event.preventDefault(); const form = event.target as HTMLFormElement; @@ -179,6 +189,9 @@ bind:value={newAdventure.description} class="input input-bordered w-full max-w-xs mt-1 mb-2" /> +
diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts new file mode 100644 index 00000000..ca1b2a67 --- /dev/null +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -0,0 +1,58 @@ +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 }) { + 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); +} + +export async function PATCH({ url, params, request, fetch, cookies }) { + return handleRequest(url, params, request, fetch, cookies); +} + +export async function PUT({ url, params, request, fetch, cookies }) { + return handleRequest(url, params, request, fetch, cookies); +} + +export async function DELETE({ url, params, request, fetch, cookies }) { + return handleRequest(url, params, request, fetch, cookies); +} + +// Implement other HTTP methods as needed (PUT, DELETE, etc.) + +async function handleRequest(url: any, params: any, request: any, fetch: any, cookies: any) { + const path = params.path; + const targetUrl = `${endpoint}/api/${path}${url.search}/`; + + 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 + }); + + 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 }); + } +} diff --git a/frontend/src/routes/collections/+page.svelte b/frontend/src/routes/collections/+page.svelte index 9e0f35ac..113e7611 100644 --- a/frontend/src/routes/collections/+page.svelte +++ b/frontend/src/routes/collections/+page.svelte @@ -179,7 +179,12 @@ {#if currentView == 'cards'}
{#each collections as collection} - + {/each}
{/if} @@ -229,8 +234,7 @@ class="radio radio-primary" />
-

Order By

- +