From 9ca0a2044a1eca325fa976b635108252ab3be8fe Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 7 May 2026 22:54:52 -0400 Subject: [PATCH] feat(itinerary): add validation for global and dated itinerary items --- backend/server/adventures/serializers.py | 28 ++++++++++++ backend/server/adventures/tests.py | 58 +++++++++++++++++++++++- frontend/src/lib/config.ts | 2 +- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 5b0115e8..2ccf7b6e 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -1060,6 +1060,7 @@ class CollectionItineraryDaySerializer(CustomModelSerializer): return super().update(instance, validated_data) class CollectionItineraryItemSerializer(CustomModelSerializer): + date = serializers.DateField(required=False, allow_null=True) item = serializers.SerializerMethodField() start_datetime = serializers.ReadOnlyField() end_datetime = serializers.ReadOnlyField() @@ -1069,6 +1070,33 @@ class CollectionItineraryItemSerializer(CustomModelSerializer): model = CollectionItineraryItem fields = ['id', 'collection', 'content_type', 'object_id', 'item', 'date', 'is_global', 'order', 'start_datetime', 'end_datetime', 'created_at', 'object_name'] read_only_fields = ['id', 'created_at', 'start_datetime', 'end_datetime', 'item', 'object_name'] + + def validate(self, attrs): + data = super().validate(attrs) + + is_global = data.get('is_global') + if is_global is None and self.instance is not None: + is_global = self.instance.is_global + + if 'date' in data: + date = data.get('date') + elif self.instance is not None: + date = self.instance.date + else: + date = None + + if is_global and date is not None: + raise serializers.ValidationError({ + 'date': 'Global items must not have a date.', + 'is_global': 'Provide either a date or set is_global, not both.', + }) + + if not is_global and date is None and self.instance is None: + raise serializers.ValidationError({ + 'date': 'Dated items must include a date. To create a trip-wide item, set is_global=true.', + }) + + return data def update(self, instance, validated_data): # Security: Prevent changing collection, content_type, or object_id after creation diff --git a/backend/server/adventures/tests.py b/backend/server/adventures/tests.py index 7ce503c2..3041c9d2 100644 --- a/backend/server/adventures/tests.py +++ b/backend/server/adventures/tests.py @@ -1,3 +1,57 @@ -from django.test import TestCase +from rest_framework.test import APITestCase -# Create your tests here. +from adventures.models import Collection, CollectionItineraryItem, Location +from users.models import CustomUser + + +class ItineraryAPITestCase(APITestCase): + def setUp(self): + self.user = CustomUser.objects.create_user( + username='itinerary-user', + email='itinerary-user@example.com', + password='testpassword123', + ) + self.collection = Collection.objects.create(user=self.user, name='Test Trip') + self.location = Location.objects.create(user=self.user, name='Test Location', is_public=True) + self.client.force_authenticate(user=self.user) + + def test_create_global_itinerary_item_without_date(self): + response = self.client.post( + '/api/itineraries/', + { + 'collection': str(self.collection.id), + 'content_type': 'location', + 'object_id': str(self.location.id), + 'is_global': True, + 'order': 0, + }, + format='json', + ) + + self.assertEqual(response.status_code, 201) + self.assertEqual(CollectionItineraryItem.objects.count(), 1) + + item = CollectionItineraryItem.objects.get() + self.assertTrue(item.is_global) + self.assertIsNone(item.date) + self.assertEqual(item.collection, self.collection) + + payload = response.json() + self.assertTrue(payload['is_global']) + self.assertIsNone(payload['date']) + + def test_create_dated_itinerary_item_without_date_is_rejected(self): + response = self.client.post( + '/api/itineraries/', + { + 'collection': str(self.collection.id), + 'content_type': 'location', + 'object_id': str(self.location.id), + 'is_global': False, + 'order': 0, + }, + format='json', + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()['date'][0], 'Dated items must include a date. To create a trip-wide item, set is_global=true.') diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index fb1d8ac7..d7281d47 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,4 @@ -export let appVersion = 'v0.12.0-dev-042426'; +export let appVersion = 'v0.12.0-dev-050726'; export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.12.0'; export let appTitle = 'AdventureLog'; export let copyrightYear = '2023-2026';