Skip to content

Commit 9bc4ea1

Browse files
committed
WIP: add a Drafts commitfest
Drafts is a special always-open commitfest for the purpose of holding and testing draft patch submissions. Having such a holding area should make it easier for people to keep track of patchsets they're not quite ready to submit for review. Internally, Drafts is assigned a static ID of zero. This is chosen because a) it does not overlap with Django's default AutoField sequence, which begins at one, and b) it requires no updates to the current URL patterns, which match nonnegative integers. The Drafts entry has the special status STATUS_DRAFT so that it does not conflict with pre-existing coded assumptions on what "open", "future", etc. mean. STATUS_DRAFT CFs are excluded from the "num_cfs" count for a patch. The new /close/draft handler is added to swap patches into Drafts. Patches are then removed by moving them to the next open CF, or by closing as usual. Prior to this patch: - CFs with IDs less than the current in-progress CF could safely be assumed closed, - patches only ever moved forward through increasing CF IDs, and - the latest CF start date determined a patch's "current" CF. These assumptions all break under the current model. They have been modified: - use STATUS_CLOSED specifically when deciding whether a CF is closed - when moving a patch between CFs, allow for the possibility of an existing entry in the junction table - a patch's "current" CF is determined by its latest entry date TODO: - ensure all prior assumptions on CF ID are cleaned up
1 parent 2182e23 commit 9bc4ea1

File tree

8 files changed

+98
-10
lines changed

8 files changed

+98
-10
lines changed

media/commitfest/js/commitfest.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ function verify_next() {
1818
'Are you sure you want to move this patch to the next commitfest?\n\nThis means the patch will be marked as closed in this commitfest, but will automatically be moved to the next one. If no further work is expected on this patch, it should be closed with "Rejected" or "Returned with Feedback" instead.\n\nSo - are you sure?',
1919
);
2020
}
21+
function verify_draft() {
22+
return confirm(
23+
'Are you sure you want to move this patch to Drafts?\n\nThis means the patch will be marked as closed in this commitfest. Its status will be reset to Waiting on Author, and it will remain in Drafts until it is closed or moved to the next open commitfest.\n\nSo - are you sure?',
24+
);
25+
}
2126
function findLatestThreads() {
2227
$("#attachThreadListWrap").addClass("loading");
2328
$("#attachThreadSearchButton").addClass("disabled");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Generated by Django 4.2.19 on 2025-03-10 16:28
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("commitfest", "0012_commitfest_status_draft"),
9+
]
10+
11+
operations = [
12+
migrations.RunSQL("""
13+
INSERT INTO commitfest_commitfest (id, name, status)
14+
VALUES (0, 'Drafts', 5);
15+
"""),
16+
]

pgcommitfest/commitfest/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class Patch(models.Model, DiffableModel):
162162
}
163163

164164
def current_commitfest(self):
165-
return self.commitfests.order_by("-startdate").first()
165+
return self.commitfests.order_by("-patchoncommitfest__enterdate").first()
166166

167167
def current_patch_on_commitfest(self):
168168
cf = self.current_commitfest()

pgcommitfest/commitfest/templates/home.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ <h3>Commands</h3>
2424
</form>
2525
<h3>List of commitfests</h3>
2626
<ul>
27+
<li><a href="/0/">Drafts</a></li>
2728
{%for c in commitfests%}
2829
<li><a href="/{{c.id}}/">{{c}}</a> ({{c.statusstring}}{%if c.startdate%} - {{c.periodstring}}{%endif%})</li>
2930
{%endfor%}

pgcommitfest/commitfest/templates/patch_commands.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<li role="presentation"><a href="close/withdrawn/" onclick="return verify_withdrawn()">Withdrawn</a></li>
2323
<li role="presentation"><a href="close/feedback/" onclick="return verify_returned()">Returned with feedback</a></li>
2424
<li role="presentation"><a href="close/next/?cfid={{cf.id}}" onclick="return verify_next()">Move to next CF</a></li>
25+
<li role="presentation"><a href="close/draft/" onclick="return verify_draft()">Move to Drafts</a></li>
2526
<li role="presentation"><a href="close/committed/" onclick="return flagCommitted({%if patch.committer%}'{{patch.committer}}'{%elif is_committer%}'{{user.username}}'{%else%}null{%endif%})">Committed</a></li>
2627
</ul>
2728
</div>

pgcommitfest/commitfest/views.py

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646

4747

