Skip to content

Conversation

Saksham-Sirohi
Copy link
Contributor

@Saksham-Sirohi Saksham-Sirohi commented Aug 2, 2025

Fixes #397
Google Exporters now uses HTTP, and a new calendar option has been added to support webcal-based calendars

Summary by Sourcery

Add support for tokenized starred session exports and a unified calendar redirect handler for both Google and other calendar protocols

New Features:

  • Support tokenized ICS exports for users’ starred sessions with signed tokens and expiry checks
  • Add webcal and my-webcal exporter options alongside existing Google Calendar endpoints

Enhancements:

  • Replace GoogleCalendarRedirectView with a unified CalendarRedirectView handling public and starred exports and redirect logic for Google and webcal schemes
  • Refactor exporter classes by introducing a BaseCalendarExporter base, updating icons, and registering new WebcalExporter and MyWebcalExporter

Copy link

sourcery-ai bot commented Aug 2, 2025

Reviewer's Guide

This PR refactors calendar export to support webcal-based calendars and tokenized starred session exports by consolidating calendar redirect logic into a generic view, implementing a signed token mechanism for “my” calendars with expiry checks, extending exporter classes for webcal, and updating URL patterns and signals to register the new exporters.

ER diagram for tokenized starred session export

erDiagram
    USER ||--o{ SUBMISSION_FAVOURITE : has
    SUBMISSION_FAVOURITE }o--|| SUBMISSION : refers
    USER ||--o{ TOKEN : generates
    TOKEN {
        user_id int
        exp int
    }
Loading

Class diagram for updated and new calendar exporter classes

classDiagram
    class BaseExporter
    class BaseCalendarExporter {
        +public
        +show_qrcode
        +icon
        +show_public
    }
    class GoogleCalendarExporter {
        +identifier
        +verbose_name
        +icon
        +ical_exporter_cls
    }
    class MyGoogleCalendarExporter {
        +identifier
        +icon
        +verbose_name
        +ical_exporter_cls
    }
    class WebcalExporter {
        +identifier
        +verbose_name
        +ical_exporter_cls
    }
    class MyWebcalExporter {
        +identifier
        +verbose_name
        +ical_exporter_cls
    }
    BaseExporter <|-- BaseCalendarExporter
    BaseCalendarExporter <|-- GoogleCalendarExporter
    BaseCalendarExporter <|-- MyGoogleCalendarExporter
    BaseCalendarExporter <|-- WebcalExporter
    BaseCalendarExporter <|-- MyWebcalExporter
Loading

Class diagram for updated CalendarRedirectView and token logic

classDiagram
    class CalendarRedirectView {
        +MY_STARRED_ICS_TOKEN_SESSION_KEY
        +get(request, *args, **kwargs)
        +generate_ics_token(user_id)
        +parse_ics_token(token)
        +check_token_expiry(token)
    }
Loading

File-Level Changes

Change Details Files
Introduce ICS token mechanism for starred sessions
  • Add generate_ics_token, parse_ics_token, and check_token_expiry methods
  • Integrate token parsing in ExporterView.get to set fav talk_ids without login
  • Store and validate tokens in session for starred redirects
src/pretalx/agenda/views/schedule.py
Consolidate and extend calendar redirect view
  • Rename GoogleCalendarRedirectView to CalendarRedirectView
  • Unify Google vs other calendars and build appropriate URLs
  • Implement HTML meta refresh and JS redirect responses
  • Manage session tokens and token expiry logic for starred exports
src/pretalx/agenda/views/schedule.py
Expand and rename exporter base classes
  • Rename BaseGoogleCalendarExporter to BaseCalendarExporter and adjust default icon
  • Add WebcalExporter and MyWebcalExporter for other calendars
  • Restore Google-specific icon on Google exporter classes
src/pretalx/schedule/exporters.py
Update URL patterns for new routes
  • Switch calendar routes to use CalendarRedirectView
  • Add /export/webcal and /export/my-webcal endpoints
  • Add export-tokenized path to support tokenized ICS URLs
src/pretalx/agenda/urls.py
Register new exporters via signals
  • Add receivers to register WebcalExporter
  • Add receivers to register MyWebcalExporter
src/pretalx/schedule/signals.py

Assessment against linked issues

Issue Objective Addressed Explanation
#397 Implement cross-platform support for the 'Add to Google Calendar' button, with correct behavior for Android, iOS/macOS, and Desktop/other platforms.
#397 On iOS/macOS, ensure the button opens a webcal:// link for native Calendar subscription.
#397 On Android, ensure the button redirects to a Google Calendar subscription URL; on Desktop/other, provide a menu with Google Calendar and Apple Calendar (webcal://) options.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Saksham-Sirohi - I've reviewed your changes - here's some feedback:

  • Extract magic values like the token salt, expiry durations (15 days, 4 days), and session key into module‐level constants or settings so they aren’t hard‐coded in multiple places.
  • The token generation/validation currently maintains its own “exp” field in addition to using signing.loads(max_age); consider relying on one mechanism for expiry (either the JWT exp claim or Django signing max_age) to simplify the logic.
  • CalendarRedirectView contains duplicated URL‐building and HTML redirect snippets for Google vs webcal—pull that into helper methods or a small template to reduce code repetition.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Extract magic values like the token salt, expiry durations (15 days, 4 days), and session key into module‐level constants or settings so they aren’t hard‐coded in multiple places.
- The token generation/validation currently maintains its own “exp” field in addition to using signing.loads(max_age); consider relying on one mechanism for expiry (either the JWT exp claim or Django signing max_age) to simplify the logic.
- CalendarRedirectView contains duplicated URL‐building and HTML redirect snippets for Google vs webcal—pull that into helper methods or a small template to reduce code repetition.

## Individual Comments

### Comment 1
<location> `src/pretalx/agenda/views/schedule.py:79` </location>
<code_context>
+        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 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)
+            return None
+    
</code_context>

