Skip to content

Commit 30fb47e

Browse files
committed
Add an API for cfbot to get the needed data
1 parent c0345bf commit 30fb47e

File tree

3 files changed

+192
-3
lines changed

3 files changed

+192
-3
lines changed

pgcommitfest/commitfest/apiv1.py

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
from django.http import (
22
HttpResponse,
33
)
4+
from django.shortcuts import get_object_or_404
45

56
import json
67
from datetime import date, datetime, timedelta, timezone
78

89
from .models import (
910
CommitFest,
11+
Patch,
12+
PatchOnCommitFest,
1013
)
1114

1215

1316
def datetime_serializer(obj):
14-
if isinstance(obj, date):
15-
return obj.isoformat()
16-
17+
# datetime must be checked before date, since datetime is a subclass of date
1718
if isinstance(obj, datetime):
1819
return obj.replace(tzinfo=timezone.utc).isoformat()
1920

21+
if isinstance(obj, date):
22+
return obj.isoformat()
23+
2024
if hasattr(obj, "to_json"):
2125
return obj.to_json()
2226

@@ -44,3 +48,70 @@ def commitfestst_that_need_ci(request):
4448
del cfs["final"]
4549

4650
return api_response({"commitfests": cfs})
51+
52+
53+
def commitfest_patches(request, cfid):
54+
"""Return all patches for a commitfest.
55+
56+
This endpoint provides the data that cfbot previously scraped from the
57+
commitfest HTML page.
58+
"""
59+
cf = get_object_or_404(CommitFest, pk=cfid)
60+
61+
pocs = (
62+
PatchOnCommitFest.objects.filter(commitfest=cf)
63+
.select_related("patch")
64+
.prefetch_related("patch__authors")
65+
.order_by("patch__id")
66+
)
67+
68+
patches = []
69+
for poc in pocs:
70+
patch = poc.patch
71+
authors = [f"{a.first_name} {a.last_name}" for a in patch.authors.all()]
72+
patches.append(
73+
{
74+
"id": patch.id,
75+
"name": patch.name,
76+
"status": poc.statusstring,
77+
"authors": authors,
78+
"last_email_time": patch.lastmail,
79+
}
80+
)
81+
82+
return api_response(
83+
{
84+
"commitfest_id": cf.id,
85+
"patches": patches,
86+
}
87+
)
88+
89+
90+
def patch_threads(request, patch_id):
91+
"""Return thread information for a patch.
92+
93+
This endpoint provides the data that cfbot previously scraped from the
94+
patch HTML page to construct thread URLs.
95+
"""
96+
patch = get_object_or_404(Patch, pk=patch_id)
97+
98+
threads = []
99+
for thread in patch.mailthread_set.all():
100+
latest_attachment = thread.mailthreadattachment_set.first()
101+
threads.append(
102+
{
103+
"messageid": thread.messageid,
104+
"subject": thread.subject,
105+
"latest_message_id": thread.latestmsgid,
106+
"latest_message_time": thread.latestmessage,
107+
"has_attachment": latest_attachment is not None,
108+
}
109+
)
110+
111+
return api_response(
112+
{
113+
"patch_id": patch.id,
114+
"name": patch.name,
115+
"threads": threads,
116+
}
117+
)

pgcommitfest/commitfest/tests/test_apiv1.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import json
2+
from datetime import datetime, timezone
23

34
import pytest
45

6+
from pgcommitfest.commitfest.models import (
7+
MailThread,
8+
Patch,
9+
PatchOnCommitFest,
10+
)
11+
512
pytestmark = pytest.mark.django_db
613

714

@@ -44,3 +51,112 @@ def test_needs_ci_endpoint(client, commitfests):
4451
}
4552

