Skip to content

Commit f5c8b37

Browse files
committed
Automatically move "active" patches
1 parent ddfe04d commit f5c8b37

File tree

5 files changed

+466
-37
lines changed

5 files changed

+466
-37
lines changed

pgcommitfest/commitfest/models.py

Lines changed: 121 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -110,34 +110,123 @@ def to_json(self):
110110
"enddate": self.enddate.isoformat(),
111111
}
112112

113-
def send_closure_notifications(self):
114-
"""Send email notifications to authors of open patches when this commitfest is closed."""
113+
def _should_auto_move_patch(self, patch, current_date):
114+
"""Determine if a patch should be automatically moved to the next commitfest.
115+
116+
A patch qualifies for auto-move if it both:
117+
1. Has had email activity within the configured number of days
118+
2. Hasn't been failing CI for longer than the configured threshold
119+
"""
120+
activity_cutoff = current_date - timedelta(
121+
days=settings.AUTO_MOVE_EMAIL_ACTIVITY_DAYS
122+
)
123+
failing_cutoff = current_date - timedelta(
124+
days=settings.AUTO_MOVE_MAX_FAILING_DAYS
125+
)
126+
127+
# Check for recent email activity
128+
if not patch.lastmail or patch.lastmail < activity_cutoff:
129+
return False
130+
131+
# Check if CI has been failing too long
132+
try:
133+
cfbot_branch = patch.cfbot_branch
134+
if (
135+
cfbot_branch.failing_since
136+
and cfbot_branch.failing_since < failing_cutoff
137+
):
138+
return False
139+
except CfbotBranch.DoesNotExist:
140+
pass
141+
142+
return True
143+
144+
def auto_move_active_patches(self):
145+
"""Automatically move active patches to the next commitfest.
146+
147+
A patch is moved if it has recent email activity and hasn't been
148+
failing CI for too long.
149+
150+
Returns a set of patch IDs that were moved.
151+
"""
152+
current_date = datetime.now()
153+
154+
# Get the next open commitfest
155+
# For draft CFs, find the next draft CF
156+
# For regular CFs, find the next regular CF by start date
157+
if self.draft:
158+
next_cf = (
159+
CommitFest.objects.filter(
160+
status=CommitFest.STATUS_OPEN,
161+
draft=True,
162+
startdate__gt=self.enddate,
163+
)
164+
.order_by("startdate")
165+
.first()
166+
)
167+
else:
168+
next_cf = (
169+
CommitFest.objects.filter(
170+
status=CommitFest.STATUS_OPEN,
171+
draft=False,
172+
startdate__gt=self.enddate,
173+
)
174+
.order_by("startdate")
175+
.first()
176+
)
177+
178+
if not next_cf:
179+
return set()
115180

