diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index ed8f8bf2..a855f026 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -1,11 +1,13 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import AdventureViewSet, TripViewSet, StatsViewSet +from .views import AdventureViewSet, TripViewSet, StatsViewSet, GenerateDescription router = DefaultRouter() router.register(r'adventures', AdventureViewSet, basename='adventures') router.register(r'trips', TripViewSet, basename='trips') router.register(r'stats', StatsViewSet, basename='stats') +router.register(r'generate', GenerateDescription, basename='generate') + urlpatterns = [ # Include the router under the 'api/' prefix diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index cce69469..f7a27308 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -1,5 +1,7 @@ +import requests from rest_framework.decorators import action from rest_framework import viewsets +from django.db.models.functions import Lower from rest_framework.response import Response from .models import Adventure, Trip from worldtravel.models import VisitedRegion, Region, Country @@ -7,38 +9,66 @@ 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 = 10 + 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( + lower_name = Lower('name') + queryset = Adventure.objects.annotate( + ).filter( Q(is_public=True) | Q(user_id=self.request.user.id) - ) - + ).order_by(lower_name) # Sort by the annotated lowercase name + return queryset + 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) + + lower_name = Lower('name') + queryset = queryset.order_by(lower_name) + adventures = self.paginate_and_respond(queryset, request) + return adventures + + 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 +87,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): @@ -103,4 +134,37 @@ def counts(self, request): 'total_regions': total_regions, 'country_count': country_count, 'total_countries': total_countries - }) \ No newline at end of file + }) + +class GenerateDescription(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + @action(detail=False, methods=['get'],) + def desc(self, request): + name = self.request.query_params.get('name', '') + # un url encode the name + name = name.replace('%20', ' ') + print(name) + url = 'https://en.wikipedia.org/w/api.php?origin=*&action=query&prop=extracts&exintro&explaintext&format=json&titles=%s' % name + response = requests.get(url) + data = response.json() + data = response.json() + page_id = next(iter(data["query"]["pages"])) + extract = data["query"]["pages"][page_id] + if extract.get('extract') is None: + return Response({"error": "No description found"}, status=400) + return Response(extract) + @action(detail=False, methods=['get'],) + def img(self, request): + name = self.request.query_params.get('name', '') + # un url encode the name + name = name.replace('%20', ' ') + url = 'https://en.wikipedia.org/w/api.php?origin=*&action=query&prop=pageimages&format=json&piprop=original&titles=%s' % name + response = requests.get(url) + data = response.json() + page_id = next(iter(data["query"]["pages"])) + extract = data["query"]["pages"][page_id] + if extract.get('original') is None: + return Response({"error": "No image found"}, status=400) + return Response(extract["original"]) + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f4791878..a497206b 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 366539dc..8cb9577a 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 fe72c267..48af4d2c 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'; @@ -13,32 +13,38 @@ 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, + count } - }); - 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 +372,123 @@ 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/`, { + if (filterString) { + filterString += ','; + } + filterString += 'planned'; + } + if (featured) { + if (filterString) { + filterString += ','; + } + filterString += 'featured'; + } + if (!filterString) { + filterString = ''; + } + + let next = null; + let previous = null; + let count = 0; + + console.log(filterString); + + let visitedFetch = await fetch( + `${serverEndpoint}/api/adventures/filtered?types=${filterString}`, + { 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 (!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(next, previous, count); } - if (featured) { - let featuredFetch = await fetch(`${serverEndpoint}/api/adventures/featured/`, { + + return { + adventures, + next, + previous, + count + }; + }, + changePage: async (event) => { + const formData = await event.request.formData(); + const next = formData.get('next') as string; + const previous = formData.get('previous') as string; + const page = formData.get('page') as string; + + if (!page) { + return { + status: 400, + body: { error: 'Missing required fields' } + }; + } + + // Start with the current URL if next and previous are not provided + let url: string = next || previous || event.url.toString(); + + let index = url.indexOf('/api'); + let newUrl = url.substring(index); + console.log('NEW URL' + newUrl); + url = serverEndpoint + newUrl; + console.log('URL' + url); + + // Replace or add the page number in the URL + if (url.includes('page=')) { + url = url.replace(/page=\d+/, `page=${page}`); + } else { + // If 'page=' is not in the URL, add it + url += url.includes('?') ? '&' : '?'; + url += `page=${page}`; + } + + try { + const response = await fetch(url, { headers: { + 'Content-Type': 'application/json', 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 (!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' } + }; } - // console.log(adventures); - return adventures as Adventure[]; } }; diff --git a/frontend/src/routes/adventures/+page.svelte b/frontend/src/routes/adventures/+page.svelte index 1f3083ff..c54cd1ba 100644 --- a/frontend/src/routes/adventures/+page.svelte +++ b/frontend/src/routes/adventures/+page.svelte @@ -1,5 +1,5 @@