From 0c27f4b8a4e51cce01ba1f3af0f8d2702f37aeb3 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 14 Dec 2024 14:37:16 -0500 Subject: [PATCH] Add download adventures as ICS calendar --- backend/server/adventures/urls.py | 3 +- backend/server/adventures/views.py | 58 +++++++++++++++++++- backend/server/requirements.txt | 4 +- frontend/src/locales/de.json | 3 +- frontend/src/locales/en.json | 1 + frontend/src/locales/es.json | 3 +- frontend/src/locales/fr.json | 3 +- frontend/src/locales/it.json | 3 +- frontend/src/locales/nl.json | 3 +- frontend/src/locales/pl.json | 3 +- frontend/src/locales/sv.json | 3 +- frontend/src/locales/zh.json | 3 +- frontend/src/routes/calendar/+page.server.ts | 10 +++- frontend/src/routes/calendar/+page.svelte | 11 ++++ 14 files changed, 99 insertions(+), 12 deletions(-) diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 9b7566d..d035522 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -1,6 +1,6 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet +from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet, IcsCalendarGeneratorViewSet router = DefaultRouter() router.register(r'adventures', AdventureViewSet, basename='adventures') @@ -14,6 +14,7 @@ router.register(r'images', AdventureImageViewSet, basename='images') router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode') router.register(r'categories', CategoryViewSet, basename='categories') +router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar') urlpatterns = [ diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 808b9aa..acb03f0 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -17,6 +17,9 @@ from django.shortcuts import get_object_or_404 from rest_framework import status from django.contrib.auth import get_user_model +from icalendar import Calendar, Event, vText, vCalAddress +from django.http import HttpResponse +from datetime import datetime User = get_user_model() @@ -1203,4 +1206,57 @@ def mark_visited_region(self, request): visited_region.save() new_region_count += 1 new_regions[region.id] = region.name - return Response({"new_regions": new_region_count, "regions": new_regions}) \ No newline at end of file + return Response({"new_regions": new_region_count, "regions": new_regions}) + + +from django.http import HttpResponse +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from icalendar import Calendar, Event, vText, vCalAddress +from datetime import datetime, timedelta + +class IcsCalendarGeneratorViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + @action(detail=False, methods=['get']) + def generate(self, request): + adventures = Adventure.objects.filter(user_id=request.user) + serializer = AdventureSerializer(adventures, many=True) + user = request.user + name = f"{user.first_name} {user.last_name}" + print(serializer.data) + + cal = Calendar() + cal.add('prodid', '-//My Adventure Calendar//example.com//') + cal.add('version', '2.0') + + for adventure in serializer.data: + if adventure['visits']: + for visit in adventure['visits']: + event = Event() + event.add('summary', adventure['name']) + start_date = datetime.strptime(visit['start_date'], '%Y-%m-%d').date() + end_date = datetime.strptime(visit['end_date'], '%Y-%m-%d').date() + timedelta(days=1) if visit['end_date'] else start_date + timedelta(days=1) + event.add('dtstart', start_date) + event.add('dtend', end_date) + event.add('dtstamp', datetime.now()) + event.add('transp', 'TRANSPARENT') + event.add('class', 'PUBLIC') + event.add('created', datetime.now()) + event.add('last-modified', datetime.now()) + event.add('description', adventure['description']) + if adventure.get('location'): + event.add('location', adventure['location']) + if adventure.get('link'): + event.add('url', adventure['link']) + + organizer = vCalAddress(f'MAILTO:{user.email}') + organizer.params['cn'] = vText(name) + event.add('organizer', organizer) + + cal.add_component(event) + + response = HttpResponse(cal.to_ical(), content_type='text/calendar') + response['Content-Disposition'] = 'attachment; filename=adventures.ics' + return response diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index 3f6b406..bae189f 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -15,4 +15,6 @@ gunicorn==23.0.0 qrcode==8.0 # slippers==0.6.2 # django-allauth-ui==1.5.1 -# django-widget-tweaks==1.5.0 \ No newline at end of file +# django-widget-tweaks==1.5.0 +django-ical==1.9.2 +icalendar==6.1.0 \ No newline at end of file diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 53e620e..25e2b41 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -194,7 +194,8 @@ "adventure_calendar": "Abenteuerkalender", "emoji_picker": "Emoji-Picker", "hide": "Verstecken", - "show": "Zeigen" + "show": "Zeigen", + "download_calendar": "Kalender herunterladen" }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index b7603b2..cd9b824 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -217,6 +217,7 @@ "show": "Show", "hide": "Hide", "emoji_picker": "Emoji Picker", + "download_calendar": "Download Calendar", "days": "days", "activities": { "general": "General 🌍", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 5283e35..42da49e 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -241,7 +241,8 @@ "adventure_calendar": "Calendario de aventuras", "emoji_picker": "Selector de emojis", "hide": "Esconder", - "show": "Espectáculo" + "show": "Espectáculo", + "download_calendar": "Descargar Calendario" }, "worldtravel": { "all": "Todo", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 1f58959..9a532de 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -194,7 +194,8 @@ "adventure_calendar": "Calendrier d'aventure", "emoji_picker": "Sélecteur d'émoticônes", "hide": "Cacher", - "show": "Montrer" + "show": "Montrer", + "download_calendar": "Télécharger le calendrier" }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 77b5498..fa0b1fb 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -194,7 +194,8 @@ "adventure_calendar": "Calendario delle avventure", "emoji_picker": "Selettore di emoji", "hide": "Nascondere", - "show": "Spettacolo" + "show": "Spettacolo", + "download_calendar": "Scarica Calendario" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 4ebd11b..90ac331 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -194,7 +194,8 @@ "adventure_calendar": "Avonturenkalender", "emoji_picker": "Emoji-kiezer", "hide": "Verbergen", - "show": "Show" + "show": "Show", + "download_calendar": "Agenda downloaden" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 7e50cc8..db3e758 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -241,7 +241,8 @@ "adventure_calendar": "Kalendarz przygód", "emoji_picker": "Wybór emoji", "hide": "Ukrywać", - "show": "Pokazywać" + "show": "Pokazywać", + "download_calendar": "Pobierz Kalendarz" }, "worldtravel": { "country_list": "Lista krajów", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index fe462f9..aae55d5 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -194,7 +194,8 @@ "adventure_calendar": "Äventyrskalender", "emoji_picker": "Emoji-väljare", "hide": "Dölja", - "show": "Visa" + "show": "Visa", + "download_calendar": "Ladda ner kalender" }, "home": { "desc_1": "Upptäck, planera och utforska med lätthet", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 228f664..56cf01f 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -194,7 +194,8 @@ "adventure_calendar": "冒险日历", "emoji_picker": "表情符号选择器", "hide": "隐藏", - "show": "展示" + "show": "展示", + "download_calendar": "下载日历" }, "home": { "desc_1": "轻松发现、规划和探索", diff --git a/frontend/src/routes/calendar/+page.server.ts b/frontend/src/routes/calendar/+page.server.ts index 0305b93..6c9ede0 100644 --- a/frontend/src/routes/calendar/+page.server.ts +++ b/frontend/src/routes/calendar/+page.server.ts @@ -30,10 +30,18 @@ export const load = (async (event) => { }); }); + let icsFetch = await fetch(`${endpoint}/api/ics-calendar/generate`, { + headers: { + Cookie: `sessionid=${sessionId}` + } + }); + let ics_calendar = await icsFetch.text(); + return { props: { adventures, - dates + dates, + ics_calendar } }; }) satisfies PageServerLoad; diff --git a/frontend/src/routes/calendar/+page.svelte b/frontend/src/routes/calendar/+page.svelte index 3dc5b2c..476519d 100644 --- a/frontend/src/routes/calendar/+page.svelte +++ b/frontend/src/routes/calendar/+page.svelte @@ -14,6 +14,10 @@ let adventures = data.props.adventures; let dates = data.props.dates; + let icsCalendar = data.props.ics_calendar; + // turn the ics calendar into a data URL + let icsCalendarDataUrl = URL.createObjectURL(new Blob([icsCalendar], { type: 'text/calendar' })); + let plugins = [TimeGrid, DayGrid]; let options = { view: 'dayGridMonth', @@ -24,3 +28,10 @@

{$t('adventures.adventure_calendar')}

+ + +