From ddfe04dc3e48bc59f1efe6081af51137aa20b76a Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Tue, 9 Dec 2025 23:34:54 +0100
Subject: [PATCH 01/16] Send email for CF close
---
pgcommitfest/commitfest/models.py | 76 ++++++-
.../templates/mail/commitfest_closure.txt | 21 ++
.../tests/test_closure_notifications.py | 196 ++++++++++++++++++
3 files changed, 292 insertions(+), 1 deletion(-)
create mode 100644 pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
create mode 100644 pgcommitfest/commitfest/tests/test_closure_notifications.py
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 26444990..3cad00e9 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -6,6 +6,7 @@
from datetime import datetime, timedelta, timezone
+from pgcommitfest.mailqueue.util import send_template_mail
from pgcommitfest.userprofile.models import UserProfile
from .util import DiffableModel
@@ -109,6 +110,75 @@ def to_json(self):
"enddate": self.enddate.isoformat(),
}
+ def send_closure_notifications(self):
+ """Send email notifications to authors of open patches when this commitfest is closed."""
+
+ # Get all patches with open status in this commitfest
+ open_patches = (
+ self.patchoncommitfest_set.filter(
+ status__in=[
+ PatchOnCommitFest.STATUS_REVIEW,
+ PatchOnCommitFest.STATUS_AUTHOR,
+ PatchOnCommitFest.STATUS_COMMITTER,
+ ]
+ )
+ .select_related("patch")
+ .prefetch_related("patch__authors")
+ )
+
+ # Collect unique authors across all open patches
+ authors_to_notify = set()
+ for poc in open_patches:
+ for author in poc.patch.authors.all():
+ if author.email:
+ authors_to_notify.add(author)
+
+ # Get the next open commitfest if available
+ next_cf = (
+ CommitFest.objects.filter(
+ status=CommitFest.STATUS_OPEN,
+ draft=False,
+ startdate__gt=self.enddate,
+ )
+ .order_by("startdate")
+ .first()
+ )
+
+ if next_cf:
+ next_cf_url = f"https://commitfest.postgresql.org/{next_cf.id}/"
+ else:
+ next_cf_url = "https://commitfest.postgresql.org/"
+
+ # Send email to each author
+ for author in authors_to_notify:
+ # Get user's notification email preference
+ email = author.email
+ try:
+ if author.userprofile and author.userprofile.notifyemail:
+ email = author.userprofile.notifyemail.email
+ except UserProfile.DoesNotExist:
+ pass
+
+ # Get user's open patches in this commitfest
+ user_patches = [
+ poc for poc in open_patches if author in poc.patch.authors.all()
+ ]
+
+ send_template_mail(
+ settings.NOTIFICATION_FROM,
+ None,
+ email,
+ f"Commitfest {self.name} has closed",
+ "mail/commitfest_closure.txt",
+ {
+ "user": author,
+ "commitfest": self,
+ "patches": user_patches,
+ "next_cf": next_cf,
+ "next_cf_url": next_cf_url,
+ },
+ )
+
@staticmethod
def _are_relevant_commitfests_up_to_date(cfs, current_date):
inprogress_cf = cfs["in_progress"]
@@ -143,15 +213,18 @@ def _refresh_relevant_commitfests(cls, for_update):
if inprogress_cf and inprogress_cf.enddate < current_date:
inprogress_cf.status = CommitFest.STATUS_CLOSED
inprogress_cf.save()
+ inprogress_cf.send_closure_notifications()
open_cf = cfs["open"]
if open_cf.startdate <= current_date:
if open_cf.enddate < current_date:
open_cf.status = CommitFest.STATUS_CLOSED
+ open_cf.save()
+ open_cf.send_closure_notifications()
else:
open_cf.status = CommitFest.STATUS_INPROGRESS
- open_cf.save()
+ open_cf.save()
cls.next_open_cf(current_date).save()
@@ -162,6 +235,7 @@ def _refresh_relevant_commitfests(cls, for_update):
# If the draft commitfest has started, we need to update it
draft_cf.status = CommitFest.STATUS_CLOSED
draft_cf.save()
+ draft_cf.send_closure_notifications()
cls.next_draft_cf(current_date).save()
return cls.relevant_commitfests(for_update=for_update)
diff --git a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
new file mode 100644
index 00000000..4d47177c
--- /dev/null
+++ b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
@@ -0,0 +1,21 @@
+Hello {{user.first_name|default:user.username}},
+
+Commitfest {{commitfest.name}} has now closed. You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} in this commitfest:
+
+{%for poc in patches%}
+ - {{poc.patch.name}}
+ https://commitfest.postgresql.org/{{commitfest.id}}/{{poc.patch.id}}/
+{%endfor%}
+
+Please take action on {{patches|length|pluralize:"this patch,these patches"}}:
+
+1. If you want to continue working on {{patches|length|pluralize:"it,them"}}, move {{patches|length|pluralize:"it,them"}} to the next commitfest{% if next_cf %}: {{next_cf_url}}{% endif %}
+
+2. If you no longer wish to pursue {{patches|length|pluralize:"this patch,these patches"}}, please close {{patches|length|pluralize:"it,them"}} with an appropriate status (Withdrawn, Returned with feedback, etc.)
+
+{% if next_cf %}The next commitfest is {{next_cf.name}}, which runs from {{next_cf.startdate}} to {{next_cf.enddate}}.{% else %}Please check https://commitfest.postgresql.org/ for upcoming commitfests.{% endif %}
+
+Thank you for your contributions to PostgreSQL!
+
+--
+This is an automated message from the PostgreSQL Commitfest application.
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
new file mode 100644
index 00000000..573882e9
--- /dev/null
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -0,0 +1,196 @@
+import base64
+from datetime import datetime
+from email import message_from_string
+
+import pytest
+
+from pgcommitfest.commitfest.models import Patch, PatchOnCommitFest, Topic
+from pgcommitfest.mailqueue.models import QueuedMail
+
+pytestmark = pytest.mark.django_db
+
+
+def get_email_body(queued_mail):
+ """Extract and decode the email body from a QueuedMail object."""
+ msg = message_from_string(queued_mail.fullmsg)
+ for part in msg.walk():
+ if part.get_content_type() == "text/plain":
+ payload = part.get_payload()
+ return base64.b64decode(payload).decode("utf-8")
+ return ""
+
+
+@pytest.fixture
+def topic():
+ """Create a test topic."""
+ return Topic.objects.create(topic="General")
+
+
+def test_send_closure_notifications_to_authors_of_open_patches(
+ alice, in_progress_cf, topic
+):
+ """Authors of patches with open status should receive closure notifications."""
+ patch = Patch.objects.create(name="Test Patch", topic=topic)
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 1
+ mail = QueuedMail.objects.first()
+ assert mail.receiver == alice.email
+ assert f"Commitfest {in_progress_cf.name} has closed" in mail.fullmsg
+ body = get_email_body(mail)
+ assert "Test Patch" in body
+
+
+def test_no_notification_for_committed_patches(alice, in_progress_cf, topic):
+ """Authors of committed patches should not receive notifications."""
+ patch = Patch.objects.create(name="Committed Patch", topic=topic)
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ leavedate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_COMMITTED,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 0
+
+
+def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, topic):
+ """Authors of withdrawn patches should not receive notifications."""
+ patch = Patch.objects.create(name="Withdrawn Patch", topic=topic)
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ leavedate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_WITHDRAWN,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 0
+
+
+def test_one_email_per_author_with_multiple_patches(alice, in_progress_cf, topic):
+ """An author with multiple open patches should receive one email listing all patches."""
+ patch1 = Patch.objects.create(name="Patch One", topic=topic)
+ patch1.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch1,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ patch2 = Patch.objects.create(name="Patch Two", topic=topic)
+ patch2.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch2,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_AUTHOR,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 1
+ mail = QueuedMail.objects.first()
+ body = get_email_body(mail)
+ assert "Patch One" in body
+ assert "Patch Two" in body
+
+
+def test_multiple_authors_receive_separate_emails(alice, bob, in_progress_cf, topic):
+ """Each author of open patches should receive their own notification."""
+ patch1 = Patch.objects.create(name="Alice Patch", topic=topic)
+ patch1.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch1,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ patch2 = Patch.objects.create(name="Bob Patch", topic=topic)
+ patch2.authors.add(bob)
+ PatchOnCommitFest.objects.create(
+ patch=patch2,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_COMMITTER,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 2
+ receivers = set(QueuedMail.objects.values_list("receiver", flat=True))
+ assert receivers == {alice.email, bob.email}
+
+
+def test_notification_includes_next_commitfest_info(alice, in_progress_cf, open_cf, topic):
+ """Notification should include information about the next open commitfest."""
+ patch = Patch.objects.create(name="Test Patch", topic=topic)
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ mail = QueuedMail.objects.first()
+ body = get_email_body(mail)
+ assert open_cf.name in body
+ assert f"https://commitfest.postgresql.org/{open_cf.id}/" in body
+
+
+def test_coauthors_both_receive_notification(alice, bob, in_progress_cf, topic):
+ """Both co-authors of a patch should receive notifications."""
+ patch = Patch.objects.create(name="Coauthored Patch", topic=topic)
+ patch.authors.add(alice)
+ patch.authors.add(bob)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 2
+ receivers = set(QueuedMail.objects.values_list("receiver", flat=True))
+ assert receivers == {alice.email, bob.email}
+
+
+def test_no_notification_for_author_without_email(bob, in_progress_cf, topic):
+ """Authors without email addresses should be skipped."""
+ bob.email = ""
+ bob.save()
+
+ patch = Patch.objects.create(name="Test Patch", topic=topic)
+ patch.authors.add(bob)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 0
From f5c8b3796b4d1f628eaada29f0a74b3ea04b82ce Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Thu, 11 Dec 2025 13:26:16 +0100
Subject: [PATCH 02/16] Automatically move "active" patches
---
pgcommitfest/commitfest/models.py | 142 ++++++--
.../templates/mail/commitfest_closure.txt | 14 +-
pgcommitfest/commitfest/tests/conftest.py | 7 +-
.../tests/test_closure_notifications.py | 334 +++++++++++++++++-
pgcommitfest/settings.py | 6 +
5 files changed, 466 insertions(+), 37 deletions(-)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 3cad00e9..796c9eb2 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -110,11 +110,103 @@ def to_json(self):
"enddate": self.enddate.isoformat(),
}
- def send_closure_notifications(self):
- """Send email notifications to authors of open patches when this commitfest is closed."""
+ def _should_auto_move_patch(self, patch, current_date):
+ """Determine if a patch should be automatically moved to the next commitfest.
+
+ A patch qualifies for auto-move if it both:
+ 1. Has had email activity within the configured number of days
+ 2. Hasn't been failing CI for longer than the configured threshold
+ """
+ activity_cutoff = current_date - timedelta(
+ days=settings.AUTO_MOVE_EMAIL_ACTIVITY_DAYS
+ )
+ failing_cutoff = current_date - timedelta(
+ days=settings.AUTO_MOVE_MAX_FAILING_DAYS
+ )
+
+ # Check for recent email activity
+ if not patch.lastmail or patch.lastmail < activity_cutoff:
+ return False
+
+ # Check if CI has been failing too long
+ try:
+ cfbot_branch = patch.cfbot_branch
+ if (
+ cfbot_branch.failing_since
+ and cfbot_branch.failing_since < failing_cutoff
+ ):
+ return False
+ except CfbotBranch.DoesNotExist:
+ pass
+
+ return True
+
+ def auto_move_active_patches(self):
+ """Automatically move active patches to the next commitfest.
+
+ A patch is moved if it has recent email activity and hasn't been
+ failing CI for too long.
+
+ Returns a set of patch IDs that were moved.
+ """
+ current_date = datetime.now()
+
+ # Get the next open commitfest
+ # For draft CFs, find the next draft CF
+ # For regular CFs, find the next regular CF by start date
+ if self.draft:
+ next_cf = (
+ CommitFest.objects.filter(
+ status=CommitFest.STATUS_OPEN,
+ draft=True,
+ startdate__gt=self.enddate,
+ )
+ .order_by("startdate")
+ .first()
+ )
+ else:
+ next_cf = (
+ CommitFest.objects.filter(
+ status=CommitFest.STATUS_OPEN,
+ draft=False,
+ startdate__gt=self.enddate,
+ )
+ .order_by("startdate")
+ .first()
+ )
+
+ if not next_cf:
+ return set()
# Get all patches with open status in this commitfest
- open_patches = (
+ open_pocs = self.patchoncommitfest_set.filter(
+ status__in=[
+ PatchOnCommitFest.STATUS_REVIEW,
+ PatchOnCommitFest.STATUS_AUTHOR,
+ PatchOnCommitFest.STATUS_COMMITTER,
+ ]
+ ).select_related("patch")
+
+ moved_patch_ids = set()
+ for poc in open_pocs:
+ if self._should_auto_move_patch(poc.patch, current_date):
+ poc.patch.move(self, next_cf, by_user=None, by_cfbot=True)
+ moved_patch_ids.add(poc.patch.id)
+
+ return moved_patch_ids
+
+ def send_closure_notifications(self, moved_patch_ids=None):
+ """Send email notifications to authors of patches that weren't auto-moved.
+
+ Args:
+ moved_patch_ids: Set of patch IDs that were auto-moved to the next commitfest.
+ These patches are excluded since the move triggers its own notification.
+ """
+ if moved_patch_ids is None:
+ moved_patch_ids = set()
+
+ # Get patches that still need action (not moved, not closed)
+ open_pocs = list(
self.patchoncommitfest_set.filter(
status__in=[
PatchOnCommitFest.STATUS_REVIEW,
@@ -122,22 +214,19 @@ def send_closure_notifications(self):
PatchOnCommitFest.STATUS_COMMITTER,
]
)
+ .exclude(patch_id__in=moved_patch_ids)
.select_related("patch")
.prefetch_related("patch__authors")
)
- # Collect unique authors across all open patches
- authors_to_notify = set()
- for poc in open_patches:
- for author in poc.patch.authors.all():
- if author.email:
- authors_to_notify.add(author)
+ if not open_pocs:
+ return
# Get the next open commitfest if available
next_cf = (
CommitFest.objects.filter(
status=CommitFest.STATUS_OPEN,
- draft=False,
+ draft=self.draft,
startdate__gt=self.enddate,
)
.order_by("startdate")
@@ -149,8 +238,18 @@ def send_closure_notifications(self):
else:
next_cf_url = "https://commitfest.postgresql.org/"
+ # Collect unique authors and their patches
+ authors_patches = {}
+ for poc in open_pocs:
+ for author in poc.patch.authors.all():
+ if not author.email:
+ continue
+ if author not in authors_patches:
+ authors_patches[author] = []
+ authors_patches[author].append(poc)
+
# Send email to each author
- for author in authors_to_notify:
+ for author, patches in authors_patches.items():
# Get user's notification email preference
email = author.email
try:
@@ -159,11 +258,6 @@ def send_closure_notifications(self):
except UserProfile.DoesNotExist:
pass
- # Get user's open patches in this commitfest
- user_patches = [
- poc for poc in open_patches if author in poc.patch.authors.all()
- ]
-
send_template_mail(
settings.NOTIFICATION_FROM,
None,
@@ -173,7 +267,7 @@ def send_closure_notifications(self):
{
"user": author,
"commitfest": self,
- "patches": user_patches,
+ "patches": patches,
"next_cf": next_cf,
"next_cf_url": next_cf_url,
},
@@ -211,17 +305,19 @@ def _refresh_relevant_commitfests(cls, for_update):
inprogress_cf = cfs["in_progress"]
if inprogress_cf and inprogress_cf.enddate < current_date:
+ moved_patch_ids = inprogress_cf.auto_move_active_patches()
inprogress_cf.status = CommitFest.STATUS_CLOSED
inprogress_cf.save()
- inprogress_cf.send_closure_notifications()
+ inprogress_cf.send_closure_notifications(moved_patch_ids)
open_cf = cfs["open"]
if open_cf.startdate <= current_date:
if open_cf.enddate < current_date:
+ moved_patch_ids = open_cf.auto_move_active_patches()
open_cf.status = CommitFest.STATUS_CLOSED
open_cf.save()
- open_cf.send_closure_notifications()
+ open_cf.send_closure_notifications(moved_patch_ids)
else:
open_cf.status = CommitFest.STATUS_INPROGRESS
open_cf.save()
@@ -233,9 +329,10 @@ def _refresh_relevant_commitfests(cls, for_update):
cls.next_draft_cf(current_date).save()
elif draft_cf.enddate < current_date:
# If the draft commitfest has started, we need to update it
+ moved_patch_ids = draft_cf.auto_move_active_patches()
draft_cf.status = CommitFest.STATUS_CLOSED
draft_cf.save()
- draft_cf.send_closure_notifications()
+ draft_cf.send_closure_notifications(moved_patch_ids)
cls.next_draft_cf(current_date).save()
return cls.relevant_commitfests(for_update=for_update)
@@ -530,7 +627,9 @@ def update_lastmail(self):
else:
self.lastmail = max(threads, key=lambda t: t.latestmessage).latestmessage
- def move(self, from_cf, to_cf, by_user, allow_move_to_in_progress=False):
+ def move(
+ self, from_cf, to_cf, by_user, allow_move_to_in_progress=False, by_cfbot=False
+ ):
"""Returns the new PatchOnCommitFest object, or raises UserInputError"""
current_poc = self.current_patch_on_commitfest()
@@ -575,6 +674,7 @@ def move(self, from_cf, to_cf, by_user, allow_move_to_in_progress=False):
PatchHistory(
patch=self,
by=by_user,
+ by_cfbot=by_cfbot,
what=f"Moved from CF {from_cf} to CF {to_cf}",
).save_and_notify()
diff --git a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
index 4d47177c..1f6acfe1 100644
--- a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
+++ b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
@@ -1,17 +1,19 @@
Hello {{user.first_name|default:user.username}},
-Commitfest {{commitfest.name}} has now closed. You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} in this commitfest:
+Commitfest {{commitfest.name}} has now closed.
-{%for poc in patches%}
+You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} that need{{patches|length|pluralize:"s,"}} attention:
+
+{% for poc in patches %}
- {{poc.patch.name}}
https://commitfest.postgresql.org/{{commitfest.id}}/{{poc.patch.id}}/
-{%endfor%}
+{% endfor %}
-Please take action on {{patches|length|pluralize:"this patch,these patches"}}:
+Please take action on {{patches|length|pluralize:"these patches,this patch"}}:
-1. If you want to continue working on {{patches|length|pluralize:"it,them"}}, move {{patches|length|pluralize:"it,them"}} to the next commitfest{% if next_cf %}: {{next_cf_url}}{% endif %}
+1. If you want to continue working on {{patches|length|pluralize:"them,it"}}, move {{patches|length|pluralize:"them,it"}} to the next commitfest{% if next_cf %}: {{next_cf_url}}{% endif %}
-2. If you no longer wish to pursue {{patches|length|pluralize:"this patch,these patches"}}, please close {{patches|length|pluralize:"it,them"}} with an appropriate status (Withdrawn, Returned with feedback, etc.)
+2. If you no longer wish to pursue {{patches|length|pluralize:"these patches,this patch"}}, please close {{patches|length|pluralize:"them,it"}} with an appropriate status (Withdrawn, Returned with feedback, etc.)
{% if next_cf %}The next commitfest is {{next_cf.name}}, which runs from {{next_cf.startdate}} to {{next_cf.enddate}}.{% else %}Please check https://commitfest.postgresql.org/ for upcoming commitfests.{% endif %}
diff --git a/pgcommitfest/commitfest/tests/conftest.py b/pgcommitfest/commitfest/tests/conftest.py
index 9ce147e6..54ee47a8 100644
--- a/pgcommitfest/commitfest/tests/conftest.py
+++ b/pgcommitfest/commitfest/tests/conftest.py
@@ -7,17 +7,20 @@
import pytest
from pgcommitfest.commitfest.models import CommitFest
+from pgcommitfest.userprofile.models import UserProfile
@pytest.fixture
def alice():
- """Create test user Alice."""
- return User.objects.create_user(
+ """Create test user Alice with notify_all_author enabled."""
+ user = User.objects.create_user(
username="alice",
first_name="Alice",
last_name="Anderson",
email="alice@example.com",
)
+ UserProfile.objects.create(user=user, notify_all_author=True)
+ return user
@pytest.fixture
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
index 573882e9..6c56d111 100644
--- a/pgcommitfest/commitfest/tests/test_closure_notifications.py
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -1,10 +1,20 @@
+from django.conf import settings
+
import base64
-from datetime import datetime
+from datetime import date, datetime, timedelta
from email import message_from_string
import pytest
-from pgcommitfest.commitfest.models import Patch, PatchOnCommitFest, Topic
+from pgcommitfest.commitfest.models import (
+ CfbotBranch,
+ CommitFest,
+ Patch,
+ PatchHistory,
+ PatchOnCommitFest,
+ PendingNotification,
+ Topic,
+)
from pgcommitfest.mailqueue.models import QueuedMail
pytestmark = pytest.mark.django_db
@@ -66,9 +76,13 @@ def test_no_notification_for_committed_patches(alice, in_progress_cf, topic):
assert QueuedMail.objects.count() == 0
-def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, topic):
- """Authors of withdrawn patches should not receive notifications."""
- patch = Patch.objects.create(name="Withdrawn Patch", topic=topic)
+def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, open_cf, topic):
+ """Withdrawn patches should not receive notifications or be auto-moved."""
+ patch = Patch.objects.create(
+ name="Withdrawn Patch",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
patch.authors.add(alice)
PatchOnCommitFest.objects.create(
patch=patch,
@@ -78,8 +92,10 @@ def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, topic):
status=PatchOnCommitFest.STATUS_WITHDRAWN,
)
- in_progress_cf.send_closure_notifications()
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications(moved_patch_ids)
+ assert patch.id not in moved_patch_ids
assert QueuedMail.objects.count() == 0
@@ -139,7 +155,9 @@ def test_multiple_authors_receive_separate_emails(alice, bob, in_progress_cf, to
assert receivers == {alice.email, bob.email}
-def test_notification_includes_next_commitfest_info(alice, in_progress_cf, open_cf, topic):
+def test_notification_includes_next_commitfest_info(
+ alice, in_progress_cf, open_cf, topic
+):
"""Notification should include information about the next open commitfest."""
patch = Patch.objects.create(name="Test Patch", topic=topic)
patch.authors.add(alice)
@@ -155,7 +173,6 @@ def test_notification_includes_next_commitfest_info(alice, in_progress_cf, open_
mail = QueuedMail.objects.first()
body = get_email_body(mail)
assert open_cf.name in body
- assert f"https://commitfest.postgresql.org/{open_cf.id}/" in body
def test_coauthors_both_receive_notification(alice, bob, in_progress_cf, topic):
@@ -194,3 +211,304 @@ def test_no_notification_for_author_without_email(bob, in_progress_cf, topic):
in_progress_cf.send_closure_notifications()
assert QueuedMail.objects.count() == 0
+
+
+# Auto-move tests
+
+
+def test_auto_move_patch_with_recent_email_activity(
+ alice, bob, in_progress_cf, open_cf, topic
+):
+ """Patches with recent email activity should be auto-moved to the next commitfest."""
+ patch = Patch.objects.create(
+ name="Active Patch",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
+ patch.authors.add(alice)
+ patch.subscribers.add(bob) # Bob subscribes to get notifications
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications(moved_patch_ids)
+
+ # Patch should be moved
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == open_cf.id
+
+ # Move should create a history entry with by_cfbot=True
+ history = PatchHistory.objects.filter(patch=patch).first()
+ assert history is not None
+ assert history.by_cfbot is True
+ assert "Moved from CF" in history.what
+
+ # PendingNotification should be created for author and subscriber
+ assert PendingNotification.objects.filter(history=history, user=alice).exists()
+ assert PendingNotification.objects.filter(history=history, user=bob).exists()
+
+ # No closure email for moved patches (move triggers its own notification)
+ assert QueuedMail.objects.count() == 0
+
+
+def test_no_auto_move_without_email_activity(alice, in_progress_cf, open_cf, topic):
+ """Patches without recent email activity should NOT be auto-moved."""
+ patch = Patch.objects.create(
+ name="Inactive Patch",
+ topic=topic,
+ lastmail=datetime.now()
+ - timedelta(days=settings.AUTO_MOVE_EMAIL_ACTIVITY_DAYS + 10),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications(moved_patch_ids)
+
+ # Patch should NOT be moved
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == in_progress_cf.id
+
+ # Closure email should be sent for non-moved patches
+ assert QueuedMail.objects.count() == 1
+ mail = QueuedMail.objects.first()
+ body = get_email_body(mail)
+ assert "Inactive Patch" in body
+ assert "need" in body # "needs attention"
+
+
+def test_no_auto_move_when_failing_too_long(alice, in_progress_cf, open_cf, topic):
+ """Patches failing CI for too long should NOT be auto-moved even with recent activity."""
+ patch = Patch.objects.create(
+ name="Failing Patch",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ # Add CfbotBranch with long-standing failure
+ CfbotBranch.objects.create(
+ patch=patch,
+ branch_id=1,
+ branch_name="test-branch",
+ apply_url="https://example.com",
+ status="failed",
+ failing_since=datetime.now()
+ - timedelta(days=settings.AUTO_MOVE_MAX_FAILING_DAYS + 10),
+ )
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications(moved_patch_ids)
+
+ # Patch should NOT be moved
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == in_progress_cf.id
+
+
+def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf, topic):
+ """Patches failing CI within the threshold should still be auto-moved."""
+ patch = Patch.objects.create(
+ name="Recently Failing Patch",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ # Add CfbotBranch with recent failure (within threshold)
+ CfbotBranch.objects.create(
+ patch=patch,
+ branch_id=2,
+ branch_name="test-branch-2",
+ apply_url="https://example.com",
+ status="failed",
+ failing_since=datetime.now()
+ - timedelta(days=settings.AUTO_MOVE_MAX_FAILING_DAYS - 5),
+ )
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications(moved_patch_ids)
+
+ # Patch should be moved (failure is recent enough)
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == open_cf.id
+
+ # No closure email for moved patches
+ assert QueuedMail.objects.count() == 0
+
+
+def test_no_auto_move_without_next_commitfest(alice, in_progress_cf, topic):
+ """Patches should not be auto-moved if there's no next commitfest."""
+ patch = Patch.objects.create(
+ name="Active Patch No Next CF",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications(moved_patch_ids)
+
+ # Patch should NOT be moved (no next CF)
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == in_progress_cf.id
+
+
+def test_no_auto_move_with_null_lastmail(alice, in_progress_cf, open_cf, topic):
+ """Patches with no email activity (null lastmail) should NOT be auto-moved."""
+ patch = Patch.objects.create(
+ name="No Activity Patch",
+ topic=topic,
+ lastmail=None,
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+
+ assert patch.id not in moved_patch_ids
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == in_progress_cf.id
+
+
+def test_auto_move_patch_without_cfbot_branch(alice, in_progress_cf, open_cf, topic):
+ """Patches with recent activity but no CI branch should be auto-moved."""
+ patch = Patch.objects.create(
+ name="No CI Patch",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ # No CfbotBranch created - CI never ran
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications(moved_patch_ids)
+
+ assert patch.id in moved_patch_ids
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == open_cf.id
+
+ # No closure email for moved patches
+ assert QueuedMail.objects.count() == 0
+
+
+def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf, topic):
+ """Regular commitfest should not move patches to a draft commitfest."""
+ # Create only a draft CF as the "next" option (should be ignored)
+ CommitFest.objects.create(
+ name="2025-05-draft",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 5, 1),
+ enddate=date(2025, 5, 31),
+ draft=True,
+ )
+
+ patch = Patch.objects.create(
+ name="Regular Patch",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ moved_patch_ids = in_progress_cf.auto_move_active_patches()
+
+ # Should not be moved since only draft CF is available
+ assert patch.id not in moved_patch_ids
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == in_progress_cf.id
+
+
+def test_draft_cf_moves_active_patches_to_next_draft(alice, bob, topic):
+ """Active patches in a draft commitfest should be auto-moved to the next draft CF."""
+ # Create two draft CFs - one closing and one to receive patches
+ closing_draft_cf = CommitFest.objects.create(
+ name="2025-03-draft",
+ status=CommitFest.STATUS_INPROGRESS,
+ startdate=date(2025, 3, 1),
+ enddate=date(2025, 3, 31),
+ draft=True,
+ )
+ next_draft_cf = CommitFest.objects.create(
+ name="2026-03-draft",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2026, 3, 1),
+ enddate=date(2026, 3, 31),
+ draft=True,
+ )
+
+ patch = Patch.objects.create(
+ name="Draft Patch",
+ topic=topic,
+ lastmail=datetime.now() - timedelta(days=5),
+ )
+ patch.authors.add(alice)
+ patch.subscribers.add(bob) # Bob subscribes to get notifications
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=closing_draft_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ moved_patch_ids = closing_draft_cf.auto_move_active_patches()
+ closing_draft_cf.send_closure_notifications(moved_patch_ids)
+
+ # Patch should be moved to the next draft CF
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == next_draft_cf.id
+
+ # Move should create a history entry with by_cfbot=True
+ history = PatchHistory.objects.filter(patch=patch).first()
+ assert history is not None
+ assert history.by_cfbot is True
+
+ # PendingNotification should be created for author and subscriber
+ assert PendingNotification.objects.filter(history=history, user=alice).exists()
+ assert PendingNotification.objects.filter(history=history, user=bob).exists()
+
+ # No closure email for moved patches
+ assert QueuedMail.objects.count() == 0
diff --git a/pgcommitfest/settings.py b/pgcommitfest/settings.py
index e48cf09e..07e4adaf 100644
--- a/pgcommitfest/settings.py
+++ b/pgcommitfest/settings.py
@@ -168,6 +168,12 @@
CFBOT_API_URL = "https://cfbot.cputube.org/api"
+# Auto-move settings for commitfest closure
+# Patches with email activity within this many days are considered active
+AUTO_MOVE_EMAIL_ACTIVITY_DAYS = 30
+# Patches failing CI for longer than this many days will NOT be auto-moved
+AUTO_MOVE_MAX_FAILING_DAYS = 21
+
# Load local settings overrides
try:
from .local_settings import * # noqa: F403
From 2de44cb37b18f5e0666ac07ff0ded3bd118a7024 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 21:51:20 +0100
Subject: [PATCH 03/16] Only notify on email if opted in and show warnings on
page
---
.../commitfest/fixtures/commitfest_data.json | 58 +++++++++++++++++++
pgcommitfest/commitfest/models.py | 13 +++--
.../commitfest/templates/commitfest.html | 6 ++
pgcommitfest/commitfest/templates/help.html | 5 ++
pgcommitfest/commitfest/templates/patch.html | 7 +++
.../tests/test_closure_notifications.py | 33 ++++++++++-
6 files changed, 114 insertions(+), 8 deletions(-)
diff --git a/pgcommitfest/commitfest/fixtures/commitfest_data.json b/pgcommitfest/commitfest/fixtures/commitfest_data.json
index 944ed3cb..eac5a329 100644
--- a/pgcommitfest/commitfest/fixtures/commitfest_data.json
+++ b/pgcommitfest/commitfest/fixtures/commitfest_data.json
@@ -1351,5 +1351,63 @@
"created": "2025-01-26T22:11:40.000",
"modified": "2025-01-26T22:12:00.000"
}
+},
+{
+ "model": "commitfest.patch",
+ "pk": 9,
+ "fields": {
+ "name": "Abandoned patch in closed commitfest",
+ "topic": 3,
+ "wikilink": "",
+ "gitlink": "",
+ "targetversion": null,
+ "committer": null,
+ "created": "2024-10-15T10:00:00",
+ "modified": "2024-10-15T10:00:00",
+ "lastmail": "2024-10-01T10:00:00",
+ "tags": [],
+ "authors": [],
+ "reviewers": [],
+ "subscribers": [],
+ "mailthread_set": [
+ 9
+ ]
+ }
+},
+{
+ "model": "commitfest.mailthread",
+ "pk": 9,
+ "fields": {
+ "messageid": "abandoned@message-09",
+ "subject": "Abandoned patch in closed commitfest",
+ "firstmessage": "2024-10-01T10:00:00",
+ "firstauthor": "test@test.com",
+ "latestmessage": "2024-10-01T10:00:00",
+ "latestauthor": "test@test.com",
+ "latestsubject": "Abandoned patch in closed commitfest",
+ "latestmsgid": "abandoned@message-09"
+ }
+},
+{
+ "model": "commitfest.patchoncommitfest",
+ "pk": 10,
+ "fields": {
+ "patch": 9,
+ "commitfest": 1,
+ "enterdate": "2024-10-15T10:00:00",
+ "leavedate": null,
+ "status": 1
+ }
+},
+{
+ "model": "commitfest.patchhistory",
+ "pk": 23,
+ "fields": {
+ "patch": 9,
+ "date": "2024-10-15T10:00:00",
+ "by": 1,
+ "by_cfbot": false,
+ "what": "Created patch record"
+ }
}
]
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 796c9eb2..96ac893a 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -239,11 +239,17 @@ def send_closure_notifications(self, moved_patch_ids=None):
next_cf_url = "https://commitfest.postgresql.org/"
# Collect unique authors and their patches
+ # Only include authors who have notify_all_author enabled
authors_patches = {}
for poc in open_pocs:
for author in poc.patch.authors.all():
if not author.email:
continue
+ try:
+ if not author.userprofile.notify_all_author:
+ continue
+ except UserProfile.DoesNotExist:
+ continue
if author not in authors_patches:
authors_patches[author] = []
authors_patches[author].append(poc)
@@ -252,11 +258,8 @@ def send_closure_notifications(self, moved_patch_ids=None):
for author, patches in authors_patches.items():
# Get user's notification email preference
email = author.email
- try:
- if author.userprofile and author.userprofile.notifyemail:
- email = author.userprofile.notifyemail.email
- except UserProfile.DoesNotExist:
- pass
+ if author.userprofile.notifyemail:
+ email = author.userprofile.notifyemail.email
send_template_mail(
settings.NOTIFICATION_FROM,
diff --git a/pgcommitfest/commitfest/templates/commitfest.html b/pgcommitfest/commitfest/templates/commitfest.html
index 082eb72c..357d6bc1 100644
--- a/pgcommitfest/commitfest/templates/commitfest.html
+++ b/pgcommitfest/commitfest/templates/commitfest.html
@@ -2,6 +2,12 @@
{%load commitfest %}
{%block contents%}
+ {%if cf.is_closed%}
+
+ This commitfest is closed. Open patches in this commitfest are no longer being picked up by CI. If you want CI to run on your patch, move it to an open 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.
+
Commitfest closure
+
+ When a Commitfest closes, patches that have been active recently are automatically moved to the next Commitfest. A patch is considered "active" if it has had email activity in the past 30 days and has not been failing CI for more than 21 days. Patches that are not automatically moved will stay in the closed Commitfest, where they will no longer be picked up by CI. Authors of such patches that have enabled "Notify on all where author" in their profile settings will receive an email notification asking them to either move the patch to the next Commitfest or close it with an appropriate status.
+
+
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.
diff --git a/pgcommitfest/commitfest/templates/patch.html b/pgcommitfest/commitfest/templates/patch.html
index 03cafb5c..e48b5bd2 100644
--- a/pgcommitfest/commitfest/templates/patch.html
+++ b/pgcommitfest/commitfest/templates/patch.html
@@ -1,6 +1,13 @@
{%extends "base.html"%}
{%load commitfest%}
{%block contents%}
+ {%with current_poc=patch_commitfests.0%}
+ {%if cf.is_closed and not current_poc.is_closed%}
+
+ This patch is part of a closed commitfest. It is no longer being picked up by CI. If you want CI to run on this patch, move it to an open commitfest.
+
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
index 6c56d111..c8eb6415 100644
--- a/pgcommitfest/commitfest/tests/test_closure_notifications.py
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -16,6 +16,7 @@
Topic,
)
from pgcommitfest.mailqueue.models import QueuedMail
+from pgcommitfest.userprofile.models import UserProfile
pytestmark = pytest.mark.django_db
@@ -129,7 +130,10 @@ def test_one_email_per_author_with_multiple_patches(alice, in_progress_cf, topic
def test_multiple_authors_receive_separate_emails(alice, bob, in_progress_cf, topic):
- """Each author of open patches should receive their own notification."""
+ """Each author of open patches should receive their own notification (if opted in)."""
+ # Bob also needs notify_all_author enabled to receive closure emails
+ UserProfile.objects.create(user=bob, notify_all_author=True)
+
patch1 = Patch.objects.create(name="Alice Patch", topic=topic)
patch1.authors.add(alice)
PatchOnCommitFest.objects.create(
@@ -176,7 +180,10 @@ def test_notification_includes_next_commitfest_info(
def test_coauthors_both_receive_notification(alice, bob, in_progress_cf, topic):
- """Both co-authors of a patch should receive notifications."""
+ """Both co-authors of a patch should receive notifications (if opted in)."""
+ # Bob also needs notify_all_author enabled to receive closure emails
+ UserProfile.objects.create(user=bob, notify_all_author=True)
+
patch = Patch.objects.create(name="Coauthored Patch", topic=topic)
patch.authors.add(alice)
patch.authors.add(bob)
@@ -195,7 +202,8 @@ def test_coauthors_both_receive_notification(alice, bob, in_progress_cf, topic):
def test_no_notification_for_author_without_email(bob, in_progress_cf, topic):
- """Authors without email addresses should be skipped."""
+ """Authors without email addresses should be skipped even if opted in."""
+ UserProfile.objects.create(user=bob, notify_all_author=True)
bob.email = ""
bob.save()
@@ -213,6 +221,25 @@ def test_no_notification_for_author_without_email(bob, in_progress_cf, topic):
assert QueuedMail.objects.count() == 0
+def test_no_notification_for_author_without_notify_all_author(
+ bob, in_progress_cf, topic
+):
+ """Authors without notify_all_author enabled should not receive closure notifications."""
+ # bob has no UserProfile, so notify_all_author is not enabled
+ patch = Patch.objects.create(name="Test Patch", topic=topic)
+ patch.authors.add(bob)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ in_progress_cf.send_closure_notifications()
+
+ assert QueuedMail.objects.count() == 0
+
+
# Auto-move tests
From 17b685c7b3d3b7ea61ba2290f64e91338111e52d Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 21:59:57 +0100
Subject: [PATCH 04/16] Add comment
---
pgcommitfest/commitfest/models.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 96ac893a..5125ab37 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -137,6 +137,9 @@ def _should_auto_move_patch(self, patch, current_date):
):
return False
except CfbotBranch.DoesNotExist:
+ # IF no CFBot data exists, the patch is probably very new (i.e. no
+ # CI run has ever taken place for it yet). So we auto-move it in
+ # that case.
pass
return True
From 0b2c142fd1a90bd423badc9501d469f13d5d32be Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 22:07:52 +0100
Subject: [PATCH 05/16] Initial batch of fixes
---
pgcommitfest/commitfest/models.py | 45 ++++++-------------
.../tests/test_closure_notifications.py | 44 +++++++-----------
2 files changed, 28 insertions(+), 61 deletions(-)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 5125ab37..0e2447e2 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -154,40 +154,25 @@ def auto_move_active_patches(self):
"""
current_date = datetime.now()
- # Get the next open commitfest
+ # Get the next open commitfest (must exist, raises IndexError otherwise)
# For draft CFs, find the next draft CF
# For regular CFs, find the next regular CF by start date
if self.draft:
- next_cf = (
- CommitFest.objects.filter(
- status=CommitFest.STATUS_OPEN,
- draft=True,
- startdate__gt=self.enddate,
- )
- .order_by("startdate")
- .first()
- )
+ next_cf = CommitFest.objects.filter(
+ status=CommitFest.STATUS_OPEN,
+ draft=True,
+ startdate__gt=self.enddate,
+ ).order_by("startdate")[0]
else:
- next_cf = (
- CommitFest.objects.filter(
- status=CommitFest.STATUS_OPEN,
- draft=False,
- startdate__gt=self.enddate,
- )
- .order_by("startdate")
- .first()
- )
-
- if not next_cf:
- return set()
+ next_cf = CommitFest.objects.filter(
+ status=CommitFest.STATUS_OPEN,
+ draft=False,
+ startdate__gt=self.enddate,
+ ).order_by("startdate")[0]
# Get all patches with open status in this commitfest
open_pocs = self.patchoncommitfest_set.filter(
- status__in=[
- PatchOnCommitFest.STATUS_REVIEW,
- PatchOnCommitFest.STATUS_AUTHOR,
- PatchOnCommitFest.STATUS_COMMITTER,
- ]
+ status__in=PatchOnCommitFest.OPEN_STATUSES
).select_related("patch")
moved_patch_ids = set()
@@ -211,11 +196,7 @@ def send_closure_notifications(self, moved_patch_ids=None):
# Get patches that still need action (not moved, not closed)
open_pocs = list(
self.patchoncommitfest_set.filter(
- status__in=[
- PatchOnCommitFest.STATUS_REVIEW,
- PatchOnCommitFest.STATUS_AUTHOR,
- PatchOnCommitFest.STATUS_COMMITTER,
- ]
+ status__in=PatchOnCommitFest.OPEN_STATUSES
)
.exclude(patch_id__in=moved_patch_ids)
.select_related("patch")
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
index c8eb6415..889135e7 100644
--- a/pgcommitfest/commitfest/tests/test_closure_notifications.py
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -384,29 +384,6 @@ def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf,
assert QueuedMail.objects.count() == 0
-def test_no_auto_move_without_next_commitfest(alice, in_progress_cf, topic):
- """Patches should not be auto-moved if there's no next commitfest."""
- patch = Patch.objects.create(
- name="Active Patch No Next CF",
- topic=topic,
- lastmail=datetime.now() - timedelta(days=5),
- )
- patch.authors.add(alice)
- PatchOnCommitFest.objects.create(
- patch=patch,
- commitfest=in_progress_cf,
- enterdate=datetime.now(),
- status=PatchOnCommitFest.STATUS_REVIEW,
- )
-
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications(moved_patch_ids)
-
- # Patch should NOT be moved (no next CF)
- patch.refresh_from_db()
- assert patch.current_commitfest().id == in_progress_cf.id
-
-
def test_no_auto_move_with_null_lastmail(alice, in_progress_cf, open_cf, topic):
"""Patches with no email activity (null lastmail) should NOT be auto-moved."""
patch = Patch.objects.create(
@@ -458,15 +435,23 @@ def test_auto_move_patch_without_cfbot_branch(alice, in_progress_cf, open_cf, to
def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf, topic):
- """Regular commitfest should not move patches to a draft commitfest."""
- # Create only a draft CF as the "next" option (should be ignored)
- CommitFest.objects.create(
+ """Regular commitfest should move patches to the next regular CF, not a draft CF."""
+ # Create a draft CF - should be ignored for regular CF patches
+ draft_cf = CommitFest.objects.create(
name="2025-05-draft",
status=CommitFest.STATUS_OPEN,
startdate=date(2025, 5, 1),
enddate=date(2025, 5, 31),
draft=True,
)
+ # Create a regular CF - this is where patches should go
+ regular_cf = CommitFest.objects.create(
+ name="2025-01",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 1, 1),
+ enddate=date(2025, 1, 31),
+ draft=False,
+ )
patch = Patch.objects.create(
name="Regular Patch",
@@ -483,10 +468,11 @@ def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf, topic):
moved_patch_ids = in_progress_cf.auto_move_active_patches()
- # Should not be moved since only draft CF is available
- assert patch.id not in moved_patch_ids
+ # Should be moved to regular CF, not draft CF
+ assert patch.id in moved_patch_ids
patch.refresh_from_db()
- assert patch.current_commitfest().id == in_progress_cf.id
+ assert patch.current_commitfest().id == regular_cf.id
+ assert patch.current_commitfest().id != draft_cf.id
def test_draft_cf_moves_active_patches_to_next_draft(alice, bob, topic):
From 51776931a16f8390dc3bc08ceffe5f73d2ceca79 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 22:09:52 +0100
Subject: [PATCH 06/16] fixes
---
pgcommitfest/commitfest/models.py | 31 +++++-----------
.../tests/test_closure_notifications.py | 36 +++++++++----------
2 files changed, 24 insertions(+), 43 deletions(-)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 0e2447e2..888cdd9c 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -149,8 +149,6 @@ def auto_move_active_patches(self):
A patch is moved if it has recent email activity and hasn't been
failing CI for too long.
-
- Returns a set of patch IDs that were moved.
"""
current_date = datetime.now()
@@ -175,30 +173,17 @@ def auto_move_active_patches(self):
status__in=PatchOnCommitFest.OPEN_STATUSES
).select_related("patch")
- moved_patch_ids = set()
for poc in open_pocs:
if self._should_auto_move_patch(poc.patch, current_date):
poc.patch.move(self, next_cf, by_user=None, by_cfbot=True)
- moved_patch_ids.add(poc.patch.id)
-
- return moved_patch_ids
-
- def send_closure_notifications(self, moved_patch_ids=None):
- """Send email notifications to authors of patches that weren't auto-moved.
-
- Args:
- moved_patch_ids: Set of patch IDs that were auto-moved to the next commitfest.
- These patches are excluded since the move triggers its own notification.
- """
- if moved_patch_ids is None:
- moved_patch_ids = set()
+ def send_closure_notifications(self):
+ """Send email notifications to authors of patches that are still open."""
# Get patches that still need action (not moved, not closed)
open_pocs = list(
self.patchoncommitfest_set.filter(
status__in=PatchOnCommitFest.OPEN_STATUSES
)
- .exclude(patch_id__in=moved_patch_ids)
.select_related("patch")
.prefetch_related("patch__authors")
)
@@ -292,19 +277,19 @@ def _refresh_relevant_commitfests(cls, for_update):
inprogress_cf = cfs["in_progress"]
if inprogress_cf and inprogress_cf.enddate < current_date:
- moved_patch_ids = inprogress_cf.auto_move_active_patches()
+ inprogress_cf.auto_move_active_patches()
inprogress_cf.status = CommitFest.STATUS_CLOSED
inprogress_cf.save()
- inprogress_cf.send_closure_notifications(moved_patch_ids)
+ inprogress_cf.send_closure_notifications()
open_cf = cfs["open"]
if open_cf.startdate <= current_date:
if open_cf.enddate < current_date:
- moved_patch_ids = open_cf.auto_move_active_patches()
+ open_cf.auto_move_active_patches()
open_cf.status = CommitFest.STATUS_CLOSED
open_cf.save()
- open_cf.send_closure_notifications(moved_patch_ids)
+ open_cf.send_closure_notifications()
else:
open_cf.status = CommitFest.STATUS_INPROGRESS
open_cf.save()
@@ -316,10 +301,10 @@ def _refresh_relevant_commitfests(cls, for_update):
cls.next_draft_cf(current_date).save()
elif draft_cf.enddate < current_date:
# If the draft commitfest has started, we need to update it
- moved_patch_ids = draft_cf.auto_move_active_patches()
+ draft_cf.auto_move_active_patches()
draft_cf.status = CommitFest.STATUS_CLOSED
draft_cf.save()
- draft_cf.send_closure_notifications(moved_patch_ids)
+ draft_cf.send_closure_notifications()
cls.next_draft_cf(current_date).save()
return cls.relevant_commitfests(for_update=for_update)
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
index 889135e7..dc92d719 100644
--- a/pgcommitfest/commitfest/tests/test_closure_notifications.py
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -93,10 +93,9 @@ def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, open_cf, t
status=PatchOnCommitFest.STATUS_WITHDRAWN,
)
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications(moved_patch_ids)
+ in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications()
- assert patch.id not in moved_patch_ids
assert QueuedMail.objects.count() == 0
@@ -261,8 +260,8 @@ def test_auto_move_patch_with_recent_email_activity(
status=PatchOnCommitFest.STATUS_REVIEW,
)
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications(moved_patch_ids)
+ in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications()
# Patch should be moved
patch.refresh_from_db()
@@ -298,8 +297,8 @@ def test_no_auto_move_without_email_activity(alice, in_progress_cf, open_cf, top
status=PatchOnCommitFest.STATUS_REVIEW,
)
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications(moved_patch_ids)
+ in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications()
# Patch should NOT be moved
patch.refresh_from_db()
@@ -339,8 +338,8 @@ def test_no_auto_move_when_failing_too_long(alice, in_progress_cf, open_cf, topi
- timedelta(days=settings.AUTO_MOVE_MAX_FAILING_DAYS + 10),
)
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications(moved_patch_ids)
+ in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications()
# Patch should NOT be moved
patch.refresh_from_db()
@@ -373,8 +372,8 @@ def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf,
- timedelta(days=settings.AUTO_MOVE_MAX_FAILING_DAYS - 5),
)
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications(moved_patch_ids)
+ in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications()
# Patch should be moved (failure is recent enough)
patch.refresh_from_db()
@@ -399,9 +398,8 @@ def test_no_auto_move_with_null_lastmail(alice, in_progress_cf, open_cf, topic):
status=PatchOnCommitFest.STATUS_REVIEW,
)
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.auto_move_active_patches()
- assert patch.id not in moved_patch_ids
patch.refresh_from_db()
assert patch.current_commitfest().id == in_progress_cf.id
@@ -423,10 +421,9 @@ def test_auto_move_patch_without_cfbot_branch(alice, in_progress_cf, open_cf, to
# No CfbotBranch created - CI never ran
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications(moved_patch_ids)
+ in_progress_cf.auto_move_active_patches()
+ in_progress_cf.send_closure_notifications()
- assert patch.id in moved_patch_ids
patch.refresh_from_db()
assert patch.current_commitfest().id == open_cf.id
@@ -466,10 +463,9 @@ def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf, topic):
status=PatchOnCommitFest.STATUS_REVIEW,
)
- moved_patch_ids = in_progress_cf.auto_move_active_patches()
+ in_progress_cf.auto_move_active_patches()
# Should be moved to regular CF, not draft CF
- assert patch.id in moved_patch_ids
patch.refresh_from_db()
assert patch.current_commitfest().id == regular_cf.id
assert patch.current_commitfest().id != draft_cf.id
@@ -507,8 +503,8 @@ def test_draft_cf_moves_active_patches_to_next_draft(alice, bob, topic):
status=PatchOnCommitFest.STATUS_REVIEW,
)
- moved_patch_ids = closing_draft_cf.auto_move_active_patches()
- closing_draft_cf.send_closure_notifications(moved_patch_ids)
+ closing_draft_cf.auto_move_active_patches()
+ closing_draft_cf.send_closure_notifications()
# Patch should be moved to the next draft CF
patch.refresh_from_db()
From 8e6738be59511453fb22745031118b540a7ddf09 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 22:28:29 +0100
Subject: [PATCH 07/16] More fixes
---
pgcommitfest/commitfest/models.py | 22 ++++++++-----------
.../tests/test_closure_notifications.py | 20 -----------------
2 files changed, 9 insertions(+), 33 deletions(-)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 888cdd9c..f2b74129 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -208,27 +208,23 @@ def send_closure_notifications(self):
next_cf_url = "https://commitfest.postgresql.org/"
# Collect unique authors and their patches
- # Only include authors who have notify_all_author enabled
authors_patches = {}
for poc in open_pocs:
for author in poc.patch.authors.all():
- if not author.email:
- continue
- try:
- if not author.userprofile.notify_all_author:
- continue
- except UserProfile.DoesNotExist:
- continue
if author not in authors_patches:
authors_patches[author] = []
authors_patches[author].append(poc)
- # Send email to each author
+ # Send email to each author who has notifications enabled
for author, patches in authors_patches.items():
- # Get user's notification email preference
- email = author.email
- if author.userprofile.notifyemail:
- email = author.userprofile.notifyemail.email
+ try:
+ if not author.userprofile.notify_all_author:
+ continue
+ notifyemail = author.userprofile.notifyemail
+ except UserProfile.DoesNotExist:
+ continue
+
+ email = notifyemail.email if notifyemail else author.email
send_template_mail(
settings.NOTIFICATION_FROM,
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
index dc92d719..6d068636 100644
--- a/pgcommitfest/commitfest/tests/test_closure_notifications.py
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -200,26 +200,6 @@ def test_coauthors_both_receive_notification(alice, bob, in_progress_cf, topic):
assert receivers == {alice.email, bob.email}
-def test_no_notification_for_author_without_email(bob, in_progress_cf, topic):
- """Authors without email addresses should be skipped even if opted in."""
- UserProfile.objects.create(user=bob, notify_all_author=True)
- bob.email = ""
- bob.save()
-
- patch = Patch.objects.create(name="Test Patch", topic=topic)
- patch.authors.add(bob)
- PatchOnCommitFest.objects.create(
- patch=patch,
- commitfest=in_progress_cf,
- enterdate=datetime.now(),
- status=PatchOnCommitFest.STATUS_REVIEW,
- )
-
- in_progress_cf.send_closure_notifications()
-
- assert QueuedMail.objects.count() == 0
-
-
def test_no_notification_for_author_without_notify_all_author(
bob, in_progress_cf, topic
):
From cd8fee975e784549edbb8c0ca7395bc50cb43a1e Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 22:30:00 +0100
Subject: [PATCH 08/16] Move stuff together
---
pgcommitfest/commitfest/models.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index f2b74129..1d90c1a5 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -273,18 +273,18 @@ def _refresh_relevant_commitfests(cls, for_update):
inprogress_cf = cfs["in_progress"]
if inprogress_cf and inprogress_cf.enddate < current_date:
- inprogress_cf.auto_move_active_patches()
inprogress_cf.status = CommitFest.STATUS_CLOSED
inprogress_cf.save()
+ inprogress_cf.auto_move_active_patches()
inprogress_cf.send_closure_notifications()
open_cf = cfs["open"]
if open_cf.startdate <= current_date:
if open_cf.enddate < current_date:
- open_cf.auto_move_active_patches()
open_cf.status = CommitFest.STATUS_CLOSED
open_cf.save()
+ open_cf.auto_move_active_patches()
open_cf.send_closure_notifications()
else:
open_cf.status = CommitFest.STATUS_INPROGRESS
@@ -297,9 +297,9 @@ def _refresh_relevant_commitfests(cls, for_update):
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.auto_move_active_patches()
draft_cf.status = CommitFest.STATUS_CLOSED
draft_cf.save()
+ draft_cf.auto_move_active_patches()
draft_cf.send_closure_notifications()
cls.next_draft_cf(current_date).save()
From 276fd6e89882a3c5d0257bb7bf3633b7931b8d96 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 22:33:06 +0100
Subject: [PATCH 09/16] More changes
---
pgcommitfest/commitfest/models.py | 8 ++++----
.../commitfest/templates/mail/commitfest_closure.txt | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 1d90c1a5..76c56fb8 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -284,24 +284,24 @@ def _refresh_relevant_commitfests(cls, for_update):
if open_cf.enddate < current_date:
open_cf.status = CommitFest.STATUS_CLOSED
open_cf.save()
+ cls.next_open_cf(current_date).save()
open_cf.auto_move_active_patches()
open_cf.send_closure_notifications()
else:
open_cf.status = CommitFest.STATUS_INPROGRESS
open_cf.save()
-
- cls.next_open_cf(current_date).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
+ # Create next CF first so auto_move has somewhere to move patches
draft_cf.status = CommitFest.STATUS_CLOSED
draft_cf.save()
+ cls.next_draft_cf(current_date).save()
draft_cf.auto_move_active_patches()
draft_cf.send_closure_notifications()
- cls.next_draft_cf(current_date).save()
return cls.relevant_commitfests(for_update=for_update)
diff --git a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
index 1f6acfe1..faafd074 100644
--- a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
+++ b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
@@ -6,7 +6,7 @@ You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} that nee
{% for poc in patches %}
- {{poc.patch.name}}
- https://commitfest.postgresql.org/{{commitfest.id}}/{{poc.patch.id}}/
+ https://commitfest.postgresql.org/patch/{{poc.patch.id}}/
{% endfor %}
Please take action on {{patches|length|pluralize:"these patches,this patch"}}:
From 88553f3ac3dcb35f50530edef71b444ba8eec448 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 22:50:04 +0100
Subject: [PATCH 10/16] More changes
---
pgcommitfest/commitfest/templates/help.html | 2 +-
.../templates/mail/commitfest_closure.txt | 9 +-
.../tests/test_refresh_commitfests.py | 315 ++++++++++++++++++
pgcommitfest/commitfest/views.py | 2 +
pyproject.toml | 1 +
5 files changed, 320 insertions(+), 9 deletions(-)
create mode 100644 pgcommitfest/commitfest/tests/test_refresh_commitfests.py
diff --git a/pgcommitfest/commitfest/templates/help.html b/pgcommitfest/commitfest/templates/help.html
index fd087b7a..5807add2 100644
--- a/pgcommitfest/commitfest/templates/help.html
+++ b/pgcommitfest/commitfest/templates/help.html
@@ -17,7 +17,7 @@
Commitfest
Commitfest closure
- When a Commitfest closes, patches that have been active recently are automatically moved to the next Commitfest. A patch is considered "active" if it has had email activity in the past 30 days and has not been failing CI for more than 21 days. Patches that are not automatically moved will stay in the closed Commitfest, where they will no longer be picked up by CI. Authors of such patches that have enabled "Notify on all where author" in their profile settings will receive an email notification asking them to either move the patch to the next Commitfest or close it with an appropriate status.
+ When a Commitfest closes, patches that have been active recently are automatically moved to the next Commitfest. A patch is considered "active" if it has had email activity in the past {{auto_move_email_activity_days}} days and has not been failing CI for more than {{auto_move_max_failing_days}} days. Patches that are not automatically moved will stay in the closed Commitfest, where they will no longer be picked up by CI. Authors of such patches that have enabled "Notify on all where author" in their profile settings will receive an email notification asking them to either move the patch to the next Commitfest or close it with an appropriate status.
Patches
diff --git a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
index faafd074..ed6d42cd 100644
--- a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
+++ b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
@@ -9,15 +9,8 @@ You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} that nee
https://commitfest.postgresql.org/patch/{{poc.patch.id}}/
{% endfor %}
-Please take action on {{patches|length|pluralize:"these patches,this patch"}}:
+Please take action on {{patches|length|pluralize:"these patches,this patch"}} by doing either of the following:
1. If you want to continue working on {{patches|length|pluralize:"them,it"}}, move {{patches|length|pluralize:"them,it"}} to the next commitfest{% if next_cf %}: {{next_cf_url}}{% endif %}
2. If you no longer wish to pursue {{patches|length|pluralize:"these patches,this patch"}}, please close {{patches|length|pluralize:"them,it"}} with an appropriate status (Withdrawn, Returned with feedback, etc.)
-
-{% if next_cf %}The next commitfest is {{next_cf.name}}, which runs from {{next_cf.startdate}} to {{next_cf.enddate}}.{% else %}Please check https://commitfest.postgresql.org/ for upcoming commitfests.{% endif %}
-
-Thank you for your contributions to PostgreSQL!
-
---
-This is an automated message from the PostgreSQL Commitfest application.
diff --git a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
new file mode 100644
index 00000000..4c926567
--- /dev/null
+++ b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
@@ -0,0 +1,315 @@
+from datetime import date, datetime
+
+import pytest
+from freezegun import freeze_time
+
+from pgcommitfest.commitfest.models import (
+ CommitFest,
+ Patch,
+ PatchOnCommitFest,
+ Topic,
+)
+from pgcommitfest.userprofile.models import UserProfile
+
+
+@pytest.fixture
+def topic(db):
+ return Topic.objects.create(topic="General")
+
+
+@pytest.fixture
+def alice(db, django_user_model):
+ user = django_user_model.objects.create_user(
+ username="alice",
+ email="alice@example.com",
+ first_name="Alice",
+ last_name="Smith",
+ )
+ UserProfile.objects.create(user=user, notify_all_author=True)
+ return user
+
+
+def create_closed_cf(name, startdate, enddate):
+ """Helper to create a closed CF for padding."""
+ return CommitFest.objects.create(
+ name=name,
+ status=CommitFest.STATUS_CLOSED,
+ startdate=startdate,
+ enddate=enddate,
+ )
+
+
+@pytest.mark.django_db
+@freeze_time("2024-12-05")
+def test_inprogress_cf_closes_when_enddate_passed(topic, alice):
+ """When an in_progress CF's enddate has passed, it should be closed."""
+ # Create some closed CFs for padding (relevant_commitfests needs history)
+ create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
+ create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
+
+ # Create an in_progress CF that ended
+ in_progress_cf = CommitFest.objects.create(
+ name="2024-11",
+ status=CommitFest.STATUS_INPROGRESS,
+ startdate=date(2024, 11, 1),
+ enddate=date(2024, 11, 30),
+ )
+ # Create the next open CF (required for auto_move)
+ open_cf = CommitFest.objects.create(
+ name="2025-01",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 1, 1),
+ enddate=date(2025, 1, 31),
+ )
+ # Create draft CF
+ CommitFest.objects.create(
+ name="2025-draft",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2024, 7, 1),
+ enddate=date(2025, 3, 31),
+ draft=True,
+ )
+
+ # Create a patch with recent activity that should be auto-moved
+ patch = Patch.objects.create(
+ name="Test Patch",
+ topic=topic,
+ lastmail=datetime(2024, 11, 25),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=in_progress_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ CommitFest._refresh_relevant_commitfests(for_update=False)
+
+ in_progress_cf.refresh_from_db()
+ assert in_progress_cf.status == CommitFest.STATUS_CLOSED
+
+ # Patch should have been moved to the open CF
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == open_cf.id
+
+
+@pytest.mark.django_db
+@freeze_time("2025-01-15")
+def test_open_cf_becomes_inprogress_when_startdate_reached():
+ """When an open CF's startdate is reached, it becomes in_progress."""
+ # Create some closed CFs for padding
+ create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
+ create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
+ create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
+
+ open_cf = CommitFest.objects.create(
+ name="2025-01",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 1, 1),
+ enddate=date(2025, 1, 31),
+ )
+ # Create draft CF
+ CommitFest.objects.create(
+ name="2025-draft",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2024, 7, 1),
+ enddate=date(2025, 3, 31),
+ draft=True,
+ )
+
+ CommitFest._refresh_relevant_commitfests(for_update=False)
+
+ open_cf.refresh_from_db()
+ assert open_cf.status == CommitFest.STATUS_INPROGRESS
+
+ # A new open CF should have been created
+ new_open = CommitFest.objects.filter(
+ status=CommitFest.STATUS_OPEN, draft=False
+ ).first()
+ assert new_open is not None
+ assert new_open.startdate > open_cf.enddate
+
+
+@pytest.mark.django_db
+@freeze_time("2025-02-05")
+def test_open_cf_closes_when_enddate_passed(topic, alice):
+ """When an open CF's enddate has passed (skipping in_progress), it closes."""
+ # Create some closed CFs for padding
+ create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
+ create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
+ create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
+
+ open_cf = CommitFest.objects.create(
+ name="2025-01",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 1, 1),
+ enddate=date(2025, 1, 31),
+ )
+ # Create draft CF
+ CommitFest.objects.create(
+ name="2025-draft",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2024, 7, 1),
+ enddate=date(2025, 3, 31),
+ draft=True,
+ )
+
+ # Create a patch with recent activity
+ patch = Patch.objects.create(
+ name="Test Patch",
+ topic=topic,
+ lastmail=datetime(2025, 1, 25),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=open_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ CommitFest._refresh_relevant_commitfests(for_update=False)
+
+ open_cf.refresh_from_db()
+ assert open_cf.status == CommitFest.STATUS_CLOSED
+
+ # A new open CF should have been created
+ new_open = CommitFest.objects.filter(
+ status=CommitFest.STATUS_OPEN, draft=False
+ ).first()
+ assert new_open is not None
+
+ # Patch should have been moved to the new open CF
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == new_open.id
+
+
+@pytest.mark.django_db
+@freeze_time("2025-01-15")
+def test_draft_cf_created_when_missing():
+ """When no draft CF exists, one should be created."""
+ # Create closed CFs for padding
+ create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
+ create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
+ create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
+
+ # Create only regular CFs
+ CommitFest.objects.create(
+ name="2025-01",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 3, 1),
+ enddate=date(2025, 3, 31),
+ )
+
+ assert not CommitFest.objects.filter(draft=True).exists()
+
+ CommitFest._refresh_relevant_commitfests(for_update=False)
+
+ # A draft CF should have been created
+ draft_cf = CommitFest.objects.filter(draft=True).first()
+ assert draft_cf is not None
+ assert draft_cf.status == CommitFest.STATUS_OPEN
+
+
+@pytest.mark.django_db
+@freeze_time("2025-04-05")
+def test_draft_cf_closes_when_enddate_passed(topic, alice):
+ """When a draft CF's enddate has passed, it should be closed."""
+ # Create closed CFs for padding
+ create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
+ create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
+ create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
+
+ # Create an open regular CF (required)
+ CommitFest.objects.create(
+ name="2025-03",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 5, 1),
+ enddate=date(2025, 5, 31),
+ )
+
+ # Create a draft CF that ended
+ draft_cf = CommitFest.objects.create(
+ name="2025-draft",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 1, 1),
+ enddate=date(2025, 3, 31),
+ draft=True,
+ )
+
+ # Create a patch with recent activity
+ patch = Patch.objects.create(
+ name="Draft Patch",
+ topic=topic,
+ lastmail=datetime(2025, 3, 25),
+ )
+ patch.authors.add(alice)
+ PatchOnCommitFest.objects.create(
+ patch=patch,
+ commitfest=draft_cf,
+ enterdate=datetime.now(),
+ status=PatchOnCommitFest.STATUS_REVIEW,
+ )
+
+ CommitFest._refresh_relevant_commitfests(for_update=False)
+
+ draft_cf.refresh_from_db()
+ assert draft_cf.status == CommitFest.STATUS_CLOSED
+
+ # A new draft CF should have been created
+ new_draft = CommitFest.objects.filter(
+ draft=True, status=CommitFest.STATUS_OPEN
+ ).first()
+ assert new_draft is not None
+ assert new_draft.startdate > draft_cf.enddate
+
+ # Patch should have been moved to the new draft CF
+ patch.refresh_from_db()
+ assert patch.current_commitfest().id == new_draft.id
+
+
+@pytest.mark.django_db
+@freeze_time("2025-01-15")
+def test_no_changes_when_up_to_date():
+ """When commitfests are up to date, no changes should be made."""
+ # Create closed CFs for padding
+ create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
+ create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
+
+ # Create CFs that are all up to date
+ in_progress_cf = CommitFest.objects.create(
+ name="2025-01",
+ status=CommitFest.STATUS_INPROGRESS,
+ startdate=date(2025, 1, 1),
+ enddate=date(2025, 1, 31),
+ )
+ open_cf = CommitFest.objects.create(
+ name="2025-03",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 3, 1),
+ enddate=date(2025, 3, 31),
+ )
+ draft_cf = CommitFest.objects.create(
+ name="2025-draft",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 1, 1),
+ enddate=date(2025, 3, 31),
+ draft=True,
+ )
+
+ initial_cf_count = CommitFest.objects.count()
+
+ CommitFest._refresh_relevant_commitfests(for_update=False)
+
+ # All statuses should remain unchanged
+ in_progress_cf.refresh_from_db()
+ open_cf.refresh_from_db()
+ draft_cf.refresh_from_db()
+
+ assert in_progress_cf.status == CommitFest.STATUS_INPROGRESS
+ assert open_cf.status == CommitFest.STATUS_OPEN
+ assert draft_cf.status == CommitFest.STATUS_OPEN
+
+ # No new CFs should have been created
+ assert CommitFest.objects.count() == initial_cf_count
diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py
index 1f6a30d9..b2d64c48 100644
--- a/pgcommitfest/commitfest/views.py
+++ b/pgcommitfest/commitfest/views.py
@@ -174,6 +174,8 @@ def help(request):
"help.html",
{
"title": "What is the Commitfest app?",
+ "auto_move_email_activity_days": settings.AUTO_MOVE_EMAIL_ACTIVITY_DAYS,
+ "auto_move_max_failing_days": settings.AUTO_MOVE_MAX_FAILING_DAYS,
},
)
diff --git a/pyproject.toml b/pyproject.toml
index 411d4075..4ed170eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,6 +20,7 @@ dev = [
"djhtml",
"pytest",
"pytest-django",
+ "freezegun",
]
[tool.setuptools.packages.find]
From 6f799fb71ea40223ca2a16d039ac7ad5bb9cfffa Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 22:53:29 +0100
Subject: [PATCH 11/16] No topic
---
.../tests/test_closure_notifications.py | 86 +++++--------------
1 file changed, 23 insertions(+), 63 deletions(-)
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
index 6d068636..f573d222 100644
--- a/pgcommitfest/commitfest/tests/test_closure_notifications.py
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -13,7 +13,6 @@
PatchHistory,
PatchOnCommitFest,
PendingNotification,
- Topic,
)
from pgcommitfest.mailqueue.models import QueuedMail
from pgcommitfest.userprofile.models import UserProfile
@@ -31,17 +30,9 @@ def get_email_body(queued_mail):
return ""
-@pytest.fixture
-def topic():
- """Create a test topic."""
- return Topic.objects.create(topic="General")
-
-
-def test_send_closure_notifications_to_authors_of_open_patches(
- alice, in_progress_cf, topic
-):
+def test_send_closure_notifications_to_authors_of_open_patches(alice, in_progress_cf):
"""Authors of patches with open status should receive closure notifications."""
- patch = Patch.objects.create(name="Test Patch", topic=topic)
+ patch = Patch.objects.create(name="Test Patch")
patch.authors.add(alice)
PatchOnCommitFest.objects.create(
patch=patch,
@@ -60,9 +51,9 @@ def test_send_closure_notifications_to_authors_of_open_patches(
assert "Test Patch" in body
-def test_no_notification_for_committed_patches(alice, in_progress_cf, topic):
+def test_no_notification_for_committed_patches(alice, in_progress_cf):
"""Authors of committed patches should not receive notifications."""
- patch = Patch.objects.create(name="Committed Patch", topic=topic)
+ patch = Patch.objects.create(name="Committed Patch")
patch.authors.add(alice)
PatchOnCommitFest.objects.create(
patch=patch,
@@ -77,11 +68,10 @@ def test_no_notification_for_committed_patches(alice, in_progress_cf, topic):
assert QueuedMail.objects.count() == 0
-def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, open_cf, topic):
+def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, open_cf):
"""Withdrawn patches should not receive notifications or be auto-moved."""
patch = Patch.objects.create(
name="Withdrawn Patch",
- topic=topic,
lastmail=datetime.now() - timedelta(days=5),
)
patch.authors.add(alice)
@@ -99,9 +89,9 @@ def test_no_notification_for_withdrawn_patches(alice, in_progress_cf, open_cf, t
assert QueuedMail.objects.count() == 0
-def test_one_email_per_author_with_multiple_patches(alice, in_progress_cf, topic):
+def test_one_email_per_author_with_multiple_patches(alice, in_progress_cf):
"""An author with multiple open patches should receive one email listing all patches."""
- patch1 = Patch.objects.create(name="Patch One", topic=topic)
+ patch1 = Patch.objects.create(name="Patch One")
patch1.authors.add(alice)
PatchOnCommitFest.objects.create(
patch=patch1,
@@ -110,7 +100,7 @@ def test_one_email_per_author_with_multiple_patches(alice, in_progress_cf, topic
status=PatchOnCommitFest.STATUS_REVIEW,
)
- patch2 = Patch.objects.create(name="Patch Two", topic=topic)
+ patch2 = Patch.objects.create(name="Patch Two")
patch2.authors.add(alice)
PatchOnCommitFest.objects.create(
patch=patch2,
@@ -128,12 +118,12 @@ def test_one_email_per_author_with_multiple_patches(alice, in_progress_cf, topic
assert "Patch Two" in body
-def test_multiple_authors_receive_separate_emails(alice, bob, in_progress_cf, topic):
+def test_multiple_authors_receive_separate_emails(alice, bob, in_progress_cf):
"""Each author of open patches should receive their own notification (if opted in)."""
# Bob also needs notify_all_author enabled to receive closure emails
UserProfile.objects.create(user=bob, notify_all_author=True)
- patch1 = Patch.objects.create(name="Alice Patch", topic=topic)
+ patch1 = Patch.objects.create(name="Alice Patch")
patch1.authors.add(alice)
PatchOnCommitFest.objects.create(
patch=patch1,
@@ -142,7 +132,7 @@ def test_multiple_authors_receive_separate_emails(alice, bob, in_progress_cf, to
status=PatchOnCommitFest.STATUS_REVIEW,
)
- patch2 = Patch.objects.create(name="Bob Patch", topic=topic)
+ patch2 = Patch.objects.create(name="Bob Patch")
patch2.authors.add(bob)
PatchOnCommitFest.objects.create(
patch=patch2,
@@ -158,32 +148,12 @@ def test_multiple_authors_receive_separate_emails(alice, bob, in_progress_cf, to
assert receivers == {alice.email, bob.email}
-def test_notification_includes_next_commitfest_info(
- alice, in_progress_cf, open_cf, topic
-):
- """Notification should include information about the next open commitfest."""
- patch = Patch.objects.create(name="Test Patch", topic=topic)
- patch.authors.add(alice)
- PatchOnCommitFest.objects.create(
- patch=patch,
- commitfest=in_progress_cf,
- enterdate=datetime.now(),
- status=PatchOnCommitFest.STATUS_REVIEW,
- )
-
- in_progress_cf.send_closure_notifications()
-
- mail = QueuedMail.objects.first()
- body = get_email_body(mail)
- assert open_cf.name in body
-
-
-def test_coauthors_both_receive_notification(alice, bob, in_progress_cf, topic):
+def test_coauthors_both_receive_notification(alice, bob, in_progress_cf):
"""Both co-authors of a patch should receive notifications (if opted in)."""
# Bob also needs notify_all_author enabled to receive closure emails
UserProfile.objects.create(user=bob, notify_all_author=True)
- patch = Patch.objects.create(name="Coauthored Patch", topic=topic)
+ patch = Patch.objects.create(name="Coauthored Patch")
patch.authors.add(alice)
patch.authors.add(bob)
PatchOnCommitFest.objects.create(
@@ -200,12 +170,10 @@ def test_coauthors_both_receive_notification(alice, bob, in_progress_cf, topic):
assert receivers == {alice.email, bob.email}
-def test_no_notification_for_author_without_notify_all_author(
- bob, in_progress_cf, topic
-):
+def test_no_notification_for_author_without_notify_all_author(bob, in_progress_cf):
"""Authors without notify_all_author enabled should not receive closure notifications."""
# bob has no UserProfile, so notify_all_author is not enabled
- patch = Patch.objects.create(name="Test Patch", topic=topic)
+ patch = Patch.objects.create(name="Test Patch")
patch.authors.add(bob)
PatchOnCommitFest.objects.create(
patch=patch,
@@ -223,12 +191,11 @@ def test_no_notification_for_author_without_notify_all_author(
def test_auto_move_patch_with_recent_email_activity(
- alice, bob, in_progress_cf, open_cf, topic
+ alice, bob, in_progress_cf, open_cf
):
"""Patches with recent email activity should be auto-moved to the next commitfest."""
patch = Patch.objects.create(
name="Active Patch",
- topic=topic,
lastmail=datetime.now() - timedelta(days=5),
)
patch.authors.add(alice)
@@ -261,11 +228,10 @@ def test_auto_move_patch_with_recent_email_activity(
assert QueuedMail.objects.count() == 0
-def test_no_auto_move_without_email_activity(alice, in_progress_cf, open_cf, topic):
+def test_no_auto_move_without_email_activity(alice, in_progress_cf, open_cf):
"""Patches without recent email activity should NOT be auto-moved."""
patch = Patch.objects.create(
name="Inactive Patch",
- topic=topic,
lastmail=datetime.now()
- timedelta(days=settings.AUTO_MOVE_EMAIL_ACTIVITY_DAYS + 10),
)
@@ -292,11 +258,10 @@ def test_no_auto_move_without_email_activity(alice, in_progress_cf, open_cf, top
assert "need" in body # "needs attention"
-def test_no_auto_move_when_failing_too_long(alice, in_progress_cf, open_cf, topic):
+def test_no_auto_move_when_failing_too_long(alice, in_progress_cf, open_cf):
"""Patches failing CI for too long should NOT be auto-moved even with recent activity."""
patch = Patch.objects.create(
name="Failing Patch",
- topic=topic,
lastmail=datetime.now() - timedelta(days=5),
)
patch.authors.add(alice)
@@ -326,11 +291,10 @@ def test_no_auto_move_when_failing_too_long(alice, in_progress_cf, open_cf, topi
assert patch.current_commitfest().id == in_progress_cf.id
-def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf, topic):
+def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf):
"""Patches failing CI within the threshold should still be auto-moved."""
patch = Patch.objects.create(
name="Recently Failing Patch",
- topic=topic,
lastmail=datetime.now() - timedelta(days=5),
)
patch.authors.add(alice)
@@ -363,11 +327,10 @@ def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf,
assert QueuedMail.objects.count() == 0
-def test_no_auto_move_with_null_lastmail(alice, in_progress_cf, open_cf, topic):
+def test_no_auto_move_with_null_lastmail(alice, in_progress_cf, open_cf):
"""Patches with no email activity (null lastmail) should NOT be auto-moved."""
patch = Patch.objects.create(
name="No Activity Patch",
- topic=topic,
lastmail=None,
)
patch.authors.add(alice)
@@ -384,11 +347,10 @@ def test_no_auto_move_with_null_lastmail(alice, in_progress_cf, open_cf, topic):
assert patch.current_commitfest().id == in_progress_cf.id
-def test_auto_move_patch_without_cfbot_branch(alice, in_progress_cf, open_cf, topic):
+def test_auto_move_patch_without_cfbot_branch(alice, in_progress_cf, open_cf):
"""Patches with recent activity but no CI branch should be auto-moved."""
patch = Patch.objects.create(
name="No CI Patch",
- topic=topic,
lastmail=datetime.now() - timedelta(days=5),
)
patch.authors.add(alice)
@@ -411,7 +373,7 @@ def test_auto_move_patch_without_cfbot_branch(alice, in_progress_cf, open_cf, to
assert QueuedMail.objects.count() == 0
-def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf, topic):
+def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf):
"""Regular commitfest should move patches to the next regular CF, not a draft CF."""
# Create a draft CF - should be ignored for regular CF patches
draft_cf = CommitFest.objects.create(
@@ -432,7 +394,6 @@ def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf, topic):
patch = Patch.objects.create(
name="Regular Patch",
- topic=topic,
lastmail=datetime.now() - timedelta(days=5),
)
patch.authors.add(alice)
@@ -451,7 +412,7 @@ def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf, topic):
assert patch.current_commitfest().id != draft_cf.id
-def test_draft_cf_moves_active_patches_to_next_draft(alice, bob, topic):
+def test_draft_cf_moves_active_patches_to_next_draft(alice, bob):
"""Active patches in a draft commitfest should be auto-moved to the next draft CF."""
# Create two draft CFs - one closing and one to receive patches
closing_draft_cf = CommitFest.objects.create(
@@ -471,7 +432,6 @@ def test_draft_cf_moves_active_patches_to_next_draft(alice, bob, topic):
patch = Patch.objects.create(
name="Draft Patch",
- topic=topic,
lastmail=datetime.now() - timedelta(days=5),
)
patch.authors.add(alice)
From 7a401c0688677f5d1d3db1996df7b874ba73fd23 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 23:06:03 +0100
Subject: [PATCH 12/16] Simplify tests
---
.../tests/test_closure_notifications.py | 78 ++++++-------------
.../tests/test_refresh_commitfests.py | 15 +---
2 files changed, 26 insertions(+), 67 deletions(-)
diff --git a/pgcommitfest/commitfest/tests/test_closure_notifications.py b/pgcommitfest/commitfest/tests/test_closure_notifications.py
index f573d222..3973d43d 100644
--- a/pgcommitfest/commitfest/tests/test_closure_notifications.py
+++ b/pgcommitfest/commitfest/tests/test_closure_notifications.py
@@ -255,7 +255,7 @@ def test_no_auto_move_without_email_activity(alice, in_progress_cf, open_cf):
mail = QueuedMail.objects.first()
body = get_email_body(mail)
assert "Inactive Patch" in body
- assert "need" in body # "needs attention"
+ assert "needs attention" in body
def test_no_auto_move_when_failing_too_long(alice, in_progress_cf, open_cf):
@@ -290,6 +290,12 @@ def test_no_auto_move_when_failing_too_long(alice, in_progress_cf, open_cf):
patch.refresh_from_db()
assert patch.current_commitfest().id == in_progress_cf.id
+ # Author should receive closure notification
+ assert QueuedMail.objects.count() == 1
+ mail = QueuedMail.objects.first()
+ body = get_email_body(mail)
+ assert "Failing Patch" in body
+
def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf):
"""Patches failing CI within the threshold should still be auto-moved."""
@@ -327,68 +333,22 @@ def test_auto_move_when_failing_within_threshold(alice, in_progress_cf, open_cf)
assert QueuedMail.objects.count() == 0
-def test_no_auto_move_with_null_lastmail(alice, in_progress_cf, open_cf):
- """Patches with no email activity (null lastmail) should NOT be auto-moved."""
- patch = Patch.objects.create(
- name="No Activity Patch",
- lastmail=None,
- )
- patch.authors.add(alice)
- PatchOnCommitFest.objects.create(
- patch=patch,
- commitfest=in_progress_cf,
- enterdate=datetime.now(),
- status=PatchOnCommitFest.STATUS_REVIEW,
- )
-
- in_progress_cf.auto_move_active_patches()
-
- patch.refresh_from_db()
- assert patch.current_commitfest().id == in_progress_cf.id
-
-
-def test_auto_move_patch_without_cfbot_branch(alice, in_progress_cf, open_cf):
- """Patches with recent activity but no CI branch should be auto-moved."""
- patch = Patch.objects.create(
- name="No CI Patch",
- lastmail=datetime.now() - timedelta(days=5),
- )
- patch.authors.add(alice)
- PatchOnCommitFest.objects.create(
- patch=patch,
- commitfest=in_progress_cf,
- enterdate=datetime.now(),
- status=PatchOnCommitFest.STATUS_REVIEW,
- )
-
- # No CfbotBranch created - CI never ran
-
- in_progress_cf.auto_move_active_patches()
- in_progress_cf.send_closure_notifications()
-
- patch.refresh_from_db()
- assert patch.current_commitfest().id == open_cf.id
-
- # No closure email for moved patches
- assert QueuedMail.objects.count() == 0
-
-
def test_regular_cf_does_not_move_to_draft_cf(alice, in_progress_cf):
"""Regular commitfest should move patches to the next regular CF, not a draft CF."""
- # Create a draft CF - should be ignored for regular CF patches
+ # Create a draft CF with earlier startdate - should be ignored for regular CF patches
draft_cf = CommitFest.objects.create(
- name="2025-05-draft",
+ name="2024-12-draft",
status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 5, 1),
- enddate=date(2025, 5, 31),
+ startdate=date(2024, 12, 1),
+ enddate=date(2024, 12, 31),
draft=True,
)
- # Create a regular CF - this is where patches should go
+ # Create a regular CF with later startdate - this is where patches should go
regular_cf = CommitFest.objects.create(
- name="2025-01",
+ name="2025-03",
status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 1, 1),
- enddate=date(2025, 1, 31),
+ startdate=date(2025, 3, 1),
+ enddate=date(2025, 3, 31),
draft=False,
)
@@ -422,6 +382,14 @@ def test_draft_cf_moves_active_patches_to_next_draft(alice, bob):
enddate=date(2025, 3, 31),
draft=True,
)
+ # Create a regular open CF with earlier startdate - should be ignored for draft patches
+ CommitFest.objects.create(
+ name="2025-05",
+ status=CommitFest.STATUS_OPEN,
+ startdate=date(2025, 5, 1),
+ enddate=date(2025, 5, 31),
+ draft=False,
+ )
next_draft_cf = CommitFest.objects.create(
name="2026-03-draft",
status=CommitFest.STATUS_OPEN,
diff --git a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
index 4c926567..11e4f4d7 100644
--- a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
+++ b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
@@ -7,16 +7,10 @@
CommitFest,
Patch,
PatchOnCommitFest,
- Topic,
)
from pgcommitfest.userprofile.models import UserProfile
-@pytest.fixture
-def topic(db):
- return Topic.objects.create(topic="General")
-
-
@pytest.fixture
def alice(db, django_user_model):
user = django_user_model.objects.create_user(
@@ -41,7 +35,7 @@ def create_closed_cf(name, startdate, enddate):
@pytest.mark.django_db
@freeze_time("2024-12-05")
-def test_inprogress_cf_closes_when_enddate_passed(topic, alice):
+def test_inprogress_cf_closes_when_enddate_passed(alice):
"""When an in_progress CF's enddate has passed, it should be closed."""
# Create some closed CFs for padding (relevant_commitfests needs history)
create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
@@ -73,7 +67,6 @@ def test_inprogress_cf_closes_when_enddate_passed(topic, alice):
# Create a patch with recent activity that should be auto-moved
patch = Patch.objects.create(
name="Test Patch",
- topic=topic,
lastmail=datetime(2024, 11, 25),
)
patch.authors.add(alice)
@@ -133,7 +126,7 @@ def test_open_cf_becomes_inprogress_when_startdate_reached():
@pytest.mark.django_db
@freeze_time("2025-02-05")
-def test_open_cf_closes_when_enddate_passed(topic, alice):
+def test_open_cf_closes_when_enddate_passed(alice):
"""When an open CF's enddate has passed (skipping in_progress), it closes."""
# Create some closed CFs for padding
create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
@@ -158,7 +151,6 @@ def test_open_cf_closes_when_enddate_passed(topic, alice):
# Create a patch with recent activity
patch = Patch.objects.create(
name="Test Patch",
- topic=topic,
lastmail=datetime(2025, 1, 25),
)
patch.authors.add(alice)
@@ -214,7 +206,7 @@ def test_draft_cf_created_when_missing():
@pytest.mark.django_db
@freeze_time("2025-04-05")
-def test_draft_cf_closes_when_enddate_passed(topic, alice):
+def test_draft_cf_closes_when_enddate_passed(alice):
"""When a draft CF's enddate has passed, it should be closed."""
# Create closed CFs for padding
create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
@@ -241,7 +233,6 @@ def test_draft_cf_closes_when_enddate_passed(topic, alice):
# Create a patch with recent activity
patch = Patch.objects.create(
name="Draft Patch",
- topic=topic,
lastmail=datetime(2025, 3, 25),
)
patch.authors.add(alice)
From 4e606368c0e3199dfa170ea7b0825e705f9ec36b Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 23:10:34 +0100
Subject: [PATCH 13/16] Simplify
---
.../tests/test_refresh_commitfests.py | 195 +++---------------
1 file changed, 30 insertions(+), 165 deletions(-)
diff --git a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
index 11e4f4d7..88d1ec0a 100644
--- a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
+++ b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
@@ -1,4 +1,4 @@
-from datetime import date, datetime
+from datetime import datetime
import pytest
from freezegun import freeze_time
@@ -8,63 +8,15 @@
Patch,
PatchOnCommitFest,
)
-from pgcommitfest.userprofile.models import UserProfile
-
-
-@pytest.fixture
-def alice(db, django_user_model):
- user = django_user_model.objects.create_user(
- username="alice",
- email="alice@example.com",
- first_name="Alice",
- last_name="Smith",
- )
- UserProfile.objects.create(user=user, notify_all_author=True)
- return user
-
-
-def create_closed_cf(name, startdate, enddate):
- """Helper to create a closed CF for padding."""
- return CommitFest.objects.create(
- name=name,
- status=CommitFest.STATUS_CLOSED,
- startdate=startdate,
- enddate=enddate,
- )
@pytest.mark.django_db
@freeze_time("2024-12-05")
-def test_inprogress_cf_closes_when_enddate_passed(alice):
+def test_inprogress_cf_closes_when_enddate_passed(commitfests, alice):
"""When an in_progress CF's enddate has passed, it should be closed."""
- # Create some closed CFs for padding (relevant_commitfests needs history)
- create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
- create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
-
- # Create an in_progress CF that ended
- in_progress_cf = CommitFest.objects.create(
- name="2024-11",
- status=CommitFest.STATUS_INPROGRESS,
- startdate=date(2024, 11, 1),
- enddate=date(2024, 11, 30),
- )
- # Create the next open CF (required for auto_move)
- open_cf = CommitFest.objects.create(
- name="2025-01",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 1, 1),
- enddate=date(2025, 1, 31),
- )
- # Create draft CF
- CommitFest.objects.create(
- name="2025-draft",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2024, 7, 1),
- enddate=date(2025, 3, 31),
- draft=True,
- )
+ in_progress_cf = commitfests["in_progress"]
+ open_cf = commitfests["open"]
- # Create a patch with recent activity that should be auto-moved
patch = Patch.objects.create(
name="Test Patch",
lastmail=datetime(2024, 11, 25),
@@ -82,41 +34,24 @@ def test_inprogress_cf_closes_when_enddate_passed(alice):
in_progress_cf.refresh_from_db()
assert in_progress_cf.status == CommitFest.STATUS_CLOSED
- # Patch should have been moved to the open CF
patch.refresh_from_db()
assert patch.current_commitfest().id == open_cf.id
@pytest.mark.django_db
@freeze_time("2025-01-15")
-def test_open_cf_becomes_inprogress_when_startdate_reached():
+def test_open_cf_becomes_inprogress_when_startdate_reached(commitfests):
"""When an open CF's startdate is reached, it becomes in_progress."""
- # Create some closed CFs for padding
- create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
- create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
- create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
-
- open_cf = CommitFest.objects.create(
- name="2025-01",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 1, 1),
- enddate=date(2025, 1, 31),
- )
- # Create draft CF
- CommitFest.objects.create(
- name="2025-draft",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2024, 7, 1),
- enddate=date(2025, 3, 31),
- draft=True,
- )
+ open_cf = commitfests["open"]
+ # Mark in_progress as closed so open_cf becomes the active one
+ commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
+ commitfests["in_progress"].save()
CommitFest._refresh_relevant_commitfests(for_update=False)
open_cf.refresh_from_db()
assert open_cf.status == CommitFest.STATUS_INPROGRESS
- # A new open CF should have been created
new_open = CommitFest.objects.filter(
status=CommitFest.STATUS_OPEN, draft=False
).first()
@@ -126,29 +61,13 @@ def test_open_cf_becomes_inprogress_when_startdate_reached():
@pytest.mark.django_db
@freeze_time("2025-02-05")
-def test_open_cf_closes_when_enddate_passed(alice):
+def test_open_cf_closes_when_enddate_passed(commitfests, alice):
"""When an open CF's enddate has passed (skipping in_progress), it closes."""
- # Create some closed CFs for padding
- create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
- create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
- create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
-
- open_cf = CommitFest.objects.create(
- name="2025-01",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 1, 1),
- enddate=date(2025, 1, 31),
- )
- # Create draft CF
- CommitFest.objects.create(
- name="2025-draft",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2024, 7, 1),
- enddate=date(2025, 3, 31),
- draft=True,
- )
+ open_cf = commitfests["open"]
+ # Mark in_progress as closed
+ commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
+ commitfests["in_progress"].save()
- # Create a patch with recent activity
patch = Patch.objects.create(
name="Test Patch",
lastmail=datetime(2025, 1, 25),
@@ -166,39 +85,29 @@ def test_open_cf_closes_when_enddate_passed(alice):
open_cf.refresh_from_db()
assert open_cf.status == CommitFest.STATUS_CLOSED
- # A new open CF should have been created
new_open = CommitFest.objects.filter(
status=CommitFest.STATUS_OPEN, draft=False
).first()
assert new_open is not None
- # Patch should have been moved to the new open CF
patch.refresh_from_db()
assert patch.current_commitfest().id == new_open.id
@pytest.mark.django_db
@freeze_time("2025-01-15")
-def test_draft_cf_created_when_missing():
+def test_draft_cf_created_when_missing(commitfests):
"""When no draft CF exists, one should be created."""
- # Create closed CFs for padding
- create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
- create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
- create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
-
- # Create only regular CFs
- CommitFest.objects.create(
- name="2025-01",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 3, 1),
- enddate=date(2025, 3, 31),
- )
+ # Mark in_progress as closed
+ commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
+ commitfests["in_progress"].save()
+ # Delete the draft CF
+ commitfests["draft"].delete()
assert not CommitFest.objects.filter(draft=True).exists()
CommitFest._refresh_relevant_commitfests(for_update=False)
- # A draft CF should have been created
draft_cf = CommitFest.objects.filter(draft=True).first()
assert draft_cf is not None
assert draft_cf.status == CommitFest.STATUS_OPEN
@@ -206,31 +115,13 @@ def test_draft_cf_created_when_missing():
@pytest.mark.django_db
@freeze_time("2025-04-05")
-def test_draft_cf_closes_when_enddate_passed(alice):
+def test_draft_cf_closes_when_enddate_passed(commitfests, alice):
"""When a draft CF's enddate has passed, it should be closed."""
- # Create closed CFs for padding
- create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
- create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
- create_closed_cf("2024-11", date(2024, 11, 1), date(2024, 11, 30))
-
- # Create an open regular CF (required)
- CommitFest.objects.create(
- name="2025-03",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 5, 1),
- enddate=date(2025, 5, 31),
- )
-
- # Create a draft CF that ended
- draft_cf = CommitFest.objects.create(
- name="2025-draft",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 1, 1),
- enddate=date(2025, 3, 31),
- draft=True,
- )
+ draft_cf = commitfests["draft"]
+ # Mark in_progress as closed
+ commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
+ commitfests["in_progress"].save()
- # Create a patch with recent activity
patch = Patch.objects.create(
name="Draft Patch",
lastmail=datetime(2025, 3, 25),
@@ -248,52 +139,28 @@ def test_draft_cf_closes_when_enddate_passed(alice):
draft_cf.refresh_from_db()
assert draft_cf.status == CommitFest.STATUS_CLOSED
- # A new draft CF should have been created
new_draft = CommitFest.objects.filter(
draft=True, status=CommitFest.STATUS_OPEN
).first()
assert new_draft is not None
assert new_draft.startdate > draft_cf.enddate
- # Patch should have been moved to the new draft CF
patch.refresh_from_db()
assert patch.current_commitfest().id == new_draft.id
@pytest.mark.django_db
-@freeze_time("2025-01-15")
-def test_no_changes_when_up_to_date():
+@freeze_time("2024-11-15")
+def test_no_changes_when_up_to_date(commitfests):
"""When commitfests are up to date, no changes should be made."""
- # Create closed CFs for padding
- create_closed_cf("2024-07", date(2024, 7, 1), date(2024, 7, 31))
- create_closed_cf("2024-09", date(2024, 9, 1), date(2024, 9, 30))
-
- # Create CFs that are all up to date
- in_progress_cf = CommitFest.objects.create(
- name="2025-01",
- status=CommitFest.STATUS_INPROGRESS,
- startdate=date(2025, 1, 1),
- enddate=date(2025, 1, 31),
- )
- open_cf = CommitFest.objects.create(
- name="2025-03",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 3, 1),
- enddate=date(2025, 3, 31),
- )
- draft_cf = CommitFest.objects.create(
- name="2025-draft",
- status=CommitFest.STATUS_OPEN,
- startdate=date(2025, 1, 1),
- enddate=date(2025, 3, 31),
- draft=True,
- )
+ in_progress_cf = commitfests["in_progress"]
+ open_cf = commitfests["open"]
+ draft_cf = commitfests["draft"]
initial_cf_count = CommitFest.objects.count()
CommitFest._refresh_relevant_commitfests(for_update=False)
- # All statuses should remain unchanged
in_progress_cf.refresh_from_db()
open_cf.refresh_from_db()
draft_cf.refresh_from_db()
@@ -301,6 +168,4 @@ def test_no_changes_when_up_to_date():
assert in_progress_cf.status == CommitFest.STATUS_INPROGRESS
assert open_cf.status == CommitFest.STATUS_OPEN
assert draft_cf.status == CommitFest.STATUS_OPEN
-
- # No new CFs should have been created
assert CommitFest.objects.count() == initial_cf_count
From a2a9b6a997f9ca9f72372058ad1268731892f4d7 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 23:42:07 +0100
Subject: [PATCH 14/16] Simplify more
---
pgcommitfest/commitfest/tests/test_refresh_commitfests.py | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
index 88d1ec0a..6df66931 100644
--- a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
+++ b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
@@ -9,8 +9,9 @@
PatchOnCommitFest,
)
+pytestmark = pytest.mark.django_db
+
-@pytest.mark.django_db
@freeze_time("2024-12-05")
def test_inprogress_cf_closes_when_enddate_passed(commitfests, alice):
"""When an in_progress CF's enddate has passed, it should be closed."""
@@ -38,7 +39,6 @@ def test_inprogress_cf_closes_when_enddate_passed(commitfests, alice):
assert patch.current_commitfest().id == open_cf.id
-@pytest.mark.django_db
@freeze_time("2025-01-15")
def test_open_cf_becomes_inprogress_when_startdate_reached(commitfests):
"""When an open CF's startdate is reached, it becomes in_progress."""
@@ -59,7 +59,6 @@ def test_open_cf_becomes_inprogress_when_startdate_reached(commitfests):
assert new_open.startdate > open_cf.enddate
-@pytest.mark.django_db
@freeze_time("2025-02-05")
def test_open_cf_closes_when_enddate_passed(commitfests, alice):
"""When an open CF's enddate has passed (skipping in_progress), it closes."""
@@ -94,7 +93,6 @@ def test_open_cf_closes_when_enddate_passed(commitfests, alice):
assert patch.current_commitfest().id == new_open.id
-@pytest.mark.django_db
@freeze_time("2025-01-15")
def test_draft_cf_created_when_missing(commitfests):
"""When no draft CF exists, one should be created."""
@@ -113,7 +111,6 @@ def test_draft_cf_created_when_missing(commitfests):
assert draft_cf.status == CommitFest.STATUS_OPEN
-@pytest.mark.django_db
@freeze_time("2025-04-05")
def test_draft_cf_closes_when_enddate_passed(commitfests, alice):
"""When a draft CF's enddate has passed, it should be closed."""
@@ -149,7 +146,6 @@ def test_draft_cf_closes_when_enddate_passed(commitfests, alice):
assert patch.current_commitfest().id == new_draft.id
-@pytest.mark.django_db
@freeze_time("2024-11-15")
def test_no_changes_when_up_to_date(commitfests):
"""When commitfests are up to date, no changes should be made."""
From 5fa6e0449508d2184b1d49fecd0f0ed248ab8165 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Sun, 4 Jan 2026 23:45:28 +0100
Subject: [PATCH 15/16] Simplify more
---
.../commitfest/tests/test_refresh_commitfests.py | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
index 6df66931..87850af8 100644
--- a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
+++ b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
@@ -43,9 +43,6 @@ def test_inprogress_cf_closes_when_enddate_passed(commitfests, alice):
def test_open_cf_becomes_inprogress_when_startdate_reached(commitfests):
"""When an open CF's startdate is reached, it becomes in_progress."""
open_cf = commitfests["open"]
- # Mark in_progress as closed so open_cf becomes the active one
- commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
- commitfests["in_progress"].save()
CommitFest._refresh_relevant_commitfests(for_update=False)
@@ -63,9 +60,6 @@ def test_open_cf_becomes_inprogress_when_startdate_reached(commitfests):
def test_open_cf_closes_when_enddate_passed(commitfests, alice):
"""When an open CF's enddate has passed (skipping in_progress), it closes."""
open_cf = commitfests["open"]
- # Mark in_progress as closed
- commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
- commitfests["in_progress"].save()
patch = Patch.objects.create(
name="Test Patch",
@@ -96,9 +90,6 @@ def test_open_cf_closes_when_enddate_passed(commitfests, alice):
@freeze_time("2025-01-15")
def test_draft_cf_created_when_missing(commitfests):
"""When no draft CF exists, one should be created."""
- # Mark in_progress as closed
- commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
- commitfests["in_progress"].save()
# Delete the draft CF
commitfests["draft"].delete()
@@ -115,9 +106,6 @@ def test_draft_cf_created_when_missing(commitfests):
def test_draft_cf_closes_when_enddate_passed(commitfests, alice):
"""When a draft CF's enddate has passed, it should be closed."""
draft_cf = commitfests["draft"]
- # Mark in_progress as closed
- commitfests["in_progress"].status = CommitFest.STATUS_CLOSED
- commitfests["in_progress"].save()
patch = Patch.objects.create(
name="Draft Patch",
From e5f615b3ebad62d9dd80edbdf687d64eb7f65f99 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio
Date: Mon, 5 Jan 2026 09:09:10 +0100
Subject: [PATCH 16/16] Additional comments
---
pgcommitfest/commitfest/models.py | 20 +------------------
.../templates/mail/commitfest_closure.txt | 4 +---
.../tests/test_refresh_commitfests.py | 3 +++
3 files changed, 5 insertions(+), 22 deletions(-)
diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py
index 76c56fb8..921fcf22 100644
--- a/pgcommitfest/commitfest/models.py
+++ b/pgcommitfest/commitfest/models.py
@@ -191,22 +191,6 @@ def send_closure_notifications(self):
if not open_pocs:
return
- # Get the next open commitfest if available
- next_cf = (
- CommitFest.objects.filter(
- status=CommitFest.STATUS_OPEN,
- draft=self.draft,
- startdate__gt=self.enddate,
- )
- .order_by("startdate")
- .first()
- )
-
- if next_cf:
- next_cf_url = f"https://commitfest.postgresql.org/{next_cf.id}/"
- else:
- next_cf_url = "https://commitfest.postgresql.org/"
-
# Collect unique authors and their patches
authors_patches = {}
for poc in open_pocs:
@@ -230,14 +214,12 @@ def send_closure_notifications(self):
settings.NOTIFICATION_FROM,
None,
email,
- f"Commitfest {self.name} has closed",
+ f"Commitfest {self.name} has closed and you have unmoved patches",
"mail/commitfest_closure.txt",
{
"user": author,
"commitfest": self,
"patches": patches,
- "next_cf": next_cf,
- "next_cf_url": next_cf_url,
},
)
diff --git a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
index ed6d42cd..298ff090 100644
--- a/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
+++ b/pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
@@ -1,5 +1,3 @@
-Hello {{user.first_name|default:user.username}},
-
Commitfest {{commitfest.name}} has now closed.
You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} that need{{patches|length|pluralize:"s,"}} attention:
@@ -11,6 +9,6 @@ You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} that nee
Please take action on {{patches|length|pluralize:"these patches,this patch"}} by doing either of the following:
-1. If you want to continue working on {{patches|length|pluralize:"them,it"}}, move {{patches|length|pluralize:"them,it"}} to the next commitfest{% if next_cf %}: {{next_cf_url}}{% endif %}
+1. If you want to continue working on {{patches|length|pluralize:"them,it"}}, move {{patches|length|pluralize:"them,it"}} to the next commitfest.
2. If you no longer wish to pursue {{patches|length|pluralize:"these patches,this patch"}}, please close {{patches|length|pluralize:"them,it"}} with an appropriate status (Withdrawn, Returned with feedback, etc.)
diff --git a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
index 87850af8..dda1fc9d 100644
--- a/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
+++ b/pgcommitfest/commitfest/tests/test_refresh_commitfests.py
@@ -35,6 +35,7 @@ def test_inprogress_cf_closes_when_enddate_passed(commitfests, alice):
in_progress_cf.refresh_from_db()
assert in_progress_cf.status == CommitFest.STATUS_CLOSED
+ # Patch should be auto-moved
patch.refresh_from_db()
assert patch.current_commitfest().id == open_cf.id
@@ -83,6 +84,7 @@ def test_open_cf_closes_when_enddate_passed(commitfests, alice):
).first()
assert new_open is not None
+ # Patch should be auto-moved
patch.refresh_from_db()
assert patch.current_commitfest().id == new_open.id
@@ -130,6 +132,7 @@ def test_draft_cf_closes_when_enddate_passed(commitfests, alice):
assert new_draft is not None
assert new_draft.startdate > draft_cf.enddate
+ # Patch should be auto-moved
patch.refresh_from_db()
assert patch.current_commitfest().id == new_draft.id