Skip to content

Commit

Permalink
Refactor adventure category handling: update type definitions, enhanc…
Browse files Browse the repository at this point in the history
…e category management in UI components, and implement user-specific category deletion logic in the backend
  • Loading branch information
seanmorley15 committed Nov 23, 2024
1 parent 736ede2 commit 8e5a20e
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 93 deletions.
89 changes: 66 additions & 23 deletions backend/server/adventures/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -46,33 +56,54 @@ 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:
model = Adventure
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):
Expand All @@ -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()
Expand Down
50 changes: 35 additions & 15 deletions backend/server/adventures/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
29 changes: 23 additions & 6 deletions frontend/src/lib/components/AdventureModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand All @@ -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;
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -450,7 +464,8 @@
</div>
<div class="collapse-content">
<div>
<label for="name">{$t('adventures.name')}</label><br />
<label for="name">{$t('adventures.name')}<span class="text-red-500">*</span></label
><br />
<input
type="text"
id="name"
Expand All @@ -461,9 +476,11 @@
/>
</div>
<div>
<label for="link">{$t('adventures.category')}</label><br />
<label for="link"
>{$t('adventures.category')}<span class="text-red-500">*</span></label
><br />

<CategoryDropdown bind:categories bind:category_id={adventure.category} />
<CategoryDropdown bind:categories bind:selected_category={adventure.category} />
</div>
<div>
<label for="rating">{$t('adventures.rating')}</label><br />
Expand Down
Loading

0 comments on commit 8e5a20e

Please sign in to comment.