From 337bafe504e15193d2aa00846fd39718814452b0 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 2 Mar 2025 15:43:42 +0100 Subject: [PATCH 1/3] Revamp the homepage to show you patches you might be interested in --- media/commitfest/css/commitfest.css | 4 + pgcommitfest/commitfest/forms.py | 5 +- pgcommitfest/commitfest/models.py | 6 + .../commitfest/templates/archive.html | 9 + pgcommitfest/commitfest/templates/base.html | 6 +- pgcommitfest/commitfest/templates/home.html | 148 +++++++++-- .../commitfest/templatetags/commitfest.py | 16 +- pgcommitfest/commitfest/views.py | 246 +++++++++++++----- pgcommitfest/urls.py | 1 + 9 files changed, 355 insertions(+), 86 deletions(-) create mode 100644 pgcommitfest/commitfest/templates/archive.html diff --git a/media/commitfest/css/commitfest.css b/media/commitfest/css/commitfest.css index 0d9aaa35..72cae0e9 100644 --- a/media/commitfest/css/commitfest.css +++ b/media/commitfest/css/commitfest.css @@ -87,3 +87,7 @@ div.form-group div.controls input.threadpick-input { font-weight: bold; color: red; } + +.search-bar { + display: inline-block; +} diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index 6284f0b3..a0d625e5 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -26,7 +26,10 @@ def __init__(self, cf, *args, **kwargs): c = [(-1, "* All")] + list(PatchOnCommitFest._STATUS_CHOICES) self.fields["status"] = forms.ChoiceField(choices=c, required=False) - q = Q(patch_author__commitfests=cf) | Q(patch_reviewer__commitfests=cf) + if cf: + q = Q(patch_author__commitfests=cf) | Q(patch_reviewer__commitfests=cf) + else: + q = Q() userchoices = [(-1, "* All"), (-2, "* None"), (-3, "* Yourself")] + [ (u.id, "%s %s (%s)" % (u.first_name, u.last_name, u.username)) for u in User.objects.filter(q) diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index ee58af8d..fcd9edb9 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -44,6 +44,12 @@ class CommitFest(models.Model): (STATUS_INPROGRESS, "In Progress"), (STATUS_CLOSED, "Closed"), ) + _STATUS_LABELS = ( + (STATUS_FUTURE, "default"), + (STATUS_OPEN, "info"), + (STATUS_INPROGRESS, "success"), + (STATUS_CLOSED, "danger"), + ) name = models.CharField(max_length=100, blank=False, null=False, unique=True) status = models.IntegerField( null=False, blank=False, default=1, choices=_STATUS_CHOICES diff --git a/pgcommitfest/commitfest/templates/archive.html b/pgcommitfest/commitfest/templates/archive.html new file mode 100644 index 00000000..f22c6161 --- /dev/null +++ b/pgcommitfest/commitfest/templates/archive.html @@ -0,0 +1,9 @@ +{%extends "base.html"%} +{%block contents%} + +{%endblock%} + diff --git a/pgcommitfest/commitfest/templates/base.html b/pgcommitfest/commitfest/templates/base.html index 4e564908..550cdf02 100644 --- a/pgcommitfest/commitfest/templates/base.html +++ b/pgcommitfest/commitfest/templates/base.html @@ -2,7 +2,7 @@ - {{title}} + {{title|default:'Commitfest' }} @@ -29,7 +29,9 @@ {%if header_activity%}
  • {{header_activity}}
  • {%endif%} -

    {{title}}

    + {%if title %} +

    {{title}}

    + {%endif%} {%if messages%} {%for m in messages%} diff --git a/pgcommitfest/commitfest/templates/home.html b/pgcommitfest/commitfest/templates/home.html index b0bbf909..4f7fa9f2 100644 --- a/pgcommitfest/commitfest/templates/home.html +++ b/pgcommitfest/commitfest/templates/home.html @@ -1,31 +1,131 @@ {%extends "base.html"%} +{%load commitfest %} {%block contents%} -

    - {%if inprogresscf%}A commitfest is currently in progress: {{inprogresscf}}.{%endif%} -

    -

    - Useful links that you can use and bookmark: -

    - -

    Commands

    -
    + New patch + Current commitfest + Open commitfest + Archive + +
    - +
    -

    List of commitfests

    - -
    +
    +
    + + + + {%for f in form%} + {%if not f.is_hidden%} + + {%else%} + + {%endif%} + {%endfor%} + {%comment%} Add one extra for buttons {%endcomment%} + + + + + + {%for f in form%} + + {%endfor%} + + + +
    {{f.label}}
    {{f|field_class:"form-control"}} + + Clear +
    +
    +
    + {%for p in patches %} + {%ifchanged p.is_open%} + {%if not forloop.first%} + + + {%endif%} +

    {{p.is_open|yesno:"Active patches,Closed patches"}}

    + + + + + + + + + + + + + + + + {%if user.is_staff%} + + {%endif%} + + + + {%endifchanged%} + + {%if grouping%} + {%ifchanged p.topic%} + + {%endifchanged%} + {%endif%} + + + + + + + + + {%endwith%} + + + + + + {%if user.is_staff%} + + {%endif%} + + {%if forloop.last%} + +
    Patch{%if sortkey == 5%}
    {%elif sortkey == -5%}
    {%endif%}
    ID{%if sortkey == 4%}
    {%elif sortkey == -4%}
    {%endif%}
    CF{%if sortkey == 8%}
    {%elif sortkey == -8%}
    {%endif%}
    StatusVerCI status{%if sortkey == 7%}
    {%elif sortkey == -7%}
    {%endif%}
    Stats{%if sortkey == 6%}
    {%elif sortkey == -6%}
    {%endif%}
    AuthorReviewersCommitterNum cfs{%if sortkey == 3%}
    {%elif sortkey == -3%}
    {%endif%}
    Latest mail{%if sortkey == 2%}
    {%elif sortkey == -2%}
    {%endif%}
    Select
    {{p.topic}}
    {{p.name}}{{p.id}}{{p.cf_name}}{{p.status|patchstatusstring}}{%if p.targetversion%}{{p.targetversion}}{%endif%} + {%with p.cfbot_results as cfb%} + {%if not cfb %} + Not processed + {%elif p.needs_rebase_since %} + + Needs rebase! + + {%else%} + + + {%if cfb.failed > 0 or cfb.branch_status == 'failed' or cfb.branch_status == 'timeout' %} + + {%elif cfb.completed < cfb.total %} + + {%else%} + + {%endif%} + + {{cfb.completed}}/{{cfb.total}} + + + {%endif%} + + {%if cfb and cfb.all_additions is not none %} + +{{ cfb.all_additions }}−{{ cfb.all_deletions }} + {%endif%} + {{p.author_names|default:''}}{{p.reviewer_names|default:''}}{{p.committer|default:''}}{{p.num_cfs}}{%if p.lastmail %}{{p.lastmail|timesince}} ago{%endif%}Author
    Reviewer
    + {%endif%} + {%endfor%} {%endblock%} diff --git a/pgcommitfest/commitfest/templatetags/commitfest.py b/pgcommitfest/commitfest/templatetags/commitfest.py index f24dbfd8..30242705 100644 --- a/pgcommitfest/commitfest/templatetags/commitfest.py +++ b/pgcommitfest/commitfest/templatetags/commitfest.py @@ -3,11 +3,25 @@ from uuid import uuid4 -from pgcommitfest.commitfest.models import PatchOnCommitFest +from pgcommitfest.commitfest.models import CommitFest, PatchOnCommitFest register = template.Library() +@register.filter(name="commitfeststatusstring") +@stringfilter +def commitfeststatusstring(value): + i = int(value) + return [v for k, v in CommitFest._STATUS_CHOICES if k == i][0] + + +@register.filter(name="commitfeststatuslabel") +@stringfilter +def commitfeststatuslabel(value): + i = int(value) + return [v for k, v in CommitFest._STATUS_LABELS if k == i][0] + + @register.filter(name="patchstatusstring") @stringfilter def patchstatusstring(value): diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index cbad8580..a3397b19 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -13,6 +13,7 @@ from django.shortcuts import get_object_or_404, render from django.views.decorators.csrf import csrf_exempt +import collections import hmac import json import urllib @@ -44,22 +45,56 @@ def home(request): - commitfests = list(CommitFest.objects.all()) - opencf = next((c for c in commitfests if c.status == CommitFest.STATUS_OPEN), None) - inprogresscf = next( - (c for c in commitfests if c.status == CommitFest.STATUS_INPROGRESS), None - ) + cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_INPROGRESS)) + if len(cfs) == 0: + cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_OPEN)) + + if len(cfs) > 0: + cf = cfs[0] + else: + cf = None + + # Generates a fairly expensive query, which we shouldn't do unless + # the user is logged in. XXX: Figure out how to avoid doing that.. + form = CommitFestFilterForm(None, request.GET) + + if request.user.is_authenticated: + patch_list = patchlist(request, cf, personalized=True) + else: + patch_list = patchlist(request, cf) + + if patch_list.redirect: + return HttpResponseRedirect("/") return render( request, "home.html", + { + "form": form, + "title": None, + "patches": patch_list.patches, + "statussummary": "", + "has_filter": patch_list.has_filter, + "grouping": patch_list.sortkey == 0, + "sortkey": patch_list.sortkey, + "openpatchids": [p["id"] for p in patch_list.patches if p["is_open"]], + "header_activity": "Activity log", + "header_activity_link": "/activity/", + }, + ) + + +def archive(request): + commitfests = list(CommitFest.objects.all()) + + return render( + request, + "archive.html", { "commitfests": commitfests, - "opencf": opencf, - "inprogresscf": inprogresscf, "title": "Commitfests", "header_activity": "Activity log", - "header_activity_link": "/activity/", + "header_activity_link": "activity/", }, ) @@ -150,13 +185,16 @@ def redir(request, what, end): return HttpResponseRedirect(f"/{cfs[0].id}/{end}{query_string}") -def commitfest(request, cfid): - # Find ourselves - cf = get_object_or_404(CommitFest, pk=cfid) +PatchList = collections.namedtuple( + "PatchList", ["patches", "has_filter", "sortkey", "redirect"] +) + +def patchlist(request, cf, personalized=False): # Build a dynamic filter based on the filtering options entered whereclauses = [] whereparams = {} + if request.GET.get("status", "-1") != "-1": try: whereparams["status"] = int(request.GET["status"]) @@ -232,68 +270,144 @@ def commitfest(request, cfid): has_filter = len(whereclauses) > 0 + if personalized: + whereclauses.append(""" + EXISTS ( + SELECT 1 FROM commitfest_patch_reviewers cpr WHERE cpr.patch_id=p.id AND cpr.user_id=%(self)s + ) OR EXISTS ( + SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(self)s + ) OR p.committer_id=%(self)s""") + whereparams["self"] = request.user.id + + whereclauses.append("poc.status=ANY(%(openstatuses)s)") + else: + whereclauses.append("poc.commitfest_id=%(cid)s") + + if personalized: + # For now we can just order by these names in descending order, because + # they are crafted such that they alphabetically sort in the intended + # order. + columns_str = """ + CASE WHEN + EXISTS ( + SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(self)s + ) AND ( + poc.commitfest_id < %(cid)s + ) + THEN 'Your still open patches in a closed commitfest (you should move or close these)' + WHEN + EXISTS ( + SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(self)s + ) AND ( + poc.status=%(needs_author)s + OR branch.needs_rebase_since IS NOT NULL + OR branch.failing_since + interval '4 days' < now() + OR (%(is_committer)s AND poc.status=%(needs_committer)s) + ) + THEN 'Your patches that need changes from you' + WHEN + NOT EXISTS ( + SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(self)s + ) AND ( + poc.status=ANY(%(review_statuses)s) + ) + THEN 'Patches you might want to review' + ELSE 'Blocked on others' + END AS topic, + cf.id AS cf_id, + cf.name AS cf_name, + cf.status AS cf_status, + """ + whereparams["needs_author"] = PatchOnCommitFest.STATUS_AUTHOR + whereparams["needs_committer"] = PatchOnCommitFest.STATUS_COMMITTER + is_committer = bool(Committer.objects.filter(user=request.user, active=True)) + whereparams["is_committer"] = is_committer + + if is_committer: + whereparams["review_statuses"] = [ + PatchOnCommitFest.STATUS_REVIEW, + PatchOnCommitFest.STATUS_COMMITTER, + ] + else: + whereparams["review_statuses"] = [ + PatchOnCommitFest.STATUS_REVIEW, + ] + joins_str = "INNER JOIN commitfest_commitfest cf ON poc.commitfest_id=cf.id" + groupby_str = "cf.id," + else: + columns_str = "t.topic as topic," + joins_str = "" + groupby_str = "" + # Figure out custom ordering - if request.GET.get("sortkey", "") != "": - try: - sortkey = int(request.GET["sortkey"]) - except ValueError: - sortkey = 0 - - if sortkey == 2: - orderby_str = "lastmail, created" - elif sortkey == -2: - orderby_str = "lastmail DESC, created DESC" - elif sortkey == 3: - orderby_str = "num_cfs DESC, modified, created" - elif sortkey == -3: - orderby_str = "num_cfs ASC, modified DESC, created DESC" - elif sortkey == 4: - orderby_str = "p.id" - elif sortkey == -4: - orderby_str = "p.id DESC" - elif sortkey == 5: - orderby_str = "p.name, created" - elif sortkey == -5: - orderby_str = "p.name DESC, created DESC" - elif sortkey == 6: + try: + sortkey = int(request.GET.get("sortkey", "0")) + except ValueError: + sortkey = 0 + + if sortkey == 2: + orderby_str = "lastmail, created" + elif sortkey == -2: + orderby_str = "lastmail DESC, created DESC" + elif sortkey == 3: + orderby_str = "num_cfs DESC, modified, created" + elif sortkey == -3: + orderby_str = "num_cfs ASC, modified DESC, created DESC" + elif sortkey == 4: + orderby_str = "p.id" + elif sortkey == -4: + orderby_str = "p.id DESC" + elif sortkey == 5: + orderby_str = "p.name, created" + elif sortkey == -5: + orderby_str = "p.name DESC, created DESC" + elif sortkey == 6: + orderby_str = "branch.all_additions + branch.all_deletions NULLS LAST, created" + elif sortkey == -6: + orderby_str = ( + "branch.all_additions + branch.all_deletions DESC NULLS LAST, created DESC" + ) + elif sortkey == 7: + orderby_str = "branch.failing_since DESC NULLS FIRST, branch.created DESC" + elif sortkey == -7: + orderby_str = "branch.failing_since NULLS LAST, branch.created" + elif sortkey == 8: + orderby_str = "poc.commitfest_id, lastmail DESC" + elif sortkey == -8: + orderby_str = "poc.commitfest_id DESC, lastmail" + else: + if personalized: orderby_str = ( - "branch.all_additions + branch.all_deletions NULLS LAST, created" + "topic DESC, branch.failing_since NULLS FIRST, cf.id, lastmail DESC" ) - elif sortkey == -6: - orderby_str = "branch.all_additions + branch.all_deletions DESC NULLS LAST, created DESC" - elif sortkey == 7: - orderby_str = "branch.failing_since DESC NULLS FIRST, branch.created DESC" - elif sortkey == -7: - orderby_str = "branch.failing_since NULLS LAST, branch.created" else: - orderby_str = "p.id" - sortkey = 0 - else: - orderby_str = "topic, created" + orderby_str = "topic, created" sortkey = 0 if not has_filter and sortkey == 0 and request.GET: # Redirect to get rid of the ugly url - return HttpResponseRedirect("/%s/" % cf.id) + return PatchList(patches=[], has_filter=False, sortkey=0, redirect=True) if whereclauses: - where_str = "AND ({0})".format(" AND ".join(whereclauses)) + where_str = "({0})".format(") AND (".join(whereclauses)) else: - where_str = "" + where_str = "true" params = { - "cid": cf.id, "openstatuses": PatchOnCommitFest.OPEN_STATUSES, + "cid": cf.id, } params.update(whereparams) # Let's not overload the poor django ORM curs = connection.cursor() curs.execute( - """SELECT p.id, p.name, poc.status, v.version AS targetversion, p.created, p.modified, p.lastmail, committer.first_name || ' ' || committer.last_name || ' (' || committer.username || ')' AS committer, t.topic, + f"""SELECT p.id, p.name, poc.status, v.version AS targetversion, p.created, p.modified, p.lastmail, committer.first_name || ' ' || committer.last_name || ' (' || committer.username || ')' AS committer, + {columns_str} (poc.status=ANY(%(openstatuses)s)) AS is_open, (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, (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, (SELECT count(1) FROM commitfest_patchoncommitfest pcf WHERE pcf.patch_id=p.id) AS num_cfs, + branch.needs_rebase_since, branch.failing_since, ( @@ -319,17 +433,33 @@ def commitfest(request, cfid): FROM commitfest_patch p INNER JOIN commitfest_patchoncommitfest poc ON poc.patch_id=p.id INNER JOIN commitfest_topic t ON t.id=p.topic_id +{joins_str} LEFT JOIN auth_user committer ON committer.id=p.committer_id LEFT JOIN commitfest_targetversion v ON p.targetversion_id=v.id LEFT JOIN commitfest_cfbotbranch branch ON branch.patch_id=p.id -WHERE poc.commitfest_id=%(cid)s {0} -GROUP BY p.id, poc.id, committer.id, t.id, v.version, branch.patch_id -ORDER BY is_open DESC, {1}""".format(where_str, orderby_str), +WHERE {where_str} +GROUP BY p.id, poc.id, {groupby_str} committer.id, t.id, v.version, branch.patch_id +ORDER BY is_open DESC, {orderby_str}""", params, ) patches = [ dict(zip([col[0] for col in curs.description], row)) for row in curs.fetchall() ] + return PatchList( + patches=patches, + sortkey=sortkey, + has_filter=has_filter, + redirect=False, + ) + + +def commitfest(request, cfid): + # Find ourselves + cf = get_object_or_404(CommitFest, pk=cfid) + + patch_list = patchlist(request, cf) + if patch_list.redirect: + return HttpResponseRedirect(f"/{cf.id}/") # Generate patch status summary. curs = connection.cursor() @@ -352,13 +482,13 @@ def commitfest(request, cfid): { "cf": cf, "form": form, - "patches": patches, + "patches": patch_list.patches, "statussummary": statussummary, - "has_filter": has_filter, + "has_filter": patch_list.has_filter, "title": cf.title, - "grouping": sortkey == 0, - "sortkey": sortkey, - "openpatchids": [p["id"] for p in patches if p["is_open"]], + "grouping": patch_list.sortkey == 0, + "sortkey": patch_list.sortkey, + "openpatchids": [p["id"] for p in patch_list.patches if p["is_open"]], "header_activity": "Activity log", "header_activity_link": "activity/", }, diff --git a/pgcommitfest/urls.py b/pgcommitfest/urls.py index e94f9e94..5e83d75a 100644 --- a/pgcommitfest/urls.py +++ b/pgcommitfest/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ re_path(r"^$", views.home), + re_path(r"^archive/$", views.archive), re_path(r"^activity(?P\.rss)?/", views.activity), re_path(r"^(\d+)/$", views.commitfest), re_path(r"^(open|inprogress|current)/(.*)$", views.redir), From 8b0b7cb96d62182bb9c628e822d9e824eb6b6cf1 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 2 Mar 2025 22:34:00 +0100 Subject: [PATCH 2/3] Tweak a bunch of things --- pgcommitfest/commitfest/templates/home.html | 12 +++++++---- pgcommitfest/commitfest/views.py | 23 +++++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/pgcommitfest/commitfest/templates/home.html b/pgcommitfest/commitfest/templates/home.html index 4f7fa9f2..251d24c5 100644 --- a/pgcommitfest/commitfest/templates/home.html +++ b/pgcommitfest/commitfest/templates/home.html @@ -48,13 +48,15 @@ {%endif%} -

    {{p.is_open|yesno:"Active patches,Closed patches"}}

    +

    {{user.is_authenticated|yesno:"Your personal dashboard,Patches in the current commitfest"}}

    - + {%if user.is_authenticated %} + + {%endif%} @@ -74,13 +76,15 @@

    {{p.is_open|yesno:"Active patches,Closed patches"}}

    {%if grouping%} {%ifchanged p.topic%} - + {%endifchanged%} {%endif%} - + {%if user.is_authenticated %} + + {%endif%} + {%endifchanged%} {%endif%}
    Patch{%if sortkey == 5%}
    {%elif sortkey == -5%}
    {%endif%}
    ID{%if sortkey == 4%}
    {%elif sortkey == -4%}
    {%endif%}
    CF{%if sortkey == 8%}
    {%elif sortkey == -8%}
    {%endif%}
    CF{%if sortkey == 8%}
    {%elif sortkey == -8%}
    {%endif%}
    Status Ver CI status{%if sortkey == 7%}
    {%elif sortkey == -7%}
    {%endif%}
    {{p.topic}}
    {{p.topic}}
    {{p.name}} {{p.id}}{{p.cf_name}}{{p.cf_name}}{{p.status|patchstatusstring}} {%if p.targetversion%}{{p.targetversion}}{%endif%} diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index a3397b19..3209c331 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -311,7 +311,7 @@ def patchlist(request, cf, personalized=False): ) AND ( poc.status=ANY(%(review_statuses)s) ) - THEN 'Patches you might want to review' + THEN 'Patches that are ready for your review' ELSE 'Blocked on others' END AS topic, cf.id AS cf_id, @@ -377,9 +377,24 @@ def patchlist(request, cf, personalized=False): orderby_str = "poc.commitfest_id DESC, lastmail" else: if personalized: - orderby_str = ( - "topic DESC, branch.failing_since NULLS FIRST, cf.id, lastmail DESC" - ) + # First we sort by topic, to have the grouping work. + # Then we show non-failing patches first, and the ones that are + # shortest failing we show first. We consider patches in a closed + # commitfest, as if they are failing since that commitfest was + # closed. + # Then we sort by start date of the CF, to show entries in the "In + # progress" commitfest before ones in the "Open" commitfest. + # And then to break ties, we put ones with the most recent email at + # the top. + orderby_str = """topic DESC, + COALESCE( + branch.failing_since, + CASE WHEN cf.status = %(cf_closed_status)s + THEN enddate ELSE NULL END + ) DESC NULLS FIRST, + cf.startdate, + lastmail DESC""" + whereparams["cf_closed_status"] = CommitFest.STATUS_CLOSED else: orderby_str = "topic, created" sortkey = 0 From 7266ade1c173ca70ca266b7f8f3eb044f80ad706 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 2 Mar 2025 22:36:44 +0100 Subject: [PATCH 3/3] Fix colspan on commitfest page after added columns --- pgcommitfest/commitfest/templates/commitfest.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/templates/commitfest.html b/pgcommitfest/commitfest/templates/commitfest.html index 05f61829..0143d08c 100644 --- a/pgcommitfest/commitfest/templates/commitfest.html +++ b/pgcommitfest/commitfest/templates/commitfest.html @@ -81,7 +81,7 @@

    {{p.is_open|yesno:"Active patches,Closed patches"}}

    {%if grouping%} {%ifchanged p.topic%} -
    {{p.topic}}
    {{p.topic}}