From 2bb1d80a77f0c5d435f89bc451bb8c53af1bbd07 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 3 Aug 2024 18:56:51 -0400 Subject: [PATCH 01/10] refactor: Handle authentication cookies in authHook --- frontend/src/hooks.server.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index c4e6709..8c882ab 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -6,8 +6,14 @@ import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; export const authHook: Handle = async ({ event, resolve }) => { try { let authCookie = event.cookies.get('auth'); + let refreshCookie = event.cookies.get('refresh'); - if (!authCookie) { + if (!authCookie && !refreshCookie) { + event.locals.user = null; + return await resolve(event); + } + + if (!authCookie && refreshCookie) { event.locals.user = null; const token = await tryRefreshToken(event.cookies.get('refresh') || ''); if (token) { From b8994a531fc93361a5a1cb20b8bf9db2895bdbb4 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 3 Aug 2024 20:23:42 -0400 Subject: [PATCH 02/10] feat: Improve UI layout and functionality for adventure search --- .../src/routes/collections/[id]/+page.svelte | 34 +++++++++++-------- .../forgot-password/confirm/+page.server.ts | 31 +++++++++++++++++ .../forgot-password/confirm/+page.svelte | 5 +++ 3 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 frontend/src/routes/settings/forgot-password/confirm/+page.server.ts create mode 100644 frontend/src/routes/settings/forgot-password/confirm/+page.svelte diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 8de2cb2..91a0939 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -359,24 +359,28 @@ {/if} -

Linked Adventures

- {#if adventures.length == 0} + + {#if adventures.length == 0 && transportations.length == 0} {/if} -
- {#each adventures as adventure} - - {/each} -
+ {#if adventures.length > 0} +

Linked Adventures

- {#if collection.transportations && collection.transportations.length > 0} +
+ {#each adventures as adventure} + + {/each} +
+ {/if} + + {#if transportations.length > 0}

Transportation

{#each transportations as transportation} diff --git a/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts b/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts new file mode 100644 index 0000000..83c8466 --- /dev/null +++ b/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts @@ -0,0 +1,31 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +export const load = (async (event) => { + const token = event.url.searchParams.get('token'); + const uid = event.url.searchParams.get('uid'); + console.log('token', token); + + if (!token) { + return redirect(302, '/settings/forgot-password'); + } else { + let response = await fetch(`${serverEndpoint}/auth/password/reset/confirm/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + token: token, + uid: uid, + new_password1: 'password', + new_password2: 'password' + }) + }); + let data = await response.json(); + console.log('data', data); + } + + return {}; +}) satisfies PageServerLoad; diff --git a/frontend/src/routes/settings/forgot-password/confirm/+page.svelte b/frontend/src/routes/settings/forgot-password/confirm/+page.svelte new file mode 100644 index 0000000..0d9aa7f --- /dev/null +++ b/frontend/src/routes/settings/forgot-password/confirm/+page.svelte @@ -0,0 +1,5 @@ + From 4f1ad094709cc618d5c9bc227ad1d77e3d5d1934 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 3 Aug 2024 21:09:49 -0400 Subject: [PATCH 03/10] notes beta --- backend/server/adventures/admin.py | 3 +- .../0014_alter_transportation_type_note.py | 35 ++++++++++++++++ .../migrations/0015_alter_note_date.py | 18 +++++++++ backend/server/adventures/models.py | 24 ++++++++++- backend/server/adventures/serializers.py | 40 ++++++++++++++++--- backend/server/adventures/views.py | 9 ++++- frontend/src/lib/types.ts | 13 ++++++ .../src/routes/collections/[id]/+page.svelte | 40 ++++++++++++++++++- 8 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 backend/server/adventures/migrations/0014_alter_transportation_type_note.py create mode 100644 backend/server/adventures/migrations/0015_alter_note_date.py diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 5cc7b4b..faa6bd8 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, Transportation +from .models import Adventure, Collection, Transportation, Note from worldtravel.models import Country, Region, VisitedRegion @@ -75,6 +75,7 @@ admin.site.register(Country, CountryAdmin) admin.site.register(Region, RegionAdmin) admin.site.register(VisitedRegion) admin.site.register(Transportation) +admin.site.register(Note) admin.site.site_header = 'AdventureLog Admin' admin.site.site_title = 'AdventureLog Admin Site' diff --git a/backend/server/adventures/migrations/0014_alter_transportation_type_note.py b/backend/server/adventures/migrations/0014_alter_transportation_type_note.py new file mode 100644 index 0000000..08ecdc1 --- /dev/null +++ b/backend/server/adventures/migrations/0014_alter_transportation_type_note.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.7 on 2024-08-04 01:01 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0013_alter_adventure_type_transportation'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='transportation', + name='type', + field=models.CharField(choices=[('car', 'Car'), ('plane', 'Plane'), ('train', 'Train'), ('bus', 'Bus'), ('boat', 'Boat'), ('bike', 'Bike'), ('walking', 'Walking'), ('other', 'Other')], max_length=100), + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('content', models.TextField(blank=True, null=True)), + ('date', models.DateTimeField(blank=True, 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/migrations/0015_alter_note_date.py b/backend/server/adventures/migrations/0015_alter_note_date.py new file mode 100644 index 0000000..14aa7f9 --- /dev/null +++ b/backend/server/adventures/migrations/0015_alter_note_date.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-08-04 01:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0014_alter_transportation_type_note'), + ] + + operations = [ + migrations.AlterField( + model_name='note', + name='date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 4b42725..c38cb80 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -82,8 +82,6 @@ class Collection(models.Model): 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( @@ -111,3 +109,25 @@ class Transportation(models.Model): def __str__(self): return self.name + +class Note(models.Model): + id = models.AutoField(primary_key=True) + user_id = models.ForeignKey( + User, on_delete=models.CASCADE, default=default_user_id) + name = models.CharField(max_length=200) + content = models.TextField(blank=True, null=True) + date = models.DateField(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('Notes 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('Notes 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 3a36575..282ba15 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -1,5 +1,5 @@ import os -from .models import Adventure, Collection, Transportation +from .models import Adventure, Collection, Note, Transportation from rest_framework import serializers class AdventureSerializer(serializers.ModelSerializer): @@ -57,15 +57,45 @@ class TransportationSerializer(serializers.ModelSerializer): validated_data['user_id'] = self.context['request'].user return super().create(validated_data) +class NoteSerializer(serializers.ModelSerializer): + + class Meta: + model = Note + fields = [ + 'id', 'user_id', 'name', 'content', 'date', + 'is_public', 'collection', '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( + 'Notes 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( + 'Notes 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) class CollectionSerializer(serializers.ModelSerializer): adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set') transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set') + notes = NoteSerializer(many=True, read_only=True, source='note_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 + fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes'] diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index dbbc280..aa93c06 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -4,7 +4,7 @@ from rest_framework.decorators import action from rest_framework import viewsets from django.db.models.functions import Lower from rest_framework.response import Response -from .models import Adventure, Collection, Transportation +from .models import Adventure, Collection, Transportation, Note from worldtravel.models import VisitedRegion, Region, Country from .serializers import AdventureSerializer, CollectionSerializer, TransportationSerializer from rest_framework.permissions import IsAuthenticated @@ -279,6 +279,9 @@ class CollectionViewSet(viewsets.ModelViewSet): # do the same for transportations Transportation.objects.filter(collection=instance).update(is_public=new_public_status) + # do the same for notes + Note.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}") @@ -313,6 +316,10 @@ class CollectionViewSet(viewsets.ModelViewSet): Prefetch('transportation_set', queryset=Transportation.objects.filter( Q(is_public=True) | Q(user_id=self.request.user.id) )) + ).prefetch_related( + Prefetch('note_set', queryset=Note.objects.filter( + Q(is_public=True) | Q(user_id=self.request.user.id) + )) ) return self.apply_sorting(adventures) diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index ce66789..064442c 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -67,6 +67,7 @@ export type Collection = { start_date?: string; end_date?: string; transportations?: Transportation[]; + notes?: Note[]; }; export type OpenStreetMapPlace = { @@ -103,3 +104,15 @@ export type Transportation = { created_at: string; // ISO 8601 date string updated_at: string; // ISO 8601 date string }; + +export type Note = { + id: number; + user_id: number; + name: string; + content: string | null; + date: string | null; // ISO 8601 date string + 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 91a0939..8c8fb60 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -1,5 +1,5 @@ + +
+
+

{note.name}

+
+ + +
+
+
diff --git a/frontend/src/lib/components/NoteModal.svelte b/frontend/src/lib/components/NoteModal.svelte new file mode 100644 index 0000000..e165632 --- /dev/null +++ b/frontend/src/lib/components/NoteModal.svelte @@ -0,0 +1,75 @@ + + + + + + + diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 8c8fb60..4623ebe 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -15,6 +15,8 @@ import TransportationCard from '$lib/components/TransportationCard.svelte'; import EditTransportation from '$lib/components/EditTransportation.svelte'; import NewTransportation from '$lib/components/NewTransportation.svelte'; + import NoteCard from '$lib/components/NoteCard.svelte'; + import NoteModal from '$lib/components/NoteModal.svelte'; export let data: PageData; console.log(data); @@ -180,6 +182,8 @@ let transportationToEdit: Transportation; let isEditModalOpen: boolean = false; let isTransportationEditModalOpen: boolean = false; + let isNoteModalOpen: boolean = false; + let noteToEdit: Note; let newType: string; @@ -239,6 +243,15 @@ /> {/if} +{#if isNoteModalOpen} + (isNoteModalOpen = false)} + startDate={collection.start_date} + endDate={collection.end_date} + /> +{/if} + {#if isShowingCreateModal} 0} {#each notes as note} {#if note.date && new Date(note.date).toISOString().split('T')[0] === dateString} -
-

{note.name}

-

{note.date}

-

{note.content}

-
+ { + noteToEdit = event.detail; + isNoteModalOpen = true; + }} + /> {/if} {/each} {/if} @@ -502,7 +517,7 @@
diff --git a/frontend/src/lib/components/NoteModal.svelte b/frontend/src/lib/components/NoteModal.svelte index e165632..7d38701 100644 --- a/frontend/src/lib/components/NoteModal.svelte +++ b/frontend/src/lib/components/NoteModal.svelte @@ -1,15 +1,31 @@ @@ -36,8 +85,8 @@
@@ -58,18 +107,26 @@ name="date" min={startDate || ''} max={endDate || ''} - bind:value={note.date} + bind:value={newNote.date} class="input input-bordered w-full max-w-xs mt-1" />
-
- - + {#if collection.is_public} +

+ This note will be public because it is in a public collection. +

+ {/if} +
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 064442c..f3aafe8 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -110,9 +110,10 @@ export type Note = { user_id: number; name: string; content: string | null; + links: string[] | null; date: string | null; // ISO 8601 date string is_public: boolean; - collection: Collection | null; + collection: number | 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 4623ebe..dd47c05 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -249,6 +249,17 @@ on:close={() => (isNoteModalOpen = false)} startDate={collection.start_date} endDate={collection.end_date} + {collection} + on:save={(event) => { + notes = notes.map((note) => { + if (note.id === event.detail.id) { + return event.detail; + } + return note; + }); + isNoteModalOpen = false; + }} + on:close={() => (isNoteModalOpen = false)} /> {/if} @@ -371,6 +382,15 @@ > Transportation +