From 7454ca964cd90ef53ec52bee46b5b94f4de3faa9 Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 4 Oct 2023 22:51:10 -0700 Subject: [PATCH 1/9] Add ical view model --- hknweb/events/admin/__init__.py | 14 ++++---- hknweb/events/admin/ical_view.py | 14 ++++++++ hknweb/events/migrations/0012_icalview.py | 39 +++++++++++++++++++++++ hknweb/events/models/__init__.py | 1 + hknweb/events/models/ical_view.py | 18 +++++++++++ 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 hknweb/events/admin/ical_view.py create mode 100644 hknweb/events/migrations/0012_icalview.py create mode 100644 hknweb/events/models/ical_view.py diff --git a/hknweb/events/admin/__init__.py b/hknweb/events/admin/__init__.py index 3ffe5345..a20255a0 100644 --- a/hknweb/events/admin/__init__.py +++ b/hknweb/events/admin/__init__.py @@ -1,16 +1,14 @@ -from hknweb.events.admin.attendance import ( - AttendanceFormAdmin, - AttendanceResponseAdmin, -) +from django.contrib import admin + +from hknweb.events.admin.attendance import AttendanceFormAdmin, AttendanceResponseAdmin +from hknweb.events.admin.event import EventAdmin +from hknweb.events.admin.event_type import EventTypeAdmin from hknweb.events.admin.google_calendar import ( GCalAccessLevelMappingAdmin, GoogleCalendarCredentialsAdmin, ) -from hknweb.events.admin.event import EventAdmin -from hknweb.events.admin.event_type import EventTypeAdmin +from hknweb.events.admin.ical_view import ICalViewAdmin from hknweb.events.admin.rsvp import RsvpAdmin - -from django.contrib import admin from hknweb.events.models import EventPhoto admin.site.register(EventPhoto) diff --git a/hknweb/events/admin/ical_view.py b/hknweb/events/admin/ical_view.py new file mode 100644 index 00000000..66801f81 --- /dev/null +++ b/hknweb/events/admin/ical_view.py @@ -0,0 +1,14 @@ +from django.contrib import admin + +from hknweb.events.models import ICalView + + +@admin.register(ICalView) +class ICalViewAdmin(admin.ModelAdmin): + fields = ["user", "show_rsvpd", "show_not_rsvpd"] + list_display = ["id", "user"] + search_fields = [ + "user__username", + "user__first_name", + "user__last_name", + ] diff --git a/hknweb/events/migrations/0012_icalview.py b/hknweb/events/migrations/0012_icalview.py new file mode 100644 index 00000000..0c18b515 --- /dev/null +++ b/hknweb/events/migrations/0012_icalview.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.5 on 2023-10-05 05:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("events", "0011_eventphoto"), + ] + + operations = [ + migrations.CreateModel( + name="ICalView", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("show_rsvpd", models.BooleanField(default=True)), + ("show_not_rsvpd", models.BooleanField(default=False)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/hknweb/events/models/__init__.py b/hknweb/events/models/__init__.py index 12a6d77b..74b925d6 100644 --- a/hknweb/events/models/__init__.py +++ b/hknweb/events/models/__init__.py @@ -7,3 +7,4 @@ ) from hknweb.events.models.attendance import AttendanceForm, AttendanceResponse from hknweb.events.models.event_photo import EventPhoto +from hknweb.events.models.ical_view import ICalView diff --git a/hknweb/events/models/ical_view.py b/hknweb/events/models/ical_view.py new file mode 100644 index 00000000..3290abe8 --- /dev/null +++ b/hknweb/events/models/ical_view.py @@ -0,0 +1,18 @@ +import uuid + +import icalendar +from django.conf import settings +from django.db import models + +from hknweb.events.utils import get_events + + +class ICalView(models.Model): + class Meta: + verbose_name = "iCal view" + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + show_rsvpd = models.BooleanField(default=True) + show_not_rsvpd = models.BooleanField(default=False) + From f7e43c820e9c121effd348ef212470dd0d68dc2e Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 4 Oct 2023 22:52:28 -0700 Subject: [PATCH 2/9] Extract getting events to helper function --- hknweb/events/utils.py | 31 +++++++++++++++++++ .../views/aggregate_displays/calendar.py | 11 ++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/hknweb/events/utils.py b/hknweb/events/utils.py index 6278fd85..0a66b406 100644 --- a/hknweb/events/utils.py +++ b/hknweb/events/utils.py @@ -7,6 +7,37 @@ from hknweb.events.constants import ATTR from hknweb.events.models import Event +from hknweb.utils import get_access_level + + +def get_events(user, show_rsvpd, show_not_rsvpd): + """Retrieves the events a user can see. + + Parameters + ---------- + user: django.contrib.auth.models.User + The user authenticating the request (can be anonymous) + show_rsvpd: bool + Whether to include events the user has RSVP'd for + show_not_rsvpd: bool + Whether to include events the user has not RSVP'd for + + Returns + ------- + QuerySet of Event objects + """ + + events = Event.objects.order_by("-start_time").filter( + access_level__gte=get_access_level(user) + ) + + if user.is_authenticated: + if not show_rsvpd: + events = events.exclude(rsvp__user=user) + if not show_not_rsvpd: + events = events.filter(rsvp__user=user) + + return events def create_event(data, start_time, end_time, user): diff --git a/hknweb/events/views/aggregate_displays/calendar.py b/hknweb/events/views/aggregate_displays/calendar.py index 58b8bb88..d6281407 100644 --- a/hknweb/events/views/aggregate_displays/calendar.py +++ b/hknweb/events/views/aggregate_displays/calendar.py @@ -5,6 +5,7 @@ from hknweb.models import Profile from hknweb.events.models import Event, EventType, GCalAccessLevelMapping from hknweb.events.models.constants import ACCESS_LEVELS +from hknweb.events.utils import get_events from hknweb.utils import allow_public_access, get_access_level from hknweb.events.google_calendar_utils import get_calendar_link @@ -37,15 +38,7 @@ def calendar_helper( show_sidebar=False, ): user_access_level = get_access_level(request.user) - - events = Event.objects.order_by("-start_time").filter( - access_level__gte=user_access_level - ) - if request.user.is_authenticated: - if not rsvpd_display: - events = events.exclude(rsvp__user=request.user) - if not not_rsvpd_display: - events = events.filter(rsvp__user=request.user) + events = get_events(request.user, rsvpd_display, not_rsvpd_display) all_event_types = event_types = EventType.objects.order_by("type") if event_type_types: From 249acf21069efafd62b4441c25ecb41833fa0848 Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 4 Oct 2023 22:51:31 -0700 Subject: [PATCH 3/9] Add conversions from models to ical format --- hknweb/events/models/event.py | 33 ++++++++++++++++++++++++++----- hknweb/events/models/ical_view.py | 11 +++++++++++ poetry.lock | 31 ++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/hknweb/events/models/event.py b/hknweb/events/models/event.py index f55e63bd..41e893e0 100644 --- a/hknweb/events/models/event.py +++ b/hknweb/events/models/event.py @@ -1,14 +1,14 @@ -from django.db import models +import icalendar from django.contrib.auth.models import User - +from django.db import models +from icalendar import vCalAddress, vText from markdownx.models import MarkdownxField -from hknweb.utils import get_semester import hknweb.events.google_calendar_utils as gcal - +from hknweb.events.models.constants import ACCESS_LEVELS from hknweb.events.models.event_type import EventType from hknweb.events.models.google_calendar import GCalAccessLevelMapping -from hknweb.events.models.constants import ACCESS_LEVELS +from hknweb.utils import get_semester class Event(models.Model): @@ -41,6 +41,29 @@ def semester(self): Example: "Spring 2020" """ return get_semester(self.start_time) + def to_ical_obj(self): + event = icalendar.Event() + event.add("uid", self.id) + event.add("summary", self.name) + event.add("location", self.location) + event.add("description", self.description) + event.add("dtstart", self.start_time) + event.add("dtend", self.end_time) + event.add("dtstamp", self.created_at) + + def make_attendee(user, status): + attendee = vCalAddress(f"MAILTO:{user.email}") + attendee.params["PARTSTAT"] = vText(status) + attendee.params["CN"] = vText(f"{user.first_name} {user.last_name}") + return attendee + + for rsvp in self.admitted_set(): + event.add("attendee", make_attendee(rsvp.user, "ACCEPTED"), encode=0) + for rsvp in self.waitlist_set(): + event.add("attendee", make_attendee(rsvp.user, "TENTATIVE"), encode=0) + + return event + def get_absolute_url(self): return "/events/{}".format(self.id) diff --git a/hknweb/events/models/ical_view.py b/hknweb/events/models/ical_view.py index 3290abe8..bbb292eb 100644 --- a/hknweb/events/models/ical_view.py +++ b/hknweb/events/models/ical_view.py @@ -16,3 +16,14 @@ class Meta: show_rsvpd = models.BooleanField(default=True) show_not_rsvpd = models.BooleanField(default=False) + def to_ical_obj(self): + cal = icalendar.Calendar() + cal.add("prodid", "-//Eta Kappa Nu, Mu Chapter//Calendar//EN") + cal.add("version", "2.0") + cal.add("summary", f"HKN Personal Calendar for {self.user}") + + events = get_events(self.user, self.show_rsvpd, self.show_not_rsvpd) + for event in events: + cal.add_component(event.to_ical_obj()) + + return cal diff --git a/poetry.lock b/poetry.lock index f7dfe39d..7c0c451f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -684,6 +684,21 @@ files = [ [package.dependencies] pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} +[[package]] +name = "icalendar" +version = "5.0.10" +description = "iCalendar parser/generator" +optional = false +python-versions = ">=3.7" +files = [ + {file = "icalendar-5.0.10-py3-none-any.whl", hash = "sha256:6e392c2f301b6b5f49433e14c905db3de444b12876f3345f1856a75e9cd8be6f"}, + {file = "icalendar-5.0.10.tar.gz", hash = "sha256:34f0ca020b804758ddf316eb70d1d46f769bce64638d5a080cb65dd46cfee642"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = "*" + [[package]] name = "idna" version = "3.4" @@ -993,6 +1008,20 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pytz" version = "2023.3.post1" @@ -1257,4 +1286,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "~3.9" -content-hash = "97fc202265ce2631e4dfff957dbf0e5bb325823aa9d44aa8561c912c4ffbd415" +content-hash = "72c98c5ebe3db16f613c40c9ef8e80ac30f7e48aeb69ff86d63fb7583adb9dc3" diff --git a/pyproject.toml b/pyproject.toml index 1fe9d715..ffb34b44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ fabric = "^3.2.2" django-autocomplete-light = "^3.9.7" djangorestframework = "^3.14.0" google-api-python-client = "^2.99.0" +icalendar = "^5.0.10" [tool.poetry.group.prod] optional = true From 6d4bcf8559261af6792711368c8e78c8bf34a94c Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 4 Oct 2023 22:52:43 -0700 Subject: [PATCH 4/9] Add ical view --- hknweb/events/urls.py | 3 ++- hknweb/events/views/__init__.py | 1 + .../events/views/aggregate_displays/__init__.py | 2 +- .../events/views/aggregate_displays/calendar.py | 16 ++++++++++++---- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/hknweb/events/urls.py b/hknweb/events/urls.py index 1c4244d4..c5cfc5f9 100644 --- a/hknweb/events/urls.py +++ b/hknweb/events/urls.py @@ -1,11 +1,12 @@ from django.urls import path -import hknweb.events.views as views +import hknweb.events.views as views app_name = "events" aggregate_display_urls = [ path("", views.index, name="index"), + path("ical/.ics", views.ical, name="ical"), path("leaderboard", views.get_leaderboard, name="leaderboard"), path("photos", views.photos, name="photos"), ] diff --git a/hknweb/events/views/__init__.py b/hknweb/events/views/__init__.py index 284892fb..488d2ea2 100644 --- a/hknweb/events/views/__init__.py +++ b/hknweb/events/views/__init__.py @@ -1,5 +1,6 @@ from hknweb.events.views.aggregate_displays import ( index, + ical, get_leaderboard, photos, ) diff --git a/hknweb/events/views/aggregate_displays/__init__.py b/hknweb/events/views/aggregate_displays/__init__.py index 8a51e531..710f8df3 100644 --- a/hknweb/events/views/aggregate_displays/__init__.py +++ b/hknweb/events/views/aggregate_displays/__init__.py @@ -1,3 +1,3 @@ -from hknweb.events.views.aggregate_displays.calendar import index +from hknweb.events.views.aggregate_displays.calendar import ical, index from hknweb.events.views.aggregate_displays.leaderboard import get_leaderboard from hknweb.events.views.aggregate_displays.photos import photos diff --git a/hknweb/events/views/aggregate_displays/calendar.py b/hknweb/events/views/aggregate_displays/calendar.py index d6281407..cd95e50f 100644 --- a/hknweb/events/views/aggregate_displays/calendar.py +++ b/hknweb/events/views/aggregate_displays/calendar.py @@ -1,13 +1,15 @@ +import uuid from typing import List -from django.shortcuts import render +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, render -from hknweb.models import Profile -from hknweb.events.models import Event, EventType, GCalAccessLevelMapping +from hknweb.events.google_calendar_utils import get_calendar_link +from hknweb.events.models import Event, EventType, GCalAccessLevelMapping, ICalView from hknweb.events.models.constants import ACCESS_LEVELS from hknweb.events.utils import get_events +from hknweb.models import Profile from hknweb.utils import allow_public_access, get_access_level -from hknweb.events.google_calendar_utils import get_calendar_link @allow_public_access @@ -29,6 +31,12 @@ def index(request): ) +@allow_public_access +def ical(request, *, id: uuid.UUID): + ical_view = get_object_or_404(ICalView, pk=id) + return HttpResponse(ical_view.to_ical_obj().to_ical()) + + def calendar_helper( request, title, From 65e714feff981bf791340f57385475122feb79b2 Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 4 Oct 2023 22:57:44 -0700 Subject: [PATCH 5/9] Set content type --- hknweb/events/views/aggregate_displays/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hknweb/events/views/aggregate_displays/calendar.py b/hknweb/events/views/aggregate_displays/calendar.py index cd95e50f..4d037d14 100644 --- a/hknweb/events/views/aggregate_displays/calendar.py +++ b/hknweb/events/views/aggregate_displays/calendar.py @@ -34,7 +34,7 @@ def index(request): @allow_public_access def ical(request, *, id: uuid.UUID): ical_view = get_object_or_404(ICalView, pk=id) - return HttpResponse(ical_view.to_ical_obj().to_ical()) + return HttpResponse(ical_view.to_ical_obj().to_ical(), content_type="text/calendar") def calendar_helper( From b08c3895cea42ff027e3d6bf2585ab9cb297943e Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 4 Oct 2023 23:21:52 -0700 Subject: [PATCH 6/9] Automatically provision icalviews and show link --- hknweb/events/models/ical_view.py | 5 +++++ hknweb/events/models/rsvp.py | 12 +++++++----- hknweb/events/views/aggregate_displays/calendar.py | 8 ++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/hknweb/events/models/ical_view.py b/hknweb/events/models/ical_view.py index bbb292eb..d665cae8 100644 --- a/hknweb/events/models/ical_view.py +++ b/hknweb/events/models/ical_view.py @@ -3,6 +3,7 @@ import icalendar from django.conf import settings from django.db import models +from django.urls import reverse from hknweb.events.utils import get_events @@ -16,6 +17,10 @@ class Meta: show_rsvpd = models.BooleanField(default=True) show_not_rsvpd = models.BooleanField(default=False) + @property + def url(self): + return reverse("events:ical", args=[self.id]) + def to_ical_obj(self): cal = icalendar.Calendar() cal.add("prodid", "-//Eta Kappa Nu, Mu Chapter//Calendar//EN") diff --git a/hknweb/events/models/rsvp.py b/hknweb/events/models/rsvp.py index b87a1b82..87788bf7 100644 --- a/hknweb/events/models/rsvp.py +++ b/hknweb/events/models/rsvp.py @@ -1,9 +1,9 @@ -from django.db import models from django.contrib.auth.models import User +from django.db import models -from hknweb.models import Profile -from hknweb.events.models.event import Event import hknweb.events.google_calendar_utils as gcal +from hknweb.events.models.event import Event +from hknweb.models import Profile class Rsvp(models.Model): @@ -30,8 +30,10 @@ def has_not_rsvpd(cls, user, event): def save(self, *args, **kwargs): profile = Profile.objects.filter(user=self.user).first() if not profile.google_calendar_id: - profile.google_calendar_id = gcal.create_personal_calendar() - profile.save() + # we no longer provision new personal google calendars + # instead, we generate a ICalView and a route to view it + # so they can add it to any calendar app + return if self.google_calendar_event_id is None: self.google_calendar_event_id = gcal.create_event( diff --git a/hknweb/events/views/aggregate_displays/calendar.py b/hknweb/events/views/aggregate_displays/calendar.py index 4d037d14..5f3a82e1 100644 --- a/hknweb/events/views/aggregate_displays/calendar.py +++ b/hknweb/events/views/aggregate_displays/calendar.py @@ -91,6 +91,14 @@ def get_calendars(request, user_access_level: int): } ) + ical_view, _ = ICalView.objects.get_or_create(user=request.user) + calendars.append( + { + "name": "personal (ics)", + "link": get_calendar_link(calendar_id=ical_view.url), + } + ) + for calendar in calendars[:-1]: calendar["separator"] = "/" if len(calendars) > 0: From 156c243be27d956255011e403423fcd1204330c9 Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Wed, 4 Oct 2023 23:24:53 -0700 Subject: [PATCH 7/9] Fix link --- hknweb/events/views/aggregate_displays/calendar.py | 8 +++++--- hknweb/settings/prod.py | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hknweb/events/views/aggregate_displays/calendar.py b/hknweb/events/views/aggregate_displays/calendar.py index 5f3a82e1..e25ee6e2 100644 --- a/hknweb/events/views/aggregate_displays/calendar.py +++ b/hknweb/events/views/aggregate_displays/calendar.py @@ -4,7 +4,7 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404, render -from hknweb.events.google_calendar_utils import get_calendar_link +from hknweb.events.google_calendar_utils import SHARE_LINK_TEMPLATE, get_calendar_link from hknweb.events.models import Event, EventType, GCalAccessLevelMapping, ICalView from hknweb.events.models.constants import ACCESS_LEVELS from hknweb.events.utils import get_events @@ -86,16 +86,18 @@ def get_calendars(request, user_access_level: int): if profile.google_calendar_id: calendars.append( { - "name": "personal", + "name": "personal (gcal)", "link": get_calendar_link(calendar_id=profile.google_calendar_id), } ) ical_view, _ = ICalView.objects.get_or_create(user=request.user) + ical_url = request.build_absolute_uri(ical_view.url) + ical_url = ical_url.replace("https://", "webcal://") calendars.append( { "name": "personal (ics)", - "link": get_calendar_link(calendar_id=ical_view.url), + "link": SHARE_LINK_TEMPLATE.format(cid=ical_url), } ) diff --git a/hknweb/settings/prod.py b/hknweb/settings/prod.py index 86b07062..dc41bdcb 100644 --- a/hknweb/settings/prod.py +++ b/hknweb/settings/prod.py @@ -25,3 +25,6 @@ # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = "https://www.ocf.berkeley.edu/~hkn/hknweb/static/" STATIC_ROOT = "/home/h/hk/hkn/public_html/hknweb/static/" + +USE_X_FORWARDED_HOST = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") From 1a1988f3d0c46ce982810f001eb3996d920ef22b Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Thu, 5 Oct 2023 00:08:47 -0700 Subject: [PATCH 8/9] Fix issue --- hknweb/events/models/rsvp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hknweb/events/models/rsvp.py b/hknweb/events/models/rsvp.py index 87788bf7..717e63c6 100644 --- a/hknweb/events/models/rsvp.py +++ b/hknweb/events/models/rsvp.py @@ -33,7 +33,7 @@ def save(self, *args, **kwargs): # we no longer provision new personal google calendars # instead, we generate a ICalView and a route to view it # so they can add it to any calendar app - return + return super().save(*args, **kwargs) if self.google_calendar_event_id is None: self.google_calendar_event_id = gcal.create_event( From eaf79b1abcaf6ac4eec8055a71bf793a31e810c7 Mon Sep 17 00:00:00 2001 From: Oliver Ni Date: Thu, 5 Oct 2023 14:07:06 -0700 Subject: [PATCH 9/9] Add dummy event for sync frequency --- hknweb/events/models/ical_view.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/hknweb/events/models/ical_view.py b/hknweb/events/models/ical_view.py index d665cae8..5739c6f1 100644 --- a/hknweb/events/models/ical_view.py +++ b/hknweb/events/models/ical_view.py @@ -1,10 +1,13 @@ +import random import uuid +from datetime import datetime, timedelta import icalendar from django.conf import settings from django.db import models from django.urls import reverse +from hknweb.events.models import Event from hknweb.events.utils import get_events @@ -31,4 +34,30 @@ def to_ical_obj(self): for event in events: cal.add_component(event.to_ical_obj()) + cal.add_component(self.dummy_event()) return cal + + def dummy_event(self): + # Google Calendar doesn't let you configure how often to sync iCal feeds + # like Apple's Calendar app does. They say this can take up to 24 hours. + + # According to https://webapps.stackexchange.com/a/66686, they probably + # look at how often the iCal feed itself changes and syncs more or less + # frequently based on that. + + # So we add a dummy event in the far future that's randomized every time + # the feed is requested in hopes of making Google Calendar sync faster. + + dt = datetime(3000, 1, 1) + timedelta(days=random.randrange(365)) + + event = icalendar.Event() + event.add("uid", "dummy") + event.add("summary", "Dummy Event") + event.add( + "description", + "Randomized dummy event to make Google Calendar sync faster", + ) + event.add("dtstart", dt) + event.add("dtend", dt) + event.add("dtstamp", dt) + return event