Skip to content
Open
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
41 changes: 41 additions & 0 deletions pgcommitfest/commitfest/feeds.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
from __future__ import annotations

from django.contrib.syndication.views import Feed

from collections.abc import Iterable
from datetime import timedelta
from typing import TYPE_CHECKING

from icalendar import Calendar, Event

if TYPE_CHECKING:
from collections.abc import Iterable

from .models import CommitFest


class ActivityFeed(Feed):
title = description = "Commitfest Activity Log"
Expand Down Expand Up @@ -34,3 +47,31 @@ def item_link(self, item):

def item_pubdate(self, item):
return item["date"]


def calendar(commitfests: Iterable[CommitFest]) -> Calendar:
"""
Build an RFC 5545 iCalendar document with one all-day VEVENT per
commitfest, covering its start and end date.
"""
calendar = Calendar.new(
prodid="-//PostgreSQL Commitfest//commitfest.postgresql.org//EN",
name="PostgreSQL Commitfests",
description="Start and end dates of PostgreSQL Commitfests",
method="PUBLISH",
)

for cf in commitfests:
event = Event.new(
uid=f"commitfest-{cf.name}@commitfest.postgresql.org",
summary=cf.title,
start=cf.startdate,
# DTEND is exclusive for all-day events, so add a day to make the
# last day of the commitfest show up as included on the calendar.
end=cf.enddate + timedelta(days=1),
description=f"Status: {cf.statusstring}",
url=f"https://commitfest.postgresql.org/{cf.id}/",
)
calendar.add_component(event)

return calendar
2 changes: 1 addition & 1 deletion pgcommitfest/commitfest/templates/all_commitfests.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{%extends "base.html"%}
{%block contents%}
<p><a href="/commitfests.ics">Subscribe to Commitfest dates (ICS)</a></p>
<ul>
{%for c in commitfests%}
<li><a href="/{{c.id}}/">{{c}}</a> ({{c.statusstring}} - {{c.periodstring}})</li>
{%endfor%}
</ul>
<br/>
{%endblock%}

2 changes: 1 addition & 1 deletion pgcommitfest/commitfest/templates/archive.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{%extends "base.html"%}
{%block contents%}
<p><a href="/commitfests.ics">Subscribe to Commitfest dates (ICS)</a></p>
<ul>
{%for c in commitfests%}
<li><a href="/{{c.id}}/">{{c}}</a> ({{c.statusstring}}{%if c.startdate%} - {{c.periodstring}}{%endif%})</li>
{%endfor%}
</ul>
{%endblock%}

57 changes: 57 additions & 0 deletions pgcommitfest/commitfest/tests/test_ics_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Tests for the /commitfests.ics calendar feed.
"""

from __future__ import annotations

from datetime import timedelta
from typing import TYPE_CHECKING, cast

import pytest
from icalendar import Calendar, Event

if TYPE_CHECKING:
from pgcommitfest.commitfest.models import CommitFest

pytestmark = pytest.mark.django_db


def _get_calendar(client) -> Calendar:
response = client.get("/commitfests.ics")
assert response.status_code == 200
assert response["Content-Type"] == "text/calendar; charset=utf-8"
return Calendar.from_ical(response.content)


def _get_events(calendar: Calendar) -> list[Event]:
return cast("list[Event]", calendar.walk("VEVENT"))


def test_commitfests_ics_empty(client):
"""
With no commitfests, we still get a valid, empty calendar.
"""
calendar = _get_calendar(client)

assert len(_get_events(calendar)) == 0


def test_commitfests_ics_contains_all_commitfests(client, commitfests: dict[str, CommitFest]):
"""
Every commitfest, regardless of status, shows up as a VEVENT with the right
dates, DTEND being the day after enddate (exclusive).
"""
calendar = _get_calendar(client)

events = _get_events(calendar)
assert len(events) == len(commitfests)

events_by_uid = {cast("str", event.uid): event for event in events}

for cf in commitfests.values():
event = events_by_uid[f"commitfest-{cf.name}@commitfest.postgresql.org"]
assert event.summary == f"Commitfest {cf.name}"
assert event.url == f"https://commitfest.postgresql.org/{cf.pk}/"
assert event.description == f"Status: {cf.statusstring}"
assert event.start == cf.startdate
assert event.end == cf.enddate + timedelta(days=1)
17 changes: 16 additions & 1 deletion pgcommitfest/commitfest/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db.models import Count, Q
from django.http import (
Http404,
HttpRequest,
HttpResponse,
HttpResponseForbidden,
HttpResponseRedirect,
Expand All @@ -27,7 +28,7 @@
from pgcommitfest.userprofile.util import UserWrapper

from .ajax import _archivesAPI, doAttachThread, refresh_single_thread
from .feeds import ActivityFeed
from .feeds import ActivityFeed, calendar
from .forms import (
BulkEmailForm,
CommentForm,
Expand Down Expand Up @@ -195,6 +196,20 @@ def archive(request):
)


def commitfests_ics(_: HttpRequest) -> HttpResponse:
"""
Calendar feed of all commitfests in iCalendar format (RFC 5545).
"""
commitfests = CommitFest.objects.order_by("startdate")

response = HttpResponse(
calendar(commitfests).to_ical(),
content_type="text/calendar; charset=utf-8"
)
response["Content-Disposition"] = 'inline; filename="commitfests.ics"'
return response


def activity(request, cfid=None, rss=None):
# Number of notes to fetch
if rss:
Expand Down
1 change: 1 addition & 0 deletions pgcommitfest/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
re_path(r"^commitfest_history/$", views.commitfest_history),
re_path(r"^me/$", views.me_legacy_redirect),
re_path(r"^archive/$", views.archive),
re_path(r"^commitfests\.ics$", views.commitfests_ics),
re_path(r"^activity(?P<rss>\.rss)?/", views.activity),
re_path(r"^(\d+)/$", views.commitfest),
re_path(r"^(open|inprogress|current|draft)/(.*)$", views.redir),
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ readme = "README.md"
license = "PostgreSQL"
dependencies = [
"django>=5.2,<6.0",
"icalendar",
"psycopg2",
"simplejson",
"requests",
Expand Down