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 @@