diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index faa6bd8..0029499 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, Note +from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note from worldtravel.models import Country, Region, VisitedRegion @@ -76,6 +76,8 @@ admin.site.register(Region, RegionAdmin) admin.site.register(VisitedRegion) admin.site.register(Transportation) admin.site.register(Note) +admin.site.register(Checklist) +admin.site.register(ChecklistItem) admin.site.site_header = 'AdventureLog Admin' admin.site.site_title = 'AdventureLog Admin Site' diff --git a/backend/server/adventures/migrations/0020_checklist_checklistitem.py b/backend/server/adventures/migrations/0020_checklist_checklistitem.py new file mode 100644 index 0000000..8d82ddc --- /dev/null +++ b/backend/server/adventures/migrations/0020_checklist_checklistitem.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.7 on 2024-08-05 19:52 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0019_collection_updated_at'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Checklist', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('date', models.DateField(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)), + ], + ), + migrations.CreateModel( + name='ChecklistItem', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('is_checked', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('checklist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='adventures.checklist')), + ('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 d138307..7c3c0e5 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -134,3 +134,43 @@ class Note(models.Model): def __str__(self): return self.name + +class Checklist(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) + 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('Checklists associated with a public collection must be public. Collection: ' + self.collection.name + ' Checklist: ' + self.name) + if self.user_id != self.collection.user_id: + raise ValidationError('Checklists must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Checklist owner: ' + self.user_id.username) + + def __str__(self): + return self.name + +class ChecklistItem(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) + is_checked = models.BooleanField(default=False) + checklist = models.ForeignKey('Checklist', on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def clean(self): + if self.checklist.is_public and not self.checklist.is_public: + raise ValidationError('Checklist items associated with a public checklist must be public. Checklist: ' + self.checklist.name + ' Checklist item: ' + self.name) + if self.user_id != self.checklist.user_id: + raise ValidationError('Checklist items must be associated with checklists owned by the same user. Checklist owner: ' + self.checklist.user_id.username + ' Checklist item 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 2c0ab10..517a55c 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -1,5 +1,5 @@ import os -from .models import Adventure, Collection, Note, Transportation +from .models import Adventure, ChecklistItem, Collection, Note, Transportation, Checklist from rest_framework import serializers class AdventureSerializer(serializers.ModelSerializer): @@ -32,7 +32,7 @@ class TransportationSerializer(serializers.ModelSerializer): 'link', 'date', 'flight_number', 'from_location', 'to_location', 'is_public', 'collection', 'created_at', 'updated_at' ] - read_only_fields = ['id', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] def validate(self, data): # Check if the collection is public and the transportation is not @@ -65,7 +65,7 @@ class NoteSerializer(serializers.ModelSerializer): 'id', 'user_id', 'name', 'content', 'date', 'links', 'is_public', 'collection', 'created_at', 'updated_at' ] - read_only_fields = ['id', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] def validate(self, data): # Check if the collection is public and the transportation is not @@ -90,13 +90,119 @@ class NoteSerializer(serializers.ModelSerializer): validated_data['user_id'] = self.context['request'].user return super().create(validated_data) +class ChecklistItemSerializer(serializers.ModelSerializer): + class Meta: + model = ChecklistItem + fields = [ + 'id', 'user_id', 'name', 'is_checked', 'checklist', 'created_at', 'updated_at' + ] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'checklist'] + + def validate(self, data): + # Check if the checklist is public and the checklist item is not + checklist = data.get('checklist') + is_checked = data.get('is_checked', False) + if checklist and checklist.is_public and not is_checked: + raise serializers.ValidationError( + 'Checklist items associated with a public checklist must be checked.' + ) + + # Check if the user owns the checklist + request = self.context.get('request') + if request and checklist and checklist.user_id != request.user: + raise serializers.ValidationError( + 'Checklist items must be associated with checklists 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 ChecklistSerializer(serializers.ModelSerializer): + items = ChecklistItemSerializer(many=True, source='checklistitem_set') + class Meta: + model = Checklist + fields = [ + 'id', 'user_id', 'name', 'date', 'is_public', 'collection', 'created_at', 'updated_at', 'items' + ] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + + def create(self, validated_data): + items_data = validated_data.pop('checklistitem_set') + checklist = Checklist.objects.create(**validated_data) + for item_data in items_data: + ChecklistItem.objects.create(checklist=checklist, **item_data) + return checklist + + def update(self, instance, validated_data): + items_data = validated_data.pop('checklistitem_set', []) + + # Update Checklist fields + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + + # Get current items + current_items = instance.checklistitem_set.all() + current_item_ids = set(current_items.values_list('id', flat=True)) + + # Update or create items + updated_item_ids = set() + for item_data in items_data: + item_id = item_data.get('id') + if item_id: + if item_id in current_item_ids: + item = current_items.get(id=item_id) + for attr, value in item_data.items(): + setattr(item, attr, value) + item.save() + updated_item_ids.add(item_id) + else: + # If ID is provided but doesn't exist, create new item + ChecklistItem.objects.create(checklist=instance, **item_data) + else: + # If no ID is provided, create new item + ChecklistItem.objects.create(checklist=instance, **item_data) + + # Delete items that are not in the updated data + items_to_delete = current_item_ids - updated_item_ids + instance.checklistitem_set.filter(id__in=items_to_delete).delete() + + return instance + + def validate(self, data): + # Check if the collection is public and the checklist 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( + 'Checklists associated with a public collection must be public.' + ) + + # Check if the user owns the checklist + request = self.context.get('request') + if request and collection and collection.user_id != request.user: + raise serializers.ValidationError( + 'Checklists must be associated with collections owned by the same user.' + ) + + return 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') + checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_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', 'notes', 'updated_at'] + fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists'] read_only_fields = ['id', 'created_at', 'updated_at'] diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index a914fc6..8bdd6b7 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, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet +from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet router = DefaultRouter() router.register(r'adventures', AdventureViewSet, basename='adventures') @@ -10,6 +10,7 @@ router.register(r'generate', GenerateDescription, basename='generate') router.register(r'activity-types', ActivityTypesView, basename='activity-types') router.register(r'transportations', TransportationViewSet, basename='transportations') router.register(r'notes', NoteViewSet, basename='notes') +router.register(r'checklists', ChecklistViewSet, basename='checklists') urlpatterns = [ diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 58b45a0..93a5c43 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -4,9 +4,9 @@ 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, Note +from .models import Adventure, Checklist, Collection, Transportation, Note from worldtravel.models import VisitedRegion, Region, Country -from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer +from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer from rest_framework.permissions import IsAuthenticated from django.db.models import Q, Prefetch from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly @@ -310,19 +310,23 @@ class CollectionViewSet(viewsets.ModelViewSet): # For other actions, only include user's own collections adventures = Collection.objects.filter(user_id=self.request.user.id) - adventures = adventures.prefetch_related( - 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) - )) - ).prefetch_related( - Prefetch('note_set', queryset=Note.objects.filter( - Q(is_public=True) | Q(user_id=self.request.user.id) - )) - ) + # adventures = adventures.prefetch_related( + # 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) + # )) + # ).prefetch_related( + # Prefetch('note_set', queryset=Note.objects.filter( + # Q(is_public=True) | Q(user_id=self.request.user.id) + # )) + # ).prefetch_related( + # Prefetch('checklist_set', queryset=Checklist.objects.filter( + # Q(is_public=True) | Q(user_id=self.request.user.id) + # )) + # ) return self.apply_sorting(adventures) def perform_create(self, serializer): @@ -491,5 +495,40 @@ class NoteViewSet(viewsets.ModelViewSet): user = self.request.user return Note.objects.filter(user_id=user) + def perform_create(self, serializer): + serializer.save(user_id=self.request.user) + +class ChecklistViewSet(viewsets.ModelViewSet): + queryset = Checklist.objects.all() + serializer_class = ChecklistSerializer + permission_classes = [IsAuthenticated] + filterset_fields = ['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 checklists 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 = Checklist.objects.filter( + Q(user_id=request.user.id) + ) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + + def get_queryset(self): + + """ + This view should return a list of all checklists + for the currently authenticated user. + """ + user = self.request.user + return Checklist.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/ChecklistCard.svelte b/frontend/src/lib/components/ChecklistCard.svelte new file mode 100644 index 0000000..6df1d8b --- /dev/null +++ b/frontend/src/lib/components/ChecklistCard.svelte @@ -0,0 +1,67 @@ + + +
{checklist.items.length} {checklist.items.length > 1 ? 'Items' : 'Item'}
+ {/if} + {#if checklist.date && checklist.date !== ''} +{new Date(checklist.date).toLocaleDateString('en-US', { timeZone: 'UTC' })}
+{note.links.length} links
+{note.links.length} {note.links.length > 1 ? 'Links' : 'Link'}
+ {/if} + {#if note.date && note.date !== ''} +{new Date(note.date).toLocaleDateString('en-US', { timeZone: 'UTC' })}
+