diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 1ee8fd24..87a26a52 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -21,18 +21,28 @@ def to_representation(self, instance): representation['image'] = f"{public_url}/media/{instance.image.name}" return representation -class CategorySerializer(CustomModelSerializer): +class CategorySerializer(serializers.ModelSerializer): num_adventures = serializers.SerializerMethodField() class Meta: model = Category - fields = ['id', 'name', 'display_name', 'icon', 'user_id', 'num_adventures'] - read_only_fields = ['id', 'user_id', 'num_adventures'] + fields = ['id', 'name', 'display_name', 'icon', 'num_adventures'] + read_only_fields = ['id', 'num_adventures'] def validate_name(self, value): - if Category.objects.filter(name=value).exists(): - raise serializers.ValidationError('Category with this name already exists.') - - return value + return value.lower() + + def create(self, validated_data): + user = self.context['request'].user + validated_data['name'] = validated_data['name'].lower() + return Category.objects.create(user_id=user, **validated_data) + + def update(self, instance, validated_data): + for attr, value in validated_data.items(): + setattr(instance, attr, value) + if 'name' in validated_data: + instance.name = validated_data['name'].lower() + instance.save() + return instance def get_num_adventures(self, obj): return Adventure.objects.filter(category=obj, user_id=obj.user_id).count() @@ -46,13 +56,8 @@ class Meta: class AdventureSerializer(CustomModelSerializer): images = AdventureImageSerializer(many=True, read_only=True) - visits = VisitSerializer(many=True, read_only=False) - category = serializers.PrimaryKeyRelatedField( - queryset=Category.objects.all(), - write_only=True, - required=False - ) - category_object = CategorySerializer(source='category', read_only=True) + visits = VisitSerializer(many=True, read_only=False, required=False) + category = CategorySerializer(read_only=False, required=False) is_visited = serializers.SerializerMethodField() class Meta: @@ -60,19 +65,45 @@ class Meta: fields = [ 'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'longitude', - 'latitude', 'visits', 'is_visited', 'category', 'category_object' + 'latitude', 'visits', 'is_visited', 'category' ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited'] - def to_representation(self, instance): - representation = super().to_representation(instance) - representation['category'] = representation.pop('category_object') - return representation + def validate_category(self, category_data): + if isinstance(category_data, Category): + return category_data + if category_data: + user = self.context['request'].user + name = category_data.get('name', '').lower() + existing_category = Category.objects.filter(user_id=user, name=name).first() + if existing_category: + return existing_category + category_data['name'] = name + return category_data - def validate_category(self, category): - # Check that the category belongs to the same user - if category.user_id != self.context['request'].user: - raise serializers.ValidationError('Category does not belong to the user.') + def get_or_create_category(self, category_data): + user = self.context['request'].user + + if isinstance(category_data, Category): + return category_data + + if isinstance(category_data, dict): + name = category_data.get('name', '').lower() + display_name = category_data.get('display_name', name) + icon = category_data.get('icon', '🌎') + else: + name = category_data.name.lower() + display_name = category_data.display_name + icon = category_data.icon + + category, created = Category.objects.get_or_create( + user_id=user, + name=name, + defaults={ + 'display_name': display_name, + 'icon': icon + } + ) return category def get_is_visited(self, obj): @@ -86,16 +117,28 @@ def get_is_visited(self, obj): def create(self, validated_data): visits_data = validated_data.pop('visits', []) + category_data = validated_data.pop('category', None) adventure = Adventure.objects.create(**validated_data) for visit_data in visits_data: Visit.objects.create(adventure=adventure, **visit_data) + + if category_data: + category = self.get_or_create_category(category_data) + adventure.category = category + adventure.save() + return adventure def update(self, instance, validated_data): visits_data = validated_data.pop('visits', []) + category_data = validated_data.pop('category', None) for attr, value in validated_data.items(): setattr(instance, attr, value) + + if category_data: + category = self.get_or_create_category(category_data) + instance.category = category instance.save() current_visits = instance.visits.all() diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 2405f15c..fb85e37e 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -614,23 +614,42 @@ def types(self, request): return Response(allTypes) -class CategoryViewSet(viewsets.ViewSet): +class CategoryViewSet(viewsets.ModelViewSet): + queryset = Category.objects.all() + serializer_class = CategorySerializer permission_classes = [IsAuthenticated] + def get_queryset(self): + return Category.objects.filter(user_id=self.request.user) + @action(detail=False, methods=['get']) def categories(self, request): """ Retrieve a list of distinct categories for adventures associated with the current user. - - Args: - request (HttpRequest): The HTTP request object. - - Returns: - Response: A response containing a list of distinct categories. """ - categories = Category.objects.filter(user_id=request.user.id).distinct() - serializer = CategorySerializer(categories, many=True) + categories = self.get_queryset().distinct() + serializer = self.get_serializer(categories, many=True) return Response(serializer.data) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if instance.user_id != request.user: + return Response({"error": "User does not own this category"}, status + =400) + + if instance.name == 'general': + return Response({"error": "Cannot delete the general category"}, status=400) + + # set any adventures with this category to a default category called general before deleting the category, if general does not exist create it for the user + general_category = Category.objects.filter(user_id=request.user, name='general').first() + + if not general_category: + general_category = Category.objects.create(user_id=request.user, name='general', icon='🌎', display_name='General') + + Adventure.objects.filter(category=instance).update(category=general_category) + + return super().destroy(request, *args, **kwargs) + class TransportationViewSet(viewsets.ModelViewSet): queryset = Transportation.objects.all() @@ -1129,12 +1148,13 @@ def extractIsoCode(self, data): print(iso_code) country_code = iso_code[:2] - if city: - display_name = f"{city}, {region.name}, {country_code}" - elif town and region.name: - display_name = f"{town}, {region.name}, {country_code}" - elif county and region.name: - display_name = f"{county}, {region.name}, {country_code}" + if region: + if city: + display_name = f"{city}, {region.name}, {country_code}" + elif town: + display_name = f"{town}, {region.name}, {country_code}" + elif county: + display_name = f"{county}, {region.name}, {country_code}" if visited_region: is_visited = True diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index eb340b1c..41485826 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -31,6 +31,7 @@ import ActivityComplete from './ActivityComplete.svelte'; import { appVersion } from '$lib/config'; import CategoryDropdown from './CategoryDropdown.svelte'; + import { findFirstValue } from '$lib'; let wikiError: string = ''; @@ -56,7 +57,13 @@ images: [], user_id: null, collection: collection?.id || null, - category: '' + category: { + id: '', + name: '', + display_name: '', + icon: '', + user_id: '' + } }; export let adventureToEdit: Adventure | null = null; @@ -78,7 +85,13 @@ collection: adventureToEdit?.collection || collection?.id || null, visits: adventureToEdit?.visits || [], is_visited: adventureToEdit?.is_visited || false, - category: adventureToEdit?.category || '' + category: adventureToEdit?.category || { + id: '', + name: '', + display_name: '', + icon: '', + user_id: '' + } }; let markers: Point[] = []; @@ -405,7 +418,8 @@ warningMessage = ''; addToast('success', $t('adventures.adventure_created')); } else { - warningMessage = Object.values(data)[0] as string; + warningMessage = findFirstValue(data) as string; + console.error(data); addToast('error', $t('adventures.adventure_create_error')); } } else { @@ -450,7 +464,8 @@
{$t('navbar.theme_selection')}