From 57eee7bb5dc509027f0b19c540cdb7a9841dbcaf Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 12:46:50 -0400 Subject: [PATCH 01/17] chore: Add GeoJSON endpoint to retrieve combined GeoJSON data from static files --- backend/server/worldtravel/urls.py | 3 +- backend/server/worldtravel/views.py | 40 ++++++++++++++++++++++- frontend/src/routes/map/+page.server.ts | 23 ++------------ frontend/src/routes/map/+page.svelte | 42 +++++++++++++++++++++++-- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/backend/server/worldtravel/urls.py b/backend/server/worldtravel/urls.py index 8f0610f..0a9a6c5 100644 --- a/backend/server/worldtravel/urls.py +++ b/backend/server/worldtravel/urls.py @@ -2,7 +2,7 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country +from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country, GeoJSONView router = DefaultRouter() router.register(r'countries', CountryViewSet, basename='countries') @@ -13,4 +13,5 @@ path('', include(router.urls)), path('/regions/', regions_by_country, name='regions-by-country'), path('/visits/', visits_by_country, name='visits-by-country'), + path('geojson/', GeoJSONView.as_view({'get': 'list'}), name='geojson'), ] diff --git a/backend/server/worldtravel/views.py b/backend/server/worldtravel/views.py index 4bcec73..0880f0a 100644 --- a/backend/server/worldtravel/views.py +++ b/backend/server/worldtravel/views.py @@ -6,6 +6,10 @@ from django.shortcuts import get_object_or_404 from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes +import os +import json +from django.conf import settings +from django.contrib.staticfiles import finders @api_view(['GET']) @permission_classes([IsAuthenticated]) @@ -49,4 +53,38 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) \ No newline at end of file + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + +class GeoJSONView(viewsets.ViewSet): + """ + Combine all GeoJSON data from .json files in static/data into a single GeoJSON object. + """ + def list(self, request): + combined_geojson = { + "type": "FeatureCollection", + "features": [] + } + + # Use Django's static file finder to locate the 'data' directory + data_dir = finders.find('data') + + if not data_dir or not os.path.isdir(data_dir): + return Response({"error": "Data directory does not exist."}, status=404) + + for filename in os.listdir(data_dir): + if filename.endswith('.json'): + file_path = os.path.join(data_dir, filename) + try: + with open(file_path, 'r') as f: + json_data = json.load(f) + # Check if the JSON data is GeoJSON + if isinstance(json_data, dict) and "type" in json_data: + if json_data["type"] == "FeatureCollection": + combined_geojson["features"].extend(json_data.get("features", [])) + elif json_data["type"] == "Feature": + combined_geojson["features"].append(json_data) + # You can add more conditions here for other GeoJSON types if needed + except (IOError, json.JSONDecodeError) as e: + return Response({"error": f"Error reading file {filename}: {str(e)}"}, status=500) + + return Response(combined_geojson) \ No newline at end of file diff --git a/frontend/src/routes/map/+page.server.ts b/frontend/src/routes/map/+page.server.ts index bdbfb43..e5a8368 100644 --- a/frontend/src/routes/map/+page.server.ts +++ b/frontend/src/routes/map/+page.server.ts @@ -5,12 +5,6 @@ import type { Adventure, VisitedRegion } from '$lib/types'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { - let countryCodesToFetch = ['FR', 'US', 'CA', 'DE', 'AU', 'MX', 'JP']; - let geoJSON = { - type: 'FeatureCollection', - features: [] - }; - if (!event.locals.user) { return redirect(302, '/login'); } else { @@ -20,6 +14,8 @@ export const load = (async (event) => { } }); + let geoJsonUrl = `${endpoint}/api/geojson/` as string; + let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, { headers: { Cookie: `${event.cookies.get('auth')}` @@ -27,19 +23,6 @@ export const load = (async (event) => { }); let visitedRegions = (await visitedRegionsFetch.json()) as VisitedRegion[]; - await Promise.all( - countryCodesToFetch.map(async (code) => { - let res = await fetch(`${endpoint}/static/data/${code.toLowerCase()}.json`); - console.log('fetching ' + code); - let json = await res.json(); - if (!json) { - console.error(`Failed to fetch ${code} GeoJSON`); - } else { - geoJSON.features = geoJSON.features.concat(json.features); - } - }) - ); - if (!visitedFetch.ok) { console.error('Failed to fetch visited adventures'); return redirect(302, '/login'); @@ -61,7 +44,7 @@ export const load = (async (event) => { return { props: { markers, - geoJSON, + geoJsonUrl, visitedRegions } }; diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index 259e54a..c425b0b 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -17,6 +17,24 @@ let clickedName = ''; + let showVisited = true; + let showPlanned = true; + + $: { + if (!showVisited) { + markers = markers.filter((marker) => marker.type !== 'visited'); + } else { + const visitedMarkers = data.props.markers.filter((marker) => marker.type === 'visited'); + markers = [...markers, ...visitedMarkers]; + } + if (!showPlanned) { + markers = markers.filter((marker) => marker.type !== 'planned'); + } else { + const plannedMarkers = data.props.markers.filter((marker) => marker.type === 'planned'); + markers = [...markers, ...plannedMarkers]; + } + } + let newMarker = []; let newLongitude = null; @@ -61,7 +79,7 @@ let visitedRegions = data.props.visitedRegions; - let geoJSON = data.props.geoJSON; + let geoJSON = []; let visitArray = []; @@ -77,11 +95,29 @@ } // mapped to the checkbox - let showGEO = true; + let showGEO = false; + $: { + if (showGEO && geoJSON.length === 0) { + (async () => { + geoJSON = await fetch(data.props.geoJsonUrl).then((res) => res.json()); + })(); + } else if (!showGEO) { + geoJSON = []; + } + } let createModalOpen = false; + + + {#if newMarker.length > 0} + Date: Sat, 27 Jul 2024 12:50:36 -0400 Subject: [PATCH 02/17] feat: Update map page to fetch GeoJSON data from new endpoint --- frontend/src/routes/map/+page.server.ts | 3 --- frontend/src/routes/map/+page.svelte | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/routes/map/+page.server.ts b/frontend/src/routes/map/+page.server.ts index e5a8368..c355bac 100644 --- a/frontend/src/routes/map/+page.server.ts +++ b/frontend/src/routes/map/+page.server.ts @@ -14,8 +14,6 @@ export const load = (async (event) => { } }); - let geoJsonUrl = `${endpoint}/api/geojson/` as string; - let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, { headers: { Cookie: `${event.cookies.get('auth')}` @@ -44,7 +42,6 @@ export const load = (async (event) => { return { props: { markers, - geoJsonUrl, visitedRegions } }; diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index c425b0b..4cdfd64 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -99,7 +99,7 @@ $: { if (showGEO && geoJSON.length === 0) { (async () => { - geoJSON = await fetch(data.props.geoJsonUrl).then((res) => res.json()); + geoJSON = await fetch('/api/geojson/').then((res) => res.json()); })(); } else if (!showGEO) { geoJSON = []; From c94a4379c7a3c23ccbfbaf0562f159c73999aec8 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 14:26:15 -0400 Subject: [PATCH 03/17] collection days --- ...llection_end_date_collection_start_date.py | 23 +++++++++++++ backend/server/adventures/models.py | 2 ++ backend/server/adventures/serializers.py | 2 +- .../src/lib/components/CollectionCard.svelte | 14 ++++++++ .../src/lib/components/EditCollection.svelte | 23 +++++++++++++ .../src/lib/components/NewCollection.svelte | 32 ++++++++++++++++--- frontend/src/lib/types.ts | 2 ++ .../src/routes/collections/+page.server.ts | 8 +++++ 8 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 backend/server/adventures/migrations/0012_collection_end_date_collection_start_date.py diff --git a/backend/server/adventures/migrations/0012_collection_end_date_collection_start_date.py b/backend/server/adventures/migrations/0012_collection_end_date_collection_start_date.py new file mode 100644 index 0000000..744d83a --- /dev/null +++ b/backend/server/adventures/migrations/0012_collection_end_date_collection_start_date.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.7 on 2024-07-27 18:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0011_adventure_updated_at'), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='end_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='collection', + name='start_date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 854ce4e..9cc642b 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -56,6 +56,8 @@ class Collection(models.Model): description = models.TextField(blank=True, null=True) is_public = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) + start_date = models.DateField(blank=True, null=True) + end_date = models.DateField(blank=True, null=True) # if connected adventures are private and collection is public, raise an error def clean(self): diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 40f24bd..84c38a2 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -29,7 +29,7 @@ class CollectionSerializer(serializers.ModelSerializer): class Meta: model = Collection # fields are all plus the adventures field - fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures'] + fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date'] \ No newline at end of file diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte index cd6d314..bb25110 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -47,6 +47,20 @@

