Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose personal calendar in iCal format instead of provisioning Google Calendars #558

Merged
merged 9 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions hknweb/events/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions hknweb/events/admin/ical_view.py
Original file line number Diff line number Diff line change
@@ -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",
]
39 changes: 39 additions & 0 deletions hknweb/events/migrations/0012_icalview.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
],
),
]
1 change: 1 addition & 0 deletions hknweb/events/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 28 additions & 5 deletions hknweb/events/models/event.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)

Expand Down
63 changes: 63 additions & 0 deletions hknweb/events/models/ical_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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


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)

@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")
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())

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
12 changes: 7 additions & 5 deletions hknweb/events/models/rsvp.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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 super().save(*args, **kwargs)

if self.google_calendar_event_id is None:
self.google_calendar_event_id = gcal.create_event(
Expand Down
3 changes: 2 additions & 1 deletion hknweb/events/urls.py
Original file line number Diff line number Diff line change
@@ -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/<uuid:id>.ics", views.ical, name="ical"),
path("leaderboard", views.get_leaderboard, name="leaderboard"),
path("photos", views.photos, name="photos"),
]
Expand Down
31 changes: 31 additions & 0 deletions hknweb/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions hknweb/events/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from hknweb.events.views.aggregate_displays import (
index,
ical,
get_leaderboard,
photos,
)
Expand Down
2 changes: 1 addition & 1 deletion hknweb/events/views/aggregate_displays/__init__.py
Original file line number Diff line number Diff line change
@@ -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
39 changes: 25 additions & 14 deletions hknweb/events/views/aggregate_displays/calendar.py
Original file line number Diff line number Diff line change
@@ -1,12 +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 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
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
Expand All @@ -28,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(), content_type="text/calendar")


def calendar_helper(
request,
title,
Expand All @@ -37,15 +46,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:
Expand Down Expand Up @@ -85,11 +86,21 @@ 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": SHARE_LINK_TEMPLATE.format(cid=ical_url),
}
)

for calendar in calendars[:-1]:
calendar["separator"] = "/"
if len(calendars) > 0:
Expand Down
3 changes: 3 additions & 0 deletions hknweb/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Loading