4848
def home(request):
49-
commitfests = list(CommitFest.objects.all())
49+
# Skip "special" commitfest holding areas (primary keys <= 0); they're
50+
# handled separately.
51+
commitfests = list(CommitFest.objects.filter(pk__gt=0))
5052
opencf = next((c for c in commitfests if c.status == CommitFest.STATUS_OPEN), None)
5153
inprogresscf = next(
5254
(c for c in commitfests if c.status == CommitFest.STATUS_INPROGRESS), None
@@ -474,6 +476,7 @@ def patchlist(request, cf, personalized=False):
474476
params = {
475477
"openstatuses": PatchOnCommitFest.OPEN_STATUSES,
476478
"cid": cf.id,
479+
"draft": CommitFest.STATUS_DRAFT,
477480
}
478481
params.update(whereparams)
479482

@@ -485,7 +488,9 @@ def patchlist(request, cf, personalized=False):
485488
(poc.status=ANY(%(openstatuses)s)) AS is_open,
486489
(SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_authors cpa ON cpa.user_id=auth_user.id WHERE cpa.patch_id=p.id) AS author_names,
487490
(SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_reviewers cpr ON cpr.user_id=auth_user.id WHERE cpr.patch_id=p.id) AS reviewer_names,
488-
(SELECT count(1) FROM commitfest_patchoncommitfest pcf WHERE pcf.patch_id=p.id) AS num_cfs,
491+
(SELECT count(1) FROM commitfest_patchoncommitfest pcf
492+
INNER JOIN commitfest_commitfest cf ON cf.id = pcf.commitfest_id
493+
WHERE pcf.patch_id=p.id AND cf.status != %(draft)s) AS num_cfs,
489494
490495
branch.needs_rebase_since,
491496
branch.failing_since,
@@ -665,7 +670,7 @@ def patch(request, patchid):
665670
patch_commitfests = (
666671
PatchOnCommitFest.objects.select_related("commitfest")
667672
.filter(patch=patch)
668-
.order_by("-commitfest__startdate")
673+
.order_by("-enterdate")
669674
.all()
670675
)
671676
cf = patch_commitfests[0].commitfest
@@ -1061,6 +1066,7 @@ def close(request, patchid, status):
10611066
PatchOnCommitFest.STATUS_REVIEW,
10621067
PatchOnCommitFest.STATUS_AUTHOR,
10631068
PatchOnCommitFest.STATUS_COMMITTER,
1069+
PatchOnCommitFest.STATUS_DRAFT,
10641070
):
10651071
# This one can be moved
10661072
pass
@@ -1113,14 +1119,70 @@ def close(request, patchid, status):
11131119
"/%s/%s/" % (poc.commitfest.id, poc.patch.id)
11141120
)
11151121
# Create a mapping to the new commitfest that we are bouncing
1116-
# this patch to.
1117-
newpoc = PatchOnCommitFest(
1122+
# this patch to. Patches may be bounced back and forth from draft,
1123+
# so we have to handle a potential previous entry for this patch.
1124+
PatchOnCommitFest.objects.update_or_create(
11181125
patch=poc.patch,
11191126
commitfest=newcf[0],
1120-
status=oldstatus,
1121-
enterdate=datetime.now(),
1127+
defaults=dict(
1128+
status=oldstatus,
1129+
enterdate=datetime.now(),
1130+
leavedate=None,
1131+
),
1132+
)
1133+
elif status == "draft":
1134+
# The Drafts CF has similar considerations to "next", but we're more
1135+
# lenient about what can be moved in.
1136+
if poc.status in (
1137+
PatchOnCommitFest.STATUS_COMMITTED,
1138+
PatchOnCommitFest.STATUS_DRAFT,
1139+
PatchOnCommitFest.STATUS_NEXT,
1140+
PatchOnCommitFest.STATUS_REJECTED,
1141+
):
1142+
# Can't be moved!
1143+
messages.error(
1144+
request,
1145+
"A patch in status {0} cannot be moved to Drafts.".format(
1146+
poc.statusstring
1147+
),
1148+
)
1149+
return HttpResponseRedirect("/%s/%s/" % (poc.commitfest.id, poc.patch.id))
1150+
elif poc.status in (
1151+
PatchOnCommitFest.STATUS_AUTHOR,
1152+
PatchOnCommitFest.STATUS_COMMITTER,
1153+
PatchOnCommitFest.STATUS_RETURNED,
1154+
PatchOnCommitFest.STATUS_REVIEW,
1155+
PatchOnCommitFest.STATUS_WITHDRAWN,
1156+
):
1157+
# This one can be moved
1158+
pass
1159+
else:
1160+
messages.error(request, "Invalid existing patch status")
1161+
1162+
poc.status = PatchOnCommitFest.STATUS_DRAFT
1163+
1164+
# Map this patch directly to the Drafts CF.
1165+
try:
1166+
drafts = CommitFest.objects.get(pk=0)
1167+
except CommitFest.DoesNotExist:
1168+
messages.error(request, "No draft commitfest exists!")
1169+
return HttpResponseRedirect("/%s/%s/" % (poc.commitfest.id, poc.patch.id))
1170+
1171+
if poc.commitfest == drafts:
1172+
messages.error(request, "Patch is already in Drafts.")
1173+
return HttpResponseRedirect("/%s/%s/" % (poc.commitfest.id, poc.patch.id))
1174+
1175+
# Patches may be bounced back and forth from draft, so we have
1176+
# to handle a potential previous entry for this patch.
1177+
PatchOnCommitFest.objects.update_or_create(
1178+
patch=poc.patch,
1179+
commitfest=drafts,
1180+
defaults=dict(
1181+
status=PatchOnCommitFest.STATUS_AUTHOR,
1182+
enterdate=datetime.now(),
1183+
leavedate=None,
1184+
),
11221185
)
1123-
newpoc.save()
11241186
elif status == "committed":
11251187
committer = get_object_or_404(Committer, user__username=request.GET["c"])
11261188
if committer != poc.patch.committer:

pgcommitfest/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
}
2020
}
2121

22+
# "Static" commitfests use primary key values <= 0, so this must be an unsigned
23+
# field type.
2224
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
2325

2426
# Local time zone for this installation. Choices can be found here:

pgcommitfest/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
re_path(r"^(\d+)/new/$", views.newpatch),
2828
re_path(r"^patch/(\d+)/status/(review|author|committer)/$", views.status),
2929
re_path(
30-
r"^patch/(\d+)/close/(reject|withdrawn|feedback|committed|next)/$", views.close
30+
r"^patch/(\d+)/close/(reject|withdrawn|feedback|committed|next|draft)/$",
31+
views.close,
3132
),
3233
re_path(r"^patch/(\d+)/reviewer/(become|remove)/$", views.reviewer),
3334
re_path(r"^patch/(\d+)/committer/(become|remove)/$", views.committer),

0 commit comments

Comments
 (0)