From c0b0dc71cbe267f78b12e2f941c774f1e814c966 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Fri, 30 May 2025 10:00:33 -0700 Subject: [PATCH 01/22] Add Tags for patches A Tag is an arbitrary label for a patch in the Commitfest UI. Other than helping users identify patches of interest, it has no other semantic meaning to the CF application. Tags are created using the administrator interface. They consist of a unique name and a background color. The color should be sent from compliant browsers in #rrggbb format, which is stored without backend validation; to avoid CSS injection, any non-conforming values are replaced with black during templating. --- pgcommitfest/commitfest/admin.py | 19 +++++++++ .../migrations/0011_tag_patch_tags.py | 39 ++++++++++++++++++ pgcommitfest/commitfest/models.py | 26 ++++++++++++ .../commitfest/templates/commitfest.html | 6 +++ pgcommitfest/commitfest/templates/patch.html | 8 ++++ .../commitfest/templatetags/commitfest.py | 40 +++++++++++++++++++ pgcommitfest/commitfest/views.py | 4 ++ 7 files changed, 142 insertions(+) create mode 100644 pgcommitfest/commitfest/migrations/0011_tag_patch_tags.py diff --git a/pgcommitfest/commitfest/admin.py b/pgcommitfest/commitfest/admin.py index 8c8d62e5..2023cbe2 100644 --- a/pgcommitfest/commitfest/admin.py +++ b/pgcommitfest/commitfest/admin.py @@ -1,8 +1,10 @@ from django.contrib import admin +from django.forms import widgets from .models import ( CfbotBranch, CfbotTask, + ColorField, CommitFest, Committer, MailThread, @@ -10,6 +12,7 @@ Patch, PatchHistory, PatchOnCommitFest, + Tag, TargetVersion, Topic, ) @@ -38,8 +41,24 @@ class MailThreadAttachmentAdmin(admin.ModelAdmin): ) +class ColorInput(widgets.Input): + """ + A color picker widget. + TODO: this will be natively available in Django 5.2. + """ + + input_type = "color" + + +class TagAdmin(admin.ModelAdmin): + formfield_overrides = { + ColorField: {"widget": ColorInput}, + } + + admin.site.register(Committer, CommitterAdmin) admin.site.register(CommitFest) +admin.site.register(Tag, TagAdmin) admin.site.register(Topic) admin.site.register(Patch, PatchAdmin) admin.site.register(PatchHistory) diff --git a/pgcommitfest/commitfest/migrations/0011_tag_patch_tags.py b/pgcommitfest/commitfest/migrations/0011_tag_patch_tags.py new file mode 100644 index 00000000..9ab361ad --- /dev/null +++ b/pgcommitfest/commitfest/migrations/0011_tag_patch_tags.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.19 on 2025-05-30 18:09 + +from django.db import migrations, models +import pgcommitfest.commitfest.models + + +class Migration(migrations.Migration): + dependencies = [ + ("commitfest", "0010_add_failing_since_column"), + ] + + operations = [ + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50, unique=True)), + ("color", pgcommitfest.commitfest.models.ColorField(max_length=7)), + ], + options={ + "ordering": ("name",), + }, + ), + migrations.AddField( + model_name="patch", + name="tags", + field=models.ManyToManyField( + blank=True, related_name="patches", to="commitfest.tag" + ), + ), + ] diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index fcd9edb9..f795f16a 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -102,11 +102,37 @@ def __str__(self): return self.version +class ColorField(models.CharField): + """ + A small wrapper around a CharField that can hold a #RRGGBB color code. The + primary reason to have this wrapper class is so that the TagAdmin class can + explicitly key off of it to inject a color picker in the admin interface. + """ + + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 7 # for `#RRGGBB` format + super().__init__(*args, **kwargs) + + +class Tag(models.Model): + """Represents a tag/label on a patch.""" + + name = models.CharField(max_length=50, unique=True) + color = ColorField() + + class Meta: + ordering = ("name",) + + def __str__(self): + return self.name + + class Patch(models.Model, DiffableModel): name = models.CharField( max_length=500, blank=False, null=False, verbose_name="Description" ) topic = models.ForeignKey(Topic, blank=False, null=False, on_delete=models.CASCADE) + tags = models.ManyToManyField(Tag, related_name="patches", blank=True) # One patch can be in multiple commitfests, if it has history commitfests = models.ManyToManyField(CommitFest, through="PatchOnCommitFest") diff --git a/pgcommitfest/commitfest/templates/commitfest.html b/pgcommitfest/commitfest/templates/commitfest.html index 972e0bf8..ce056b1f 100644 --- a/pgcommitfest/commitfest/templates/commitfest.html +++ b/pgcommitfest/commitfest/templates/commitfest.html @@ -34,6 +34,7 @@

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

Patch{%if sortkey == 5%}
{%elif sortkey == -5%}
{%endif%} ID{%if sortkey == 4%}
{%elif sortkey == -4%}
{%endif%} Status + Tags Ver CI status{%if sortkey == 7%}
{%elif sortkey == -7%}
{%endif%} Stats{%if sortkey == 6%}
{%elif sortkey == -6%}
{%endif%} @@ -59,6 +60,11 @@

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

