From ad5a16003f40c70e83f1957ca950805438ccc97f Mon Sep 17 00:00:00 2001 From: Saksham Sirohi Date: Tue, 29 Jul 2025 21:02:29 +0000 Subject: [PATCH 1/6] fixed google calander star sessions --- src/pretalx/agenda/urls.py | 5 ++ src/pretalx/agenda/views/schedule.py | 121 ++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 20 deletions(-) diff --git a/src/pretalx/agenda/urls.py b/src/pretalx/agenda/urls.py index d8e4b772f..9de0b8274 100644 --- a/src/pretalx/agenda/urls.py +++ b/src/pretalx/agenda/urls.py @@ -45,6 +45,11 @@ def get_schedule_urls(regex_prefix, name_prefix=""): widget.widget_script, name="widget.script", ), + path( + "export///", + schedule.ExporterView.as_view(), + name="export-tokenized", + ), path("static/event.css", widget.event_css, name="event.css"), path( "schedule/changelog/", diff --git a/src/pretalx/agenda/views/schedule.py b/src/pretalx/agenda/views/schedule.py index badab52d2..2143aed93 100644 --- a/src/pretalx/agenda/views/schedule.py +++ b/src/pretalx/agenda/views/schedule.py @@ -6,6 +6,9 @@ from urllib.parse import unquote, urlencode, urlparse, urlunparse from django.contrib import messages +from django.core import signing +from django.utils import timezone +from datetime import timedelta from django.http import ( Http404, HttpResponse, @@ -65,6 +68,44 @@ def dispatch(self, request, *args, **kwargs): ) return super().dispatch(request, *args, **kwargs) + @staticmethod + def generate_ics_token(user_id): + """Generate a signed token with user ID and 15-day expiry""" + expiry = timezone.now() + timedelta(days=15) + value = {"user_id": user_id, "exp": int(expiry.timestamp())} + return signing.dumps(value, salt="my-starred-ics") + + @staticmethod + def parse_ics_token(token): + """Parse and validate the token, return user_id if valid""" + try: + value = signing.loads(token, salt="my-starred-ics", max_age=15*24*60*60) + if value["exp"] < int(timezone.now().timestamp()): + raise Exception("Token expired") + return value["user_id"] + except Exception: + return None + + @staticmethod + def check_token_expiry(token): + """Check if a token exists and has more than 4 days until expiry + + Returns: + - None if token is invalid + - False if token is valid but expiring soon (< 4 days) + - True if token is valid and not expiring soon (>= 4 days) + """ + try: + value = signing.loads(token, salt="my-starred-ics") + expiry_date = timezone.datetime.fromtimestamp(value["exp"], tz=timezone.utc) + days_until_expiry = (expiry_date - timezone.now()).days + + # Token is valid but check if it's expiring soon + if days_until_expiry < 4: + return False # Valid but expiring soon + return True # Valid and not expiring soon + except Exception: + return None # Invalid token class ExporterView(EventPermissionRequired, ScheduleMixin, TemplateView): permission_required = "agenda.view_schedule" @@ -88,7 +129,8 @@ def get_context_data(self, **kwargs): def get_exporter(self, public=True): url = resolve(self.request.path_info) - if url.url_name == "export": + # Handle both export and export-tokenized URLs + if url.url_name in ["export", "export-tokenized"]: exporter = url.kwargs.get("name") or unquote( self.request.GET.get("exporter") ) @@ -118,19 +160,33 @@ def get(self, request, *args, **kwargs): elif "lang" in request.GET: activate(request.event.locale) - exporter.schedule = self.schedule - if "-my" in exporter.identifier and self.request.user.id is None: + # Handle tokenized access for Google Calendar integration + token = kwargs.get('token') + if token and "-my" in exporter.identifier: + user_id = ScheduleMixin.parse_ics_token(token) + if not user_id: + raise Http404() + + # Set up exporter for this user without requiring login + favs_talks = SubmissionFavourite.objects.filter(user=user_id) + if favs_talks.exists(): + exporter.talk_ids = list( + favs_talks.values_list("submission_id", flat=True) + ) + elif "-my" in exporter.identifier and self.request.user.id is None: if request.GET.get("talks"): exporter.talk_ids = request.GET.get("talks").split(",") else: return HttpResponseRedirect(self.request.event.urls.login) - favs_talks = SubmissionFavourite.objects.filter( - user=self.request.user.id - ) - if favs_talks.exists(): - exporter.talk_ids = list( - favs_talks.values_list("submission_id", flat=True) + elif "-my" in exporter.identifier: + favs_talks = SubmissionFavourite.objects.filter( + user=self.request.user.id ) + if favs_talks.exists(): + exporter.talk_ids = list( + favs_talks.values_list("submission_id", flat=True) + ) + exporter.is_orga = getattr(self.request, "is_orga", False) try: @@ -313,18 +369,43 @@ def get(self, request, *args, **kwargs): # Use resolver_match.url_name for robust route detection url_name = request.resolver_match.url_name if request.resolver_match else None if url_name == 'export.my-google-calendar': - ics_name = 'schedule-my.ics' + # Generate tokenized URL for my starred sessions + if not request.user.is_authenticated: + return HttpResponseRedirect(self.request.event.urls.login) + + # Get existing token from session if available + existing_token = request.session.get('my_starred_ics_token') + generate_new_token = True + + # If we have an existing token, check if it's still valid and not expiring soon + if existing_token: + token_status = self.check_token_expiry(existing_token) + if token_status is True: # Token is valid and not expiring soon + token = existing_token + generate_new_token = False + + # Generate a new token if needed + if generate_new_token: + token = self.generate_ics_token(request.user.id) + # Store the token in the session for future use + request.session['my_starred_ics_token'] = token + + ics_url = request.build_absolute_uri( + reverse('agenda:export-tokenized', kwargs={ + 'event': self.request.event.slug, + 'name': 'schedule-my.ics', + 'token': token + }) + ) else: - ics_name = 'schedule.ics' - - # Build the iCal URL - ics_url = request.build_absolute_uri( - reverse('agenda:export', kwargs={ - 'event': self.request.event.slug, - 'name': ics_name - }) - ) - + # Regular public calendar + ics_url = request.build_absolute_uri( + reverse('agenda:export', kwargs={ + 'event': self.request.event.slug, + 'name': 'schedule.ics' + }) + ) + # Change scheme to webcal parsed = urlparse(ics_url) ics_url = urlunparse(('webcal',) + parsed[1:]) From 6cd299c6754b48cef846bdd929e1fb011cfcf223 Mon Sep 17 00:00:00 2001 From: Saksham Sirohi Date: Wed, 30 Jul 2025 08:12:47 +0000 Subject: [PATCH 2/6] sorcery suggested fixes --- src/pretalx/agenda/views/schedule.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pretalx/agenda/views/schedule.py b/src/pretalx/agenda/views/schedule.py index 2143aed93..c9baafd33 100644 --- a/src/pretalx/agenda/views/schedule.py +++ b/src/pretalx/agenda/views/schedule.py @@ -81,9 +81,10 @@ def parse_ics_token(token): try: value = signing.loads(token, salt="my-starred-ics", max_age=15*24*60*60) if value["exp"] < int(timezone.now().timestamp()): - raise Exception("Token expired") + raise ValueError("Token expired") return value["user_id"] - except Exception: + except (signing.BadSignature, signing.SignatureExpired, KeyError, ValueError) as e: + logger.warning(f"Failed to parse ICS token: {e}") return None @staticmethod @@ -104,7 +105,8 @@ def check_token_expiry(token): if days_until_expiry < 4: return False # Valid but expiring soon return True # Valid and not expiring soon - except Exception: + except (signing.BadSignature, KeyError, ValueError) as e: + logger.warning(f"Failed to check token expiry: {e}") return None # Invalid token class ExporterView(EventPermissionRequired, ScheduleMixin, TemplateView): From 8b7057fb8638d74112fef35931cace02f72c7de2 Mon Sep 17 00:00:00 2001 From: Saksham Sirohi Date: Wed, 30 Jul 2025 16:41:52 +0000 Subject: [PATCH 3/6] implemented suggested changes --- src/pretalx/agenda/views/schedule.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/pretalx/agenda/views/schedule.py b/src/pretalx/agenda/views/schedule.py index c9baafd33..a757b0d5c 100644 --- a/src/pretalx/agenda/views/schedule.py +++ b/src/pretalx/agenda/views/schedule.py @@ -4,11 +4,11 @@ import textwrap from contextlib import suppress from urllib.parse import unquote, urlencode, urlparse, urlunparse +from datetime import timedelta from django.contrib import messages from django.core import signing from django.utils import timezone -from datetime import timedelta from django.http import ( Http404, HttpResponse, @@ -84,7 +84,7 @@ def parse_ics_token(token): raise ValueError("Token expired") return value["user_id"] except (signing.BadSignature, signing.SignatureExpired, KeyError, ValueError) as e: - logger.warning(f"Failed to parse ICS token: {e}") + logger.warning('Failed to parse ICS token: %s', e) return None @staticmethod @@ -99,14 +99,10 @@ def check_token_expiry(token): try: value = signing.loads(token, salt="my-starred-ics") expiry_date = timezone.datetime.fromtimestamp(value["exp"], tz=timezone.utc) - days_until_expiry = (expiry_date - timezone.now()).days - - # Token is valid but check if it's expiring soon - if days_until_expiry < 4: - return False # Valid but expiring soon - return True # Valid and not expiring soon - except (signing.BadSignature, KeyError, ValueError) as e: - logger.warning(f"Failed to check token expiry: {e}") + time_until_expiry = expiry_date - timezone.now() + return time_until_expiry >= timedelta(days=4) + except Exception as e: + logger.warning('Failed to check token expiry: %s', e) return None # Invalid token class ExporterView(EventPermissionRequired, ScheduleMixin, TemplateView): @@ -365,6 +361,8 @@ class ChangelogView(EventPermissionRequired, TemplateView): class GoogleCalendarRedirectView(EventPermissionRequired, ScheduleMixin, TemplateView): + # Define constant for session key + MY_STARRED_ICS_TOKEN_SESSION_KEY = 'my_starred_ics_token' permission_required = "agenda.view_schedule" def get(self, request, *args, **kwargs): @@ -375,22 +373,22 @@ def get(self, request, *args, **kwargs): if not request.user.is_authenticated: return HttpResponseRedirect(self.request.event.urls.login) - # Get existing token from session if available - existing_token = request.session.get('my_starred_ics_token') + # Use constant instead of hardcoded string + existing_token = request.session.get(self.MY_STARRED_ICS_TOKEN_SESSION_KEY) generate_new_token = True # If we have an existing token, check if it's still valid and not expiring soon if existing_token: token_status = self.check_token_expiry(existing_token) - if token_status is True: # Token is valid and not expiring soon + if token_status is True: token = existing_token generate_new_token = False # Generate a new token if needed if generate_new_token: token = self.generate_ics_token(request.user.id) - # Store the token in the session for future use - request.session['my_starred_ics_token'] = token + # Use constant here too + request.session[self.MY_STARRED_ICS_TOKEN_SESSION_KEY] = token ics_url = request.build_absolute_uri( reverse('agenda:export-tokenized', kwargs={ From 179037b780257eeedf3b5fc99f14b8d1dfc6d5c4 Mon Sep 17 00:00:00 2001 From: Saksham Sirohi Date: Sat, 2 Aug 2025 13:21:23 +0000 Subject: [PATCH 4/6] cross platform fix --- src/pretalx/agenda/urls.py | 6 ++- src/pretalx/agenda/views/schedule.py | 57 ++++++++++++++++++---------- src/pretalx/schedule/exporters.py | 21 ++++++++-- src/pretalx/schedule/signals.py | 10 +++++ 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/pretalx/agenda/urls.py b/src/pretalx/agenda/urls.py index 9de0b8274..bc3dbed41 100644 --- a/src/pretalx/agenda/urls.py +++ b/src/pretalx/agenda/urls.py @@ -23,8 +23,10 @@ def get_schedule_urls(regex_prefix, name_prefix=""): (".xcal", schedule.ExporterView.as_view(), "export.schedule.xcal"), (".json", schedule.ExporterView.as_view(), "export.schedule.json"), (".ics", schedule.ExporterView.as_view(), "export.schedule.ics"), - ("/export/google-calendar", schedule.GoogleCalendarRedirectView.as_view(), "export.google-calendar"), - ("/export/my-google-calendar", schedule.GoogleCalendarRedirectView.as_view(), "export.my-google-calendar"), + ("/export/google-calendar", schedule.CalendarRedirectView.as_view(), "export.google-calendar"), + ("/export/my-google-calendar", schedule.CalendarRedirectView.as_view(), "export.my-google-calendar"), + ("/export/webcal", schedule.CalendarRedirectView.as_view(), "export.webcal"), + ("/export/my-webcal", schedule.CalendarRedirectView.as_view(), "export.my-webcal"), ("/export/", schedule.ExporterView.as_view(), "export"), ("/widgets/schedule.json", widget.widget_data, "widget.data"), # Legacy widget data URL, but expected in old widget code. diff --git a/src/pretalx/agenda/views/schedule.py b/src/pretalx/agenda/views/schedule.py index a757b0d5c..0dfe2791c 100644 --- a/src/pretalx/agenda/views/schedule.py +++ b/src/pretalx/agenda/views/schedule.py @@ -127,13 +127,12 @@ def get_context_data(self, **kwargs): def get_exporter(self, public=True): url = resolve(self.request.path_info) - # Handle both export and export-tokenized URLs + calendar_exports = ["export.google-calendar", "export.my-google-calendar", "export.other-calendar", "export.my-other-calendar"] if url.url_name in ["export", "export-tokenized"]: exporter = url.kwargs.get("name") or unquote( self.request.GET.get("exporter") ) - elif url.url_name in ["export.google-calendar", "export.my-google-calendar"]: - # Handle our explicit Google Calendar URL patterns + elif url.url_name in calendar_exports: exporter = url.url_name.replace("export.", "") else: exporter = url.url_name @@ -360,20 +359,25 @@ class ChangelogView(EventPermissionRequired, TemplateView): permission_required = "agenda.view_schedule" -class GoogleCalendarRedirectView(EventPermissionRequired, ScheduleMixin, TemplateView): - # Define constant for session key +class CalendarRedirectView(EventPermissionRequired, ScheduleMixin, TemplateView): + """Handles redirects for both Google Calendar and other calendar applications""" MY_STARRED_ICS_TOKEN_SESSION_KEY = 'my_starred_ics_token' permission_required = "agenda.view_schedule" def get(self, request, *args, **kwargs): - # Use resolver_match.url_name for robust route detection + # Get URL name from resolver url_name = request.resolver_match.url_name if request.resolver_match else None - if url_name == 'export.my-google-calendar': - # Generate tokenized URL for my starred sessions + + # Determine calendar type and starred status from URL pattern + is_google = "google" in url_name + is_my = "my" in url_name + + if is_my: + # For starred sessions if not request.user.is_authenticated: return HttpResponseRedirect(self.request.event.urls.login) - # Use constant instead of hardcoded string + # Check for existing valid token existing_token = request.session.get(self.MY_STARRED_ICS_TOKEN_SESSION_KEY) generate_new_token = True @@ -384,12 +388,12 @@ def get(self, request, *args, **kwargs): token = existing_token generate_new_token = False - # Generate a new token if needed + # Generate new token if needed if generate_new_token: token = self.generate_ics_token(request.user.id) - # Use constant here too request.session[self.MY_STARRED_ICS_TOKEN_SESSION_KEY] = token + # Build tokenized URL for starred sessions ics_url = request.build_absolute_uri( reverse('agenda:export-tokenized', kwargs={ 'event': self.request.event.slug, @@ -398,7 +402,7 @@ def get(self, request, *args, **kwargs): }) ) else: - # Regular public calendar + # Build public calendar URL ics_url = request.build_absolute_uri( reverse('agenda:export', kwargs={ 'event': self.request.event.slug, @@ -406,11 +410,24 @@ def get(self, request, *args, **kwargs): }) ) - # Change scheme to webcal - parsed = urlparse(ics_url) - ics_url = urlunparse(('webcal',) + parsed[1:]) - - # Create Google Calendar URL - google_url = f"https://calendar.google.com/calendar/render?{urlencode({'cid': ics_url})}" - - return HttpResponseRedirect(google_url) + # Handle redirect based on calendar type + if is_google: + # Google Calendar requires special URL format + google_url = f"https://calendar.google.com/calendar/render?{urlencode({'cid': ics_url})}" + response = HttpResponse( + f'' + f'

Redirecting to Google Calendar: {google_url}

', + content_type='text/html' + ) + return response + else: + # Other calendars use webcal protocol + parsed = urlparse(ics_url) + webcal_url = urlunparse(('webcal',) + parsed[1:]) + # Create a simple HTML redirect with meta refresh + response = HttpResponse( + f'' + f'

Redirecting to: {webcal_url}

', + content_type='text/html' + ) + return response diff --git a/src/pretalx/schedule/exporters.py b/src/pretalx/schedule/exporters.py index 20b125d1c..7a54eb0bc 100644 --- a/src/pretalx/schedule/exporters.py +++ b/src/pretalx/schedule/exporters.py @@ -429,20 +429,33 @@ def render(self, request, **kwargs): return f"{self.event.slug}-favs.ics", "text/calendar", cal.serialize() -class BaseGoogleCalendarExporter(BaseExporter): +class BaseCalendarExporter(BaseExporter): public = True show_qrcode = False - icon = "fa-google" + icon = "fa-calendar" + @property def show_public(self): return self.ical_exporter_cls(self.event).show_public -class GoogleCalendarExporter(BaseGoogleCalendarExporter): +class GoogleCalendarExporter(BaseCalendarExporter): identifier = "google-calendar" verbose_name = "Add to Google Calendar" + icon = "fa-google" ical_exporter_cls = ICalExporter -class MyGoogleCalendarExporter(BaseGoogleCalendarExporter): +class MyGoogleCalendarExporter(BaseCalendarExporter): identifier = "my-google-calendar" + icon = "fa-google" verbose_name = "Add My ⭐ Sessions to Google Calendar" ical_exporter_cls = MyICalExporter + +class WebcalExporter(BaseCalendarExporter): + identifier = "webcal" + verbose_name = "Add to Other Calendar" + ical_exporter_cls = ICalExporter + +class MyWebcalExporter(BaseCalendarExporter): + identifier = "my-webcal" + verbose_name = "Add My ⭐ Sessions to Other Calendar" + ical_exporter_cls = MyICalExporter diff --git a/src/pretalx/schedule/signals.py b/src/pretalx/schedule/signals.py index d88791884..69dfdb30d 100644 --- a/src/pretalx/schedule/signals.py +++ b/src/pretalx/schedule/signals.py @@ -90,3 +90,13 @@ def register_google_calendar_exporter(sender, **kwargs): def register_my_google_calendar_exporter(sender, **kwargs): from .exporters import MyGoogleCalendarExporter return MyGoogleCalendarExporter + +@receiver(register_data_exporters, dispatch_uid="exporter_builtin_webcal") +def register_webcal_exporter(sender, **kwargs): + from .exporters import WebcalExporter + return WebcalExporter + +@receiver(register_my_data_exporters, dispatch_uid="exporter_builtin_my_webcal") +def register_my_webcal_exporter(sender, **kwargs): + from .exporters import MyWebcalExporter + return MyWebcalExporter \ No newline at end of file From 7d8cf44deb7983ed99b85434eaa0ec094ad99353 Mon Sep 17 00:00:00 2001 From: Saksham Sirohi Date: Sat, 2 Aug 2025 16:12:26 +0000 Subject: [PATCH 5/6] sorcery suggested fixes --- src/pretalx/agenda/views/schedule.py | 35 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/pretalx/agenda/views/schedule.py b/src/pretalx/agenda/views/schedule.py index 43ec9a824..f692abfdb 100644 --- a/src/pretalx/agenda/views/schedule.py +++ b/src/pretalx/agenda/views/schedule.py @@ -37,6 +37,8 @@ class ScheduleMixin: + MY_STARRED_ICS_TOKEN_SESSION_KEY = 'my_starred_ics_token' + @cached_property def version(self): if version := self.kwargs.get("version"): @@ -69,19 +71,27 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) @staticmethod - def generate_ics_token(user_id): - """Generate a signed token with user ID and 15-day expiry""" + def generate_ics_token(request, user_id): + """Generate a signed token with user ID and 15-day expiry, invalidating previous tokens""" + # Clear any existing token from the session + key = ScheduleMixin.MY_STARRED_ICS_TOKEN_SESSION_KEY + if key in request.session: + del request.session[key] + + # Generate new token expiry = timezone.now() + timedelta(days=15) value = {"user_id": user_id, "exp": int(expiry.timestamp())} - return signing.dumps(value, salt="my-starred-ics") + token = signing.dumps(value, salt="my-starred-ics") + + # Store new token in session + request.session[key] = token + return token @staticmethod def parse_ics_token(token): """Parse and validate the token, return user_id if valid""" try: value = signing.loads(token, salt="my-starred-ics", max_age=15*24*60*60) - if value["exp"] < int(timezone.now().timestamp()): - raise ValueError("Token expired") return value["user_id"] except (signing.BadSignature, signing.SignatureExpired, KeyError, ValueError) as e: logger.warning('Failed to parse ICS token: %s', e) @@ -97,7 +107,7 @@ def check_token_expiry(token): - True if token is valid and not expiring soon (>= 4 days) """ try: - value = signing.loads(token, salt="my-starred-ics") + value = signing.loads(token, salt="my-starred-ics", max_age=15*24*60*60) expiry_date = timezone.datetime.fromtimestamp(value["exp"], tz=timezone.utc) time_until_expiry = expiry_date - timezone.now() return time_until_expiry >= timedelta(days=4) @@ -357,7 +367,6 @@ class ChangelogView(EventPermissionRequired, TemplateView): class CalendarRedirectView(EventPermissionRequired, ScheduleMixin, TemplateView): """Handles redirects for both Google Calendar and other calendar applications""" - MY_STARRED_ICS_TOKEN_SESSION_KEY = 'my_starred_ics_token' permission_required = "agenda.view_schedule" def get(self, request, *args, **kwargs): @@ -371,7 +380,8 @@ def get(self, request, *args, **kwargs): if is_my: # For starred sessions if not request.user.is_authenticated: - return HttpResponseRedirect(self.request.event.urls.login) + login_url = f"{self.request.event.urls.login}?next={request.get_full_path()}" + return HttpResponseRedirect(login_url) # Check for existing valid token existing_token = request.session.get(self.MY_STARRED_ICS_TOKEN_SESSION_KEY) @@ -380,14 +390,13 @@ def get(self, request, *args, **kwargs): # If we have an existing token, check if it's still valid and not expiring soon if existing_token: token_status = self.check_token_expiry(existing_token) - if token_status is True: + if token_status is True: # Token is valid and has at least 4 days left token = existing_token generate_new_token = False - # Generate new token if needed + # Generate new token if needed (this will invalidate any existing token) if generate_new_token: - token = self.generate_ics_token(request.user.id) - request.session[self.MY_STARRED_ICS_TOKEN_SESSION_KEY] = token + token = self.generate_ics_token(request, request.user.id) # Build tokenized URL for starred sessions ics_url = request.build_absolute_uri( @@ -426,4 +435,4 @@ def get(self, request, *args, **kwargs): f'

Redirecting to: {webcal_url}

', content_type='text/html' ) - return response + return response \ No newline at end of file From 98fa758a9c4b51a3ad55b17487261d9f695f55a1 Mon Sep 17 00:00:00 2001 From: Saksham Sirohi Date: Sun, 3 Aug 2025 16:34:34 +0000 Subject: [PATCH 6/6] implemented suggested changes --- src/pretalx/agenda/views/schedule.py | 30 +++++++++++++--------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/pretalx/agenda/views/schedule.py b/src/pretalx/agenda/views/schedule.py index f692abfdb..506b008bc 100644 --- a/src/pretalx/agenda/views/schedule.py +++ b/src/pretalx/agenda/views/schedule.py @@ -38,7 +38,7 @@ class ScheduleMixin: MY_STARRED_ICS_TOKEN_SESSION_KEY = 'my_starred_ics_token' - + @cached_property def version(self): if version := self.kwargs.get("version"): @@ -372,7 +372,6 @@ class CalendarRedirectView(EventPermissionRequired, ScheduleMixin, TemplateView) def get(self, request, *args, **kwargs): # Get URL name from resolver url_name = request.resolver_match.url_name if request.resolver_match else None - # Determine calendar type and starred status from URL pattern is_google = "google" in url_name is_my = "my" in url_name @@ -380,20 +379,18 @@ def get(self, request, *args, **kwargs): if is_my: # For starred sessions if not request.user.is_authenticated: - login_url = f"{self.request.event.urls.login}?next={request.get_full_path()}" + login_url = f"{self.request.event.urls.login}?{urlencode({'next': request.get_full_path()})}" return HttpResponseRedirect(login_url) # Check for existing valid token existing_token = request.session.get(self.MY_STARRED_ICS_TOKEN_SESSION_KEY) generate_new_token = True - # If we have an existing token, check if it's still valid and not expiring soon if existing_token: token_status = self.check_token_expiry(existing_token) if token_status is True: # Token is valid and has at least 4 days left token = existing_token generate_new_token = False - # Generate new token if needed (this will invalidate any existing token) if generate_new_token: token = self.generate_ics_token(request, request.user.id) @@ -419,20 +416,21 @@ def get(self, request, *args, **kwargs): if is_google: # Google Calendar requires special URL format google_url = f"https://calendar.google.com/calendar/render?{urlencode({'cid': ics_url})}" + # HTML-based redirection works more reliably across calendar clients like Outlook and Apple Calendar which often mishandle HTTP 302s. response = HttpResponse( f'' f'

Redirecting to Google Calendar: {google_url}

', content_type='text/html' ) return response - else: - # Other calendars use webcal protocol - parsed = urlparse(ics_url) - webcal_url = urlunparse(('webcal',) + parsed[1:]) - # Create a simple HTML redirect with meta refresh - response = HttpResponse( - f'' - f'

Redirecting to: {webcal_url}

', - content_type='text/html' - ) - return response \ No newline at end of file + + # Other calendars use webcal protocol + parsed = urlparse(ics_url) + webcal_url = urlunparse(('webcal',) + parsed[1:]) + # HTML-based redirection works more reliably across calendar clients like Outlook and Apple Calendar which often mishandle HTTP 302s. + response = HttpResponse( + f'' + f'

Redirecting to: {webcal_url}

', + content_type='text/html' + ) + return response