4653
assert data == expected
54+
55+
56+
def test_commitfest_patches_endpoint(client, open_cf, alice, bob):
57+
"""Test the /api/v1/commitfests/<id>/patches endpoint."""
58+
# Create test patches
59+
patch1 = Patch.objects.create(name="Add feature X")
60+
patch1.authors.add(alice)
61+
patch1.lastmail = datetime(2025, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
62+
patch1.save()
63+
64+
patch2 = Patch.objects.create(name="Fix bug Y")
65+
patch2.authors.add(alice, bob)
66+
patch2.save()
67+
68+
# Link patches to commitfest
69+
PatchOnCommitFest.objects.create(
70+
patch=patch1,
71+
commitfest=open_cf,
72+
enterdate=datetime.now(),
73+
status=PatchOnCommitFest.STATUS_REVIEW,
74+
)
75+
PatchOnCommitFest.objects.create(
76+
patch=patch2,
77+
commitfest=open_cf,
78+
enterdate=datetime.now(),
79+
status=PatchOnCommitFest.STATUS_AUTHOR,
80+
)
81+
82+
response = client.get(f"/api/v1/commitfests/{open_cf.id}/patches")
83+
84+
assert response.status_code == 200
85+
assert response["Content-Type"] == "application/json"
86+
assert response["Access-Control-Allow-Origin"] == "*"
87+
88+
data = json.loads(response.content)
89+
90+
assert data["commitfest_id"] == open_cf.id
91+
assert len(data["patches"]) == 2
92+
93+
# Patches are ordered by id
94+
p1 = data["patches"][0]
95+
assert p1["id"] == patch1.id
96+
assert p1["name"] == "Add feature X"
97+
assert p1["status"] == "Needs review"
98+
assert p1["authors"] == ["Alice Anderson"]
99+
assert p1["last_email_time"] == "2025-01-15T10:30:00+00:00"
100+
101+
p2 = data["patches"][1]
102+
assert p2["id"] == patch2.id
103+
assert p2["name"] == "Fix bug Y"
104+
assert p2["status"] == "Waiting on Author"
105+
assert sorted(p2["authors"]) == ["Alice Anderson", "Bob Brown"]
106+
assert p2["last_email_time"] is None
107+
108+
109+
def test_commitfest_patches_endpoint_not_found(client, commitfests):
110+
"""Test the patches endpoint returns 404 for non-existent commitfest."""
111+
response = client.get("/api/v1/commitfests/99999/patches")
112+
assert response.status_code == 404
113+
114+
115+
def test_patch_threads_endpoint(client, open_cf, alice):
116+
"""Test the /api/v1/patches/<id>/threads endpoint."""
117+
patch = Patch.objects.create(name="Test patch")
118+
patch.authors.add(alice)
119+
120+
PatchOnCommitFest.objects.create(
121+
patch=patch,
122+
commitfest=open_cf,
123+
enterdate=datetime.now(),
124+
status=PatchOnCommitFest.STATUS_REVIEW,
125+
)
126+
127+
# Create mail threads
128+
thread1 = MailThread.objects.create(
129+
messageid="abc123@example.com",
130+
subject="[PATCH] Test patch v1",
131+
firstmessage=datetime(2025, 1, 10, 9, 0, 0, tzinfo=timezone.utc),
132+
firstauthor="alice@example.com",
133+
latestmessage=datetime(2025, 1, 12, 14, 30, 0, tzinfo=timezone.utc),
134+
latestauthor="bob@example.com",
135+
latestsubject="Re: [PATCH] Test patch v1",
136+
latestmsgid="def456@example.com",
137+
)
138+
patch.mailthread_set.add(thread1)
139+
140+
response = client.get(f"/api/v1/patches/{patch.id}/threads")
141+
142+
assert response.status_code == 200
143+
assert response["Content-Type"] == "application/json"
144+
145+
data = json.loads(response.content)
146+
147+
assert data["patch_id"] == patch.id
148+
assert data["name"] == "Test patch"
149+
assert len(data["threads"]) == 1
150+
151+
t = data["threads"][0]
152+
assert t["messageid"] == "abc123@example.com"
153+
assert t["subject"] == "[PATCH] Test patch v1"
154+
assert t["latest_message_id"] == "def456@example.com"
155+
assert t["latest_message_time"] == "2025-01-12T14:30:00+00:00"
156+
assert t["has_attachment"] is False
157+
158+
159+
def test_patch_threads_endpoint_not_found(client, commitfests):
160+
"""Test the threads endpoint returns 404 for non-existent patch."""
161+
response = client.get("/api/v1/patches/99999/threads")
162+
assert response.status_code == 404

pgcommitfest/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
urlpatterns = [
1818
re_path(r"^$", views.home),
1919
re_path(r"^api/v1/commitfests/needs_ci$", apiv1.commitfestst_that_need_ci),
20+
re_path(r"^api/v1/commitfests/(\d+)/patches$", apiv1.commitfest_patches),
21+
re_path(r"^api/v1/patches/(\d+)/threads$", apiv1.patch_threads),
2022
re_path(r"^help/$", views.help),
2123
re_path(r"^commitfest_history/$", views.commitfest_history),
2224
re_path(r"^me/$", views.me_legacy_redirect),

0 commit comments

Comments
 (0)