From f2888f26fe6935539678f8e2443f956ff0266f6d Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Mon, 5 Aug 2024 16:09:57 -0400 Subject: [PATCH] checklists api beta --- backend/server/adventures/admin.py | 4 +- .../0020_checklist_checklistitem.py | 41 ++++++++++ backend/server/adventures/models.py | 40 ++++++++++ backend/server/adventures/serializers.py | 74 ++++++++++++++++++- backend/server/adventures/views.py | 32 ++++---- 5 files changed, 172 insertions(+), 19 deletions(-) create mode 100644 backend/server/adventures/migrations/0020_checklist_checklistitem.py 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..7a90c52 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,79 @@ 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'] + + 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 Checklist(serializers.ModelSerializer): + items = ChecklistItemSerializer(many=True, read_only=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 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 + + 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') + checklists = Checklist(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/views.py b/backend/server/adventures/views.py index 58b45a0..a8ef9fa 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, Note +from .models import Adventure, Checklist, Collection, Transportation, Note from worldtravel.models import VisitedRegion, Region, Country from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer from rest_framework.permissions import IsAuthenticated @@ -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):