From 90792d1bb4a8da73eaa277f9ceaef9e4054dcc23 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Mon, 9 Jun 2025 12:04:04 +0200
Subject: [PATCH 1/8] Introduce Drafts page and automatic CF creation
This introduce a new type of CommitFest a "Draft" CommitFest. This
CommitFest is never "In Progress", but it can be open. It exists for a
year. It opens when the final regular CommitFest of the year becomes "In
Progress" and stays open for exactly a year. It never becomes "In
Progress" itself.
Adding a second type of CommitFest also needed a redesign of quite a few
things, like the homepage. Also management of the CommitFests needed to
be made a bit easier, so admins don't forget to close/create Draft
CommitFests. So now, closing/opening/creating CommitFests is done
automatically when the time is right for that. A help page is also
introduced to explain the CommitFest app.
The naming of CommitFests has been changed too. Since we now have a
Draft CF every year that needs a name, it seemed reasonable to align the
names of the other CFs with that too. So each PG release cycle now has 5
regular commitfests that are called:
- PG18-1
- PG18-2
- PG18-3
- PG18-4
- PG18-Final
And a single Draft CommitFest, called:
- PG18-Draft
Finally, it also adds a small initial API endpoint for the CFBot, to
request the commitfests that need CI runs. Future PRs will extend this
API surface to also include/allow requesting the actual patches that CI
should run on.
Co-Authored-By: David G. Johnston
---
pgcommitfest/commitfest/apiv1.py | 46 +++
.../commitfest/fixtures/auth_data.json | 36 +++
.../commitfest/fixtures/commitfest_data.json | 91 +++++-
.../0011_add_draft_remove_future.py | 51 ++++
.../0012_add_status_related_constraints.py | 68 +++++
pgcommitfest/commitfest/models.py | 285 ++++++++++++++++--
.../commitfest/templates/all_commitfests.html | 10 +
pgcommitfest/commitfest/templates/help.html | 51 ++++
pgcommitfest/commitfest/templates/home.html | 89 +++++-
pgcommitfest/commitfest/templates/me.html | 9 +-
.../commitfest/templates/patch_commands.inc | 22 +-
.../commitfest/templatetags/commitfest.py | 1 +
pgcommitfest/commitfest/views.py | 213 ++++++-------
pgcommitfest/urls.py | 9 +-
14 files changed, 809 insertions(+), 172 deletions(-)
create mode 100644 pgcommitfest/commitfest/apiv1.py
create mode 100644 pgcommitfest/commitfest/migrations/0011_add_draft_remove_future.py
create mode 100644 pgcommitfest/commitfest/migrations/0012_add_status_related_constraints.py
create mode 100644 pgcommitfest/commitfest/templates/all_commitfests.html
create mode 100644 pgcommitfest/commitfest/templates/help.html
diff --git a/pgcommitfest/commitfest/apiv1.py b/pgcommitfest/commitfest/apiv1.py
new file mode 100644
index 00000000..a90ba4d3
--- /dev/null
+++ b/pgcommitfest/commitfest/apiv1.py
@@ -0,0 +1,46 @@
+from django.http import (
+ HttpResponse,
+)
+
+import json
+from datetime import date, datetime, timedelta, timezone
+
+from .models import (
+ CommitFest,
+)
+
+
+def datetime_serializer(obj):
+ if isinstance(obj, date):
+ return obj.isoformat()
+
+ if isinstance(obj, datetime):
+ return obj.replace(tzinfo=timezone.utc).isoformat()
+
+ if hasattr(obj, "to_json"):
+ return obj.to_json()
+
+ raise TypeError(f"Type {type(obj)} not serializable to JSON")
+
+
+def api_response(payload, status=200, content_type="application/json"):
+ response = HttpResponse(
+ json.dumps(payload, default=datetime_serializer), status=status
+ )
+ response["Content-Type"] = content_type
+ response["Access-Control-Allow-Origin"] = "*"
+ return response
+
+
+def commitfestst_that_need_ci(request):
+ cfs = CommitFest.relevant_commitfests()
+
+ # We continue to run CI on the previous commitfest for a week after it ends
+ # to give people some time to move patches over to the next one.
+ if cfs["previous"].enddate <= datetime.now(timezone.utc).date() - timedelta(days=7):
+ del cfs["previous"]
+
+ del cfs["next_open"]
+ del cfs["final"]
+
+ return api_response({"commitfests": cfs})
diff --git a/pgcommitfest/commitfest/fixtures/auth_data.json b/pgcommitfest/commitfest/fixtures/auth_data.json
index 88d8c708..9a6e2f03 100644
--- a/pgcommitfest/commitfest/fixtures/auth_data.json
+++ b/pgcommitfest/commitfest/fixtures/auth_data.json
@@ -88,5 +88,41 @@
"groups": [],
"user_permissions": []
}
+},
+{
+ "model": "auth.user",
+ "pk": 6,
+ "fields": {
+ "password": "",
+ "last_login": null,
+ "is_superuser": false,
+ "username": "prolific-author",
+ "first_name": "Prolific",
+ "last_name": "Author",
+ "email": "",
+ "is_staff": false,
+ "is_active": true,
+ "date_joined": "2025-01-01T00:00:00",
+ "groups": [],
+ "user_permissions": []
+ }
+},
+{
+ "model": "auth.user",
+ "pk": 7,
+ "fields": {
+ "password": "",
+ "last_login": null,
+ "is_superuser": false,
+ "username": "prolific-reviewer",
+ "first_name": "Prolific",
+ "last_name": "Reviewer",
+ "email": "",
+ "is_staff": false,
+ "is_active": true,
+ "date_joined": "2025-01-01T00:00:00",
+ "groups": [],
+ "user_permissions": []
+ }
}
]
diff --git a/pgcommitfest/commitfest/fixtures/commitfest_data.json b/pgcommitfest/commitfest/fixtures/commitfest_data.json
index 6e5b32ff..d51214dc 100644
--- a/pgcommitfest/commitfest/fixtures/commitfest_data.json
+++ b/pgcommitfest/commitfest/fixtures/commitfest_data.json
@@ -24,40 +24,44 @@
"model": "commitfest.commitfest",
"pk": 1,
"fields": {
- "name": "Sample Old Commitfest",
+ "name": "PG18-3",
"status": 4,
- "startdate": "2024-05-01",
- "enddate": "2024-05-31"
+ "startdate": "2024-11-01",
+ "enddate": "2024-11-30",
+ "draft": false
}
},
{
"model": "commitfest.commitfest",
"pk": 2,
"fields": {
- "name": "Sample In Progress Commitfest",
+ "name": "PG18-4",
"status": 3,
"startdate": "2025-01-01",
- "enddate": "2025-02-28"
+ "enddate": "2025-01-31",
+ "draft": false
}
},
{
"model": "commitfest.commitfest",
"pk": 3,
"fields": {
- "name": "Sample Open Commitfest",
+ "name": "PG18-Final",
"status": 2,
"startdate": "2025-03-01",
- "enddate": "2025-03-31"
+ "enddate": "2025-03-31",
+ "draft": false
}
},
{
"model": "commitfest.commitfest",
"pk": 4,
"fields": {
- "name": "Sample Future Commitfest",
- "status": 1,
- "startdate": "2025-05-01",
- "enddate": "2025-05-31"
+ "name": "PG18-Drafts",
+ "status": 2,
+ "startdate": "2024-03-01",
+ "enddate": "2025-02-28",
+ "draft": true
}
},
{
@@ -237,6 +241,33 @@
]
}
},
+{
+ "model": "commitfest.patch",
+ "pk": 8,
+ "fields": {
+ "name": "Test DGJ Multi-Author and Reviewer",
+ "topic": 3,
+ "wikilink": "",
+ "gitlink": "",
+ "targetversion": 1,
+ "committer": 4,
+ "created": "2025-02-01T00:00:00",
+ "modified": "2025-02-01T00:00:00",
+ "lastmail": "2025-02-01T00:00:00",
+ "authors": [
+ 3,
+ 6
+ ],
+ "reviewers": [
+ 1,
+ 7
+ ],
+ "subscribers": [],
+ "mailthread_set": [
+ 8
+ ]
+ }
+},
{
"model": "commitfest.patchoncommitfest",
"pk": 1,
@@ -325,6 +356,17 @@
"status": 1
}
},
+{
+ "model": "commitfest.patchoncommitfest",
+ "pk": 9,
+ "fields": {
+ "patch": 8,
+ "commitfest": 4,
+ "enterdate": "2025-02-01T00:00:00",
+ "leavedate": null,
+ "status": 1
+ }
+},
{
"model": "commitfest.patchhistory",
"pk": 1,
@@ -632,6 +674,33 @@
"latestmsgid": "example@message-31"
}
},
+{
+ "model": "commitfest.mailthread",
+ "pk": 8,
+ "fields": {
+ "messageid": "dgj-example@message-08",
+ "subject": "Test DGJ Multi-Author and Reviewer",
+ "firstmessage": "2025-02-01T00:00:00",
+ "firstauthor": "test@test.com",
+ "latestmessage": "2025-02-01T00:00:00",
+ "latestauthor": "test@test.com",
+ "latestsubject": "Test DGJ Multi-Author and Reviewer",
+ "latestmsgid": "dgj-example@message-08"
+ }
+},
+{
+ "model": "commitfest.mailthreadattachment",
+ "pk": 8,
+ "fields": {
+ "mailthread": 8,
+ "messageid": "dgj-example@message-08",
+ "attachmentid": 1,
+ "filename": "v1-0001-content.patch",
+ "date": "2025-02-01T00:00:00",
+ "author": "test@test.com",
+ "ispatch": true
+ }
+},
{
"model": "commitfest.patchstatus",
"pk": 1,
diff --git a/pgcommitfest/commitfest/migrations/0011_add_draft_remove_future.py b/pgcommitfest/commitfest/migrations/0011_add_draft_remove_future.py
new file mode 100644
index 00000000..740aa175
--- /dev/null
+++ b/pgcommitfest/commitfest/migrations/0011_add_draft_remove_future.py
@@ -0,0 +1,51 @@
+# Generated by Django 4.2.19 on 2025-06-08 10:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("commitfest", "0010_add_failing_since_column"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="commitfest",
+ name="draft",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name="commitfest",
+ name="status",
+ field=models.IntegerField(
+ choices=[(2, "Open"), (3, "In Progress"), (4, "Closed")], default=2
+ ),
+ ),
+ migrations.AlterField(
+ model_name="commitfest",
+ name="enddate",
+ field=models.DateField(),
+ ),
+ migrations.AlterField(
+ model_name="commitfest",
+ name="startdate",
+ field=models.DateField(),
+ ),
+ migrations.AlterField(
+ model_name="patchoncommitfest",
+ name="status",
+ field=models.IntegerField(
+ choices=[
+ (1, "Needs review"),
+ (2, "Waiting on Author"),
+ (3, "Ready for Committer"),
+ (4, "Committed"),
+ (5, "Moved to different CF"),
+ (6, "Rejected"),
+ (7, "Returned with feedback"),
+ (8, "Withdrawn"),
+ ],
+ default=1,
+ ),
+ ),
+ ]
diff --git a/pgcommitfest/commitfest/migrations/0012_add_status_related_constraints.py b/pgcommitfest/commitfest/migrations/0012_add_status_related_constraints.py
new file mode 100644
index 00000000..4340c5a1
--- /dev/null
+++ b/pgcommitfest/commitfest/migrations/0012_add_status_related_constraints.py
@@ -0,0 +1,68 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("commitfest", "0011_add_draft_remove_future"),
+ ]
+ operations = [
+ migrations.RunSQL(
+ """
+CREATE UNIQUE INDEX cf_enforce_maxoneopen_idx
+ON commitfest_commitfest (status, draft)
+WHERE status not in (1,4);
+""",
+ reverse_sql="""
+DROP INDEX IF EXISTS cf_enforce_maxoneopen_idx;
+""",
+ ),
+ migrations.RunSQL(
+ """
+CREATE UNIQUE INDEX poc_enforce_maxoneoutcome_idx
+ON commitfest_patchoncommitfest (patch_id)
+WHERE status not in (5);
+""",
+ reverse_sql="""
+DROP INDEX IF EXISTS poc_enforce_maxoneoutcome_idx;
+""",
+ ),
+ migrations.RunSQL(
+ """
+ALTER TABLE commitfest_patchoncommitfest
+ADD CONSTRAINT status_and_leavedate_correlation
+CHECK ((status IN (4,5,6,7,8)) = (leavedate IS NOT NULL));
+""",
+ reverse_sql="""
+ALTER TABLE commitfest_patchoncommitfest
+DROP CONSTRAINT IF EXISTS status_and_leavedate_correlation;
+""",
+ ),
+ migrations.RunSQL(
+ """
+COMMENT ON COLUMN commitfest_patchoncommitfest.leavedate IS
+$$A leave date is recorded in two situations, both of which
+means this particular patch-cf combination became inactive
+on the corresponding date. For status 5 the patch was moved
+to some other cf. For 4,6,7, and 8, this was the final cf.
+$$
+""",
+ reverse_sql="""
+COMMENT ON COLUMN commitfest_patchoncommitfest.leavedate IS NULL;
+""",
+ ),
+ migrations.RunSQL(
+ """
+COMMENT ON TABLE commitfest_patchoncommitfest IS
+$$This is a re-entrant table: patches may become associated
+with a given cf multiple times, resetting the entrydate and clearing
+the leavedate each time. Non-final statuses never have a leavedate
+while final statuses always do. The final status of 5 (moved) is
+special in that all but one of the rows a patch has in this table
+must have it as the status.
+$$
+""",
+ reverse_sql="""
+COMMENT ON TABLE commitfest_patchoncommitfest IS NULL;
+""",
+ ),
+ ]
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index fcd9edb9..f67ca1c5 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -1,8 +1,9 @@
from django.contrib.auth.models import User
-from django.db import models
+from django.db import models, transaction
+from django.db.models import Q
from django.shortcuts import get_object_or_404
-from datetime import datetime
+from datetime import datetime, timedelta, timezone
from pgcommitfest.userprofile.models import UserProfile
@@ -34,28 +35,26 @@ class Meta:
class CommitFest(models.Model):
- STATUS_FUTURE = 1
STATUS_OPEN = 2
STATUS_INPROGRESS = 3
STATUS_CLOSED = 4
_STATUS_CHOICES = (
- (STATUS_FUTURE, "Future"),
(STATUS_OPEN, "Open"),
(STATUS_INPROGRESS, "In Progress"),
(STATUS_CLOSED, "Closed"),
)
_STATUS_LABELS = (
- (STATUS_FUTURE, "default"),
(STATUS_OPEN, "info"),
(STATUS_INPROGRESS, "success"),
(STATUS_CLOSED, "danger"),
)
name = models.CharField(max_length=100, blank=False, null=False, unique=True)
status = models.IntegerField(
- null=False, blank=False, default=1, choices=_STATUS_CHOICES
+ null=False, blank=False, default=2, choices=_STATUS_CHOICES
)
- startdate = models.DateField(blank=True, null=True)
- enddate = models.DateField(blank=True, null=True)
+ startdate = models.DateField(blank=False, null=False)
+ enddate = models.DateField(blank=False, null=False)
+ draft = models.BooleanField(blank=False, null=False, default=False)
@property
def statusstring(self):
@@ -63,18 +62,195 @@ def statusstring(self):
@property
def periodstring(self):
- if self.startdate and self.enddate:
- return "{0} - {1}".format(self.startdate, self.enddate)
- return ""
+ return "{0} - {1}".format(self.startdate, self.enddate)
+
+ @property
+ def dev_cycle(self) -> int:
+ if self.startdate.month in [1, 3]:
+ return self.startdate.year - 2007
+ else:
+ return self.startdate.year - 2006
@property
def title(self):
return "Commitfest %s" % self.name
@property
- def isopen(self):
+ def is_closed(self):
+ return self.status == self.STATUS_CLOSED
+
+ @property
+ def is_open(self):
return self.status == self.STATUS_OPEN
+ @property
+ def is_open_regular(self):
+ return self.is_open and not self.draft
+
+ @property
+ def is_open_draft(self):
+ return self.is_open and self.draft
+
+ @property
+ def is_in_progress(self):
+ return self.status == self.STATUS_INPROGRESS
+
+ def to_json(self):
+ return {
+ "id": self.id,
+ "name": self.name,
+ "status": self.statusstring,
+ "startdate": self.startdate.isoformat(),
+ "enddate": self.enddate.isoformat(),
+ }
+
+ @staticmethod
+ def _are_relevant_commitfests_up_to_date(cfs, current_date):
+ inprogress_cf = cfs["in_progress"]
+
+ if inprogress_cf and inprogress_cf.enddate < current_date:
+ return False
+
+ if cfs["open"].startdate <= current_date:
+ return False
+
+ if not cfs["draft"] or cfs["draft"].enddate < current_date:
+ return False
+
+ return True
+
+ @classmethod
+ def _refresh_relevant_commitfests(cls, for_update):
+ cfs = CommitFest.relevant_commitfests(for_update=for_update, refresh=False)
+ current_date = datetime.now(timezone.utc).date()
+
+ if cls._are_relevant_commitfests_up_to_date(cfs, current_date):
+ return cfs
+
+ with transaction.atomic():
+ cfs = CommitFest.relevant_commitfests(for_update=True, refresh=False)
+ if cls._are_relevant_commitfests_up_to_date(cfs, current_date):
+ # Some other request has already updated the commitfests, so we
+ # return the new version
+ return cfs
+
+ inprogress_cf = cfs["in_progress"]
+ if inprogress_cf and inprogress_cf.enddate < current_date:
+ inprogress_cf.status = CommitFest.STATUS_CLOSED
+ inprogress_cf.save()
+
+ open_cf = cfs["open"]
+
+ if open_cf.startdate <= current_date:
+ if open_cf.enddate < current_date:
+ open_cf.status = CommitFest.STATUS_CLOSED
+ else:
+ open_cf.status = CommitFest.STATUS_INPROGRESS
+ open_cf.save()
+
+ cls.next_open_cf(current_date).save()
+
+ draft_cf = cfs["draft"]
+ if not draft_cf:
+ cls.next_draft_cf(current_date).save()
+ elif draft_cf.enddate < current_date:
+ # If the draft commitfest has started, we need to update it
+ draft_cf.status = CommitFest.STATUS_CLOSED
+ draft_cf.save()
+ cls.next_draft_cf(current_date).save()
+
+ return cls.relevant_commitfests(for_update=for_update)
+
+ @classmethod
+ def relevant_commitfests(cls, for_update=False, refresh=True):
+ if refresh:
+ return cls._refresh_relevant_commitfests(for_update=for_update)
+
+ query_base = CommitFest.objects.order_by("-enddate")
+ if for_update:
+ query_base = query_base.select_for_update(no_key=True)
+ last_three_commitfests = query_base.filter(draft=False)[:3]
+
+ cfs = {}
+ cfs["open"] = last_three_commitfests[0]
+
+ if last_three_commitfests[1].status == CommitFest.STATUS_INPROGRESS:
+ cfs["in_progress"] = last_three_commitfests[1]
+ cfs["previous"] = last_three_commitfests[2]
+
+ else:
+ cfs["in_progress"] = None
+ cfs["previous"] = last_three_commitfests[1]
+ if cfs["open"].startdate.month == 3:
+ cfs["final"] = cfs["open"]
+
+ if cfs["in_progress"] and cfs["in_progress"].startdate.month == 3:
+ cfs["final"] = cfs["in_progress"]
+ elif cfs["open"].startdate.month == 3:
+ cfs["final"] = cfs["open"]
+ else:
+ cfs["final"] = cls.next_open_cf(
+ datetime(year=cfs["open"].dev_cycle + 2007, month=2, day=1)
+ )
+
+ cfs["draft"] = query_base.filter(draft=True).order_by("-startdate").first()
+ cfs["next_open"] = cls.next_open_cf(cfs["open"].enddate + timedelta(days=1))
+
+ return cfs
+
+ @staticmethod
+ def next_open_cf(from_date):
+ # We don't have a CF in december, so we don't need to worry about 12 mod 12 being 0
+ cf_months = [7, 9, 11, 1, 3]
+ next_open_cf_month = min(
+ (month for month in cf_months if month > from_date.month), default=1
+ )
+ next_open_cf_year = from_date.year
+ if next_open_cf_month == 1:
+ next_open_cf_year += 1
+
+ next_open_dev_cycle = next_open_cf_year - 2006
+ if next_open_cf_month in [1, 3]:
+ next_open_dev_cycle -= 1
+
+ if next_open_cf_month == 3:
+ name = f"PG{next_open_dev_cycle}-Final"
+ else:
+ cf_number = cf_months.index(next_open_cf_month) + 1
+ name = f"PG{next_open_dev_cycle}-{cf_number}"
+ start_date = datetime(
+ year=next_open_cf_year, month=next_open_cf_month, day=1
+ ).date()
+ end_date = datetime(
+ year=next_open_cf_year, month=next_open_cf_month + 1, day=1
+ ).date() - timedelta(days=1)
+
+ return CommitFest(
+ name=name,
+ status=CommitFest.STATUS_OPEN,
+ startdate=start_date,
+ enddate=end_date,
+ )
+
+ @staticmethod
+ def next_draft_cf(start_date):
+ dev_cycle = start_date.year - 2006
+ if start_date.month < 3:
+ dev_cycle -= 1
+
+ end_year = dev_cycle + 2007
+
+ name = f"PG{dev_cycle}-Drafts"
+ end_date = datetime(year=end_year, month=3, day=1).date() - timedelta(days=1)
+
+ return CommitFest(
+ name=name,
+ status=CommitFest.STATUS_OPEN,
+ startdate=start_date,
+ enddate=end_date,
+ draft=True,
+ )
+
def __str__(self):
return self.name
@@ -102,6 +278,10 @@ def __str__(self):
return self.version
+class UserInputError(ValueError):
+ pass
+
+
class Patch(models.Model, DiffableModel):
name = models.CharField(
max_length=500, blank=False, null=False, verbose_name="Description"
@@ -159,11 +339,15 @@ class Patch(models.Model, DiffableModel):
}
def current_commitfest(self):
- return self.commitfests.order_by("-startdate").first()
+ return self.current_patch_on_commitfest().commitfest
def current_patch_on_commitfest(self):
- cf = self.current_commitfest()
- return get_object_or_404(PatchOnCommitFest, patch=self, commitfest=cf)
+ # The unique partial index poc_enforce_maxoneoutcome_idx stores the PoC
+ # No caching here (inside the instance) since the caller should just need
+ # the PoC once per request.
+ return get_object_or_404(
+ PatchOnCommitFest, Q(patch=self) & ~Q(status=PatchOnCommitFest.STATUS_MOVED)
+ )
# Some accessors
@property
@@ -208,6 +392,43 @@ def update_lastmail(self):
else:
self.lastmail = max(threads, key=lambda t: t.latestmessage).latestmessage
+ def move(self, from_cf, to_cf):
+ current_poc = self.current_patch_on_commitfest()
+ if from_cf.id != current_poc.commitfest.id:
+ raise UserInputError("Patch not in source commitfest.")
+
+ if from_cf.id == to_cf.id:
+ raise UserInputError("Source and target commitfest are the same.")
+
+ if current_poc.status not in (
+ PatchOnCommitFest.STATUS_REVIEW,
+ PatchOnCommitFest.STATUS_AUTHOR,
+ PatchOnCommitFest.STATUS_COMMITTER,
+ ):
+ raise UserInputError(
+ f"Patch in state {current_poc.statusstring} cannot be moved."
+ )
+
+ if not to_cf.is_open:
+ raise UserInputError("Patch can only be moved to an open commitfest")
+
+ old_status = current_poc.status
+
+ current_poc.set_status(PatchOnCommitFest.STATUS_MOVED)
+
+ new_poc, _ = PatchOnCommitFest.objects.update_or_create(
+ patch=current_poc.patch,
+ commitfest=to_cf,
+ defaults=dict(
+ status=old_status,
+ enterdate=datetime.now(),
+ leavedate=None,
+ ),
+ )
+ new_poc.save()
+ self.set_modified()
+ self.save()
+
def __str__(self):
return self.name
@@ -224,7 +445,7 @@ class PatchOnCommitFest(models.Model):
STATUS_AUTHOR = 2
STATUS_COMMITTER = 3
STATUS_COMMITTED = 4
- STATUS_NEXT = 5
+ STATUS_MOVED = 5
STATUS_REJECTED = 6
STATUS_RETURNED = 7
STATUS_WITHDRAWN = 8
@@ -233,7 +454,7 @@ class PatchOnCommitFest(models.Model):
(STATUS_AUTHOR, "Waiting on Author"),
(STATUS_COMMITTER, "Ready for Committer"),
(STATUS_COMMITTED, "Committed"),
- (STATUS_NEXT, "Moved to next CF"),
+ (STATUS_MOVED, "Moved to different CF"),
(STATUS_REJECTED, "Rejected"),
(STATUS_RETURNED, "Returned with feedback"),
(STATUS_WITHDRAWN, "Withdrawn"),
@@ -243,7 +464,7 @@ class PatchOnCommitFest(models.Model):
(STATUS_AUTHOR, "primary"),
(STATUS_COMMITTER, "info"),
(STATUS_COMMITTED, "success"),
- (STATUS_NEXT, "warning"),
+ (STATUS_MOVED, "warning"),
(STATUS_REJECTED, "danger"),
(STATUS_RETURNED, "danger"),
(STATUS_WITHDRAWN, "danger"),
@@ -273,10 +494,38 @@ def is_closed(self):
def is_open(self):
return not self.is_closed
+ @property
+ def is_committed(self):
+ return self.status == self.STATUS_COMMITTED
+
+ @property
+ def needs_committer(self):
+ return self.status == self.STATUS_COMMITTER
+
@property
def statusstring(self):
return [v for k, v in self._STATUS_CHOICES if k == self.status][0]
+ @classmethod
+ def current_for_patch(cls, patch_id):
+ return get_object_or_404(
+ cls, Q(patch_id=patch_id) & ~Q(status=cls.STATUS_MOVED)
+ )
+
+ def set_status(self, status):
+ self.status = status
+ if not self.leavedate and not self.is_open:
+ # If the patch was not closed before, we need to set the leavedate
+ # now.
+ self.leavedate = datetime.now()
+ elif self.is_open:
+ self.leavedate = None
+
+ self.patch.set_modified()
+
+ self.patch.save()
+ self.save()
+
class Meta:
unique_together = (
(
diff --git a/pgcommitfest/commitfest/templates/all_commitfests.html b/pgcommitfest/commitfest/templates/all_commitfests.html
new file mode 100644
index 00000000..6d983df1
--- /dev/null
+++ b/pgcommitfest/commitfest/templates/all_commitfests.html
@@ -0,0 +1,10 @@
+{%extends "base.html"%}
+{%block contents%}
+
+ {%for c in commitfests%}
+ {{c}} ({{c.statusstring}} - {{c.periodstring}})
+ {%endfor%}
+
+
+{%endblock%}
+
diff --git a/pgcommitfest/commitfest/templates/help.html b/pgcommitfest/commitfest/templates/help.html
new file mode 100644
index 00000000..c6234366
--- /dev/null
+++ b/pgcommitfest/commitfest/templates/help.html
@@ -0,0 +1,51 @@
+{%extends "base.html"%}
+{%block contents%}
+
+ This is the "CommitFest app", the website where the PostgreSQL community tracks proposed changes to PostgreSQL. If you're familiar with GitHub, then this website fulfills a similar purpose to the list of Pull Requests (PRs) on GitHub repo. The most important difference is that the CommitFest app is not the "source of truth", instead the PostgreSQL mailinglists are. The "CommitFest app" is simply a tool to help the communtiy keep track of changes proposed on the mailinglist in an organized manner. Below are the most important concepts that you should know about when using the CommitFest app.
+
+ CommitFest
+
+ PostgreSQL development is organized into "CommitFests" (often abbreviated to "CF"). Each CommitFest contains a list of entries called patches (similar to PRs, see below for details). The main purpose of CommitFests are to make sure patches that people are interested in are not forgotten about, as well as running CI (aka CFBot) on these patches. Each CommitFest has a period where it is "Open", in which people can add patches to the CommitFest. This "Open" period is followed by an "In Progress" period, in which the idea is that committers and reviewers focus on reviewing and committing the patches in this "In Progress" CommitFest. At the end of the month a CommitFest gets "Closed" (and stays closed forever). Any not yet committed patches can be moved to the following "Open" CommitFest by their authors, to try again in the next cycle. Having these timebound periods has several benefits:
+
+ It gives a regular cadence of development to people who like to have this.
+ It provides a natural age-out mechanism for patches that the submitter has lost interest in.
+ It provides an easy way for reviewers/committers to prioritize which patches to review.
+
+ This does not mean that patches are only committed during a CommitFest, nor that people will only respond on the mainlinglist to patches in the "In Progress" CommitFest.
+
+ There are 5 CommitFests per year. The first one is "In Progress" in July and starts the nine months feature development cycle of PostgreSQL. The next three are "In Progress" in September , November and January . The last CommitFest of the feature development cycle is "In Progress" in March , and ends a when the feature freeze starts. The exact date of the feature freeze depends on the year, but it's usually in early April.
+
+ Patches
+
+ A "patch" is a bit of an overloaded term in the PostgreSQL community. Email threads on the mailing list often contain "patch files" as attachments, such a file is often referred to as a "patch". A single email can even contain multiple related "patch files", which are called a "patchset". However, in the context of a CommitFest app a "patch" usually means a "patch entry" in the CommitFest app. Such a "patch entry" is a reference to a mailinglist thread on which change to PostgreSQL has been proposed, by someone sending an email that contain one or more "patch files". The CommitFest app will automatically detect new versions of the patch files and update the "patch entry" accordingly.
+
+
+ There are three active categories of patch status:
+
+ Waiting on Author - the author needs to make changes
+ Needs Reviewer - the patch needs guidance or a review
+ Ready for Committer - the patch is ready to be committed
+
+ And there are three preferred inactive categories of patch status for when
+ a patch has been resolved, either by being committed or not:
+
+ Committed - a committer has applied the patches to the git repo
+ Withdrawn - the author has withdrawn the patch from consideration
+ Rejected - a committer has decided that the patch should not be applied
+
+
+
+ Drafts
+
+ Appart from the regular CommitFests, there is also a "Drafts" CommitFest that is used to collect patches for new features that are not yet ready for general reviewing. There are various reasons why a patch might not be ready for general reviewing but the author still wants to track it publicly in the "CommitFest app", this is usually due to the combination of one of the following reasons:
+
+ The author has temporarily lost interest, but expects to come back in the future.
+ The author does not want to forget abuot
+ The author wants feedback from a specific subset of people before requesting general feedback
+ The author wants to have CI run on the patch, while they are polishing it further
+ The author would like to be notified when the patch is in need of a rebase
+
+ Like regular CommitFests, a Draft CommitFest also has an "Open" period and a "Closed" state, but it has no "In Progress" period. The "Open" period for a Draft CommitFest last lasts a year. When the last CommitFest of the development cycle becomes "In Progress", the Draft CommitFest for that PostgreSQL version is closed, and a new one is immediately opened for the next PostgreSQL release.
+
+ Another difference between Draft CommitFests and regular CommitFests is that Draft CommitFests don't list resolved patches.
+{%endblock%}
diff --git a/pgcommitfest/commitfest/templates/home.html b/pgcommitfest/commitfest/templates/home.html
index a3f26da0..0933fe1d 100644
--- a/pgcommitfest/commitfest/templates/home.html
+++ b/pgcommitfest/commitfest/templates/home.html
@@ -1,11 +1,82 @@
{%extends "base.html"%}
{%block contents%}
- {%if inprogresscf%}A commitfest is currently in progress: {{inprogresscf}} .{%endif%}
+ First time user? Here is a help page for you to understand how this website works.
- Useful links that you can use and bookmark:
+
+
+
+
+
+ Details
+ In Progress
+
+
+
+
+ Open:
+ {{cfs.open}}
+ {{cfs.open.periodstring}}
+
+
+ In Progress:
+
+ {%if cfs.in_progress %}
+ {{cfs.in_progress}}
+ {%else%}
+ None in progress
+ {%endif%}
+
+
+ {%if cfs.in_progress %}
+ {{cfs.in_progress.periodstring}}
+ {%endif%}
+
+
+
+ Previous:
+ {{cfs.previous}}
+ {{cfs.previous.periodstring}}
+
+
+ Draft:
+ {{cfs.draft}}
+ {{cfs.draft.periodstring}}
+
+
+ Next open:
+ {{cfs.next_open}}
+ {{cfs.next_open.periodstring}}
+
+
+ Final of this release:
+
+ {%if cfs.final.id %}
+ {{cfs.final}}
+ {%else%}
+ {{cfs.final}}
+ {%endif%}
+
+
+ {{cfs.final.periodstring}}
+
+
+
+
+ Search
+
+ Useful pages (bookmarkable)
- Commands
-
- List of commitfests
-
{%endblock%}
diff --git a/pgcommitfest/commitfest/templates/me.html b/pgcommitfest/commitfest/templates/me.html
index b708d4de..b6736124 100644
--- a/pgcommitfest/commitfest/templates/me.html
+++ b/pgcommitfest/commitfest/templates/me.html
@@ -2,8 +2,13 @@
{%load commitfest %}
{%block contents%}
New patch
- Current commitfest
- Open commitfest
+ {%if cfs.in_progress%}
+ In Progress commitfest
+ {%endif%}
+ Open commitfest
+ {%if cfs.draft%}
+ Draft commitfest
+ {%endif%}
Search/filter