diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 7ea8f8ffd..cea279a21 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -3,7 +3,7 @@ import re import datetime import itertools -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from collections import defaultdict from PIL import Image as PilImage @@ -224,18 +224,37 @@ class Meta: model = Event list_serializer_class = EventListSerializer # Warning: registration object contains sensitive data, don't include it! - exclude = ['image', 'registration', 'event_group', 'billig_id'] + exclude = ['registration', 'event_group', 'billig_id'] # Read only properties (computed property, foreign model). total_registrations = serializers.IntegerField(read_only=True) image_url = serializers.CharField(read_only=True) + image = serializers.SerializerMethodField(read_only=True) # Custom tickets/billig custom_tickets = EventCustomTicketSerializer(many=True, read_only=True) billig = BilligEventSerializer(read_only=True) # For post/put (change image by id). - image_id = serializers.IntegerField(write_only=True) + image_id = serializers.IntegerField(write_only=True, required=True) + + def get_image(self, obj: Event) -> dict: + img = obj.image + return {'id': img.id, 'url': img.image.url, 'title': img.title, 'tags': list(img.tags.values_list('id', flat=True))} + + def update(self, instance: Event, validated_data: dict[str, Any]) -> Event: + image_id = validated_data.pop('image_id', None) + if image_id is not None: + try: + instance.image = Image.objects.get(pk=image_id) + except Image.DoesNotExist as err: + raise serializers.ValidationError({'image_id': 'Invalid image id'}) from err + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + + instance.save() + return instance def validate(self, data: dict) -> dict: # Check if all required fields are present for validation @@ -260,7 +279,8 @@ def create(self, validated_data: dict) -> Event: and sets it in the new event. Read/write only fields enable us to use the same serializer for both reading and writing. """ - validated_data['image'] = Image.objects.get(pk=validated_data['image_id']) + image_id = validated_data.pop('image_id') + validated_data['image'] = Image.objects.get(pk=image_id) event = Event(**validated_data) event.save() return event diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index f95eb2c30..1ffe72c95 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -24,9 +24,9 @@ import { import type { DropdownOption } from '~/Components/Dropdown/Dropdown'; import { ImagePicker } from '~/Components/ImagePicker/ImagePicker'; import { type Tab, TabBar } from '~/Components/TabBar/TabBar'; -import { getEvent, getVenues, postEvent } from '~/api'; +import { getEvent, getVenues, postEvent, putEvent } from '~/api'; import { BACKEND_DOMAIN } from '~/constants'; -import type { EventDto, ImageDto } from '~/dto'; +import type { EventDto } from '~/dto'; import { useCustomNavigate, usePrevious, useTitle } from '~/hooks'; import { KEY } from '~/i18n/constants'; import { venueKeys } from '~/queryKeys'; @@ -112,9 +112,6 @@ export function EventCreatorAdminPage() { const eventDuration = Math.round( (new Date(eventData.end_dt).getTime() - new Date(eventData.start_dt).getTime()) / 60000, ); - const imageObject: ImageDto | undefined = eventData.image_url - ? { id: eventData.id, title: '', url: eventData.image_url, tags: [] } - : undefined; setEvent(eventData); form.reset({ title_nb: eventData.title_nb || '', @@ -134,7 +131,7 @@ export function EventCreatorAdminPage() { ticket_type: eventData.ticket_type || 'free', custom_tickets: eventData.custom_tickets || [], billig_id: eventData.billig?.id, - image: imageObject, + image: eventData.image || undefined, visibility_from_dt: eventData.visibility_from_dt ? utcTimestampToLocal(eventData.visibility_from_dt, false) : '', @@ -512,10 +509,12 @@ export function EventCreatorAdminPage() { end_dt: computedEndDt ? computedEndDt.toISOString() : '', }; - postEvent(payload) + const request = id ? putEvent(id, payload) : postEvent(payload); + + request .then(() => { navigate({ url: ROUTES.frontend.admin_events }); - toast.success(t(KEY.common_creation_successful)); + toast.success(t(id ? KEY.common_save_successful : KEY.common_creation_successful)); }) .catch((error) => { toast.error(t(KEY.common_something_went_wrong)); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index cf4793f97..6a94ac395 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -294,14 +294,17 @@ export async function getEvents(): Promise { export async function postEvent(data: Partial): Promise> { const transformed = { ...data, image_id: data.image?.id }; + transformed.image = undefined; const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__events_list; const response = await axios.post(url, transformed, { withCredentials: true }); return response; } export async function putEvent(id: string | number, data: Partial): Promise> { + const transformed = { ...data, image_id: data.image?.id }; + transformed.image = undefined; const url = BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__events_detail, urlParams: { pk: id } }); - const response = await axios.put(url, data, { withCredentials: true }); + const response = await axios.put(url, transformed, { withCredentials: true }); return response; } diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 4ecd509da..824e5b1ed 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -178,7 +178,6 @@ export type EventDto = { registration_url?: string; // Timestamps/duration - image_url: string; start_dt: string; duration: number; end_dt: string; @@ -194,6 +193,8 @@ export type EventDto = { // Write only: // Used to create new event with using id of existing imagedto + image_id?: number; + image_url: string; image?: ImageDto; capacity?: number; };