{{p.name}} {{p.id}} {{p.status|patchstatusstring}} + + {%for t in p.tag_ids%} + {{all_tags|tagname:t}} + {%endfor%} + {%if p.targetversion%}{{p.targetversion}}{%endif%} {%with p.cfbot_results as cfb%} diff --git a/pgcommitfest/commitfest/templates/patch.html b/pgcommitfest/commitfest/templates/patch.html index 2aafd141..1c286950 100644 --- a/pgcommitfest/commitfest/templates/patch.html +++ b/pgcommitfest/commitfest/templates/patch.html @@ -67,6 +67,14 @@ Topic {{patch.topic}} + + Tags + + {%for tag in patch.tags.all%} + {{tag.name}} + {%endfor%} + + Created {{patch.created}} diff --git a/pgcommitfest/commitfest/templatetags/commitfest.py b/pgcommitfest/commitfest/templatetags/commitfest.py index 25fd21f2..403d1ceb 100644 --- a/pgcommitfest/commitfest/templatetags/commitfest.py +++ b/pgcommitfest/commitfest/templatetags/commitfest.py @@ -6,6 +6,7 @@ from django.utils.translation import ngettext_lazy import datetime +import string from uuid import uuid4 from pgcommitfest.commitfest.models import CommitFest, PatchOnCommitFest @@ -41,6 +42,45 @@ def patchstatuslabel(value): return [v for k, v in PatchOnCommitFest._STATUS_LABELS if k == i][0] +@register.filter(name="tagname") +def tagname(value, arg): + """ + Looks up a tag by ID and returns its name. The filter value is the map of + tags, and the argument is the ID. (Unlike tagcolor, there is no + argument-less variant; just use tag.name directly.) + + Example: + tag_map|tagname:tag_id + """ + return value[arg].name + + +@register.filter(name="tagcolor") +def tagcolor(value, key=None): + """ + Returns the color code of a tag. The filter value is either a single tag, in + which case no argument should be given, or a map of tags with the tag ID as + the argument, as with the tagname filter. + + Since color codes are injected into CSS, any nonconforming inputs are + replaced with black here. (Prefer `tag|tagcolor` over `tag.color` in + templates, for this reason.) + """ + if key is not None: + code = value[key].color + else: + code = value.color + + if ( + len(code) == 7 + and code.startswith("#") + and all(c in string.hexdigits for c in code[1:]) + ): + return code + + return "#000000" + + @register.filter(is_safe=True) def label_class(value, arg): return value.label_tag(attrs={"class": arg}) diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index 7b6dd63d..a3aecc36 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -42,6 +42,7 @@ Patch, PatchHistory, PatchOnCommitFest, + Tag, ) @@ -485,6 +486,7 @@ def patchlist(request, cf, personalized=False): (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, +(SELECT array_agg(tag_id) FROM commitfest_patch_tags t WHERE t.patch_id=p.id) AS tag_ids, branch.needs_rebase_since, branch.failing_since, @@ -531,6 +533,7 @@ def patchlist(request, cf, personalized=False): ) +@transaction.atomic # tie the patchlist() query to Tag.objects.all() def commitfest(request, cfid): # Find ourselves cf = get_object_or_404(CommitFest, pk=cfid) @@ -562,6 +565,7 @@ def commitfest(request, cfid): "form": form, "patches": patch_list.patches, "statussummary": statussummary, + "all_tags": {t.id: t for t in Tag.objects.all()}, "has_filter": patch_list.has_filter, "title": cf.title, "grouping": patch_list.sortkey == 0, From 02aa97fded5db1390fd8fac9518cd7769a0e0c43 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Fri, 30 May 2025 22:08:39 -0700 Subject: [PATCH 02/22] Help admins choose tag colors with contrast Add front-end soft validation to the TagAdmin form. This uses WCAG 2.2 guidelines to warn the administrator if a color choice is going to be low-contrast when compared to our text/background color of white. (The warning may be ignored.) --- media/commitfest/js/change_tag.js | 44 +++++++++++++++++++ pgcommitfest/commitfest/admin.py | 2 + .../commitfest/templates/change_tag_form.html | 7 +++ 3 files changed, 53 insertions(+) create mode 100644 media/commitfest/js/change_tag.js create mode 100644 pgcommitfest/commitfest/templates/change_tag_form.html diff --git a/media/commitfest/js/change_tag.js b/media/commitfest/js/change_tag.js new file mode 100644 index 00000000..ae4c224a --- /dev/null +++ b/media/commitfest/js/change_tag.js @@ -0,0 +1,44 @@ +// An input validator for the color picker. Points out low-contrast tag color +// choices. +const input = document.getElementById("id_color"); +input.addEventListener("input", (event) => { + // Don't do anything if the color code doesn't pass default validity. + input.setCustomValidity(""); + if (!input.validity.valid) { + return; + } + + // Break the #rrggbb color code into RGB components. + color = parseInt(input.value.substr(1), 16); + red = ((color & 0xFF0000) >> 16) / 255.; + green = ((color & 0x00FF00) >> 8) / 255.; + blue = (color & 0x0000FF) / 255.; + + // Compare the contrast ratio against white. All the magic math comes from + // Web Content Accessibility Guidelines (WCAG) 2.2, Technique G18: + // + // https://www.w3.org/WAI/WCAG22/Techniques/general/G18.html + // + function l(val) { + if (val <= 0.04045) { + return val / 12.92; + } + return ((val + 0.055) / 1.055) ** 2.4; + } + + lum = 0.2126 * l(red) + 0.7152 * l(green) + 0.0722 * l(blue); + contrast = (1 + 0.05) / (lum + 0.05); + + // Complain if we're below WCAG 2.2 recommendations. + if (contrast < 4.5) { + input.setCustomValidity( + "Consider choosing a darker color. " + + "(Tag text is small and white.)\n\n" + + "Contrast ratio: " + (Math.trunc(contrast * 10) / 10) + " (< 4.5)" + ); + + // The admin form uses novalidate, so manually display the browser's + // validity popup. (The user can still ignore it if desired.) + input.reportValidity(); + } +}); diff --git a/pgcommitfest/commitfest/admin.py b/pgcommitfest/commitfest/admin.py index 2023cbe2..1cfa4bc7 100644 --- a/pgcommitfest/commitfest/admin.py +++ b/pgcommitfest/commitfest/admin.py @@ -51,6 +51,8 @@ class ColorInput(widgets.Input): class TagAdmin(admin.ModelAdmin): + # Customize the Tag form with a color picker and soft validation. + change_form_template = "change_tag_form.html" formfield_overrides = { ColorField: {"widget": ColorInput}, } diff --git a/pgcommitfest/commitfest/templates/change_tag_form.html b/pgcommitfest/commitfest/templates/change_tag_form.html new file mode 100644 index 00000000..4ec39605 --- /dev/null +++ b/pgcommitfest/commitfest/templates/change_tag_form.html @@ -0,0 +1,7 @@ +{% extends 'admin/change_form.html' %} +{% load static %} + +{% block admin_change_form_document_ready %} +{{ block.super }} + +{% endblock %} From 180ac8945bbd64ef9ae0e5b19e653f492af7e0c3 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 9 Jun 2025 22:11:29 +0200 Subject: [PATCH 03/22] Add selectize support to tag picker --- pgcommitfest/commitfest/forms.py | 7 +++++++ pgcommitfest/commitfest/templates/base_form.html | 2 +- pgcommitfest/commitfest/templates/selectize_js.html | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index c3b9a18d..9ea62923 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -72,6 +72,7 @@ class PatchForm(forms.ModelForm): selectize_fields = { "authors": "/lookups/user", "reviewers": "/lookups/user", + "tags": None, } class Meta: @@ -94,8 +95,14 @@ def __init__(self, *args, **kwargs): x.user.username, ) + self.fields["authors"].widget.attrs["class"] = "add-user-picker" + self.fields["reviewers"].widget.attrs["class"] = "add-user-picker" + # Selectize multiple fields -- don't pre-populate everything for field, url in list(self.selectize_fields.items()): + if url is None: + continue + # If this is a postback of a selectize field, it may contain ids that are not currently # stored in the field. They must still be among the *allowed* values of course, which # are handled by the existing queryset on the field. diff --git a/pgcommitfest/commitfest/templates/base_form.html b/pgcommitfest/commitfest/templates/base_form.html index 40518ca9..788627a9 100644 --- a/pgcommitfest/commitfest/templates/base_form.html +++ b/pgcommitfest/commitfest/templates/base_form.html @@ -75,7 +75,7 @@

Search user

