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

from django.contrib.syndication.views import Feed

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 +46,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.
Comment on lines +53 to +54

@JelteF JelteF Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does "all-day' have timezone? If so it should be UTC.

"""
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%}

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

from __future__ import annotations

from datetime import timedelta
from typing import TYPE_CHECKING

import pytest
from icalendar import Calendar

if TYPE_CHECKING:
from pgcommitfest.commitfest.models import CommitFest

pytestmark = pytest.mark.django_db


def test_commitfests_ics(client, commitfests: dict[str, CommitFest]):
"""
Every non-draft commitfest, regardless of status, shows up as an all-day
VEVENT with DTEND being the day after enddate (exclusive).
"""
commitfests = {k: cf for k, cf in commitfests.items() if not cf.draft}

response = client.get("/commitfests.ics")
assert response.status_code == 200
assert response["Content-Type"] == "text/calendar; charset=utf-8"

events = Calendar.from_ical(response.content).walk("VEVENT")
assert len(events) == len(commitfests)
events_by_uid = {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)
16 changes: 15 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,19 @@ def archive(request):
)


def commitfests_ics(_: HttpRequest) -> HttpResponse:
"""
Calendar feed of all non-draft commitfests in iCalendar format (RFC 5545).
"""
commitfests = CommitFest.objects.filter(draft=False).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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We don't actually use pyproject.toml dependencies in prod/staging. We'd need infra to install the python-icalendar package on the debian machines that host the commitfest app.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I requested this on the sysadmins list

"psycopg2",
"simplejson",
"requests",
Expand Down
Loading