<issue_to_address>
Token expiry is checked both by signing.loads and manually, which may be redundant.

Since signing.loads with max_age already enforces expiry, manually checking the 'exp' field is unnecessary and could introduce inconsistencies. Consider using only one expiry check.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    @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)
            return None
=======
    @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)
            return value["user_id"]
        except (signing.BadSignature, signing.SignatureExpired, KeyError) as e:
            logger.warning('Failed to parse ICS token: %s', e)
            return None
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `src/pretalx/agenda/views/schedule.py:91` </location>
<code_context>
+            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)
+            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):
</code_context>

<issue_to_address>
check_token_expiry does not enforce max_age, which could allow expired tokens to be considered valid.

Unlike parse_ics_token, this method does not use max_age in signing.loads, which may lead to inconsistent token validation. Please add max_age for consistency and to ensure expired tokens are rejected.
</issue_to_address>

### Comment 3
<location> `src/pretalx/agenda/views/schedule.py:130` </location>
<code_context>
         url = resolve(self.request.path_info)

-        if url.url_name == "export":
+        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")
</code_context>

<issue_to_address>
Hardcoded calendar_exports list includes non-existent URL names.

'export.other-calendar' and 'export.my-other-calendar' are not defined in the URL config. Please remove or update these entries to reflect actual URL patterns.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        calendar_exports = ["export.google-calendar", "export.my-google-calendar", "export.other-calendar", "export.my-other-calendar"]
=======
        calendar_exports = ["export.google-calendar", "export.my-google-calendar"]
>>>>>>> REPLACE

</suggested_fix>

### Comment 4
<location> `src/pretalx/agenda/views/schedule.py:162` </location>
<code_context>
-        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)
</code_context>

<issue_to_address>
No check for user existence when using user_id from token.

Please verify that the user exists and is active before filtering SubmissionFavourite with user_id from the token, to prevent issues with deleted or deactivated users.
</issue_to_address>

### Comment 5
<location> `src/pretalx/agenda/views/schedule.py:173` </location>
<code_context>
+                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
</code_context>

<issue_to_address>
Redirect to login may not preserve intended destination after authentication.

Consider including a 'next' parameter in the login redirect to retain the user's original request, ensuring they can complete their intended action after authentication.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
            else:
                return HttpResponseRedirect(self.request.event.urls.login)
=======
            else:
                from django.utils.http import urlquote

                next_url = urlquote(self.request.get_full_path())
                login_url = f"{self.request.event.urls.login}?next={next_url}"
                return HttpResponseRedirect(login_url)
>>>>>>> REPLACE

</suggested_fix>

### Comment 6
<location> `src/pretalx/agenda/views/schedule.py:362` </location>
<code_context>
+class CalendarRedirectView(EventPermissionRequired, ScheduleMixin, TemplateView):
</code_context>

<issue_to_address>
Redirect for unauthenticated users does not preserve the original destination.

Consider adding a 'next' parameter to the redirect so users return to their intended destination after login.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Saksham-Sirohi Saksham-Sirohi force-pushed the starred-google-calander-fix branch from c21eed4 to 3515791 Compare August 2, 2025 15:26
@Saksham-Sirohi Saksham-Sirohi force-pushed the starred-google-calander-fix branch from e18e472 to 7d8cf44 Compare August 2, 2025 17:11
@Saksham-Sirohi Saksham-Sirohi requested a review from hongquan August 3, 2025 16:38
@Saksham-Sirohi Saksham-Sirohi force-pushed the starred-google-calander-fix branch 3 times, most recently from 7815ff6 to cd7e558 Compare August 3, 2025 16:47
@Saksham-Sirohi Saksham-Sirohi force-pushed the starred-google-calander-fix branch from cd7e558 to 98fa758 Compare August 3, 2025 16:52
@mariobehling
Copy link
Member

It looks like adding sessions to Google cal creates a "Google Meet" link for each session. How to avoid this?

@Saksham-Sirohi
Copy link
Contributor Author

Saksham-Sirohi commented Aug 5, 2025

@mariobehling Unfortunately, this specific Google Calendar setting ("Automatically add Google Meet video conferences to events I create") cannot be changed programmatically via the Google Calendar API or any .ics file directive. It must be set by the user (or their admin) in the Google Calendar UI or Admin Console. There is no API parameter, property, or value you can add to your .ics export or Google Calendar API call that disables Meet links for the user.

So, The user will need to manually disable disable this under event settings in google calender settings I am still looking for a workaround will update regarding this

@hpdang
Copy link
Member

hpdang commented Aug 5, 2025

@Saksham-Sirohi confirmed No google link was created when I tested on Chrome Browser

@Saksham-Sirohi
Copy link
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add cross‑platform support for “Add to Google Calendar” button (Android/iOS/Desktop)
4 participants