116181
# Get all patches with open status in this commitfest
117-
open_patches = (
182+
open_pocs = self.patchoncommitfest_set.filter(
183+
status__in=[
184+
PatchOnCommitFest.STATUS_REVIEW,
185+
PatchOnCommitFest.STATUS_AUTHOR,
186+
PatchOnCommitFest.STATUS_COMMITTER,
187+
]
188+
).select_related("patch")
189+
190+
moved_patch_ids = set()
191+
for poc in open_pocs:
192+
if self._should_auto_move_patch(poc.patch, current_date):
193+
poc.patch.move(self, next_cf, by_user=None, by_cfbot=True)
194+
moved_patch_ids.add(poc.patch.id)
195+
196+
return moved_patch_ids
197+
198+
def send_closure_notifications(self, moved_patch_ids=None):
199+
"""Send email notifications to authors of patches that weren't auto-moved.
200+
201+
Args:
202+
moved_patch_ids: Set of patch IDs that were auto-moved to the next commitfest.
203+
These patches are excluded since the move triggers its own notification.
204+
"""
205+
if moved_patch_ids is None:
206+
moved_patch_ids = set()
207+
208+
# Get patches that still need action (not moved, not closed)
209+
open_pocs = list(
118210
self.patchoncommitfest_set.filter(
119211
status__in=[
120212
PatchOnCommitFest.STATUS_REVIEW,
121213
PatchOnCommitFest.STATUS_AUTHOR,
122214
PatchOnCommitFest.STATUS_COMMITTER,
123215
]
124216
)
217+
.exclude(patch_id__in=moved_patch_ids)
125218
.select_related("patch")
126219
.prefetch_related("patch__authors")
127220
)
128221

129-
# Collect unique authors across all open patches
130-
authors_to_notify = set()
131-
for poc in open_patches:
132-
for author in poc.patch.authors.all():
133-
if author.email:
134-
authors_to_notify.add(author)
222+
if not open_pocs:
223+
return
135224

136225
# Get the next open commitfest if available
137226
next_cf = (
138227
CommitFest.objects.filter(
139228
status=CommitFest.STATUS_OPEN,
140-
draft=False,
229+
draft=self.draft,
141230
startdate__gt=self.enddate,
142231
)
143232
.order_by("startdate")
@@ -149,8 +238,18 @@ def send_closure_notifications(self):
149238
else:
150239
next_cf_url = "https://commitfest.postgresql.org/"
151240

241+
# Collect unique authors and their patches
242+
authors_patches = {}
243+
for poc in open_pocs:
244+
for author in poc.patch.authors.all():
245+
if not author.email:
246+
continue
247+
if author not in authors_patches:
248+
authors_patches[author] = []
249+
authors_patches[author].append(poc)
250+
152251
# Send email to each author
153-
for author in authors_to_notify:
252+
for author, patches in authors_patches.items():
154253
# Get user's notification email preference
155254
email = author.email
156255
try:
@@ -159,11 +258,6 @@ def send_closure_notifications(self):
159258
except UserProfile.DoesNotExist:
160259
pass
161260

162-
# Get user's open patches in this commitfest
163-
user_patches = [
164-
poc for poc in open_patches if author in poc.patch.authors.all()
165-
]
166-
167261
send_template_mail(
168262
settings.NOTIFICATION_FROM,
169263
None,
@@ -173,7 +267,7 @@ def send_closure_notifications(self):
173267
{
174268
"user": author,
175269
"commitfest": self,
176-
"patches": user_patches,
270+
"patches": patches,
177271
"next_cf": next_cf,
178272
"next_cf_url": next_cf_url,
179273
},
@@ -211,17 +305,19 @@ def _refresh_relevant_commitfests(cls, for_update):
211305

212306
inprogress_cf = cfs["in_progress"]
213307
if inprogress_cf and inprogress_cf.enddate < current_date:
308+
moved_patch_ids = inprogress_cf.auto_move_active_patches()
214309
inprogress_cf.status = CommitFest.STATUS_CLOSED
215310
inprogress_cf.save()
216-
inprogress_cf.send_closure_notifications()
311+
inprogress_cf.send_closure_notifications(moved_patch_ids)
217312

218313
open_cf = cfs["open"]
219314

220315
if open_cf.startdate <= current_date:
221316
if open_cf.enddate < current_date:
317+
moved_patch_ids = open_cf.auto_move_active_patches()
222318
open_cf.status = CommitFest.STATUS_CLOSED
223319
open_cf.save()
224-
open_cf.send_closure_notifications()
320+
open_cf.send_closure_notifications(moved_patch_ids)
225321
else:
226322
open_cf.status = CommitFest.STATUS_INPROGRESS
227323
open_cf.save()
@@ -233,9 +329,10 @@ def _refresh_relevant_commitfests(cls, for_update):
233329
cls.next_draft_cf(current_date).save()
234330
elif draft_cf.enddate < current_date:
235331
# If the draft commitfest has started, we need to update it
332+
moved_patch_ids = draft_cf.auto_move_active_patches()
236333
draft_cf.status = CommitFest.STATUS_CLOSED
237334
draft_cf.save()
238-
draft_cf.send_closure_notifications()
335+
draft_cf.send_closure_notifications(moved_patch_ids)
239336
cls.next_draft_cf(current_date).save()
240337

241338
return cls.relevant_commitfests(for_update=for_update)
@@ -530,7 +627,9 @@ def update_lastmail(self):
530627
else:
531628
self.lastmail = max(threads, key=lambda t: t.latestmessage).latestmessage
532629

533-
def move(self, from_cf, to_cf, by_user, allow_move_to_in_progress=False):
630+
def move(
631+
self, from_cf, to_cf, by_user, allow_move_to_in_progress=False, by_cfbot=False
632+
):
534633
"""Returns the new PatchOnCommitFest object, or raises UserInputError"""
535634

536635
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):
575674
PatchHistory(
576675
patch=self,
577676
by=by_user,
677+
by_cfbot=by_cfbot,
578678
what=f"Moved from CF {from_cf} to CF {to_cf}",
579679
).save_and_notify()
580680

pgcommitfest/commitfest/templates/mail/commitfest_closure.txt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
Hello {{user.first_name|default:user.username}},
22

3-
Commitfest {{commitfest.name}} has now closed. You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} in this commitfest:
3+
Commitfest {{commitfest.name}} has now closed.
44

5-
{%for poc in patches%}
5+
You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} that need{{patches|length|pluralize:"s,"}} attention:
6+
7+
{% for poc in patches %}
68
- {{poc.patch.name}}
79
https://commitfest.postgresql.org/{{commitfest.id}}/{{poc.patch.id}}/
8-
{%endfor%}
10+
{% endfor %}
911

10-
Please take action on {{patches|length|pluralize:"this patch,these patches"}}:
12+
Please take action on {{patches|length|pluralize:"these patches,this patch"}}:
1113

12-
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 %}
14+
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 %}
1315

14-
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.)
16+
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.)
1517

1618
{% 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 %}
1719

pgcommitfest/commitfest/tests/conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@
77
import pytest
88

99
from pgcommitfest.commitfest.models import CommitFest
10+
from pgcommitfest.userprofile.models import UserProfile
1011

1112

1213
@pytest.fixture
1314
def alice():
14-
"""Create test user Alice."""
15-
return User.objects.create_user(
15+
"""Create test user Alice with notify_all_author enabled."""
16+
user = User.objects.create_user(
1617
username="alice",
1718
first_name="Alice",
1819
last_name="Anderson",
1920
email="alice@example.com",
2021
)
22+
UserProfile.objects.create(user=user, notify_all_author=True)
23+
return user
2124

2225

2326
@pytest.fixture

0 commit comments

Comments
 (0)