{collection.name}

{collection.adventures.length} Adventures

+ {#if collection.start_date && collection.end_date} +

+ Dates: {new Date(collection.start_date).toLocaleDateString()} - {new Date( + collection.end_date + ).toLocaleDateString()} +

+ +

+ Duration: {Math.floor( + (new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) / + (1000 * 60 * 60 * 24) + )}{' '} + days +

{/if}
{#if type != 'link'}
+
+
+ +
+
+
+ +

- -
- - +
+
+ +
+
+
+ +
+
+ + +
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index d0dbdf5..08db959 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -64,6 +64,8 @@ export type Collection = { is_public: boolean; adventures: Adventure[]; created_at?: string; + start_date?: string; + end_date?: string; }; export type OpenStreetMapPlace = { diff --git a/frontend/src/routes/collections/+page.server.ts b/frontend/src/routes/collections/+page.server.ts index 4f3fdfb..655f80c 100644 --- a/frontend/src/routes/collections/+page.server.ts +++ b/frontend/src/routes/collections/+page.server.ts @@ -51,6 +51,8 @@ export const actions: Actions = { const name = formData.get('name') as string; const description = formData.get('description') as string | null; + const start_date = formData.get('start_date') as string | null; + const end_date = formData.get('end_date') as string | null; if (!name) { return { @@ -62,6 +64,8 @@ export const actions: Actions = { const formDataToSend = new FormData(); formDataToSend.append('name', name); formDataToSend.append('description', description || ''); + formDataToSend.append('start_date', start_date || ''); + formDataToSend.append('end_date', end_date || ''); let auth = event.cookies.get('auth'); if (!auth) { @@ -136,6 +140,8 @@ export const actions: Actions = { const name = formData.get('name') as string; const description = formData.get('description') as string | null; let is_public = formData.get('is_public') as string | null | boolean; + const start_date = formData.get('start_date') as string | null; + const end_date = formData.get('end_date') as string | null; if (is_public) { is_public = true; @@ -154,6 +160,8 @@ export const actions: Actions = { formDataToSend.append('name', name); formDataToSend.append('description', description || ''); formDataToSend.append('is_public', is_public.toString()); + formDataToSend.append('start_date', start_date || ''); + formDataToSend.append('end_date', end_date || ''); let auth = event.cookies.get('auth'); From 59a3a2d72a2c7962e5cd6aa2f919617a36bd04c6 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 14:38:43 -0400 Subject: [PATCH 04/17] chore: Update date formatting in AdventureCard and CollectionCard components --- .../src/lib/components/AdventureCard.svelte | 2 +- .../src/lib/components/CollectionCard.svelte | 2 +- .../src/routes/collections/[id]/+page.svelte | 75 ++++++++++++++++++- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index b230b28..be80984 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -163,7 +163,7 @@ {#if adventure.date && adventure.date !== ''}
-

{new Date(adventure.date).toLocaleDateString()}

+

{new Date(adventure.date).toLocaleDateString('en-US', { timeZone: 'UTC' })}

{/if} {#if adventure.activity_types && adventure.activity_types.length > 0} diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte index bb25110..948bd26 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -58,7 +58,7 @@ Duration: {Math.floor( (new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) / (1000 * 60 * 60 * 24) - )}{' '} + ) + 1}{' '} days

{/if}
diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index cc8500d..e3813a7 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -18,6 +18,8 @@ let adventures: Adventure[] = []; let numVisited: number = 0; + let numberOfDays: number = NaN; + $: { numVisited = adventures.filter((a) => a.type === 'visited').length; } @@ -32,12 +34,44 @@ } else { notFound = true; } + if (collection.start_date && collection.end_date) { + numberOfDays = + Math.floor( + (new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) / + (1000 * 60 * 60 * 24) + ) + 1; + } }); function deleteAdventure(event: CustomEvent) { adventures = adventures.filter((a) => a.id !== event.detail); } + function groupAdventuresByDate( + adventures: Adventure[], + startDate: Date + ): Record { + const groupedAdventures: Record = {}; + + for (let i = 0; i < numberOfDays; i++) { + const currentDate = new Date(startDate); + currentDate.setDate(startDate.getDate() + i); + const dateString = currentDate.toISOString().split('T')[0]; + groupedAdventures[dateString] = []; + } + + adventures.forEach((adventure) => { + if (adventure.date) { + const adventureDate = new Date(adventure.date).toISOString().split('T')[0]; + if (groupedAdventures[adventureDate]) { + groupedAdventures[adventureDate].push(adventure); + } + } + }); + + return groupedAdventures; + } + async function addAdventure(event: CustomEvent) { console.log(event.detail); if (adventures.find((a) => a.id === event.detail.id)) { @@ -203,7 +237,44 @@ {/each}
- {#if collection.description} -

{collection.description}

+ {#if numberOfDays} +

Duration: {numberOfDays} days

+ {/if} + {#if collection.start_date && collection.end_date} +

+ Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date( + collection.end_date + ).toLocaleDateString('en-US', { timeZone: 'UTC' })} +

+ + {#each Array(numberOfDays) as _, i} + {@const currentDate = new Date(collection.start_date)} + {@const temp = currentDate.setDate(currentDate.getDate() + i)} + {@const dateString = currentDate.toISOString().split('T')[0]} + {@const dayAdventures = groupAdventuresByDate(adventures, new Date(collection.start_date))[ + dateString + ]} + +

+ Day {i + 1} - {currentDate.toLocaleDateString('en-US', { timeZone: 'UTC' })} +

+ + {#if dayAdventures.length > 0} +
+ {#each dayAdventures as adventure} + + {/each} +
+ {:else} +

No adventures planned for this day.

+ {/if} + {/each} {/if} {/if} From dbe2f01b8d1630b2e887055503e837e0f3128e92 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 14:40:33 -0400 Subject: [PATCH 05/17] refactor: Update adventure and collection page headers and date formatting --- frontend/src/routes/collections/[id]/+page.svelte | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index e3813a7..0efe833 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -220,7 +220,7 @@ {/if} -

Linked Adventures

+

Linked Adventures

{#if adventures.length == 0} {/if} @@ -237,15 +237,17 @@ {/each} - {#if numberOfDays} -

Duration: {numberOfDays} days

- {/if} {#if collection.start_date && collection.end_date} +

Itinerary

+ {#if numberOfDays} +

Duration: {numberOfDays} days

+ {/if}

Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date( collection.end_date ).toLocaleDateString('en-US', { timeZone: 'UTC' })}

+
{#each Array(numberOfDays) as _, i} {@const currentDate = new Date(collection.start_date)} From 0ea9f1d73ecd369197b547cda5477d5030594387 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 14:44:15 -0400 Subject: [PATCH 06/17] refactor: Update date formatting in CollectionCard and CollectionPage components --- frontend/src/lib/components/CollectionCard.svelte | 4 ++-- frontend/src/routes/collections/[id]/+page.svelte | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte index 948bd26..1b6df55 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -49,9 +49,9 @@

{collection.adventures.length} Adventures

{#if collection.start_date && collection.end_date}

- Dates: {new Date(collection.start_date).toLocaleDateString()} - {new Date( + Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date( collection.end_date - ).toLocaleDateString()} + ).toLocaleDateString('en-US', { timeZone: 'UTC' })}

diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 0efe833..49ba896 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -238,11 +238,11 @@ {#if collection.start_date && collection.end_date} -

Itinerary

+

Itinerary

{#if numberOfDays} -

Duration: {numberOfDays} days

+

Duration: {numberOfDays} days

{/if} -

+

Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date( collection.end_date ).toLocaleDateString('en-US', { timeZone: 'UTC' })} @@ -257,7 +257,7 @@ dateString ]} -

+

Day {i + 1} - {currentDate.toLocaleDateString('en-US', { timeZone: 'UTC' })}

From 87a804dbc2a891bfe41907dfdbcf9ace529a6b0e Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 15:41:26 -0400 Subject: [PATCH 07/17] lodging beta --- backend/server/adventures/models.py | 1 + backend/server/adventures/views.py | 3 +- .../src/lib/components/AdventureCard.svelte | 5 +- .../src/lib/components/NewAdventure.svelte | 11 +++- .../src/routes/adventures/+page.server.ts | 2 + .../src/routes/collections/[id]/+page.svelte | 54 +++++++++++++++++-- 6 files changed, 69 insertions(+), 7 deletions(-) diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 9cc642b..82063f8 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -8,6 +8,7 @@ ADVENTURE_TYPES = [ ('visited', 'Visited'), ('planned', 'Planned'), + ('lodging', 'Lodging'), ] diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index e71cd4c..e06940f 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -138,8 +138,9 @@ def all(self, request): # queryset = Adventure.objects.filter( # Q(is_public=True) | Q(user_id=request.user.id), collection=None # ) + allowed_types = ['visited', 'planned'] queryset = Adventure.objects.filter( - Q(user_id=request.user.id) + Q(user_id=request.user.id) & Q(type__in=allowed_types) ) queryset = self.apply_sorting(queryset) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index be80984..8db65f9 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -149,9 +149,12 @@
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
Visited
- {:else if user?.pk == adventure.user_id} + {:else if user?.pk == adventure.user_id && adventure.type == 'planned'}
Planned
+ {:else if user?.pk == adventure.user_id && adventure.type == 'lodging'} +
Lodging
{/if} +
{adventure.is_public ? 'Public' : 'Private'}
{#if adventure.location && adventure.location !== ''} diff --git a/frontend/src/lib/components/NewAdventure.svelte b/frontend/src/lib/components/NewAdventure.svelte index 9b39421..96ac78f 100644 --- a/frontend/src/lib/components/NewAdventure.svelte +++ b/frontend/src/lib/components/NewAdventure.svelte @@ -11,6 +11,7 @@ export let longitude: number | null = null; export let latitude: number | null = null; + export let collection_id: number | null = null; import MapMarker from '~icons/mdi/map-marker'; import Calendar from '~icons/mdi/calendar'; @@ -39,7 +40,7 @@ latitude: null, longitude: null, is_public: false, - collection: null + collection: collection_id || NaN }; if (longitude && latitude) { @@ -371,6 +372,14 @@ bind:value={newAdventure.longitude} class="input input-bordered w-full max-w-xs mt-1" /> + diff --git a/frontend/src/routes/adventures/+page.server.ts b/frontend/src/routes/adventures/+page.server.ts index 9ee8153..c93f27d 100644 --- a/frontend/src/routes/adventures/+page.server.ts +++ b/frontend/src/routes/adventures/+page.server.ts @@ -64,6 +64,7 @@ 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 collection = formData.get('collection') as string | null; // check if latitude and longitude are valid if (latitude && longitude) { @@ -108,6 +109,7 @@ export const actions: Actions = { formDataToSend.append('description', description || ''); formDataToSend.append('latitude', latitude || ''); formDataToSend.append('longitude', longitude || ''); + formDataToSend.append('collection', collection || ''); 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/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 49ba896..efa4b7d 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -10,6 +10,7 @@ import AdventureLink from '$lib/components/AdventureLink.svelte'; import EditAdventure from '$lib/components/EditAdventure.svelte'; import NotFound from '$lib/components/NotFound.svelte'; + import NewAdventure from '$lib/components/NewAdventure.svelte'; export let data: PageData; @@ -25,6 +26,7 @@ } let notFound: boolean = false; + let isShowingLinkModal: boolean = false; let isShowingCreateModal: boolean = false; onMount(() => { @@ -72,6 +74,11 @@ return groupedAdventures; } + function createAdventure(event: CustomEvent) { + adventures = [event.detail, ...adventures]; + isShowingCreateModal = false; + } + async function addAdventure(event: CustomEvent) { console.log(event.detail); if (adventures.find((a) => a.id === event.detail.id)) { @@ -111,6 +118,8 @@ let adventureToEdit: Adventure; let isEditModalOpen: boolean = false; + let newType: string; + function editAdventure(event: CustomEvent) { adventureToEdit = event.detail; isEditModalOpen = true; @@ -127,11 +136,11 @@ } -{#if isShowingCreateModal} +{#if isShowingLinkModal} { - isShowingCreateModal = false; + isShowingLinkModal = false; }} on:add={addAdventure} /> @@ -145,6 +154,15 @@ /> {/if} +{#if isShowingCreateModal} + (isShowingCreateModal = false)} + /> +{/if} + {#if notFound}
{ - isShowingCreateModal = true; + isShowingLinkModal = true; }} > Adventure +

Add new...

+ + + + {#if (adventure.collection && adventure.type == 'visited') || adventure.type == 'planned'} {/if} + + {#if adventure.collection && adventure.type == 'lodging'} + + {/if} {#if !adventure.collection} Date: Sat, 27 Jul 2024 18:42:52 -0400 Subject: [PATCH 09/17] lodging beta --- backend/server/adventures/models.py | 1 + .../src/lib/components/AdventureCard.svelte | 18 ++++++++- .../src/lib/components/EditAdventure.svelte | 30 +++++++------- .../src/lib/components/NewAdventure.svelte | 31 +++++++------- .../lib/components/PointSelectionModal.svelte | 4 +- .../src/routes/collections/[id]/+page.svelte | 40 +++++++++++++++++++ 6 files changed, 93 insertions(+), 31 deletions(-) diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 82063f8..a872abd 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -9,6 +9,7 @@ ('visited', 'Visited'), ('planned', 'Planned'), ('lodging', 'Lodging'), + ('dining', 'Dining') ] diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index b9fac8a..8afa422 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -24,8 +24,20 @@ let isCollectionModalOpen: boolean = false; + let keyword: string = ''; + export let adventure: Adventure; + if (adventure.type == 'visited') { + keyword = 'Adventure'; + } else if (adventure.type == 'planned') { + keyword = 'Adventure'; + } else if (adventure.type == 'lodging') { + keyword = 'Lodging'; + } else if (adventure.type == 'dining') { + keyword = 'Dining'; + } + let activityTypes: string[] = []; // makes it reactivty to changes so it updates automatically $: { @@ -153,6 +165,8 @@
Planned
{:else if user?.pk == adventure.user_id && adventure.type == 'lodging'}
Lodging
+ {:else if user?.pk == adventure.user_id && adventure.type == 'dining'} +
Dining
{/if}
{adventure.is_public ? 'Public' : 'Private'}
@@ -197,7 +211,7 @@ >Open Details {#if adventure.type == 'visited'} diff --git a/frontend/src/lib/components/EditAdventure.svelte b/frontend/src/lib/components/EditAdventure.svelte index 7d26bd5..b663bf4 100644 --- a/frontend/src/lib/components/EditAdventure.svelte +++ b/frontend/src/lib/components/EditAdventure.svelte @@ -222,20 +222,22 @@ >
-
-
- - -
+ {#if adventureToEdit.type == 'visited' || adventureToEdit.type == 'planned'} +
+
+ + +
+ {/if}

diff --git a/frontend/src/lib/components/NewAdventure.svelte b/frontend/src/lib/components/NewAdventure.svelte index 96ac78f..0548027 100644 --- a/frontend/src/lib/components/NewAdventure.svelte +++ b/frontend/src/lib/components/NewAdventure.svelte @@ -24,6 +24,7 @@ import Wikipedia from '~icons/mdi/wikipedia'; import ActivityComplete from './ActivityComplete.svelte'; import { appVersion } from '$lib/config'; + import AdventureCard from './AdventureCard.svelte'; let newAdventure: Adventure = { id: NaN, @@ -294,20 +295,22 @@ >
-
-
- - -
+ {#if newAdventure.type == 'visited' || newAdventure.type == 'planned'} +
+
+ + +
+ {/if}
Lodging + + + + {#each adventures as adventure} + {#if adventure.longitude && adventure.latitude} + + +
{adventure.name}
+

+ {adventure.type.charAt(0).toUpperCase() + adventure.type.slice(1)} +

+

+ {adventure.date + ? new Date(adventure.date).toLocaleDateString('en-US', { timeZone: 'UTC' }) + : ''} +

+
+
+ {/if} + {/each} + +
{/if} {/if} From ae9a7f547994ad1b77a123a9d24df964b5aaf5ae Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 19:04:55 -0400 Subject: [PATCH 10/17] transportation --- backend/server/adventures/admin.py | 3 +- ...013_alter_adventure_type_transportation.py | 41 +++++++++++++++++++ backend/server/adventures/models.py | 41 +++++++++++++++++++ backend/server/adventures/serializers.py | 35 +++++++++++++++- backend/server/adventures/urls.py | 3 +- backend/server/adventures/views.py | 34 ++++++++++++++- .../src/lib/components/AdventureCard.svelte | 4 +- 7 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 backend/server/adventures/migrations/0013_alter_adventure_type_transportation.py diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index c736aa7..5cc7b4b 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -1,7 +1,7 @@ import os from django.contrib import admin from django.utils.html import mark_safe -from .models import Adventure, Collection +from .models import Adventure, Collection, Transportation from worldtravel.models import Country, Region, VisitedRegion @@ -74,6 +74,7 @@ def adventure_count(self, obj): admin.site.register(Country, CountryAdmin) admin.site.register(Region, RegionAdmin) admin.site.register(VisitedRegion) +admin.site.register(Transportation) admin.site.site_header = 'AdventureLog Admin' admin.site.site_title = 'AdventureLog Admin Site' diff --git a/backend/server/adventures/migrations/0013_alter_adventure_type_transportation.py b/backend/server/adventures/migrations/0013_alter_adventure_type_transportation.py new file mode 100644 index 0000000..ab7402f --- /dev/null +++ b/backend/server/adventures/migrations/0013_alter_adventure_type_transportation.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.7 on 2024-07-27 22:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0012_collection_end_date_collection_start_date'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='adventure', + name='type', + field=models.CharField(choices=[('visited', 'Visited'), ('planned', 'Planned'), ('lodging', 'Lodging'), ('dining', 'Dining')], max_length=100), + ), + migrations.CreateModel( + name='Transportation', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('type', models.CharField(max_length=100)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ('rating', models.FloatField(blank=True, null=True)), + ('link', models.URLField(blank=True, null=True)), + ('date', models.DateTimeField(blank=True, null=True)), + ('flight_number', models.CharField(blank=True, max_length=100, null=True)), + ('from_location', models.CharField(blank=True, max_length=200, null=True)), + ('to_location', models.CharField(blank=True, max_length=200, null=True)), + ('is_public', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')), + ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index a872abd..e7027cd 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -12,6 +12,17 @@ ('dining', 'Dining') ] +TRANSPORTATION_TYPES = [ + ('car', 'Car'), + ('plane', 'Plane'), + ('train', 'Train'), + ('bus', 'Bus'), + ('boat', 'Boat'), + ('bike', 'Bike'), + ('walking', 'Walking'), + ('other', 'Other') +] + # Assuming you have a default user ID you want to use default_user_id = 1 # Replace with an actual user ID @@ -70,3 +81,33 @@ def clean(self): def __str__(self): return self.name + +# make a class for transportaiotn and make it linked to a collection. Make it so it can be used for different types of transportations like car, plane, train, etc. + +class Transportation(models.Model): + id = models.AutoField(primary_key=True) + user_id = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user_id) + type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES) + name = models.CharField(max_length=200) + description = models.TextField(blank=True, null=True) + rating = models.FloatField(blank=True, null=True) + link = models.URLField(blank=True, null=True) + date = models.DateTimeField(blank=True, null=True) + flight_number = models.CharField(max_length=100, blank=True, null=True) + from_location = models.CharField(max_length=200, blank=True, null=True) + to_location = models.CharField(max_length=200, blank=True, null=True) + is_public = models.BooleanField(default=False) + collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def clean(self): + if self.collection: + if self.collection.is_public and not self.is_public: + raise ValidationError('Transportations associated with a public collection must be public. Collection: ' + self.trip.name + ' Transportation: ' + self.name) + if self.user_id != self.collection.user_id: + raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username) + + def __str__(self): + return self.name diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 84c38a2..1057eee 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -1,5 +1,5 @@ import os -from .models import Adventure, Collection +from .models import Adventure, Collection, Transportation from rest_framework import serializers class AdventureSerializer(serializers.ModelSerializer): @@ -31,5 +31,38 @@ class Meta: # fields are all plus the adventures field fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date'] +class TransportationSerializer(serializers.ModelSerializer): + + class Meta: + model = Transportation + fields = [ + 'id', 'user_id', 'type', 'name', 'description', 'rating', + 'link', 'date', 'flight_number', 'from_location', 'to_location', + 'is_public', 'collection', 'collection_id', 'created_at', 'updated_at' + ] + read_only_fields = ['id', 'created_at', 'updated_at'] + + 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) \ No newline at end of file diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 10cdb1a..7f876da 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -1,6 +1,6 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView +from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet router = DefaultRouter() router.register(r'adventures', AdventureViewSet, basename='adventures') @@ -8,6 +8,7 @@ router.register(r'stats', StatsViewSet, basename='stats') router.register(r'generate', GenerateDescription, basename='generate') router.register(r'activity-types', ActivityTypesView, basename='activity-types') +router.register(r'transportations', TransportationViewSet, basename='transportations') urlpatterns = [ diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index e06940f..8d427b4 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -4,9 +4,9 @@ from rest_framework import viewsets from django.db.models.functions import Lower from rest_framework.response import Response -from .models import Adventure, Collection +from .models import Adventure, Collection, Transportation from worldtravel.models import VisitedRegion, Region, Country -from .serializers import AdventureSerializer, CollectionSerializer +from .serializers import AdventureSerializer, CollectionSerializer, TransportationSerializer from rest_framework.permissions import IsAuthenticated from django.db.models import Q, Prefetch from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly @@ -238,6 +238,9 @@ def update(self, request, *args, **kwargs): # Update associated adventures to match the collection's is_public status Adventure.objects.filter(collection=instance).update(is_public=new_public_status) + # do the same for transportations + Transportation.objects.filter(collection=instance).update(is_public=new_public_status) + # Log the action (optional) action = "public" if new_public_status else "private" print(f"Collection {instance.id} and its adventures were set to {action}") @@ -379,3 +382,30 @@ def types(self, request): allTypes.append(x) return Response(allTypes) + +class TransportationViewSet(viewsets.ModelViewSet): + queryset = Transportation.objects.all() + serializer_class = TransportationSerializer + permission_classes = [IsAuthenticated] + filterset_fields = ['type', 'is_public', 'collection'] + + # return error message if user is not authenticated on the root endpoint + 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 get_queryset(self): + + """ + This view should return a list of all transportations + for the currently authenticated user. + """ + user = self.request.user + return Transportation.objects.filter(user_id=user) + + def perform_create(self, serializer): + serializer.save(user_id=self.request.user) + + \ No newline at end of file diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 8afa422..a907e16 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -163,9 +163,11 @@
Visited
{:else if user?.pk == adventure.user_id && adventure.type == 'planned'}
Planned
+ {:else if (user?.pk !== adventure.user_id && adventure.type == 'planned') || adventure.type == 'visited'} +
Adventure
{:else if user?.pk == adventure.user_id && adventure.type == 'lodging'}
Lodging
- {:else if user?.pk == adventure.user_id && adventure.type == 'dining'} + {:else if adventure.type == 'dining'}
Dining
{/if} From 7c9afd8931684cbfa4ff6cb22ca7c4fe1b70a599 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 19:22:01 -0400 Subject: [PATCH 11/17] transportation --- backend/server/adventures/serializers.py | 19 +-- backend/server/adventures/views.py | 22 +++ .../lib/components/TransportationCard.svelte | 83 +++++++++++ frontend/src/lib/types.ts | 19 +++ .../src/routes/collections/[id]/+page.svelte | 132 ++++++++++-------- 5 files changed, 207 insertions(+), 68 deletions(-) create mode 100644 frontend/src/lib/components/TransportationCard.svelte diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 1057eee..53541b4 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -23,14 +23,6 @@ def validate_activity_types(self, value): return [activity.lower() for activity in value] return value -class CollectionSerializer(serializers.ModelSerializer): - adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set') - - class Meta: - model = Collection - # fields are all plus the adventures field - fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date'] - class TransportationSerializer(serializers.ModelSerializer): class Meta: @@ -65,4 +57,15 @@ def create(self, validated_data): validated_data['user_id'] = self.context['request'].user return super().create(validated_data) + +class CollectionSerializer(serializers.ModelSerializer): + adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set') + transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set') + + class Meta: + model = Collection + # fields are all plus the adventures field + fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations'] + + \ No newline at end of file diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 8d427b4..c15bfe0 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -261,6 +261,10 @@ def get_queryset(self): Prefetch('adventure_set', queryset=Adventure.objects.filter( Q(is_public=True) | Q(user_id=self.request.user.id) )) + ).prefetch_related( + Prefetch('transportation_set', queryset=Transportation.objects.filter( + Q(is_public=True) | Q(user_id=self.request.user.id) + )) ) return self.apply_sorting(collections) @@ -296,6 +300,14 @@ def paginate_and_respond(self, queryset, request): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + # make a view to return all of the associated transportations with a collection + @action(detail=True, methods=['get']) + def transportations(self, request, pk=None): + collection = self.get_object() + transportations = Transportation.objects.filter(collection=collection) + serializer = TransportationSerializer(transportations, many=True) + return Response(serializer.data) + class StatsViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] @@ -395,6 +407,16 @@ def list(self, request, *args, **kwargs): return Response({"detail": "Listing all adventures is not allowed."}, status=status.HTTP_403_FORBIDDEN) + @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 = Transportation.objects.filter( + Q(user_id=request.user.id) + ) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + def get_queryset(self): diff --git a/frontend/src/lib/components/TransportationCard.svelte b/frontend/src/lib/components/TransportationCard.svelte new file mode 100644 index 0000000..1b6df55 --- /dev/null +++ b/frontend/src/lib/components/TransportationCard.svelte @@ -0,0 +1,83 @@ + + +
+
+

{collection.name}

+

{collection.adventures.length} Adventures

+ {#if collection.start_date && collection.end_date} +

+ Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date( + collection.end_date + ).toLocaleDateString('en-US', { timeZone: 'UTC' })} +

+ +

+ Duration: {Math.floor( + (new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) / + (1000 * 60 * 60 * 24) + ) + 1}{' '} + days +

{/if} +
+ {#if type != 'link'} + + + + {/if} + {#if type == 'link'} + + {/if} +
+
+
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 08db959..a53fc15 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -66,6 +66,7 @@ export type Collection = { created_at?: string; start_date?: string; end_date?: string; + transportations?: Transportation[]; }; export type OpenStreetMapPlace = { @@ -84,3 +85,21 @@ export type OpenStreetMapPlace = { display_name: string; boundingbox: string[]; }; + +export type Transportation = { + id: number; + user_id: User; + type: string; + name: string; + description: string | null; + rating: number | null; + link: string | null; + date: string | null; // ISO 8601 date string + flight_number: string | null; + from_location: string | null; + to_location: string | null; + is_public: boolean; + collection: Collection | null; + created_at: string; // ISO 8601 date string + updated_at: string; // ISO 8601 date string +}; diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 75181ad..fdbbaf9 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -192,72 +192,74 @@ {/if} {#if collection} -
-
-
- + {/if} {#if collection.name}

{collection.name}

{/if} @@ -293,6 +295,16 @@ {/each} + {#if collection.transportations && collection.transportations.length > 0} +

Transportation

+ {#each collection.transportations as transportation} +

{transportation.id}

+

{transportation.name}

+

{transportation.type}

+

{transportation.date}

+ {/each} + {/if} + {#if collection.start_date && collection.end_date}

Itinerary by Date

{#if numberOfDays} From 581e5548d56add38f1b28550e4e004fac85c3458 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 21:18:15 -0400 Subject: [PATCH 12/17] refactor: Update validation error message in Transportation model clean method --- backend/server/adventures/models.py | 2 +- backend/server/adventures/views.py | 28 --- .../lib/components/EditTransportation.svelte | 184 ++++++++++++++++++ .../lib/components/TransportationCard.svelte | 76 +++----- frontend/src/routes/api/[...path]/+server.ts | 10 +- .../src/routes/collections/[id]/+page.svelte | 33 +++- 6 files changed, 249 insertions(+), 84 deletions(-) create mode 100644 frontend/src/lib/components/EditTransportation.svelte diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index e7027cd..4b42725 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -105,7 +105,7 @@ class Transportation(models.Model): def clean(self): if self.collection: if self.collection.is_public and not self.is_public: - raise ValidationError('Transportations associated with a public collection must be public. Collection: ' + self.trip.name + ' Transportation: ' + self.name) + raise ValidationError('Transportations associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name) if self.user_id != self.collection.user_id: raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username) diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index c15bfe0..e658623 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -270,26 +270,6 @@ def get_queryset(self): def perform_create(self, serializer): serializer.save(user_id=self.request.user) - - # @action(detail=False, methods=['get']) - # def filtered(self, request): - # types = request.query_params.get('types', '').split(',') - # valid_types = ['visited', 'planned'] - # types = [t for t in types if t in valid_types] - - # if not types: - # return Response({"error": "No valid types provided"}, status=400) - - # queryset = Collection.objects.none() - - # for adventure_type in types: - # if adventure_type in ['visited', 'planned']: - # queryset |= Collection.objects.filter( - # type=adventure_type, user_id=request.user.id) - - # queryset = self.apply_sorting(queryset) - # collections = self.paginate_and_respond(queryset, request) - # return collections def paginate_and_respond(self, queryset, request): paginator = self.pagination_class() @@ -300,14 +280,6 @@ def paginate_and_respond(self, queryset, request): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - # make a view to return all of the associated transportations with a collection - @action(detail=True, methods=['get']) - def transportations(self, request, pk=None): - collection = self.get_object() - transportations = Transportation.objects.filter(collection=collection) - serializer = TransportationSerializer(transportations, many=True) - return Response(serializer.data) - class StatsViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] diff --git a/frontend/src/lib/components/EditTransportation.svelte b/frontend/src/lib/components/EditTransportation.svelte new file mode 100644 index 0000000..8dd892c --- /dev/null +++ b/frontend/src/lib/components/EditTransportation.svelte @@ -0,0 +1,184 @@ + + + + + + + diff --git a/frontend/src/lib/components/TransportationCard.svelte b/frontend/src/lib/components/TransportationCard.svelte index 1b6df55..8427c8b 100644 --- a/frontend/src/lib/components/TransportationCard.svelte +++ b/frontend/src/lib/components/TransportationCard.svelte @@ -7,36 +7,32 @@ import FileDocumentEdit from '~icons/mdi/file-document-edit'; import { goto } from '$app/navigation'; - import type { Collection } from '$lib/types'; + import type { Collection, Transportation } 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 transportation: Transportation; - // export let type: String; - - function editAdventure() { - dispatch('edit', collection); + function editTransportation() { + dispatch('edit', transportation); } - export let collection: Collection; - - async function deleteCollection() { - let res = await fetch(`/collections/${collection.id}?/delete`, { - method: 'POST', + async function deleteTransportation() { + let res = await fetch(`/api/transportations/${transportation.id}`, { + method: 'DELETE', headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/json' } }); - if (res.ok) { - console.log('Collection deleted'); - addToast('info', 'Adventure deleted successfully!'); - dispatch('delete', collection.id); + if (!res.ok) { + console.log('Error deleting transportation'); } else { - console.log('Error deleting adventure'); + console.log('Collection deleted'); + addToast('info', 'Transportation deleted successfully!'); + dispatch('delete', transportation.id); } } @@ -45,39 +41,23 @@ class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-primary-content shadow-xl overflow-hidden text-base-content" >
-

{collection.name}

-

{collection.adventures.length} Adventures

- {#if collection.start_date && collection.end_date} -

- Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date( - collection.end_date - ).toLocaleDateString('en-US', { timeZone: 'UTC' })} +

{transportation.name}

+
{transportation.type}
+ {#if transportation.from_location && transportation.to_location} +

+ {transportation.from_location} to {transportation.to_location}

- -

- Duration: {Math.floor( - (new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) / - (1000 * 60 * 60 * 24) - ) + 1}{' '} - days -

{/if} + {/if} + {#if transportation.date} + {new Date(transportation.date).toLocaleString()} + {/if}
- {#if type != 'link'} - - - - {/if} - {#if type == 'link'} - - {/if} + +
diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts index 409abd2..0d237c2 100644 --- a/frontend/src/routes/api/[...path]/+server.ts +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -27,7 +27,7 @@ export async function PUT({ url, params, request, fetch, cookies }) { } export async function DELETE({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies); + return handleRequest(url, params, request, fetch, cookies, true); } // Implement other HTTP methods as needed (PUT, DELETE, etc.) @@ -62,6 +62,14 @@ async function handleRequest( 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, { diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index fdbbaf9..a005b33 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -1,5 +1,5 @@ @@ -83,17 +78,37 @@ class="modal-action items-center" style="display: flex; flex-direction: column; align-items: center; width: 100%;" > -
+
+
+
+ +
+
- -
-
Rating
+
- -
-
- -
- - {#if transportationToEdit.is_public} -
-

Share this Adventure!

-
-

- {window.location.origin}/collections/{transportationToEdit.id} -

- +
+
+ +
+
+
+ +
+ {#if transportationToEdit.type == 'plane'} +
+
+
+ {/if} +
+
+ +
+
+
+
- {/if} +
- + diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts index 0d237c2..981debb 100644 --- a/frontend/src/routes/api/[...path]/+server.ts +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -15,7 +15,7 @@ export async function GET({ 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); + return handleRequest(url, params, request, fetch, cookies, true); } export async function PATCH({ url, params, request, fetch, cookies }) { @@ -23,7 +23,7 @@ export async function PATCH({ url, params, request, fetch, cookies }) { } export async function PUT({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies); + return handleRequest(url, params, request, fetch, cookies, true); } export async function DELETE({ url, params, request, fetch, cookies }) { diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index a005b33..eeaa9ac 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -134,6 +134,16 @@ isEditModalOpen = true; } + function saveNewTransportation(event: CustomEvent) { + transportations = transportations.map((transportation) => { + if (transportation.id === event.detail.id) { + return event.detail; + } + return transportation; + }); + isTransportationEditModalOpen = false; + } + function saveEdit(event: CustomEvent) { adventures = adventures.map((adventure) => { if (adventure.id === event.detail.id) { @@ -159,6 +169,7 @@ (isTransportationEditModalOpen = false)} + on:saveEdit={saveNewTransportation} /> {/if} From 61c3d23efab4c91fe43e2e6d1c230eeef8887532 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 27 Jul 2024 22:23:23 -0400 Subject: [PATCH 14/17] refactor: Update transportation form with hidden input for is_public field --- frontend/src/lib/components/EditTransportation.svelte | 9 +++++++++ frontend/src/routes/collections/[id]/+page.svelte | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/EditTransportation.svelte b/frontend/src/lib/components/EditTransportation.svelte index 02208b6..a9cfe3a 100644 --- a/frontend/src/lib/components/EditTransportation.svelte +++ b/frontend/src/lib/components/EditTransportation.svelte @@ -89,6 +89,15 @@ bind:value={transportationToEdit.id} class="input input-bordered w-full max-w-xs mt-1" /> +

-
+
-
-
- -
+ {#if transportationToEdit.type == 'plane'}
Flight Number
{/if}
-
+
-
+
+ // let newTransportation: Transportation = { + // id:NaN, + // user_id: NaN, + // type: '', + // name: '', + // description: null, + // rating: NaN, + // link: null, + // date: null, + // flight_number: null, + // from_location: null, + // to_location: null, + // is_public: false, + // collection: null, + // created_at: '', + // updated_at: '' + // }; + import { createEventDispatcher } from 'svelte'; + import type { Collection, Transportation } from '$lib/types'; + const dispatch = createEventDispatcher(); + import { onMount } from 'svelte'; + import { addToast } from '$lib/toasts'; + let modal: HTMLDialogElement; + + export let collection: Collection; + + import MapMarker from '~icons/mdi/map-marker'; + import Calendar from '~icons/mdi/calendar'; + import Notebook from '~icons/mdi/notebook'; + import Star from '~icons/mdi/star'; + import PlaneCar from '~icons/mdi/plane-car'; + import LinkVariant from '~icons/mdi/link-variant'; + import Airplane from '~icons/mdi/airplane'; + + let type: string = ''; + + onMount(async () => { + modal = document.getElementById('my_modal_1') as HTMLDialogElement; + if (modal) { + modal.showModal(); + } + }); + + // if (newTransportation.date) { + // newTransportation.date = newTransportation.date.slice(0, 19); + // } + + function close() { + dispatch('close'); + } + + function handleKeydown(event: KeyboardEvent) { + if (event.key === 'Escape') { + close(); + } + } + + async function handleSubmit(event: Event) { + event.preventDefault(); + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + + const response = await fetch(`/api/transportations/`, { + method: 'POST', + body: formData + }); + + if (response.ok) { + const result = await response.json(); + + addToast('success', 'Transportation added successfully!'); + dispatch('add', result); + close(); + } else { + addToast('error', 'Error editing transportation'); + } + } + + + + + + + diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a53fc15..ce66789 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -88,7 +88,7 @@ export type OpenStreetMapPlace = { export type Transportation = { id: number; - user_id: User; + user_id: number; type: string; name: string; description: string | null; diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 64555c9..a83bfd6 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -14,6 +14,7 @@ import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre'; import TransportationCard from '$lib/components/TransportationCard.svelte'; import EditTransportation from '$lib/components/EditTransportation.svelte'; + import NewTransportation from '$lib/components/NewTransportation.svelte'; export let data: PageData; @@ -32,6 +33,7 @@ let notFound: boolean = false; let isShowingLinkModal: boolean = false; let isShowingCreateModal: boolean = false; + let isShowingTransportationModal: boolean = false; onMount(() => { if (data.props.adventure) { @@ -190,6 +192,17 @@ /> {/if} +{#if isShowingTransportationModal} + (isShowingTransportationModal = false)} + on:add={(event) => { + transportations = [event.detail, ...transportations]; + isShowingTransportationModal = false; + }} + {collection} + /> +{/if} + {#if notFound}
Dining + - - - {#each adventures as adventure} - {#if adventure.longitude && adventure.latitude} - - -
{adventure.name}
-

- {adventure.type.charAt(0).toUpperCase() + adventure.type.slice(1)} -

-

- {adventure.date - ? new Date(adventure.date).toLocaleDateString('en-US', { timeZone: 'UTC' }) - : ''} -

-
-
- {/if} - {/each} - -
+ + + {#each adventures as adventure} + {#if adventure.longitude && adventure.latitude} + + +
{adventure.name}
+

+ {adventure.type.charAt(0).toUpperCase() + adventure.type.slice(1)} +

+

+ {adventure.date + ? new Date(adventure.date).toLocaleDateString('en-US', { timeZone: 'UTC' }) + : ''} +

+
+
+ {/if} + {/each} + {/if} {/if}