{%include "selectize_js.html" %} + {{ block.super }} + {% endblock %} diff --git a/pgcommitfest/commitfest/templates/patch.html b/pgcommitfest/commitfest/templates/patch.html index 1c286950..f29f6089 100644 --- a/pgcommitfest/commitfest/templates/patch.html +++ b/pgcommitfest/commitfest/templates/patch.html @@ -70,9 +70,9 @@ Tags - {%for tag in patch.tags.all%} - {{tag.name}} - {%endfor%} + {%for tag in patch.tags.all%} + {{tag.name}} + {%endfor%} diff --git a/pgcommitfest/commitfest/templates/selectize_js.html b/pgcommitfest/commitfest/templates/selectize_js.html index 9c8019c4..0991ac65 100644 --- a/pgcommitfest/commitfest/templates/selectize_js.html +++ b/pgcommitfest/commitfest/templates/selectize_js.html @@ -5,21 +5,21 @@ valueField: 'id', labelField: 'value', searchField: 'value', - {%if url%} - load: function(query, callback) { - if (!query.length) return callback(); - $.ajax({ - 'url': '{{url}}', - 'type': 'GET', - 'dataType': 'json', - 'data': { - 'query': query, - }, - 'error': function() { callback();}, - 'success': function(res) { callback(res.values);}, - }); - }, - {%endif%} + {%if url%} + load: function(query, callback) { + if (!query.length) return callback(); + $.ajax({ + 'url': '{{url}}', + 'type': 'GET', + 'dataType': 'json', + 'data': { + 'query': query, + }, + 'error': function() { callback();}, + 'success': function(res) { callback(res.values);}, + }); + }, + {%endif%} onFocus: function() { if (this.$input.is('[multiple]')) { return; From 1d605a03e9910d99d096eebe0345cdd522917316 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 9 Jun 2025 23:27:45 +0200 Subject: [PATCH 06/22] Allow clicking on a tag in commitfest overview page --- pgcommitfest/commitfest/templates/commitfest.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/templates/commitfest.html b/pgcommitfest/commitfest/templates/commitfest.html index ce056b1f..234df1a6 100644 --- a/pgcommitfest/commitfest/templates/commitfest.html +++ b/pgcommitfest/commitfest/templates/commitfest.html @@ -62,7 +62,10 @@

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

{{p.status|patchstatusstring}} {%for t in p.tag_ids%} - {{all_tags|tagname:t}} + + + {{all_tags|tagname:t}} + {%endfor%} {%if p.targetversion%}{{p.targetversion}}{%endif%} From c10574637f57dde39e8e755fb83a754732980c58 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 15 Jun 2025 22:18:51 +0200 Subject: [PATCH 07/22] Fix merge --- .../{0011_tag_patch_tags.py => 0013_tag_patch_tags.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pgcommitfest/commitfest/migrations/{0011_tag_patch_tags.py => 0013_tag_patch_tags.py} (94%) diff --git a/pgcommitfest/commitfest/migrations/0011_tag_patch_tags.py b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py similarity index 94% rename from pgcommitfest/commitfest/migrations/0011_tag_patch_tags.py rename to pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py index 6bbbaba1..5f82edf5 100644 --- a/pgcommitfest/commitfest/migrations/0011_tag_patch_tags.py +++ b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ("commitfest", "0010_add_failing_since_column"), + ("commitfest", "0012_add_status_related_constraints"), ] operations = [ From d1bf0fd884ee64cfeb4eccb28eb4dc119def18fd Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 15 Jun 2025 23:24:57 +0200 Subject: [PATCH 08/22] Add default tags, descriptions, and raw hex code input --- pgcommitfest/commitfest/admin.py | 2 +- .../migrations/0013_tag_patch_tags.py | 56 +++++++++++++++++++ pgcommitfest/commitfest/models.py | 1 + .../commitfest/templates/color_input.html | 2 + 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 pgcommitfest/commitfest/templates/color_input.html diff --git a/pgcommitfest/commitfest/admin.py b/pgcommitfest/commitfest/admin.py index 1cfa4bc7..90d54c4a 100644 --- a/pgcommitfest/commitfest/admin.py +++ b/pgcommitfest/commitfest/admin.py @@ -44,10 +44,10 @@ class MailThreadAttachmentAdmin(admin.ModelAdmin): class ColorInput(widgets.Input): """ A color picker widget. - TODO: this will be natively available in Django 5.2. """ input_type = "color" + template_name = "color_input.html" class TagAdmin(admin.ModelAdmin): diff --git a/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py index 5f82edf5..d1cc9e24 100644 --- a/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py +++ b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py @@ -5,6 +5,60 @@ import pgcommitfest.commitfest.models +def add_initial_tags(apps, schema_editor): + Tag = apps.get_model("commitfest", "Tag") + Tag.objects.bulk_create( + [ + Tag(name="bugfix", color="#a51d2d", description="Fixes a bug"), + Tag( + name="backport", + color="#1a5fb4", + description="Once merged should be backported to old branches", + ), + Tag( + name="missing-tests", + color="#c66424", + description="Author should add tests", + ), + Tag( + name="missing-docs", + color="#c66424", + description="Author should add documentation", + ), + Tag( + name="missing-benchmarks", + color="#c66424", + description="Author should do additional benchmarks", + ), + Tag( + name="help-user-testing", + color="#07732e", + description="Reviewers are requested to try out the patch and provide user feedback on behaviour UX/UI", + ), + Tag( + name="help-bikeshedding", + color="#07732e", + description="Reviewers are requested to propose or vote on stylistic changes like a user facing function name", + ), + Tag( + name="help-docs", + color="#07732e", + description="Reviewers are requested to help review or write documentation", + ), + Tag( + name="help-benchmarks", + color="#07732e", + description="Reviewers are requested to help discuss or do benchmarks and discuss performance impact", + ), + Tag( + name="good-first-review", + color="#613583", + description="An easy to review patch for a new reviewer", + ), + ] + ) + + class Migration(migrations.Migration): dependencies = [ ("commitfest", "0012_add_status_related_constraints"), @@ -25,6 +79,7 @@ class Migration(migrations.Migration): ), ("name", models.CharField(max_length=50, unique=True)), ("color", pgcommitfest.commitfest.models.ColorField(max_length=7)), + ("description", models.CharField(max_length=500)), ], options={ "ordering": ("name",), @@ -37,4 +92,5 @@ class Migration(migrations.Migration): blank=True, related_name="patches", to="commitfest.tag" ), ), + migrations.RunPython(add_initial_tags, migrations.RunPython.noop), ] diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 223f9541..e27bf981 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -317,6 +317,7 @@ class Tag(models.Model): name = models.CharField(max_length=50, unique=True) color = ColorField() + description = models.CharField(max_length=500) class Meta: ordering = ("name",) diff --git a/pgcommitfest/commitfest/templates/color_input.html b/pgcommitfest/commitfest/templates/color_input.html new file mode 100644 index 00000000..7a4135f4 --- /dev/null +++ b/pgcommitfest/commitfest/templates/color_input.html @@ -0,0 +1,2 @@ + + From 7ae88aed53828698d4f750839b09a06fe9b33393 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 15 Jun 2025 23:35:36 +0200 Subject: [PATCH 09/22] Add tags to patches in dummy data and fix tag display in history --- .../commitfest/fixtures/commitfest_data.json | 149 +++++++++++++++++- pgcommitfest/commitfest/views.py | 6 +- 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/pgcommitfest/commitfest/fixtures/commitfest_data.json b/pgcommitfest/commitfest/fixtures/commitfest_data.json index d51214dc..1437a814 100644 --- a/pgcommitfest/commitfest/fixtures/commitfest_data.json +++ b/pgcommitfest/commitfest/fixtures/commitfest_data.json @@ -92,6 +92,96 @@ "version": "18" } }, +{ + "model": "commitfest.tag", + "pk": 1, + "fields": { + "name": "bugfix", + "color": "#a51d2d", + "description": "Fixes a bug" + } +}, +{ + "model": "commitfest.tag", + "pk": 2, + "fields": { + "name": "backport", + "color": "#1a5fb4", + "description": "Once merged should be backported to old branches" + } +}, +{ + "model": "commitfest.tag", + "pk": 3, + "fields": { + "name": "missing-tests", + "color": "#c66424", + "description": "Author should add tests" + } +}, +{ + "model": "commitfest.tag", + "pk": 4, + "fields": { + "name": "missing-docs", + "color": "#c66424", + "description": "Author should add documentation" + } +}, +{ + "model": "commitfest.tag", + "pk": 5, + "fields": { + "name": "missing-benchmarks", + "color": "#c66424", + "description": "Author should do additional benchmarks" + } +}, +{ + "model": "commitfest.tag", + "pk": 6, + "fields": { + "name": "help-user-testing", + "color": "#07732e", + "description": "Reviewers are requested to try out the patch and provide user feedback on behaviour UX/UI" + } +}, +{ + "model": "commitfest.tag", + "pk": 7, + "fields": { + "name": "help-bikeshedding", + "color": "#07732e", + "description": "Reviewers are requested to propose or vote on stylistic changes like a user facing function name" + } +}, +{ + "model": "commitfest.tag", + "pk": 8, + "fields": { + "name": "help-docs", + "color": "#07732e", + "description": "Reviewers are requested to help review or write documentation" + } +}, +{ + "model": "commitfest.tag", + "pk": 9, + "fields": { + "name": "help-benchmarks", + "color": "#07732e", + "description": "Reviewers are requested to help discuss or do benchmarks and discuss performance impact" + } +}, +{ + "model": "commitfest.tag", + "pk": 10, + "fields": { + "name": "good-first-review", + "color": "#613583", + "description": "An easy to review patch for a new reviewer" + } +}, { "model": "commitfest.patch", "pk": 1, @@ -103,8 +193,15 @@ "targetversion": null, "committer": null, "created": "2025-01-26T10:48:31.579", - "modified": "2025-01-26T10:53:20.498", + "modified": "2025-06-15T21:26:10.549", "lastmail": "2025-01-20T06:53:39", + "tags": [ + 2, + 1, + 9, + 6, + 4 + ], "authors": [ 1 ], @@ -126,8 +223,13 @@ "targetversion": null, "committer": null, "created": "2025-01-26T10:51:17.305", - "modified": "2025-01-26T10:51:19.631", + "modified": "2025-06-15T21:26:24.284", "lastmail": "2025-01-20T14:20:10", + "tags": [ + 10, + 7, + 6 + ], "authors": [], "reviewers": [], "subscribers": [], @@ -149,6 +251,7 @@ "created": "2025-01-26T11:02:07.467", "modified": "2025-01-26T11:02:10.911", "lastmail": "2025-01-20T13:26:55", + "tags": [], "authors": [], "reviewers": [], "subscribers": [], @@ -168,8 +271,11 @@ "targetversion": null, "committer": null, "created": "2025-01-31T13:30:19.744", - "modified": "2025-01-31T13:30:21.305", + "modified": "2025-06-15T21:27:56.667", "lastmail": "2025-01-20T12:44:40", + "tags": [ + 1 + ], "authors": [], "reviewers": [], "subscribers": [], @@ -191,6 +297,7 @@ "created": "2025-02-16T21:59:04.131", "modified": "2025-02-16T22:03:24.902", "lastmail": "2025-01-20T14:01:53", + "tags": [], "authors": [], "reviewers": [], "subscribers": [], @@ -212,6 +319,7 @@ "created": "2025-02-16T22:03:58.476", "modified": "2025-02-16T22:04:23.180", "lastmail": "2025-01-19T23:55:17", + "tags": [], "authors": [], "reviewers": [], "subscribers": [], @@ -233,6 +341,7 @@ "created": "2025-03-01T22:27:53.214", "modified": "2025-03-01T22:27:53.221", "lastmail": "2025-01-18T07:14:02", + "tags": [], "authors": [], "reviewers": [], "subscribers": [], @@ -254,6 +363,7 @@ "created": "2025-02-01T00:00:00", "modified": "2025-02-01T00:00:00", "lastmail": "2025-02-01T00:00:00", + "tags": [], "authors": [ 3, 6 @@ -576,6 +686,39 @@ "what": "Attached mail thread example@message-31" } }, +{ + "model": "commitfest.patchhistory", + "pk": 20, + "fields": { + "patch": 1, + "date": "2025-06-15T21:26:10.546", + "by": 1, + "by_cfbot": false, + "what": "Changed tags to backport, bugfix, help-benchmarks, help-user-testing, missing-docs" + } +}, +{ + "model": "commitfest.patchhistory", + "pk": 21, + "fields": { + "patch": 2, + "date": "2025-06-15T21:26:24.282", + "by": 1, + "by_cfbot": false, + "what": "Changed tags to good-first-review, help-bikeshedding, help-user-testing" + } +}, +{ + "model": "commitfest.patchhistory", + "pk": 22, + "fields": { + "patch": 4, + "date": "2025-06-15T21:27:56.664", + "by": 1, + "by_cfbot": false, + "what": "Changed tags to bugfix" + } +}, { "model": "commitfest.mailthread", "pk": 1, diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index ce6abf51..9b5486eb 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -774,10 +774,14 @@ def patchform(request, patchid): # Track all changes for field, values in r.diff.items(): + if field == "tags": + value = ", ".join(v.name for v in values[1]) + else: + value = values[1] PatchHistory( patch=patch, by=request.user, - what="Changed %s to %s" % (field, values[1]), + what="Changed %s to %s" % (field, value), ).save_and_notify( prevcommitter=prevcommitter, prevreviewers=prevreviewers, From dfc14eaefae563c70772da7df999c61041e96903 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 15 Jun 2025 23:47:23 +0200 Subject: [PATCH 10/22] Add description as hovertext for tags --- pgcommitfest/commitfest/templates/commitfest.html | 3 +-- pgcommitfest/commitfest/templatetags/commitfest.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pgcommitfest/commitfest/templates/commitfest.html b/pgcommitfest/commitfest/templates/commitfest.html index 234df1a6..8c8526a1 100644 --- a/pgcommitfest/commitfest/templates/commitfest.html +++ b/pgcommitfest/commitfest/templates/commitfest.html @@ -62,9 +62,8 @@

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

{{p.status|patchstatusstring}} {%for t in p.tag_ids%} - - {{all_tags|tagname:t}} + {{all_tags|tagname:t}} {%endfor%} diff --git a/pgcommitfest/commitfest/templatetags/commitfest.py b/pgcommitfest/commitfest/templatetags/commitfest.py index 5b04c7ac..489be096 100644 --- a/pgcommitfest/commitfest/templatetags/commitfest.py +++ b/pgcommitfest/commitfest/templatetags/commitfest.py @@ -56,6 +56,19 @@ def tagname(value, arg): return value[arg].name +@register.filter(name="tagdescription") +def tagdescription(value, arg): + """ + Looks up a tag by ID and returns its name. The filter value is the map of + tags, and the argument is the ID. (Unlike tagcolor, there is no + argument-less variant; just use tag.name directly.) + + Example: + tag_map|tagname:tag_id + """ + return value[arg].description + + @register.filter(name="tagcolor") def tagcolor(value, key=None): """ From 71222028e9a2c06f507c96b0cc47d4875608a130 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 15 Jun 2025 23:49:14 +0200 Subject: [PATCH 11/22] Add description as hovertext for tags on patch page --- pgcommitfest/commitfest/templates/patch.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/templates/patch.html b/pgcommitfest/commitfest/templates/patch.html index a6f0f811..c8f7e476 100644 --- a/pgcommitfest/commitfest/templates/patch.html +++ b/pgcommitfest/commitfest/templates/patch.html @@ -71,7 +71,7 @@ Tags {%for tag in patch.tags.all%} - {{tag.name}} + {{tag.name}} {%endfor%} From f6d239614c670245dd9687d9efbabbc67f43656d Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Sun, 15 Jun 2025 23:49:24 +0200 Subject: [PATCH 12/22] Add tags to dashboard --- pgcommitfest/commitfest/templates/me.html | 7 +++++++ pgcommitfest/commitfest/views.py | 1 + 2 files changed, 8 insertions(+) diff --git a/pgcommitfest/commitfest/templates/me.html b/pgcommitfest/commitfest/templates/me.html index b6736124..bf4761b1 100644 --- a/pgcommitfest/commitfest/templates/me.html +++ b/pgcommitfest/commitfest/templates/me.html @@ -65,6 +65,13 @@

{%if user.is_authenticated%}Open patches you are subscribed to{%elif p.is_op {{p.cf_name}} {%endif%} {{p.status|patchstatusstring}} + + {%for t in p.tag_ids%} + + {{all_tags|tagname:t}} + + {%endfor%} + {%if p.targetversion%}{{p.targetversion}}{%endif%} {%with p.cfbot_results as cfb%} diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index 9b5486eb..13f11cad 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -142,6 +142,7 @@ def me(request): "title": "Personal Dashboard", "patches": patch_list.patches, "statussummary": statussummary, + "all_tags": {t.id: t for t in Tag.objects.all()}, "has_filter": patch_list.has_filter, "grouping": patch_list.sortkey == 0, "sortkey": patch_list.sortkey, From e8ee2bb4f60163f15b8aad6811b7b4827c1941e7 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 08:43:55 +0200 Subject: [PATCH 13/22] Use spaces + title case in default tags --- .../commitfest/fixtures/commitfest_data.json | 22 +++++++++---------- .../migrations/0013_tag_patch_tags.py | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pgcommitfest/commitfest/fixtures/commitfest_data.json b/pgcommitfest/commitfest/fixtures/commitfest_data.json index 1437a814..1f32b0ed 100644 --- a/pgcommitfest/commitfest/fixtures/commitfest_data.json +++ b/pgcommitfest/commitfest/fixtures/commitfest_data.json @@ -96,7 +96,7 @@ "model": "commitfest.tag", "pk": 1, "fields": { - "name": "bugfix", + "name": "Bugfix", "color": "#a51d2d", "description": "Fixes a bug" } @@ -105,7 +105,7 @@ "model": "commitfest.tag", "pk": 2, "fields": { - "name": "backport", + "name": "Backport", "color": "#1a5fb4", "description": "Once merged should be backported to old branches" } @@ -114,7 +114,7 @@ "model": "commitfest.tag", "pk": 3, "fields": { - "name": "missing-tests", + "name": "Missing Tests", "color": "#c66424", "description": "Author should add tests" } @@ -123,7 +123,7 @@ "model": "commitfest.tag", "pk": 4, "fields": { - "name": "missing-docs", + "name": "Missing Docs", "color": "#c66424", "description": "Author should add documentation" } @@ -132,7 +132,7 @@ "model": "commitfest.tag", "pk": 5, "fields": { - "name": "missing-benchmarks", + "name": "Missing Benchmarks", "color": "#c66424", "description": "Author should do additional benchmarks" } @@ -141,16 +141,16 @@ "model": "commitfest.tag", "pk": 6, "fields": { - "name": "help-user-testing", + "name": "Help - User Testing", "color": "#07732e", - "description": "Reviewers are requested to try out the patch and provide user feedback on behaviour UX/UI" + "description": "Reviewers are requested to try out the patch and provide user feedback on UX/UI" } }, { "model": "commitfest.tag", "pk": 7, "fields": { - "name": "help-bikeshedding", + "name": "Help - Bikeshedding", "color": "#07732e", "description": "Reviewers are requested to propose or vote on stylistic changes like a user facing function name" } @@ -159,7 +159,7 @@ "model": "commitfest.tag", "pk": 8, "fields": { - "name": "help-docs", + "name": "Help - Docs", "color": "#07732e", "description": "Reviewers are requested to help review or write documentation" } @@ -168,7 +168,7 @@ "model": "commitfest.tag", "pk": 9, "fields": { - "name": "help-benchmarks", + "name": "Help - Benchmarks", "color": "#07732e", "description": "Reviewers are requested to help discuss or do benchmarks and discuss performance impact" } @@ -177,7 +177,7 @@ "model": "commitfest.tag", "pk": 10, "fields": { - "name": "good-first-review", + "name": "Good First Review", "color": "#613583", "description": "An easy to review patch for a new reviewer" } diff --git a/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py index d1cc9e24..d6319a0a 100644 --- a/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py +++ b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py @@ -9,49 +9,49 @@ def add_initial_tags(apps, schema_editor): Tag = apps.get_model("commitfest", "Tag") Tag.objects.bulk_create( [ - Tag(name="bugfix", color="#a51d2d", description="Fixes a bug"), + Tag(name="Bugfix", color="#a51d2d", description="Fixes a bug"), Tag( - name="backport", + name="Backport", color="#1a5fb4", description="Once merged should be backported to old branches", ), Tag( - name="missing-tests", + name="Missing Tests", color="#c66424", description="Author should add tests", ), Tag( - name="missing-docs", + name="Missing Docs", color="#c66424", description="Author should add documentation", ), Tag( - name="missing-benchmarks", + name="Missing Benchmarks", color="#c66424", description="Author should do additional benchmarks", ), Tag( - name="help-user-testing", + name="Help - User Testing", color="#07732e", - description="Reviewers are requested to try out the patch and provide user feedback on behaviour UX/UI", + description="Reviewers are requested to try out the patch and provide user feedback on UX/UI", ), Tag( - name="help-bikeshedding", + name="Help - Bikeshedding", color="#07732e", description="Reviewers are requested to propose or vote on stylistic changes like a user facing function name", ), Tag( - name="help-docs", + name="Help - Docs", color="#07732e", description="Reviewers are requested to help review or write documentation", ), Tag( - name="help-benchmarks", + name="Help - Benchmarks", color="#07732e", description="Reviewers are requested to help discuss or do benchmarks and discuss performance impact", ), Tag( - name="good-first-review", + name="Good First Review", color="#613583", description="An easy to review patch for a new reviewer", ), From e7fba3ed6c2e13559c67ccd331c6628737f294ec Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 09:30:23 +0200 Subject: [PATCH 14/22] Allow filtering by multiple tags at once --- pgcommitfest/commitfest/forms.py | 6 ++---- pgcommitfest/commitfest/views.py | 20 ++++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index 312e1d9a..2ec9fc2c 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -19,7 +19,7 @@ class CommitFestFilterForm(forms.Form): text = forms.CharField(max_length=50, required=False) status = forms.ChoiceField(required=False) targetversion = forms.ChoiceField(required=False) - tag = forms.ChoiceField(required=False, label="Tag (type to search)") + tag = forms.MultipleChoiceField(required=False, label="Tag (type to search)") author = forms.ChoiceField(required=False, label="Author (type to search)") reviewer = forms.ChoiceField(required=False, label="Reviewer (type to search)") sortkey = forms.IntegerField(required=False) @@ -61,9 +61,7 @@ def __init__(self, data, *args, **kwargs): ) self.fields["author"].choices = userchoices self.fields["reviewer"].choices = userchoices - self.fields["tag"].choices = [(-1, "* All"), (-2, "* None")] + list( - Tag.objects.all().values_list("id", "name") - ) + self.fields["tag"].choices = list(Tag.objects.all().values_list("id", "name")) for f in ( "status", diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index 13f11cad..8f813bed 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -285,20 +285,16 @@ def patchlist(request, cf, personalized=False): # int() failed, ignore pass - if request.GET.get("tag", "-1") != "-1": - if request.GET["tag"] == "-2": - whereclauses.append( - "NOT EXISTS (SELECT 1 FROM commitfest_patch_tags tags WHERE tags.patch_id=p.id)" - ) - else: - try: - whereparams["tag"] = int(request.GET["tag"]) + if request.GET.getlist("tag") != []: + try: + tag_ids = [int(t) for t in request.GET.getlist("tag")] + for tag_id in tag_ids: whereclauses.append( - "EXISTS (SELECT 1 FROM commitfest_patch_tags tags WHERE tags.patch_id=p.id AND tags.tag_id=%(tag)s)" + f"EXISTS (SELECT 1 FROM commitfest_patch_tags tags WHERE tags.patch_id=p.id AND tags.tag_id={tag_id})" ) - except ValueError: - # int() failed -- so just ignore this filter - pass + except ValueError: + # int() failed -- so just ignore this filter + pass if request.GET.get("author", "-1") != "-1": if request.GET["author"] == "-2": From a88dc7a9a13da119dbcb4965f5bb6ca9f1224745 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 09:31:42 +0200 Subject: [PATCH 15/22] More default tags --- .../migrations/0013_tag_patch_tags.py | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py index d6319a0a..3bc89517 100644 --- a/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py +++ b/pgcommitfest/commitfest/migrations/0013_tag_patch_tags.py @@ -12,7 +12,7 @@ def add_initial_tags(apps, schema_editor): Tag(name="Bugfix", color="#a51d2d", description="Fixes a bug"), Tag( name="Backport", - color="#1a5fb4", + color="#3d3846", description="Once merged should be backported to old branches", ), Tag( @@ -55,6 +55,97 @@ def add_initial_tags(apps, schema_editor): color="#613583", description="An easy to review patch for a new reviewer", ), + Tag( + name="libpq", + color="#1a5fb4", + description="Makes significant libpq changes", + ), + Tag( + name="psql", + color="#1a5fb4", + description="Makes significant psql changes", + ), + Tag( + name="Performance", + color="#1a5fb4", + description="Improves performance", + ), + Tag( + name="Logical Replication", + color="#1a5fb4", + description="Makes significant logical replication changes", + ), + Tag( + name="Physical Replication", + color="#1a5fb4", + description="Makes significant physical replication changes", + ), + Tag( + name="Refactoring Only", + color="#1a5fb4", + description="Only refactores code without changing functionality", + ), + Tag( + name="Comments Only", + color="#1a5fb4", + description="Only updates code comments", + ), + Tag( + name="Docs Only", + color="#1a5fb4", + description="Only updates user facing documentation", + ), + Tag( + name="CI", + color="#1a5fb4", + description="Makes changes to the CI configuration or scripts", + ), + Tag( + name="Testing", + color="#1a5fb4", + description="Adds/modifies tests or improves our testing frameworks", + ), + Tag( + name="Monitoring", + color="#1a5fb4", + description="Adds new monitoring features or improves existing ones", + ), + Tag( + name="GUC", + color="#1a5fb4", + description="Adds/modifies configuration parameters (GUCs)", + ), + Tag( + name="PL/pgSQL", + color="#1a5fb4", + description="Involves changes to PL/pgSQL", + ), + Tag( + name="PL/Python", + color="#1a5fb4", + description="Involves changes to PL/Python", + ), + Tag( + name="PL/Perl", + color="#1a5fb4", + description="Involves changes to PL/Perl", + ), + Tag( + name="PL/Tcl", + color="#1a5fb4", + description="Involves changes to PL/Perl", + ), + Tag( + name="dblink", + color="#1a5fb4", + description="Involves changes to dblink", + ), + Tag( + name="postgres_fdw", + color="#1a5fb4", + description="Involves changes to postgres_fdw", + ), + Tag(name="Security", color="#a51d2d", description="Improves security"), ] ) From 467d483cd5cc93c4027f8558578174a0b3ebac04 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 09:31:42 +0200 Subject: [PATCH 16/22] Update dummy data dump with new tags --- .../commitfest/fixtures/commitfest_data.json | 173 +++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/fixtures/commitfest_data.json b/pgcommitfest/commitfest/fixtures/commitfest_data.json index 1f32b0ed..46984a82 100644 --- a/pgcommitfest/commitfest/fixtures/commitfest_data.json +++ b/pgcommitfest/commitfest/fixtures/commitfest_data.json @@ -106,7 +106,7 @@ "pk": 2, "fields": { "name": "Backport", - "color": "#1a5fb4", + "color": "#3d3846", "description": "Once merged should be backported to old branches" } }, @@ -182,6 +182,177 @@ "description": "An easy to review patch for a new reviewer" } }, +{ + "model": "commitfest.tag", + "pk": 11, + "fields": { + "name": "libpq", + "color": "#1a5fb4", + "description": "Makes significant libpq changes" + } +}, +{ + "model": "commitfest.tag", + "pk": 12, + "fields": { + "name": "psql", + "color": "#1a5fb4", + "description": "Makes significant psql changes" + } +}, +{ + "model": "commitfest.tag", + "pk": 13, + "fields": { + "name": "Performance", + "color": "#1a5fb4", + "description": "Improves performance" + } +}, +{ + "model": "commitfest.tag", + "pk": 14, + "fields": { + "name": "Logical Replication", + "color": "#1a5fb4", + "description": "Makes significant logical replication changes" + } +}, +{ + "model": "commitfest.tag", + "pk": 15, + "fields": { + "name": "Physical Replication", + "color": "#1a5fb4", + "description": "Makes significant physical replication changes" + } +}, +{ + "model": "commitfest.tag", + "pk": 16, + "fields": { + "name": "Refactoring Only", + "color": "#1a5fb4", + "description": "Only refactores code without changing functionality" + } +}, +{ + "model": "commitfest.tag", + "pk": 17, + "fields": { + "name": "Comments Only", + "color": "#1a5fb4", + "description": "Only updates code comments" + } +}, +{ + "model": "commitfest.tag", + "pk": 18, + "fields": { + "name": "Docs Only", + "color": "#1a5fb4", + "description": "Only updates user facing documentation" + } +}, +{ + "model": "commitfest.tag", + "pk": 19, + "fields": { + "name": "CI", + "color": "#1a5fb4", + "description": "Makes changes to the CI configuration or scripts" + } +}, +{ + "model": "commitfest.tag", + "pk": 20, + "fields": { + "name": "Testing", + "color": "#1a5fb4", + "description": "Adds/modifies tests or improves our testing frameworks" + } +}, +{ + "model": "commitfest.tag", + "pk": 21, + "fields": { + "name": "Monitoring", + "color": "#1a5fb4", + "description": "Adds new monitoring features or improves existing ones" + } +}, +{ + "model": "commitfest.tag", + "pk": 22, + "fields": { + "name": "GUC", + "color": "#1a5fb4", + "description": "Adds/modifies configuration parameters (GUCs)" + } +}, +{ + "model": "commitfest.tag", + "pk": 23, + "fields": { + "name": "PL/pgSQL", + "color": "#1a5fb4", + "description": "Involves changes to PL/pgSQL" + } +}, +{ + "model": "commitfest.tag", + "pk": 24, + "fields": { + "name": "PL/Python", + "color": "#1a5fb4", + "description": "Involves changes to PL/Python" + } +}, +{ + "model": "commitfest.tag", + "pk": 25, + "fields": { + "name": "PL/Perl", + "color": "#1a5fb4", + "description": "Involves changes to PL/Perl" + } +}, +{ + "model": "commitfest.tag", + "pk": 26, + "fields": { + "name": "PL/Tcl", + "color": "#1a5fb4", + "description": "Involves changes to PL/Perl" + } +}, +{ + "model": "commitfest.tag", + "pk": 27, + "fields": { + "name": "dblink", + "color": "#1a5fb4", + "description": "Involves changes to dblink" + } +}, +{ + "model": "commitfest.tag", + "pk": 28, + "fields": { + "name": "postgres_fdw", + "color": "#1a5fb4", + "description": "Involves changes to postgres_fdw" + } +}, +{ + "model": "commitfest.tag", + "pk": 29, + "fields": { + "name": "Security", + "color": "#a51d2d", + "description": "Improves security" + } +}, { "model": "commitfest.patch", "pk": 1, From 92e98e5359ac0759bea1716b5769c2a184634fb5 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 09:43:35 +0200 Subject: [PATCH 17/22] Formatting --- pgcommitfest/commitfest/templates/patch.html | 158 +++++++++---------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/pgcommitfest/commitfest/templates/patch.html b/pgcommitfest/commitfest/templates/patch.html index c8f7e476..7fd12d3f 100644 --- a/pgcommitfest/commitfest/templates/patch.html +++ b/pgcommitfest/commitfest/templates/patch.html @@ -123,90 +123,90 @@ {% endif %} - - Emails - - {%if user.is_authenticated%} -
- {%else%} -
- {%endif%} -
- {%for t in patch.mailthread_set.all%} -
{{t.subject}}
-
- First at {{t.firstmessage}} by {{t.firstauthor|hidemail}}
- Latest at {{t.latestmessage}} by {{t.latestauthor|hidemail}}
- {%for ta in t.mailthreadattachment_set.all%} - {%if forloop.first%} - Latest attachment ({{ta.filename}}) at {{ta.date}} from {{ta.author|hidemail}} -
- {%endif%} -     Attachment ({{ta.filename}}) at {{ta.date}} from {{ta.author|hidemail}} (Patch: {{ta.ispatch|yesno:"Yes,No,Pending check"}})
- {%if forloop.last%}
{%endif%} - {%endfor%} -
- {%for a in t.mailthreadannotation_set.all%} + + Emails + + {%if user.is_authenticated%} +
+ {%else%} +
+ {%endif%} +
+ {%for t in patch.mailthread_set.all%} +
{{t.subject}}
+
+ First at {{t.firstmessage}} by {{t.firstauthor|hidemail}}
+ Latest at {{t.latestmessage}} by {{t.latestauthor|hidemail}}
+ {%for ta in t.mailthreadattachment_set.all%} {%if forloop.first%} -

Annotations

- - - - - - - - - - - {%endif%} - - - - - - - {%if forloop.last%} - -
WhenWhoMailAnnotation
{{a.date}}{{a.user_string}}From {{a.mailauthor}}
at {{a.maildate}}
{{a.annotationtext}}
+ Latest attachment ({{ta.filename}}) at {{ta.date}} from {{ta.author|hidemail}} +
{%endif%} +     Attachment ({{ta.filename}}) at {{ta.date}} from {{ta.author|hidemail}} (Patch: {{ta.ispatch|yesno:"Yes,No,Pending check"}})
+ {%if forloop.last%}
{%endif%} {%endfor%} - {%if user.is_authenticated%}{%endif%} -
-
- {%endfor%} -
- - - - History - -
- - - - - - - - - - {%for h in patch.history %} +
+ {%for a in t.mailthreadannotation_set.all%} + {%if forloop.first%} +

Annotations

+
WhenWhoWhat
+ + + + + + + + + + {%endif%} + + + + + + + {%if forloop.last%} + +
WhenWhoMailAnnotation
{{a.date}}{{a.user_string}}From {{a.mailauthor}}
at {{a.maildate}}
{{a.annotationtext}}
+ {%endif%} + {%endfor%} + {%if user.is_authenticated%}{%endif%} +
+ + {%endfor%} + + + + + History + +
+ + - - - + + + - {%endfor%} - -
{{h.date}}{{h.by_string}}{{h.what}}WhenWhoWhat
-
- {%if user.is_authenticated%} - {{is_subscribed|yesno:"Unsubscribe from patch update emails,Subscribe to patch update emails"}} - {%endif%} - - - + + + {%for h in patch.history %} + + {{h.date}} + {{h.by_string}} + {{h.what}} + + {%endfor%} + + + + {%if user.is_authenticated%} + {{is_subscribed|yesno:"Unsubscribe from patch update emails,Subscribe to patch update emails"}} + {%endif%} + + +
From 9cc55f6b76839d794faa3d837434590e38ae7dc1 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 10:14:49 +0200 Subject: [PATCH 18/22] Add validation also to text field --- biome.json | 1 + media/commitfest/js/change_tag.js | 79 ++++++++++--------- .../commitfest/templates/color_input.html | 4 +- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/biome.json b/biome.json index 1c3d6648..4e121e69 100644 --- a/biome.json +++ b/biome.json @@ -10,6 +10,7 @@ "ignore": [], "include": [ "media/commitfest/js/commitfest.js", + "media/commitfest/js/change_tag.js", "media/commitfest/css/commitfest.css", "biome.json" ] diff --git a/media/commitfest/js/change_tag.js b/media/commitfest/js/change_tag.js index ae4c224a..7baca1a5 100644 --- a/media/commitfest/js/change_tag.js +++ b/media/commitfest/js/change_tag.js @@ -1,44 +1,49 @@ // An input validator for the color picker. Points out low-contrast tag color // choices. -const input = document.getElementById("id_color"); -input.addEventListener("input", (event) => { - // Don't do anything if the color code doesn't pass default validity. - input.setCustomValidity(""); - if (!input.validity.valid) { - return; - } +const inputs = document.getElementsByClassName("color-picker"); +for (let i = 0; i < inputs.length; i++) { + inputs[i].addEventListener("change", (event) => { + // Don't do anything if the color code doesn't pass default validity. + const element = event.target; + element.setCustomValidity(""); + if (!element.validity.valid) { + return; + } - // Break the #rrggbb color code into RGB components. - color = parseInt(input.value.substr(1), 16); - red = ((color & 0xFF0000) >> 16) / 255.; - green = ((color & 0x00FF00) >> 8) / 255.; - blue = (color & 0x0000FF) / 255.; + // Break the #rrggbb color code into RGB components. + color = parseInt(element.value.substr(1), 16); + red = ((color & 0xff0000) >> 16) / 255; + green = ((color & 0x00ff00) >> 8) / 255; + blue = (color & 0x0000ff) / 255; - // Compare the contrast ratio against white. All the magic math comes from - // Web Content Accessibility Guidelines (WCAG) 2.2, Technique G18: - // - // https://www.w3.org/WAI/WCAG22/Techniques/general/G18.html - // - function l(val) { - if (val <= 0.04045) { - return val / 12.92; - } - return ((val + 0.055) / 1.055) ** 2.4; - } + // Compare the contrast ratio against white. All the magic math comes from + // Web Content Accessibility Guidelines (WCAG) 2.2, Technique G18: + // + // https://www.w3.org/WAI/WCAG22/Techniques/general/G18.html + // + function l(val) { + if (val <= 0.04045) { + return val / 12.92; + } + return ((val + 0.055) / 1.055) ** 2.4; + } - lum = 0.2126 * l(red) + 0.7152 * l(green) + 0.0722 * l(blue); - contrast = (1 + 0.05) / (lum + 0.05); + lum = 0.2126 * l(red) + 0.7152 * l(green) + 0.0722 * l(blue); + contrast = (1 + 0.05) / (lum + 0.05); - // Complain if we're below WCAG 2.2 recommendations. - if (contrast < 4.5) { - input.setCustomValidity( - "Consider choosing a darker color. " - + "(Tag text is small and white.)\n\n" - + "Contrast ratio: " + (Math.trunc(contrast * 10) / 10) + " (< 4.5)" - ); + // Complain if we're below WCAG 2.2 recommendations. + if (contrast < 4.5) { + element.setCustomValidity( + "Consider choosing a darker color. " + + "(Tag text is small and white.)\n\n" + + "Contrast ratio: " + + Math.trunc(contrast * 10) / 10 + + " (< 4.5)", + ); - // The admin form uses novalidate, so manually display the browser's - // validity popup. (The user can still ignore it if desired.) - input.reportValidity(); - } -}); + // The admin form uses novalidate, so manually display the browser's + // validity popup. (The user can still ignore it if desired.) + element.reportValidity(); + } + }); +} diff --git a/pgcommitfest/commitfest/templates/color_input.html b/pgcommitfest/commitfest/templates/color_input.html index 7a4135f4..9e4c8cc1 100644 --- a/pgcommitfest/commitfest/templates/color_input.html +++ b/pgcommitfest/commitfest/templates/color_input.html @@ -1,2 +1,2 @@ - - + + From e20ca39b9c3cb182c13bd67db8efd6f27430e377 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 11:55:34 +0200 Subject: [PATCH 19/22] Auto formatting --- media/commitfest/js/change_tag.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/media/commitfest/js/change_tag.js b/media/commitfest/js/change_tag.js index 7baca1a5..89277837 100644 --- a/media/commitfest/js/change_tag.js +++ b/media/commitfest/js/change_tag.js @@ -11,7 +11,7 @@ for (let i = 0; i < inputs.length; i++) { } // Break the #rrggbb color code into RGB components. - color = parseInt(element.value.substr(1), 16); + color = Number.parseInt(element.value.substr(1), 16); red = ((color & 0xff0000) >> 16) / 255; green = ((color & 0x00ff00) >> 8) / 255; blue = (color & 0x0000ff) / 255; @@ -34,11 +34,7 @@ for (let i = 0; i < inputs.length; i++) { // Complain if we're below WCAG 2.2 recommendations. if (contrast < 4.5) { element.setCustomValidity( - "Consider choosing a darker color. " + - "(Tag text is small and white.)\n\n" + - "Contrast ratio: " + - Math.trunc(contrast * 10) / 10 + - " (< 4.5)", + `Consider choosing a darker color. (Tag text is small and white.)\n\nContrast ratio: ${Math.trunc(contrast * 10) / 10} (< 4.5)`, ); // The admin form uses novalidate, so manually display the browser's From b8ae9954f921a32be1f62439220e06c1c962dabb Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 11:58:14 +0200 Subject: [PATCH 20/22] Update change messages with new tag names --- pgcommitfest/commitfest/fixtures/commitfest_data.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgcommitfest/commitfest/fixtures/commitfest_data.json b/pgcommitfest/commitfest/fixtures/commitfest_data.json index 46984a82..39f577f4 100644 --- a/pgcommitfest/commitfest/fixtures/commitfest_data.json +++ b/pgcommitfest/commitfest/fixtures/commitfest_data.json @@ -865,7 +865,7 @@ "date": "2025-06-15T21:26:10.546", "by": 1, "by_cfbot": false, - "what": "Changed tags to backport, bugfix, help-benchmarks, help-user-testing, missing-docs" + "what": "Changed tags to Backport, Bugfix, Help - Benchmarks, Help - User Testing, Missing Docs" } }, { @@ -876,7 +876,7 @@ "date": "2025-06-15T21:26:24.282", "by": 1, "by_cfbot": false, - "what": "Changed tags to good-first-review, help-bikeshedding, help-user-testing" + "what": "Changed tags to Good First Review, Help Bikeshedding, Help - User Testing" } }, { @@ -887,7 +887,7 @@ "date": "2025-06-15T21:27:56.664", "by": 1, "by_cfbot": false, - "what": "Changed tags to bugfix" + "what": "Changed tags to Bugfix" } }, { From 31a4afc377af13a33d9353811901edc697afe90a Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 12:03:37 +0200 Subject: [PATCH 21/22] Add comment about SQL injection consideration --- pgcommitfest/commitfest/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index 8f813bed..43b25685 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -289,6 +289,9 @@ def patchlist(request, cf, personalized=False): try: tag_ids = [int(t) for t in request.GET.getlist("tag")] for tag_id in tag_ids: + # Instead of using parameters, we just inline the tag_id. This + # is easier, and since tag_id is always an int it's safe with + # respect to SQL injection. whereclauses.append( f"EXISTS (SELECT 1 FROM commitfest_patch_tags tags WHERE tags.patch_id=p.id AND tags.tag_id={tag_id})" ) From 8c714bd4cb638518983d70d4a6ae01c0a5b3a220 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 16 Jun 2025 12:13:56 +0200 Subject: [PATCH 22/22] Use repeatable read for commitfest and dashboard query --- pgcommitfest/commitfest/views.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index 43b25685..4b3d12c8 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -88,7 +88,13 @@ def help(request): @login_required +@transaction.atomic def me(request): + curs = connection.cursor() + # Make sure the patchlist() query, the stats query and, Tag.objects.all() + # all work on the same snapshot. Needs to be first in the + # transaction.atomic decorator. + curs.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_INPROGRESS)) if len(cfs) == 0: cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_OPEN)) @@ -108,7 +114,6 @@ def me(request): return patch_list.redirect # Get stats related to user for current commitfest - curs = connection.cursor() curs.execute( """SELECT ps.status, ps.statusstring, count(*) @@ -290,8 +295,9 @@ def patchlist(request, cf, personalized=False): tag_ids = [int(t) for t in request.GET.getlist("tag")] for tag_id in tag_ids: # Instead of using parameters, we just inline the tag_id. This - # is easier, and since tag_id is always an int it's safe with - # respect to SQL injection. + # is easier because we have can have multiple tags, and since + # tag_id is always an int it's safe with respect to SQL + # injection. whereclauses.append( f"EXISTS (SELECT 1 FROM commitfest_patch_tags tags WHERE tags.patch_id=p.id AND tags.tag_id={tag_id})" ) @@ -569,8 +575,13 @@ def patchlist(request, cf, personalized=False): ) -@transaction.atomic # tie the patchlist() query to Tag.objects.all() +@transaction.atomic def commitfest(request, cfid): + curs = connection.cursor() + # Make sure the patchlist() query, the stats query and, Tag.objects.all() + # all work on the same snapshot. Needs to be first in the + # transaction.atomic decorator. + curs.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") # Find ourselves cf = get_object_or_404(CommitFest, pk=cfid) @@ -579,7 +590,6 @@ def commitfest(request, cfid): return patch_list.redirect # Generate patch status summary. - curs = connection.cursor() curs.execute( "SELECT ps.status, ps.statusstring, count(*) FROM commitfest_patchoncommitfest poc INNER JOIN commitfest_patchstatus ps ON ps.status=poc.status WHERE commitfest_id=%(id)s GROUP BY ps.status ORDER BY ps.sortkey", {