From a3784ae164888188180ab08e68725d649fb1b7fe Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 11 Jul 2024 19:27:03 -0400 Subject: [PATCH 1/5] pagination! --- backend/server/adventures/views.py | 61 +++++++--- docker-compose.yml | 12 +- frontend/src/routes/+page.server.ts | 4 +- .../src/routes/adventures/+page.server.ts | 113 ++++++++++-------- frontend/src/routes/adventures/+page.svelte | 46 ++++++- frontend/src/routes/login/+page.server.ts | 6 +- 6 files changed, 157 insertions(+), 85 deletions(-) diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index cce6946..44c8403 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -7,10 +7,23 @@ from rest_framework.permissions import IsAuthenticated from django.db.models import Q, Prefetch from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly +from rest_framework.pagination import PageNumberPagination + +class StandardResultsSetPagination(PageNumberPagination): + page_size = 6 + page_size_query_param = 'page_size' + max_page_size = 1000 + +from rest_framework.pagination import PageNumberPagination + +from rest_framework.decorators import action +from rest_framework.response import Response +from django.db.models import Q class AdventureViewSet(viewsets.ModelViewSet): serializer_class = AdventureSerializer permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly] + pagination_class = StandardResultsSetPagination def get_queryset(self): return Adventure.objects.filter( @@ -21,24 +34,33 @@ def perform_create(self, serializer): serializer.save(user_id=self.request.user) @action(detail=False, methods=['get']) - def visited(self, request): - visited_adventures = Adventure.objects.filter( - type='visited', user_id=request.user.id, trip=None) - serializer = self.get_serializer(visited_adventures, many=True) - return Response(serializer.data) + def filtered(self, request): + types = request.query_params.get('types', '').split(',') + valid_types = ['visited', 'planned', 'featured'] + types = [t for t in types if t in valid_types] - @action(detail=False, methods=['get']) - def planned(self, request): - planned_adventures = Adventure.objects.filter( - type='planned', user_id=request.user.id, trip=None) - serializer = self.get_serializer(planned_adventures, many=True) - return Response(serializer.data) + if not types: + return Response({"error": "No valid types provided"}, status=400) - @action(detail=False, methods=['get']) - def featured(self, request): - featured_adventures = Adventure.objects.filter( - type='featured', is_public=True, trip=None) - serializer = self.get_serializer(featured_adventures, many=True) + queryset = Adventure.objects.none() + + for adventure_type in types: + if adventure_type in ['visited', 'planned']: + queryset |= Adventure.objects.filter( + type=adventure_type, user_id=request.user.id, trip=None) + elif adventure_type == 'featured': + queryset |= Adventure.objects.filter( + type='featured', is_public=True, trip=None) + + return self.paginate_and_respond(queryset, request) + + def paginate_and_respond(self, queryset, request): + paginator = self.pagination_class() + page = paginator.paginate_queryset(queryset, request) + if page is not None: + serializer = self.get_serializer(page, many=True) + return paginator.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) class TripViewSet(viewsets.ModelViewSet): @@ -57,11 +79,12 @@ def get_queryset(self): def perform_create(self, serializer): serializer.save(user_id=self.request.user) + @action(detail=False, methods=['get']) @action(detail=False, methods=['get']) def visited(self, request): - trips = self.get_queryset().filter(type='visited', user_id=request.user.id) - serializer = self.get_serializer(trips, many=True) - return Response(serializer.data) + visited_adventures = Adventure.objects.filter( + type='visited', user_id=request.user.id, trip=None) + return self.get_paginated_response(visited_adventures) @action(detail=False, methods=['get']) def planned(self, request): diff --git a/docker-compose.yml b/docker-compose.yml index f479187..a497206 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,11 +2,11 @@ version: "3.9" services: web: - #build: ./frontend/ - image: ghcr.io/seanmorley15/adventurelog-frontend:latest + build: ./frontend/ + #image: ghcr.io/seanmorley15/adventurelog-frontend:latest environment: - PUBLIC_SERVER_URL=http://server:8000 - - ORIGIN=http://localhost:8080 + - ORIGIN=http://10.0.0.92:8080 - BODY_SIZE_LIMIT=Infinity ports: - "8080:3000" @@ -23,8 +23,8 @@ services: - postgres_data:/var/lib/postgresql/data/ server: - #build: ./backend/ - image: ghcr.io/seanmorley15/adventurelog-backend:latest + build: ./backend/ + #image: ghcr.io/seanmorley15/adventurelog-backend:latest environment: - PGHOST=db - PGDATABASE=database @@ -34,7 +34,7 @@ services: - DJANGO_ADMIN_USERNAME=admin - DJANGO_ADMIN_PASSWORD=admin - DJANGO_ADMIN_EMAIL=admin@example.com - - PUBLIC_URL='http://127.0.0.1:81' + - PUBLIC_URL='http://10.0.92:81' - CSRF_TRUSTED_ORIGINS=https://api.adventurelog.app,https://adventurelog.app - DEBUG=False ports: diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 366539d..8cb9577 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -34,8 +34,8 @@ export const actions: Actions = { } }); if (res.ok) { - cookies.delete('auth', { path: '/' }); - cookies.delete('refresh', { path: '/' }); + cookies.delete('auth', { path: '/', secure: false }); + cookies.delete('refresh', { path: '/', secure: false }); return redirect(302, '/login'); } else { return redirect(302, '/'); diff --git a/frontend/src/routes/adventures/+page.server.ts b/frontend/src/routes/adventures/+page.server.ts index fe72c26..efa29c3 100644 --- a/frontend/src/routes/adventures/+page.server.ts +++ b/frontend/src/routes/adventures/+page.server.ts @@ -13,32 +13,37 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { + let next = null; + let previous = null; + let count = 0; let adventures: Adventure[] = []; - let visitedFetch = await fetch(`${serverEndpoint}/api/adventures/visited/`, { - headers: { - Cookie: `${event.cookies.get('auth')}` + let initialFetch = await fetch( + `${serverEndpoint}/api/adventures/filtered?types=visited,planned`, + { + headers: { + Cookie: `${event.cookies.get('auth')}` + } } - }); - if (!visitedFetch.ok) { + ); + if (!initialFetch.ok) { console.error('Failed to fetch visited adventures'); return redirect(302, '/login'); } else { - let visited = (await visitedFetch.json()) as Adventure[]; + let res = await initialFetch.json(); + let visited = res.results as Adventure[]; + next = res.next; + previous = res.previous; + count = res.count; adventures = [...adventures, ...visited]; } - let plannedFetch = await fetch(`${serverEndpoint}/api/adventures/planned/`, { - headers: { - Cookie: `${event.cookies.get('auth')}` + + return { + props: { + adventures, + next, + previous } - }); - if (!plannedFetch.ok) { - console.error('Failed to fetch visited adventures'); - return redirect(302, '/login'); - } else { - let planned = (await plannedFetch.json()) as Adventure[]; - adventures = [...adventures, ...planned]; - } - return { adventures } as { adventures: Adventure[] }; + }; } }) satisfies PageServerLoad; @@ -366,49 +371,55 @@ export const actions: Actions = { }; } + let filterString = ''; if (visited) { - let visitedFetch = await fetch(`${serverEndpoint}/api/adventures/visited/`, { - headers: { - Cookie: `${event.cookies.get('auth')}` - } - }); - if (!visitedFetch.ok) { - console.error('Failed to fetch visited adventures'); - return redirect(302, '/login'); - } else { - let visited = (await visitedFetch.json()) as Adventure[]; - adventures = [...adventures, ...visited]; - } + filterString += 'visited'; } if (planned) { - let plannedFetch = await fetch(`${serverEndpoint}/api/adventures/planned/`, { - headers: { - Cookie: `${event.cookies.get('auth')}` - } - }); - if (!plannedFetch.ok) { - console.error('Failed to fetch visited adventures'); - return redirect(302, '/login'); - } else { - let planned = (await plannedFetch.json()) as Adventure[]; - adventures = [...adventures, ...planned]; + if (filterString) { + filterString += ','; } + filterString += 'planned'; } if (featured) { - let featuredFetch = await fetch(`${serverEndpoint}/api/adventures/featured/`, { + if (filterString) { + filterString += ','; + } + filterString += 'featured'; + } + if (!filterString) { + filterString = ''; + } + + let next = null; + let previous = null; + let count = 0; + + let visitedFetch = await fetch( + `${serverEndpoint}/api/adventures/filtered?types=${filterString}`, + { headers: { Cookie: `${event.cookies.get('auth')}` } - }); - if (!featuredFetch.ok) { - console.error('Failed to fetch visited adventures'); - return redirect(302, '/login'); - } else { - let featured = (await featuredFetch.json()) as Adventure[]; - adventures = [...adventures, ...featured]; } + ); + if (!visitedFetch.ok) { + console.error('Failed to fetch visited adventures'); + return redirect(302, '/login'); + } else { + let res = await visitedFetch.json(); + let visited = res.results as Adventure[]; + next = res.next; + previous = res.previous; + count = res.count; + adventures = [...adventures, ...visited]; } - // console.log(adventures); - return adventures as Adventure[]; + + return { + adventures, + next, + previous, + count + }; } }; diff --git a/frontend/src/routes/adventures/+page.svelte b/frontend/src/routes/adventures/+page.svelte index 1f3083f..dc5a698 100644 --- a/frontend/src/routes/adventures/+page.svelte +++ b/frontend/src/routes/adventures/+page.svelte @@ -11,13 +11,34 @@ export let data: any; console.log(data); - let adventures: Adventure[] = data.adventures || []; + let adventures: Adventure[] = data.props.adventures || []; let currentSort = { attribute: 'name', order: 'asc' }; let isShowingCreateModal: boolean = false; let newType: string = ''; + let next: string | null = null; + let previous: string | null = null; + let count = 0; + + async function changePage(direction: string) { + let url: string = ''; + if (direction === 'next' && next) { + url = next; + } else if (direction === 'previous' && previous) { + url = previous; + } else { + return; + } + let res = await fetch(url); + let result = await res.json(); + adventures = result.results as Adventure[]; + next = result.next; + previous = result.previous; + count = result.count; + } + function handleSubmit() { return async ({ result, update }: any) => { // First, call the update function with reset: false @@ -27,8 +48,11 @@ if (result.type === 'success') { if (result.data) { // console.log(result.data); - adventures = result.data as Adventure[]; - sort(currentSort); + adventures = result.data.adventures as Adventure[]; + next = result.data.next; + previous = result.data.previous; + count = result.data.count; + // sort(currentSort); } } }; @@ -39,9 +63,9 @@ currentSort.order = order; if (attribute === 'name') { if (order === 'asc') { - adventures = adventures.sort((a, b) => a.name.localeCompare(b.name)); - } else { adventures = adventures.sort((a, b) => b.name.localeCompare(a.name)); + } else { + adventures = adventures.sort((a, b) => a.name.localeCompare(b.name)); } } } @@ -140,6 +164,7 @@

