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'}
-
+ {#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
+
{
+ isShowingCreateModal = true;
+ newType = 'dining';
+ }}
+ >
+ 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}
+
+
{/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'}
+
+
+
+
+
goto(`/collections/${collection.id}`)}
+ >
+ {/if}
+ {#if type == 'link'}
+
dispatch('link', collection.id)}>
+
+
+ {/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}
-
-
-
-
-
-
- Link new...
- {
- isShowingLinkModal = true;
- }}
- >
- Adventure
- Add new...
- {
- isShowingCreateModal = true;
- newType = 'visited';
- }}
- >
- Visited Adventure
- {
- isShowingCreateModal = true;
- newType = 'planned';
- }}
- >
- Planned Adventure
- {
- isShowingCreateModal = true;
- newType = 'lodging';
- }}
- >
- Lodging
- {
- isShowingCreateModal = true;
- newType = 'dining';
- }}
+ {#if data.user}
+
-
+ {/if}
{#if 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'}
-
-
-
-
-
goto(`/collections/${collection.id}`)}
- >
- {/if}
- {#if type == 'link'}
-
dispatch('link', collection.id)}>
-
-
- {/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%;"
>
-