My Adventures

+

This search returned {count} results.

{#if adventures.length === 0} {/if} @@ -160,6 +185,17 @@ /> {/each}
+
+ {#if previous} + + {/if} + {#if next} + + {/if} +
diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 5a951a8..8b634b4 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -60,13 +60,15 @@ export const actions: Actions = { httpOnly: true, sameSite: 'lax', expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' + path: '/', + secure: false }); event.cookies.set('refresh', refreshToken, { httpOnly: true, sameSite: 'lax', expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year - path: '/' + path: '/', + secure: false }); return redirect(302, '/'); From fc9ba3ad9613c78d91519451244165a81e079820 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 11 Jul 2024 20:09:55 -0400 Subject: [PATCH 2/5] pagination! fixed for private --- .../src/routes/adventures/+page.server.ts | 55 +++++++++++++++++- frontend/src/routes/adventures/+page.svelte | 58 +++++++++---------- 2 files changed, 82 insertions(+), 31 deletions(-) diff --git a/frontend/src/routes/adventures/+page.server.ts b/frontend/src/routes/adventures/+page.server.ts index efa29c3..c120eda 100644 --- a/frontend/src/routes/adventures/+page.server.ts +++ b/frontend/src/routes/adventures/+page.server.ts @@ -3,7 +3,7 @@ import type { PageServerLoad } from './$types'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Adventure } from '$lib/types'; -import type { Actions } from '@sveltejs/kit'; +import type { Actions, RequestEvent } from '@sveltejs/kit'; import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; import { checkLink } from '$lib'; @@ -41,7 +41,8 @@ export const load = (async (event) => { props: { adventures, next, - previous + previous, + count } }; } @@ -395,6 +396,8 @@ export const actions: Actions = { let previous = null; let count = 0; + console.log(filterString); + let visitedFetch = await fetch( `${serverEndpoint}/api/adventures/filtered?types=${filterString}`, { @@ -413,6 +416,7 @@ export const actions: Actions = { previous = res.previous; count = res.count; adventures = [...adventures, ...visited]; + console.log(next, previous, count); } return { @@ -421,5 +425,52 @@ export const actions: Actions = { previous, count }; + }, + changePage: async (event) => { + const formData = await event.request.formData(); + const url = formData.get('url'); + + console.log('Received URL:', url); + + if (!url) { + return { + status: 400, + body: { error: 'URL is required' } + }; + } + + try { + const response = await fetch(url.toString(), { + headers: { + 'Content-Type': 'application/json', + Cookie: `${event.cookies.get('auth')}` + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + let adventures = data.results as Adventure[]; + let next = data.next; + let previous = data.previous; + let count = data.count; + + return { + status: 200, + body: { + adventures, + next, + previous, + count + } + }; + } catch (error) { + console.error('Error fetching data:', error); + return { + status: 500, + body: { error: 'Failed to fetch data' } + }; + } } }; diff --git a/frontend/src/routes/adventures/+page.svelte b/frontend/src/routes/adventures/+page.svelte index dc5a698..92af8cb 100644 --- a/frontend/src/routes/adventures/+page.svelte +++ b/frontend/src/routes/adventures/+page.svelte @@ -1,5 +1,5 @@