From 671e402404550a135f559b98858a4d9b4381378c Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:30:38 +0100 Subject: [PATCH 01/31] Update ruff, Biome and djhtml to their latest versions This bumps ruff from 0.15.20 to 0.17.2, Biome from 1.9.4 to 2.6.3 and djhtml to 3.0.11. ruff and djhtml are now also pinned in the `dev` extra, so that a local `pip install -e '.[dev]'` gets the same versions that CI uses. Before, whatever ruff happened to be installed locally could disagree with CI. `biome.json` was converted to the Biome 2 format with `biome migrate`, and Biome 2 formats multi-line CSS custom properties slightly differently. ruff 0.17 enables a much larger set of rules by default. The violations of those new rules are fixed in the following commits, one commit per rule. --- .github/workflows/ci.yaml | 8 ++++---- Makefile | 8 ++++---- biome.json | 19 ++++++++----------- media/commitfest/css/commitfest.css | 10 ++++++---- pyproject.toml | 4 ++-- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b8cebe4f..877a6c31 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,24 +15,24 @@ jobs: - name: Run ruff check uses: astral-sh/ruff-action@v2 with: - version: 0.15.20 + version: 0.17.2 - name: Run ruff format --check uses: astral-sh/ruff-action@v2 with: - version: 0.15.20 + version: 0.17.2 args: "format --check" - name: Setup Biome uses: biomejs/setup-biome@v2 with: - version: 1.9.4 + version: 2.6.3 - name: Run Biome run: biome ci . - name: Install djhtml - run: pip install djhtml + run: pip install djhtml==3.0.11 - name: Run djhtml run: djhtml pgcommitfest/*/templates/*.html pgcommitfest/*/templates/*.inc --tabwidth=1 --check diff --git a/Makefile b/Makefile index 566bf874..c0c0f4ba 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,18 @@ format: ruff format - npx @biomejs/biome@1.9.4 format --write + npx @biomejs/biome@2.6.3 format --write djhtml pgcommitfest/*/templates/*.html pgcommitfest/*/templates/*.inc --tabwidth=1 lint: ruff check - npx @biomejs/biome@1.9.4 check + npx @biomejs/biome@2.6.3 check lint-fix: ruff check --fix - npx @biomejs/biome@1.9.4 check --fix + npx @biomejs/biome@2.6.3 check --fix lint-fix-unsafe: ruff check --fix --unsafe-fixes - npx @biomejs/biome@1.9.4 check --fix --unsafe + npx @biomejs/biome@2.6.3 check --fix --unsafe fix: format lint-fix-unsafe diff --git a/biome.json b/biome.json index 4e121e69..97ea151c 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.6.3/schema.json", "vcs": { "enabled": false, "clientKind": "git", @@ -7,12 +7,11 @@ }, "files": { "ignoreUnknown": true, - "ignore": [], - "include": [ - "media/commitfest/js/commitfest.js", - "media/commitfest/js/change_tag.js", - "media/commitfest/css/commitfest.css", - "biome.json" + "includes": [ + "**/media/commitfest/js/commitfest.js", + "**/media/commitfest/js/change_tag.js", + "**/media/commitfest/css/commitfest.css", + "**/biome.json" ] }, "formatter": { @@ -20,13 +19,11 @@ "indentStyle": "space", "indentWidth": 4 }, - "organizeImports": { - "enabled": true - }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, "rules": { - "recommended": true + "preset": "recommended" } }, "javascript": { diff --git a/media/commitfest/css/commitfest.css b/media/commitfest/css/commitfest.css index 21cbb2d6..c8e286ea 100644 --- a/media/commitfest/css/commitfest.css +++ b/media/commitfest/css/commitfest.css @@ -4,11 +4,13 @@ body { --bs-body-font-size: 14px; - --bs-body-font-family: -apple-system, "Segoe UI", Roboto, "Helvetica Neue", - "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", + --bs-body-font-family: + -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", + "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --bs-font-sans-serif: -apple-system, "Segoe UI", Roboto, "Helvetica Neue", - "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", + --bs-font-sans-serif: + -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", + "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } diff --git a/pyproject.toml b/pyproject.toml index c3a6a178..b1167050 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dev = [ "django-stubs", "uwsgi", "pycodestyle", - "ruff", - "djhtml", + "ruff==0.17.2", + "djhtml==3.0.11", "pytest", "pytest-django", "freezegun", From 342f16a11f1a8f64f439995933693a03e4769158 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:22 +0100 Subject: [PATCH 02/31] Remove `# -*- coding: utf-8 -*-` declarations UTF-8 is the default source encoding in Python 3, so these lines do nothing (ruff UP009). --- pgcommitfest/commitfest/migrations/0001_initial.py | 1 - pgcommitfest/commitfest/migrations/0002_notifications.py | 1 - pgcommitfest/commitfest/migrations/0003_withdrawn_status.py | 1 - pgcommitfest/commitfest/migrations/0004_target_version.py | 1 - pgcommitfest/commitfest/migrations/0005_history_dateindex.py | 1 - pgcommitfest/mailqueue/migrations/0001_initial.py | 1 - pgcommitfest/userprofile/migrations/0001_initial.py | 1 - pgcommitfest/userprofile/migrations/0002_notifications.py | 1 - 8 files changed, 8 deletions(-) diff --git a/pgcommitfest/commitfest/migrations/0001_initial.py b/pgcommitfest/commitfest/migrations/0001_initial.py index 3577d436..46c79769 100644 --- a/pgcommitfest/commitfest/migrations/0001_initial.py +++ b/pgcommitfest/commitfest/migrations/0001_initial.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.conf import settings diff --git a/pgcommitfest/commitfest/migrations/0002_notifications.py b/pgcommitfest/commitfest/migrations/0002_notifications.py index 450ddfbe..010ecae5 100644 --- a/pgcommitfest/commitfest/migrations/0002_notifications.py +++ b/pgcommitfest/commitfest/migrations/0002_notifications.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.conf import settings diff --git a/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py b/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py index 2f6a5f7d..d0721ed8 100644 --- a/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py +++ b/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models diff --git a/pgcommitfest/commitfest/migrations/0004_target_version.py b/pgcommitfest/commitfest/migrations/0004_target_version.py index ad546109..d4bf6cd3 100644 --- a/pgcommitfest/commitfest/migrations/0004_target_version.py +++ b/pgcommitfest/commitfest/migrations/0004_target_version.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.17 on 2019-02-06 19:43 from __future__ import unicode_literals diff --git a/pgcommitfest/commitfest/migrations/0005_history_dateindex.py b/pgcommitfest/commitfest/migrations/0005_history_dateindex.py index 4316f212..d48fcaba 100644 --- a/pgcommitfest/commitfest/migrations/0005_history_dateindex.py +++ b/pgcommitfest/commitfest/migrations/0005_history_dateindex.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.17 on 2019-02-06 20:36 from __future__ import unicode_literals diff --git a/pgcommitfest/mailqueue/migrations/0001_initial.py b/pgcommitfest/mailqueue/migrations/0001_initial.py index 38d1214d..3f08b082 100644 --- a/pgcommitfest/mailqueue/migrations/0001_initial.py +++ b/pgcommitfest/mailqueue/migrations/0001_initial.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models diff --git a/pgcommitfest/userprofile/migrations/0001_initial.py b/pgcommitfest/userprofile/migrations/0001_initial.py index 45ad7696..c58bec71 100644 --- a/pgcommitfest/userprofile/migrations/0001_initial.py +++ b/pgcommitfest/userprofile/migrations/0001_initial.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.conf import settings diff --git a/pgcommitfest/userprofile/migrations/0002_notifications.py b/pgcommitfest/userprofile/migrations/0002_notifications.py index 0d4b0357..c051ee2a 100644 --- a/pgcommitfest/userprofile/migrations/0002_notifications.py +++ b/pgcommitfest/userprofile/migrations/0002_notifications.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.conf import settings From 2cce9edf733e18fffbb798293853e71300d0546a Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:22 +0100 Subject: [PATCH 03/31] Remove `from __future__` imports These `__future__` features (`print_function`, `unicode_literals`, ...) are always enabled in Python 3, so the imports do nothing (ruff UP010). --- pgcommitfest/commitfest/migrations/0001_initial.py | 2 -- pgcommitfest/commitfest/migrations/0002_notifications.py | 2 -- pgcommitfest/commitfest/migrations/0003_withdrawn_status.py | 2 -- pgcommitfest/commitfest/migrations/0004_target_version.py | 1 - pgcommitfest/commitfest/migrations/0005_history_dateindex.py | 1 - pgcommitfest/mailqueue/migrations/0001_initial.py | 2 -- pgcommitfest/userprofile/migrations/0001_initial.py | 2 -- pgcommitfest/userprofile/migrations/0002_notifications.py | 2 -- 8 files changed, 14 deletions(-) diff --git a/pgcommitfest/commitfest/migrations/0001_initial.py b/pgcommitfest/commitfest/migrations/0001_initial.py index 46c79769..4dd7f8c7 100644 --- a/pgcommitfest/commitfest/migrations/0001_initial.py +++ b/pgcommitfest/commitfest/migrations/0001_initial.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/pgcommitfest/commitfest/migrations/0002_notifications.py b/pgcommitfest/commitfest/migrations/0002_notifications.py index 010ecae5..b85bac82 100644 --- a/pgcommitfest/commitfest/migrations/0002_notifications.py +++ b/pgcommitfest/commitfest/migrations/0002_notifications.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py b/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py index d0721ed8..abdc9afe 100644 --- a/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py +++ b/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/pgcommitfest/commitfest/migrations/0004_target_version.py b/pgcommitfest/commitfest/migrations/0004_target_version.py index d4bf6cd3..45919fe7 100644 --- a/pgcommitfest/commitfest/migrations/0004_target_version.py +++ b/pgcommitfest/commitfest/migrations/0004_target_version.py @@ -1,5 +1,4 @@ # Generated by Django 1.11.17 on 2019-02-06 19:43 -from __future__ import unicode_literals import django.db.models.deletion from django.db import migrations, models diff --git a/pgcommitfest/commitfest/migrations/0005_history_dateindex.py b/pgcommitfest/commitfest/migrations/0005_history_dateindex.py index d48fcaba..e58cf9d3 100644 --- a/pgcommitfest/commitfest/migrations/0005_history_dateindex.py +++ b/pgcommitfest/commitfest/migrations/0005_history_dateindex.py @@ -1,5 +1,4 @@ # Generated by Django 1.11.17 on 2019-02-06 20:36 -from __future__ import unicode_literals from django.db import migrations, models diff --git a/pgcommitfest/mailqueue/migrations/0001_initial.py b/pgcommitfest/mailqueue/migrations/0001_initial.py index 3f08b082..27a5508f 100644 --- a/pgcommitfest/mailqueue/migrations/0001_initial.py +++ b/pgcommitfest/mailqueue/migrations/0001_initial.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/pgcommitfest/userprofile/migrations/0001_initial.py b/pgcommitfest/userprofile/migrations/0001_initial.py index c58bec71..5848f99b 100644 --- a/pgcommitfest/userprofile/migrations/0001_initial.py +++ b/pgcommitfest/userprofile/migrations/0001_initial.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/pgcommitfest/userprofile/migrations/0002_notifications.py b/pgcommitfest/userprofile/migrations/0002_notifications.py index c051ee2a..2f374d51 100644 --- a/pgcommitfest/userprofile/migrations/0002_notifications.py +++ b/pgcommitfest/userprofile/migrations/0002_notifications.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models From a2b81ff1c9a4fc8f5ef85f96828ffa71e4e21e03 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:23 +0100 Subject: [PATCH 04/31] Stop explicitly inheriting from `object` In Python 3 every class inherits from `object` already (ruff UP004). --- pgcommitfest/commitfest/util.py | 2 +- pgcommitfest/userprofile/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pgcommitfest/commitfest/util.py b/pgcommitfest/commitfest/util.py index 10f9c816..a971fd86 100644 --- a/pgcommitfest/commitfest/util.py +++ b/pgcommitfest/commitfest/util.py @@ -2,7 +2,7 @@ from django.forms.models import model_to_dict -class DiffableModel(object): +class DiffableModel: """ Make it possible to diff a model. """ diff --git a/pgcommitfest/userprofile/util.py b/pgcommitfest/userprofile/util.py index 78a17597..2b94fcdd 100644 --- a/pgcommitfest/userprofile/util.py +++ b/pgcommitfest/userprofile/util.py @@ -4,7 +4,7 @@ from .models import UserExtraEmail, UserProfile -class UserWrapper(object): +class UserWrapper: def __init__(self, user): self.user = user From 0dd2ea2fcb748bc08870040c5225c0d022de5729 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:23 +0100 Subject: [PATCH 05/31] Use `super()` without arguments The Python 3 form `super()` is equivalent to `super(Class, self)` and does not need to be updated when a class is renamed (ruff UP008). --- pgcommitfest/commitfest/feeds.py | 2 +- pgcommitfest/commitfest/forms.py | 8 ++++---- pgcommitfest/commitfest/models.py | 2 +- pgcommitfest/commitfest/util.py | 4 ++-- pgcommitfest/commitfest/widgets.py | 2 +- pgcommitfest/userprofile/forms.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pgcommitfest/commitfest/feeds.py b/pgcommitfest/commitfest/feeds.py index 83c47bfc..7da56347 100644 --- a/pgcommitfest/commitfest/feeds.py +++ b/pgcommitfest/commitfest/feeds.py @@ -18,7 +18,7 @@ class ActivityFeed(Feed): link = "https://commitfest.postgresql.org/" def __init__(self, activity, cf, *args, **kwargs): - super(ActivityFeed, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.activity = activity if cf: self.cfid = cf.id diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index 444905b1..f12ce12a 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -25,7 +25,7 @@ class CommitFestFilterForm(forms.Form): sortkey = forms.IntegerField(required=False) def __init__(self, data, commitfest=None, *args, **kwargs): - super(CommitFestFilterForm, self).__init__(data, *args, **kwargs) + super().__init__(data, *args, **kwargs) self.commitfest = commitfest # Update selectize_fields with cf parameter if commitfest is provided @@ -104,7 +104,7 @@ class Meta: ) def __init__(self, *args, **kwargs): - super(PatchForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields["authors"].help_text = "Enter part of name to see list" self.fields["reviewers"].help_text = "Enter part of name to see list" self.fields["committer"].label_from_instance = lambda x: ( @@ -180,7 +180,7 @@ class NewPatchForm(PatchForm): def __init__(self, *args, **kwargs): request = kwargs.pop("request", None) - super(NewPatchForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if request: self.fields["authors"].queryset = User.objects.filter(pk=request.user.id) @@ -222,4 +222,4 @@ class BulkEmailForm(forms.Form): confirm = forms.BooleanField(required=True, label="Check to confirm sending") def __init__(self, *args, **kwargs): - super(BulkEmailForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index c7b8fb4f..88c4bb4d 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -1015,7 +1015,7 @@ def save(self, *args, **kwargs): if not self.commit_id: self.commit_id = None - super(CfbotBranch, self).save(*args, **kwargs) + super().save(*args, **kwargs) class CfbotTask(models.Model): diff --git a/pgcommitfest/commitfest/util.py b/pgcommitfest/commitfest/util.py index a971fd86..8649520c 100644 --- a/pgcommitfest/commitfest/util.py +++ b/pgcommitfest/commitfest/util.py @@ -8,7 +8,7 @@ class DiffableModel: """ def __init__(self, *args, **kwargs): - super(DiffableModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.__initial = self._dict @property @@ -36,7 +36,7 @@ def diff(self): return diffs def save(self, *args, **kwargs): - super(DiffableModel, self).save(*args, **kwargs) + super().save(*args, **kwargs) self.__initial = self._dict @property diff --git a/pgcommitfest/commitfest/widgets.py b/pgcommitfest/commitfest/widgets.py index dea0c7ec..65cfeba2 100644 --- a/pgcommitfest/commitfest/widgets.py +++ b/pgcommitfest/commitfest/widgets.py @@ -5,7 +5,7 @@ class ThreadPickWidget(TextInput): def render(self, name, value, attrs=None, renderer=None): attrs["class"] += " threadpick-input" - html = super(ThreadPickWidget, self).render(name, value, attrs) + html = super().render(name, value, attrs) html = ( html + ' ' diff --git a/pgcommitfest/userprofile/forms.py b/pgcommitfest/userprofile/forms.py index e0ed5a1b..82d142a1 100644 --- a/pgcommitfest/userprofile/forms.py +++ b/pgcommitfest/userprofile/forms.py @@ -9,7 +9,7 @@ class Meta: exclude = ("user",) def __init__(self, user, *args, **kwargs): - super(UserProfileForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.user = user mailhelp = 'To add a new address to choose from, update your user profile on postgresql.org.' From a4e94d3246f4c6898706b39be3330ade277a33da Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:23 +0100 Subject: [PATCH 06/31] Use `datetime.UTC` instead of `timezone.utc` `datetime.UTC` is the shorter alias for `datetime.timezone.utc` that exists since Python 3.11 (ruff UP017). --- pgcommitfest/commitfest/apiv1.py | 6 ++--- pgcommitfest/commitfest/models.py | 4 ++-- pgcommitfest/commitfest/tests/test_apiv1.py | 24 +++++++++---------- .../tests/test_newpatch_transaction.py | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pgcommitfest/commitfest/apiv1.py b/pgcommitfest/commitfest/apiv1.py index 9d6109ad..d5c19fec 100644 --- a/pgcommitfest/commitfest/apiv1.py +++ b/pgcommitfest/commitfest/apiv1.py @@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404 import json -from datetime import date, datetime, timedelta, timezone +from datetime import UTC, date, datetime, timedelta from .models import ( CommitFest, @@ -18,7 +18,7 @@ def datetime_serializer(obj): # datetime must be checked before date, since datetime is a subclass of date if isinstance(obj, datetime): - return obj.replace(tzinfo=timezone.utc).isoformat() + return obj.replace(tzinfo=UTC).isoformat() if isinstance(obj, date): return obj.isoformat() @@ -51,7 +51,7 @@ def commitfestst_that_need_ci(request): # We continue to run CI on the previous commitfest for a week after it ends # to give people some time to move patches over to the next one. - if cfs["previous"].enddate <= datetime.now(timezone.utc).date() - timedelta(days=7): + if cfs["previous"].enddate <= datetime.now(UTC).date() - timedelta(days=7): del cfs["previous"] del cfs["next_open"] diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 88c4bb4d..6e4d4693 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -4,7 +4,7 @@ from django.db.models import Exists, OuterRef, Q from django.shortcuts import get_object_or_404 -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pgcommitfest.mailqueue.util import send_template_mail from pgcommitfest.userprofile.models import UserProfile @@ -257,7 +257,7 @@ def _are_relevant_commitfests_up_to_date(cfs, current_date): @classmethod def _refresh_relevant_commitfests(cls, for_update): cfs = CommitFest.relevant_commitfests(for_update=for_update, refresh=False) - current_date = datetime.now(timezone.utc).date() + current_date = datetime.now(UTC).date() if cls._are_relevant_commitfests_up_to_date(cfs, current_date): return cfs diff --git a/pgcommitfest/commitfest/tests/test_apiv1.py b/pgcommitfest/commitfest/tests/test_apiv1.py index 539f29e1..5184618b 100644 --- a/pgcommitfest/commitfest/tests/test_apiv1.py +++ b/pgcommitfest/commitfest/tests/test_apiv1.py @@ -2,7 +2,7 @@ from django.test.utils import CaptureQueriesContext import json -from datetime import datetime, timezone +from datetime import UTC, datetime import pytest @@ -129,7 +129,7 @@ def test_commitfest_patches_endpoint(client, open_cf, alice, bob): # Create test patches patch1 = Patch.objects.create(name="Add feature X") patch1.authors.add(alice) - patch1.lastmail = datetime(2025, 1, 15, 10, 30, 0, tzinfo=timezone.utc) + patch1.lastmail = datetime(2025, 1, 15, 10, 30, 0, tzinfo=UTC) patch1.save() patch2 = Patch.objects.create(name="Fix bug Y") @@ -140,24 +140,24 @@ def test_commitfest_patches_endpoint(client, open_cf, alice, bob): PatchOnCommitFest.objects.create( patch=patch1, commitfest=open_cf, - enterdate=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + enterdate=datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC), status=PatchOnCommitFest.STATUS_REVIEW, ) PatchOnCommitFest.objects.create( patch=patch2, commitfest=open_cf, - enterdate=datetime(2025, 1, 2, 8, 0, 0, tzinfo=timezone.utc), + enterdate=datetime(2025, 1, 2, 8, 0, 0, tzinfo=UTC), # leavedate is only allowed on statuses that close the patch out - leavedate=datetime(2025, 1, 20, 16, 45, 0, tzinfo=timezone.utc), + leavedate=datetime(2025, 1, 20, 16, 45, 0, tzinfo=UTC), status=PatchOnCommitFest.STATUS_COMMITTED, ) thread = MailThread.objects.create( messageid="abc123@example.com", subject="[PATCH] Add feature X", - firstmessage=datetime(2025, 1, 10, 9, 0, 0, tzinfo=timezone.utc), + firstmessage=datetime(2025, 1, 10, 9, 0, 0, tzinfo=UTC), firstauthor="alice@example.com", - latestmessage=datetime(2025, 1, 12, 14, 30, 0, tzinfo=timezone.utc), + latestmessage=datetime(2025, 1, 12, 14, 30, 0, tzinfo=UTC), latestauthor="bob@example.com", latestsubject="Re: [PATCH] Add feature X", latestmsgid="def456@example.com", @@ -168,7 +168,7 @@ def test_commitfest_patches_endpoint(client, open_cf, alice, bob): messageid="abc123@example.com", attachmentid=1, filename="v1.patch", - date=datetime(2025, 1, 10, 9, 0, 0, tzinfo=timezone.utc), + date=datetime(2025, 1, 10, 9, 0, 0, tzinfo=UTC), author="alice@example.com", ispatch=True, ) @@ -232,7 +232,7 @@ def test_commitfest_patches_endpoint_include_parsing(client, open_cf): PatchOnCommitFest.objects.create( patch=Patch.objects.create(name="Add feature X"), commitfest=open_cf, - enterdate=datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + enterdate=datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC), ) url = f"/api/v1/commitfests/{open_cf.id}/patches" @@ -258,7 +258,7 @@ def test_patch_threads_endpoint(client, open_cf, alice): PatchOnCommitFest.objects.create( patch=patch, commitfest=open_cf, - enterdate=datetime(2025, 1, 1, tzinfo=timezone.utc), + enterdate=datetime(2025, 1, 1, tzinfo=UTC), status=PatchOnCommitFest.STATUS_REVIEW, ) @@ -266,9 +266,9 @@ def test_patch_threads_endpoint(client, open_cf, alice): thread1 = MailThread.objects.create( messageid="abc123@example.com", subject="[PATCH] Test patch v1", - firstmessage=datetime(2025, 1, 10, 9, 0, 0, tzinfo=timezone.utc), + firstmessage=datetime(2025, 1, 10, 9, 0, 0, tzinfo=UTC), firstauthor="alice@example.com", - latestmessage=datetime(2025, 1, 12, 14, 30, 0, tzinfo=timezone.utc), + latestmessage=datetime(2025, 1, 12, 14, 30, 0, tzinfo=UTC), latestauthor="bob@example.com", latestsubject="Re: [PATCH] Test patch v1", latestmsgid="def456@example.com", diff --git a/pgcommitfest/commitfest/tests/test_newpatch_transaction.py b/pgcommitfest/commitfest/tests/test_newpatch_transaction.py index ca78d14c..403503f5 100644 --- a/pgcommitfest/commitfest/tests/test_newpatch_transaction.py +++ b/pgcommitfest/commitfest/tests/test_newpatch_transaction.py @@ -1,6 +1,6 @@ from django.db import connection -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import patch import pytest @@ -26,7 +26,7 @@ def test_newpatch_thread_attach( client.force_login(alice) message = { "msgid": MSGID, - "date": datetime(2025, 1, 10, 12, tzinfo=timezone.utc), + "date": datetime(2025, 1, 10, 12, tzinfo=UTC), "from": "author@example.com", "subj": "[PATCH] Test", "atts": [], From f37b446a9a5aebdf4447e43f55b6487908bf489e Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:23 +0100 Subject: [PATCH 07/31] Remove extraneous parentheses Parentheses around a single expression in a call like `print((x))` are redundant (ruff UP034). --- pgcommitfest/commitfest/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index b9466b5e..afb0329a 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -693,7 +693,7 @@ def commitfest(request, cfid): }, ) statussummary = curs.fetchall() - statussummary.append([-1, "Total", sum((r[2] for r in statussummary))]) + statussummary.append([-1, "Total", sum(r[2] for r in statussummary)]) # Generates a fairly expensive query, which we shouldn't do unless # the user is logged in. XXX: Figure out how to avoid doing that.. From 032b49d0aefbc720ce438d87f7e2d65255ccdc28 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:23 +0100 Subject: [PATCH 08/31] Use attribute access instead of `getattr` with a constant `getattr(obj, "name")` without a default is just a more verbose `obj.name` (ruff B009). --- pgcommitfest/commitfest/ajax.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index 9e0d92e4..7f0b820a 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -49,9 +49,7 @@ def mockArchivesAPI(path): def _archivesAPI(suburl, params=None): - if getattr(settings, "MOCK_ARCHIVES", False) and getattr( - settings, "MOCK_ARCHIVE_DATA" - ): + if getattr(settings, "MOCK_ARCHIVES", False) and settings.MOCK_ARCHIVE_DATA: return mockArchivesAPI(suburl) try: From 1497150f3dbe9a8441502ecfffdbba38622c55c2 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:23 +0100 Subject: [PATCH 09/31] Use `from x import y` instead of `import x.y as y` This is the more direct way to write the same import (ruff PLR0402). --- pgcommitfest/urls.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pgcommitfest/urls.py b/pgcommitfest/urls.py index de80e32c..8530e6aa 100644 --- a/pgcommitfest/urls.py +++ b/pgcommitfest/urls.py @@ -2,12 +2,8 @@ from django.urls import re_path import pgcommitfest.auth -import pgcommitfest.commitfest.ajax as ajax -import pgcommitfest.commitfest.apiv1 as apiv1 -import pgcommitfest.commitfest.lookups as lookups -import pgcommitfest.commitfest.reports as reports -import pgcommitfest.commitfest.views as views import pgcommitfest.userprofile.views +from pgcommitfest.commitfest import ajax, apiv1, lookups, reports, views # Uncomment the next two lines to enable the admin: # from django.contrib import admin From 3b3ef69b176813f5ccf40d1d515c5d4da86c1bb2 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:31:23 +0100 Subject: [PATCH 10/31] Use f-strings instead of `str.format()` f-strings are easier to read because the values are next to where they end up in the string (ruff UP032). --- pgcommitfest/commitfest/feeds.py | 2 +- pgcommitfest/commitfest/models.py | 2 +- pgcommitfest/commitfest/views.py | 18 +++++------------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/pgcommitfest/commitfest/feeds.py b/pgcommitfest/commitfest/feeds.py index 7da56347..ef71f345 100644 --- a/pgcommitfest/commitfest/feeds.py +++ b/pgcommitfest/commitfest/feeds.py @@ -23,7 +23,7 @@ def __init__(self, activity, cf, *args, **kwargs): if cf: self.cfid = cf.id self.title = self.description = ( - "PostgreSQL Commitfest {0} Activity Log".format(cf.name) + f"PostgreSQL Commitfest {cf.name} Activity Log" ) else: self.cfid = None diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 6e4d4693..3ca8080e 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -68,7 +68,7 @@ def statusstring(self): @property def periodstring(self): - return "{0} – {1}".format(self.startdate, self.enddate) + return f"{self.startdate} – {self.enddate}" @property def last_open_date(self): diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index afb0329a..0ed02b3a 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -257,16 +257,12 @@ def activity(request, cfid=None, rss=None): # we're evil. And also because the number has been verified # when looking up the cf itself, so nothing can be injected # there. - where = "WHERE EXISTS (SELECT 1 FROM commitfest_patchoncommitfest poc2 WHERE poc2.patch_id=p.id AND poc2.commitfest_id={0})".format( - cf.id - ) + where = f"WHERE EXISTS (SELECT 1 FROM commitfest_patchoncommitfest poc2 WHERE poc2.patch_id=p.id AND poc2.commitfest_id={cf.id})" else: cf = None where = "" - sql = "SELECT ph.date, auth_user.username AS by, ph.what, p.id AS patchid, p.name, (SELECT max(commitfest_id) FROM commitfest_patchoncommitfest poc WHERE poc.patch_id=p.id) AS cfid FROM commitfest_patchhistory ph INNER JOIN commitfest_patch p ON ph.patch_id=p.id INNER JOIN auth_user on auth_user.id=ph.by_id {0} ORDER BY ph.date DESC LIMIT {1}".format( - where, num - ) + sql = f"SELECT ph.date, auth_user.username AS by, ph.what, p.id AS patchid, p.name, (SELECT max(commitfest_id) FROM commitfest_patchoncommitfest poc WHERE poc.patch_id=p.id) AS cfid FROM commitfest_patchhistory ph INNER JOIN commitfest_patch p ON ph.patch_id=p.id INNER JOIN auth_user on auth_user.id=ph.by_id {where} ORDER BY ph.date DESC LIMIT {num}" curs = connection.cursor() curs.execute(sql) @@ -284,9 +280,7 @@ def activity(request, cfid=None, rss=None): "commitfest": cf, "activity": activity, "title": cf and "Commitfest activity" or "Global Commitfest activity", - "rss_alternate": cf - and "/{0}/activity.rss/".format(cf.id) - or "/activity.rss/", + "rss_alternate": cf and f"/{cf.id}/activity.rss/" or "/activity.rss/", "rss_alternate_title": "PostgreSQL Commitfest Activity Log", "breadcrumbs": cf and [ @@ -317,15 +311,13 @@ def redir(request, what, end): if len(cfs) == 0: messages.warning( - request, "No {0} commitfests exist, redirecting to startpage.".format(what) + request, f"No {what} commitfests exist, redirecting to startpage." ) return HttpResponseRedirect("/") if len(cfs) != 1: messages.warning( request, - "More than one {0} commitfest exists, redirecting to startpage instead.".format( - what - ), + f"More than one {what} commitfest exists, redirecting to startpage instead.", ) return HttpResponseRedirect("/") From 60487629d3de75d7e65da9e4c27538c647556714 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:34 +0100 Subject: [PATCH 11/31] Use f-strings instead of `%` formatting f-strings are easier to read because the values are next to where they end up in the string (ruff UP031). Most of this was converted automatically by ruff, but a few strings that index into a dict, which ruff would only convert to `str.format()`, were converted by hand. --- pgcommitfest/commitfest/ajax.py | 24 ++++--- pgcommitfest/commitfest/forms.py | 9 +-- pgcommitfest/commitfest/models.py | 25 +++----- pgcommitfest/commitfest/reports.py | 2 +- .../commitfest/tests/test_get_threads.py | 7 +-- pgcommitfest/commitfest/views.py | 63 +++++++++---------- pgcommitfest/commitfest/widgets.py | 3 +- pgcommitfest/mailqueue/models.py | 2 +- pgcommitfest/mailqueue/util.py | 4 +- pgcommitfest/userprofile/util.py | 6 +- tools/commitfest/check_patches_in_archives.py | 10 +-- tools/commitfest/update_archive_threads.py | 2 +- 12 files changed, 62 insertions(+), 95 deletions(-) diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index 7f0b820a..d7fa0854 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -69,13 +69,13 @@ def _archivesAPI(suburl, params=None): if resp.status_code != 200: if resp.status_code == 404: raise Http404() - raise Exception("JSON call failed: %s" % resp.status_code) + raise Exception(f"JSON call failed: {resp.status_code}") return resp.json() except Http404: raise except Exception as e: - raise Http503("Failed to communicate with archives backend: %s" % e) + raise Http503(f"Failed to communicate with archives backend: {e}") def getThreads(request): @@ -93,7 +93,7 @@ def getThreads(request): messages = {} for listname in ("pgsql-hackers", "pgsql-bugs"): - for m in _archivesAPI("/list/%s/latest.json" % listname, params): + for m in _archivesAPI(f"/list/{listname}/latest.json", params): messages.setdefault(m["msgid"], m) r = sorted(messages.values(), key=lambda x: x["date"], reverse=True) @@ -108,13 +108,13 @@ def getMessages(request): thread = MailThread.objects.get(pk=threadid) # Always make a call over to the archives api - r = _archivesAPI("/message-id.json/%s" % thread.messageid) + r = _archivesAPI(f"/message-id.json/{thread.messageid}") return sorted(r, key=lambda x: x["date"], reverse=True) def refresh_single_thread(thread): r = sorted( - _archivesAPI("/message-id.json/%s" % thread.messageid), key=lambda x: x["date"] + _archivesAPI(f"/message-id.json/{thread.messageid}"), key=lambda x: x["date"] ) if thread.latestmsgid != r[-1]["msgid"]: # There is now a newer mail in the thread! @@ -146,7 +146,7 @@ def annotateMessage(request): # Get the subject, author and date from the archives # We only have an API call to get the whole thread right now, so # do that, and then find our entry in it. - r = _archivesAPI("/message-id.json/%s" % thread.messageid) + r = _archivesAPI(f"/message-id.json/{thread.messageid}") for m in r: if m["msgid"] == msgid: annotation = MailThreadAnnotation( @@ -164,8 +164,7 @@ def annotateMessage(request): PatchHistory( patch=p, by=request.user, - what='Added annotation "%s" to %s' - % (textwrap.shorten(msg, 100), msgid), + what=f'Added annotation "{textwrap.shorten(msg, 100)}" to {msgid}', ).save_and_notify() p.set_modified() p.save() @@ -182,8 +181,7 @@ def deleteAnnotation(request): PatchHistory( patch=p, by=request.user, - what='Deleted annotation "%s" from %s' - % (annotation.annotationtext, annotation.msgid), + what=f'Deleted annotation "{annotation.annotationtext}" from {annotation.msgid}', ).save_and_notify() p.set_modified() p.save() @@ -224,7 +222,7 @@ def attachThread(request): def doAttachThread(cf, patch, msgid, user): # Note! Must be called in an open transaction! - r = sorted(_archivesAPI("/message-id.json/%s" % msgid), key=lambda x: x["date"]) + r = sorted(_archivesAPI(f"/message-id.json/{msgid}"), key=lambda x: x["date"]) # We have the full thread metadata - using the first and last entry, # construct a new mailthread in our own model. # First, though, check if it's already there. @@ -262,7 +260,7 @@ def doAttachThread(cf, patch, msgid, user): parse_and_add_attachments(r, m) PatchHistory( - patch=patch, by=user, what="Attached mail thread %s" % r[0]["msgid"] + patch=patch, by=user, what=f"Attached mail thread {r[0]['msgid']}" ).save_and_notify() patch.update_lastmail() patch.set_modified() @@ -281,7 +279,7 @@ def detachThread(request): PatchHistory( patch=patch, by=request.user, - what="Detached mail thread %s" % request.POST["msg"], + what=f"Detached mail thread {request.POST['msg']}", ).save_and_notify() patch.update_lastmail() patch.set_modified() diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index f12ce12a..261565ab 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -108,12 +108,7 @@ def __init__(self, *args, **kwargs): self.fields["authors"].help_text = "Enter part of name to see list" self.fields["reviewers"].help_text = "Enter part of name to see list" self.fields["committer"].label_from_instance = lambda x: ( - "%s %s (%s)" - % ( - x.user.first_name, - x.user.last_name, - x.user.username, - ) + f"{x.user.first_name} {x.user.last_name} ({x.user.username})" ) self.fields["authors"].widget.attrs["class"] = "add-user-picker" @@ -206,7 +201,7 @@ def clean(self): def clean_threadmsgid(self): try: - _archivesAPI("/message-id.json/%s" % self.cleaned_data["threadmsgid"]) + _archivesAPI(f"/message-id.json/{self.cleaned_data['threadmsgid']}") except Http404: raise ValidationError("Message not found in archives") except Exception: diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 3ca8080e..00bdb0c7 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -30,11 +30,7 @@ def __str__(self): @property def fullname(self): - return "%s %s (%s)" % ( - self.user.first_name, - self.user.last_name, - self.user.username, - ) + return f"{self.user.first_name} {self.user.last_name} ({self.user.username})" class Meta: ordering = ("user__last_name", "user__first_name") @@ -83,7 +79,7 @@ def dev_cycle(self) -> int: @property def title(self): - return "Commitfest %s" % self.name + return f"Commitfest {self.name}" @property def is_closed(self): @@ -558,17 +554,14 @@ def can_remove_all_reviewers(self): @property def authors_string(self): return ", ".join( - [ - "%s %s (%s)" % (a.first_name, a.last_name, a.username) - for a in self.authors.all() - ] + [f"{a.first_name} {a.last_name} ({a.username})" for a in self.authors.all()] ) @property def reviewers_string(self): return ", ".join( [ - "%s %s (%s)" % (a.first_name, a.last_name, a.username) + f"{a.first_name} {a.last_name} ({a.username})" for a in self.reviewers.all() ] ) @@ -791,10 +784,10 @@ def by_string(self): if self.by_cfbot: return "CFbot" - return "%s %s (%s)" % (self.by.first_name, self.by.last_name, self.by.username) + return f"{self.by.first_name} {self.by.last_name} ({self.by.username})" def __str__(self): - return "%s - %s" % (self.patch.name, self.date) + return f"{self.patch.name} - {self.date}" class Meta: ordering = ("-date",) @@ -952,11 +945,7 @@ class MailThreadAnnotation(models.Model): @property def user_string(self): - return "%s %s (%s)" % ( - self.user.first_name, - self.user.last_name, - self.user.username, - ) + return f"{self.user.first_name} {self.user.last_name} ({self.user.username})" class Meta: ordering = ("date",) diff --git a/pgcommitfest/commitfest/reports.py b/pgcommitfest/commitfest/reports.py index e4191e16..fe176b33 100644 --- a/pgcommitfest/commitfest/reports.py +++ b/pgcommitfest/commitfest/reports.py @@ -50,7 +50,7 @@ def authorstats(request, cfid): "report": cursor.fetchall(), "title": "Author stats", "breadcrumbs": [ - {"title": cf.title, "href": "/%s/" % cf.pk}, + {"title": cf.title, "href": f"/{cf.pk}/"}, ], }, ) diff --git a/pgcommitfest/commitfest/tests/test_get_threads.py b/pgcommitfest/commitfest/tests/test_get_threads.py index 6ca209d6..eb3b448a 100644 --- a/pgcommitfest/commitfest/tests/test_get_threads.py +++ b/pgcommitfest/commitfest/tests/test_get_threads.py @@ -11,8 +11,7 @@ def _msg(msgid, date): def test_get_threads_searches_hackers_and_bugs(): hackers = [ - _msg("h%d" % i, "2026-01-01T%02d:%02d:00" % (i // 60, i % 60)) - for i in range(100) + _msg(f"h{i}", f"2026-01-01T{i // 60:02d}:{i % 60:02d}:00") for i in range(100) ] # b1 is newer than everything on hackers, h99 was sent to both lists bugs = [_msg("b1", "2026-01-02T00:00:00"), hackers[99]] @@ -33,6 +32,4 @@ def fake(suburl, params=None): ] # Merged newest first, h99 deduplicated, and capped at 100 so the oldest # message (h0) is dropped. - assert [m["msgid"] for m in result] == ["b1"] + [ - "h%d" % i for i in range(99, 0, -1) - ] + assert [m["msgid"] for m in result] == ["b1"] + [f"h{i}" for i in range(99, 0, -1)] diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index 0ed02b3a..d27a2e07 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -284,7 +284,7 @@ def activity(request, cfid=None, rss=None): "rss_alternate_title": "PostgreSQL Commitfest Activity Log", "breadcrumbs": cf and [ - {"title": cf.title, "href": "/%s/" % cf.pk}, + {"title": cf.title, "href": f"/{cf.pk}/"}, ] or None, }, @@ -384,7 +384,7 @@ def patchlist(request, cf, personalized=False): has_filter=False, sortkey=0, redirect=HttpResponseRedirect( - "%s?next=%s" % (settings.LOGIN_URL, request.path) + f"{settings.LOGIN_URL}?next={request.path}" ), ) whereclauses.append( @@ -414,7 +414,7 @@ def patchlist(request, cf, personalized=False): has_filter=False, sortkey=0, redirect=HttpResponseRedirect( - "%s?next=%s" % (settings.LOGIN_URL, request.path) + f"{settings.LOGIN_URL}?next={request.path}" ), ) whereclauses.append( @@ -971,11 +971,11 @@ def patch(request, patchid): "attachnow": "attachthreadnow" in request.GET, "title": patch.name, "description": ( - "PostgreSQL patch by %s in the %s commitfest." - % (patch.authors_string or "unknown author", cf.title) + f"PostgreSQL patch by {patch.authors_string or 'unknown author'} " + f"in the {cf.title} commitfest." ), "breadcrumbs": [ - {"title": cf.title, "href": "/%s/" % cf.pk}, + {"title": cf.title, "href": f"/{cf.pk}/"}, ], "userprofile": getattr(request.user, "userprofile", UserProfile()), "cfs": CommitFest.relevant_commitfests(), @@ -1011,7 +1011,7 @@ def patchform(request, patchid): PatchHistory( patch=patch, by=request.user, - what="Changed %s to %s" % (field, value), + what=f"Changed {field} to {value}", ).save_and_notify( prevcommitter=prevcommitter, prevreviewers=prevreviewers, @@ -1019,7 +1019,7 @@ def patchform(request, patchid): ) r.set_modified() r.save() - return HttpResponseRedirect("../../%s/" % r.pk) + return HttpResponseRedirect(f"../../{r.pk}/") # Else fall through and render the page again else: form = PatchForm(instance=patch) @@ -1034,8 +1034,8 @@ def patchform(request, patchid): "title": "Edit patch", "tags_data": get_tags_data(), "breadcrumbs": [ - {"title": cf.title, "href": "/%s/" % cf.pk}, - {"title": "View patch", "href": "/%s/%s/" % (cf.pk, patch.pk)}, + {"title": cf.title, "href": f"/{cf.pk}/"}, + {"title": "View patch", "href": f"/{cf.pk}/{patch.pk}/"}, ], }, ) @@ -1084,7 +1084,7 @@ def newpatch(request, cfid): raise ThreadAttachFailed( "An error occurred looking up the thread in the archives." ) from e - return HttpResponseRedirect("/patch/%s/" % (patch.id,)) + return HttpResponseRedirect(f"/patch/{patch.id}/") except ThreadAttachFailed as e: form._errors["threadmsgid"] = form.error_class((str(e),)) else: @@ -1098,7 +1098,7 @@ def newpatch(request, cfid): "title": "New patch", "tags_data": get_tags_data(), "breadcrumbs": [ - {"title": f"{cf.title} ({cf.periodstring})", "href": "/%s/" % cf.pk}, + {"title": f"{cf.title} ({cf.periodstring})", "href": f"/{cf.pk}/"}, ], "savebutton": "Create patch", "threadbrowse": True, @@ -1129,10 +1129,10 @@ def status(request, patchid, status): if new_status != poc.status: poc.set_status(new_status) PatchHistory( - patch=poc.patch, by=request.user, what="New status: %s" % poc.statusstring + patch=poc.patch, by=request.user, what=f"New status: {poc.statusstring}" ).save_and_notify() - return HttpResponseRedirect("/patch/%s/" % (poc.patch.id)) + return HttpResponseRedirect(f"/patch/{poc.patch.id}/") @require_POST @@ -1205,7 +1205,7 @@ def close(request, patchid, status): PatchHistory( patch=poc.patch, by=request.user, - what="Changed committer to %s" % committer, + what=f"Changed committer to {committer}", ).save_and_notify(prevcommitter=prevcommitter) poc.status = PatchOnCommitFest.STATUS_COMMITTED @@ -1221,8 +1221,7 @@ def close(request, patchid, status): PatchHistory( patch=poc.patch, by=request.user, - what="Closed in commitfest %s with status: %s" - % (poc.commitfest, poc.statusstring), + what=f"Closed in commitfest {poc.commitfest} with status: {poc.statusstring}", ).save_and_notify() return HttpResponseRedirect(f"/patch/{patchid}") @@ -1276,7 +1275,7 @@ def reviewer(request, patchid, status): PatchHistory( patch=patch, by=request.user, - what="Added %s as reviewer" % request.user.username, + what=f"Added {request.user.username} as reviewer", ).save_and_notify() elif status == "remove" and is_reviewer: patch.reviewers.remove(request.user) @@ -1284,7 +1283,7 @@ def reviewer(request, patchid, status): PatchHistory( patch=patch, by=request.user, - what="Removed %s from reviewers" % request.user.username, + what=f"Removed {request.user.username} from reviewers", ).save_and_notify() return HttpResponseRedirect("../../") @@ -1335,7 +1334,7 @@ def committer(request, patchid, status): PatchHistory( patch=patch, by=request.user, - what="Added %s as committer" % request.user.username, + what=f"Added {request.user.username} as committer", ).save_and_notify(prevcommitter=prevcommitter) elif status == "remove" and is_committer: patch.committer = None @@ -1343,7 +1342,7 @@ def committer(request, patchid, status): PatchHistory( patch=patch, by=request.user, - what="Removed %s from committers" % request.user.username, + what=f"Removed {request.user.username} from committers", ).save_and_notify(prevcommitter=prevcommitter) patch.save() return HttpResponseRedirect("../../") @@ -1406,13 +1405,10 @@ def send_email(request, cfid): form.cleaned_data["subject"], form.cleaned_data["body"], request.user.username, - sendername="%s %s via PostgreSQL Commitfest" - % (request.user.first_name, request.user.last_name), + sendername=f"{request.user.first_name} {request.user.last_name} via PostgreSQL Commitfest", replyto=uw.encoded_email_header, ) - messages.add_message( - request, messages.INFO, "Sent email to %s" % r.email - ) + messages.add_message(request, messages.INFO, f"Sent email to {r.email}") return HttpResponseRedirect("..") else: authoridstring = request.GET.get("authors", None) @@ -1447,26 +1443,25 @@ def send_email(request, cfid): messages.add_message( request, messages.INFO, - "Email will be sent from %s, with replies going to: %s" - % (settings.NOTIFICATION_FROM, UserWrapper(request.user).email), + f"Email will be sent from {settings.NOTIFICATION_FROM}, with replies going to: {UserWrapper(request.user).email}", ) def _user_and_mail(u): - return "%s %s (%s)" % (u.first_name, u.last_name, u.email) + return f"{u.first_name} {u.last_name} ({u.email})" if len(authors): messages.add_message( request, messages.INFO, - "The email will be sent to the following authors: %s" - % ", ".join([_user_and_mail(u) for u in authors]), + "The email will be sent to the following authors: " + + ", ".join([_user_and_mail(u) for u in authors]), ) if len(reviewers): messages.add_message( request, messages.INFO, - "The email will be sent to the following reviewers: %s" - % ", ".join([_user_and_mail(u) for u in reviewers]), + "The email will be sent to the following reviewers: " + + ", ".join([_user_and_mail(u) for u in reviewers]), ) return render( @@ -1477,7 +1472,7 @@ def _user_and_mail(u): "form": form, "title": "Send email", "breadcrumbs": [ - {"title": cf.title, "href": "/%s/" % cf.pk}, + {"title": cf.title, "href": f"/{cf.pk}/"}, ], "savebutton": "Send email", }, diff --git a/pgcommitfest/commitfest/widgets.py b/pgcommitfest/commitfest/widgets.py index 65cfeba2..350a7107 100644 --- a/pgcommitfest/commitfest/widgets.py +++ b/pgcommitfest/commitfest/widgets.py @@ -8,7 +8,6 @@ def render(self, name, value, attrs=None, renderer=None): html = super().render(name, value, attrs) html = ( html - + ' ' - % name + + f' ' ) return mark_safe(html) diff --git a/pgcommitfest/mailqueue/models.py b/pgcommitfest/mailqueue/models.py index 6b002e7e..7c4d3a02 100644 --- a/pgcommitfest/mailqueue/models.py +++ b/pgcommitfest/mailqueue/models.py @@ -9,4 +9,4 @@ class QueuedMail(models.Model): fullmsg = models.TextField(null=False, blank=False) def __str__(self): - return "%s: %s -> %s" % (self.pk, self.sender, self.receiver) + return f"{self.pk}: {self.sender} -> {self.receiver}" diff --git a/pgcommitfest/mailqueue/util.py b/pgcommitfest/mailqueue/util.py index 48fbbef0..54f578f0 100644 --- a/pgcommitfest/mailqueue/util.py +++ b/pgcommitfest/mailqueue/util.py @@ -43,9 +43,7 @@ def send_simple_mail( main, sub = contenttype.split("/") part = MIMENonMultipart(main, sub) part.set_payload(content) - part.add_header( - "Content-Disposition", 'attachment; filename="%s"' % filename - ) + part.add_header("Content-Disposition", f'attachment; filename="{filename}"') encoders.encode_base64(part) msg.attach(part) diff --git a/pgcommitfest/userprofile/util.py b/pgcommitfest/userprofile/util.py index 2b94fcdd..19faa5b7 100644 --- a/pgcommitfest/userprofile/util.py +++ b/pgcommitfest/userprofile/util.py @@ -23,11 +23,7 @@ def email(self): def encoded_email_header(self): return formataddr( ( - str( - Header( - "%s %s" % (self.user.first_name, self.user.last_name), "utf-8" - ) - ), + str(Header(f"{self.user.first_name} {self.user.last_name}", "utf-8")), self.email, ) ) diff --git a/tools/commitfest/check_patches_in_archives.py b/tools/commitfest/check_patches_in_archives.py index e68150ce..2cb53791 100755 --- a/tools/commitfest/check_patches_in_archives.py +++ b/tools/commitfest/check_patches_in_archives.py @@ -47,8 +47,8 @@ # should generally not be a problem because it's not going to be # downloading a lot... for a in MailThreadAttachment.objects.filter(ispatch__isnull=True): - url = "/message-id/attachment/%s/attach" % a.attachmentid - logging.debug("Checking attachment %s" % a.attachmentid) + url = f"/message-id/attachment/{a.attachmentid}/attach" + logging.debug(f"Checking attachment {a.attachmentid}") resp = requests.get( "http{0}://{1}:{2}{3}".format( @@ -64,12 +64,12 @@ ) if resp.status_code != 200: - logging.error("Failed to get %s: %s" % (url, resp.status_code)) + logging.error(f"Failed to get {url}: {resp.status_code}") continue # Attempt to identify the file using magic information mtype = mag.buffer(resp.content) - logging.debug("Detected MIME type is %s" % mtype) + logging.debug(f"Detected MIME type is {mtype}") # We don't support gzipped or tar:ed patches or anything like # that at this point - just plain patches. @@ -77,7 +77,7 @@ a.ispatch = True else: a.ispatch = False - logging.info("Attachment %s is patch: %s" % (a.id, a.ispatch)) + logging.info(f"Attachment {a.id} is patch: {a.ispatch}") a.save() connection.close() diff --git a/tools/commitfest/update_archive_threads.py b/tools/commitfest/update_archive_threads.py index b90348b5..80674e5e 100755 --- a/tools/commitfest/update_archive_threads.py +++ b/tools/commitfest/update_archive_threads.py @@ -36,7 +36,7 @@ for thread in MailThread.objects.filter( patches__commitfests__status__in=(1, 2, 3) ).distinct(): - logging.debug("Checking %s in the archives" % thread.messageid) + logging.debug(f"Checking {thread.messageid} in the archives") refresh_single_thread(thread) connection.close() From 9156e74631c3a857c0ec8c505e6102c1e78042fb Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:53 +0100 Subject: [PATCH 12/31] Remove explicit positional indices from format strings ruff UP030 flags `"{0} {1}".format(a, b)`, because the indices are redundant. These were converted by hand instead of with ruff's fix, because that fix turned `"http{0}://{1}:{2}{3}"` into `"http{}://{}:{}{3}"`, which mixes automatic and manual numbering and raises a `ValueError` at runtime. The archive URLs are now f-strings, and the `WHERE` clause is built with plain concatenation. --- pgcommitfest/commitfest/ajax.py | 8 ++------ pgcommitfest/commitfest/views.py | 2 +- tools/commitfest/check_patches_in_archives.py | 8 ++------ 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index d7fa0854..ebd41ac5 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -54,12 +54,8 @@ def _archivesAPI(suburl, params=None): try: resp = requests.get( - "http{0}://{1}:{2}{3}".format( - settings.ARCHIVES_PORT == 443 and "s" or "", - settings.ARCHIVES_SERVER, - settings.ARCHIVES_PORT, - suburl, - ), + f"{'https' if settings.ARCHIVES_PORT == 443 else 'http'}://" + f"{settings.ARCHIVES_SERVER}:{settings.ARCHIVES_PORT}{suburl}", params=params, headers={ "Host": settings.ARCHIVES_HOST, diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index d27a2e07..a24a48f4 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -593,7 +593,7 @@ def patchlist(request, cf, personalized=False): ) if whereclauses: - where_str = "({0})".format(") AND (".join(whereclauses)) + where_str = "(" + ") AND (".join(whereclauses) + ")" else: where_str = "true" params = { diff --git a/tools/commitfest/check_patches_in_archives.py b/tools/commitfest/check_patches_in_archives.py index 2cb53791..1c24c2e7 100755 --- a/tools/commitfest/check_patches_in_archives.py +++ b/tools/commitfest/check_patches_in_archives.py @@ -51,12 +51,8 @@ logging.debug(f"Checking attachment {a.attachmentid}") resp = requests.get( - "http{0}://{1}:{2}{3}".format( - settings.ARCHIVES_PORT == 443 and "s" or "", - settings.ARCHIVES_SERVER, - settings.ARCHIVES_PORT, - url, - ), + f"{'https' if settings.ARCHIVES_PORT == 443 else 'http'}://" + f"{settings.ARCHIVES_SERVER}:{settings.ARCHIVES_PORT}{url}", headers={ "Host": settings.ARCHIVES_HOST, }, From 4cf65cfdf16c44144027c6b5866df9e560f05ed8 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:53 +0100 Subject: [PATCH 13/31] Use set comprehensions instead of `set([...])` This avoids building an intermediate list (ruff C403). --- pgcommitfest/userprofile/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgcommitfest/userprofile/util.py b/pgcommitfest/userprofile/util.py index 19faa5b7..e9236e7a 100644 --- a/pgcommitfest/userprofile/util.py +++ b/pgcommitfest/userprofile/util.py @@ -40,6 +40,6 @@ def handle_user_data(sender, **kwargs): UserExtraEmail.objects.filter(user=user).exclude(email__in=secondary).delete() # Then add back any of the ones that aren't there - current = set([e.email for e in UserExtraEmail.objects.filter(user=user)]) + current = {e.email for e in UserExtraEmail.objects.filter(user=user)} for e in set(secondary).difference(current): UserExtraEmail(user=user, email=e).save() From 1b7802a1f6418cb7818e759cd1772a578ff70c30 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:53 +0100 Subject: [PATCH 14/31] Use dict comprehensions instead of `dict([...])` This avoids building an intermediate list of tuples (ruff C404). --- pgcommitfest/commitfest/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgcommitfest/commitfest/util.py b/pgcommitfest/commitfest/util.py index 8649520c..84f23aea 100644 --- a/pgcommitfest/commitfest/util.py +++ b/pgcommitfest/commitfest/util.py @@ -16,7 +16,7 @@ def diff(self): manytomanyfieldnames = [f.name for f in self._meta.many_to_many] d1 = self.__initial d2 = self._dict - diffs = dict([(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]) + diffs = {k: (v, d2[k]) for k, v in d1.items() if v != d2[k]} # Foreign key lookups for k, v in diffs.items(): if ( From bb0ba447d741046b51f2629d445de0e5316a8afc Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:53 +0100 Subject: [PATCH 15/31] Use set literals instead of `set([...])` `{a, b}` is shorter and avoids building an intermediate list (ruff C405). --- pgcommitfest/commitfest/migrations/0001_initial.py | 4 ++-- pgcommitfest/userprofile/migrations/0001_initial.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pgcommitfest/commitfest/migrations/0001_initial.py b/pgcommitfest/commitfest/migrations/0001_initial.py index 4dd7f8c7..851da57a 100644 --- a/pgcommitfest/commitfest/migrations/0001_initial.py +++ b/pgcommitfest/commitfest/migrations/0001_initial.py @@ -315,10 +315,10 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name="patchoncommitfest", - unique_together=set([("patch", "commitfest")]), + unique_together={("patch", "commitfest")}, ), migrations.AlterUniqueTogether( name="mailthreadattachment", - unique_together=set([("mailthread", "messageid")]), + unique_together={("mailthread", "messageid")}, ), ] diff --git a/pgcommitfest/userprofile/migrations/0001_initial.py b/pgcommitfest/userprofile/migrations/0001_initial.py index 5848f99b..4d9325bb 100644 --- a/pgcommitfest/userprofile/migrations/0001_initial.py +++ b/pgcommitfest/userprofile/migrations/0001_initial.py @@ -67,6 +67,6 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name="userextraemail", - unique_together=set([("user", "email")]), + unique_together={("user", "email")}, ), ] From f97b741e43255a1969a763d22f7cec9089448dfd Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:53 +0100 Subject: [PATCH 16/31] Use a literal instead of `dict()` A `{...}` literal is shorter and faster than calling `dict()` with keyword arguments (ruff C408). --- pgcommitfest/commitfest/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 00bdb0c7..59936959 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -628,11 +628,11 @@ def move( new_poc, _ = PatchOnCommitFest.objects.update_or_create( patch=current_poc.patch, commitfest=to_cf, - defaults=dict( - status=old_status, - enterdate=now, - leavedate=now if was_closed else None, - ), + defaults={ + "status": old_status, + "enterdate": now, + "leavedate": now if was_closed else None, + }, ) new_poc.save() self.set_modified() From 25f68e9a565e8b5d2e8820608c1508afa438744c Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:54 +0100 Subject: [PATCH 17/31] Use `next(...)` instead of `[...][0]` This stops at the first match instead of building the whole list (ruff RUF015). --- pgcommitfest/commitfest/models.py | 4 ++-- pgcommitfest/commitfest/templatetags/commitfest.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 59936959..47917c12 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -60,7 +60,7 @@ class CommitFest(models.Model): @property def statusstring(self): - return [v for k, v in self._STATUS_CHOICES if k == self.status][0] + return next(v for k, v in self._STATUS_CHOICES if k == self.status) @property def periodstring(self): @@ -730,7 +730,7 @@ def needs_committer(self): @property def statusstring(self): - return [v for k, v in self._STATUS_CHOICES if k == self.status][0] + return next(v for k, v in self._STATUS_CHOICES if k == self.status) def to_json(self): return { diff --git a/pgcommitfest/commitfest/templatetags/commitfest.py b/pgcommitfest/commitfest/templatetags/commitfest.py index 5c6d22e9..58a22bfe 100644 --- a/pgcommitfest/commitfest/templatetags/commitfest.py +++ b/pgcommitfest/commitfest/templatetags/commitfest.py @@ -18,28 +18,28 @@ @stringfilter def commitfeststatusstring(value): i = int(value) - return [v for k, v in CommitFest._STATUS_CHOICES if k == i][0] + return next(v for k, v in CommitFest._STATUS_CHOICES if k == i) @register.filter(name="commitfeststatuslabel") @stringfilter def commitfeststatuslabel(value): i = int(value) - return [v for k, v in CommitFest._STATUS_LABELS if k == i][0] + return next(v for k, v in CommitFest._STATUS_LABELS if k == i) @register.filter(name="patchstatusstring") @stringfilter def patchstatusstring(value): i = int(value) - return [v for k, v in PatchOnCommitFest._STATUS_CHOICES if k == i][0] + return next(v for k, v in PatchOnCommitFest._STATUS_CHOICES if k == i) @register.filter(name="patchstatuslabel") @stringfilter def patchstatuslabel(value): i = int(value) - return [v for k, v in PatchOnCommitFest._STATUS_LABELS if k == i][0] + return next(v for k, v in PatchOnCommitFest._STATUS_LABELS if k == i) @register.filter(name="tagname") From e3162407e35f493336f0c640e64ea41b3fea0d1a Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:54 +0100 Subject: [PATCH 18/31] Prefix unused unpacked variables with an underscore This makes it explicit that the value is intentionally ignored (ruff RUF059). --- pgcommitfest/userprofile/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgcommitfest/userprofile/views.py b/pgcommitfest/userprofile/views.py index 9f2e7e60..2075aa03 100644 --- a/pgcommitfest/userprofile/views.py +++ b/pgcommitfest/userprofile/views.py @@ -11,7 +11,7 @@ @login_required @transaction.atomic def userprofile(request): - (profile, created) = UserProfile.objects.get_or_create(user=request.user) + (profile, _created) = UserProfile.objects.get_or_create(user=request.user) if request.method == "POST": form = UserProfileForm(request.user, request.POST, instance=profile) From 0a8227287c86ae676c6bc937d0d176d7bfcbbfac Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:54 +0100 Subject: [PATCH 19/31] Return boolean conditions directly Replaces `if cond: return True; return False` with `return cond` (ruff SIM103). --- pgcommitfest/commitfest/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 47917c12..d6eb6416 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -245,10 +245,7 @@ def _are_relevant_commitfests_up_to_date(cfs, current_date): if cfs["open"].startdate <= current_date: return False - if not cfs["draft"] or cfs["draft"].enddate < current_date: - return False - - return True + return bool(cfs["draft"]) and cfs["draft"].enddate >= current_date @classmethod def _refresh_relevant_commitfests(cls, for_update): From a4b49196b3e9911e5098098537786c94844dc1d1 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:32:54 +0100 Subject: [PATCH 20/31] Avoid a mutable default argument A mutable default like `[]` is shared between all calls of the function, so changing it in one call affects later calls (ruff B006). --- pgcommitfest/mailqueue/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgcommitfest/mailqueue/util.py b/pgcommitfest/mailqueue/util.py index 54f578f0..72dbebb2 100644 --- a/pgcommitfest/mailqueue/util.py +++ b/pgcommitfest/mailqueue/util.py @@ -62,9 +62,11 @@ def send_template_mail( receiver, subject, templatename, - templateattr={}, + templateattr=None, usergenerated=False, ): + if templateattr is None: + templateattr = {} send_simple_mail( sender, receiver, From 2da1f1d8efe2e2f3b39d61b667bf74531ca33842 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:34:01 +0100 Subject: [PATCH 21/31] Annotate mutable class attributes with `ClassVar` ruff RUF012 flags mutable class attributes, because a list or dict defined on a class is shared by all instances, and mutating it through one instance changes it for all of them. Annotating them as `ClassVar` makes explicit that they are meant to be class level settings, like Django's `Meta.ordering` or a form's `field_order`. I annotated them instead of converting the lists to tuples, because changing `choices` from a list to a tuple could make Django think the field changed and generate a new migration. Migrations are excluded from this rule, because `makemigrations` generates `dependencies` and `operations` as plain class attributes, and annotating every new migration by hand would be busywork. --- pgcommitfest/commitfest/admin.py | 4 +++- pgcommitfest/commitfest/forms.py | 8 +++++--- pgcommitfest/commitfest/models.py | 15 ++++++++------- pyproject.toml | 5 +++++ 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/pgcommitfest/commitfest/admin.py b/pgcommitfest/commitfest/admin.py index cb9ac9f6..caaf2d39 100644 --- a/pgcommitfest/commitfest/admin.py +++ b/pgcommitfest/commitfest/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin from django.forms import widgets +from typing import ClassVar + from .models import ( CfbotBranch, CfbotTask, @@ -63,7 +65,7 @@ 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 = { + formfield_overrides: ClassVar[dict] = { ColorField: {"widget": ColorInput}, } diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index 261565ab..92e93eb2 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -4,13 +4,15 @@ from django.forms.widgets import HiddenInput from django.http import Http404 +from typing import ClassVar + from .ajax import _archivesAPI from .models import Patch, PatchOnCommitFest, Tag, TargetVersion from .widgets import ThreadPickWidget class CommitFestFilterForm(forms.Form): - selectize_fields = { + selectize_fields: ClassVar[dict] = { "author": "/lookups/user", "reviewer": "/lookups/user", "tag": None, @@ -85,7 +87,7 @@ def __init__(self, data, commitfest=None, *args, **kwargs): class PatchForm(forms.ModelForm): - selectize_fields = { + selectize_fields: ClassVar[dict] = { "authors": "/lookups/user", "reviewers": "/lookups/user", "tags": None, @@ -157,7 +159,7 @@ def __init__(self, *args, **kwargs): class NewPatchForm(PatchForm): # Put threadmsgid first - field_order = ["threadmsgid"] + field_order: ClassVar[list] = ["threadmsgid"] threadmsgid = forms.CharField( max_length=200, diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index d6eb6416..6bb02844 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404 from datetime import UTC, datetime, timedelta +from typing import ClassVar from pgcommitfest.mailqueue.util import send_template_mail from pgcommitfest.userprofile.models import UserProfile @@ -435,7 +436,7 @@ class TargetVersion(models.Model): version = models.CharField(max_length=8, blank=False, null=False, unique=True) class Meta: - ordering = [ + ordering: ClassVar[list] = [ "-version", ] @@ -527,7 +528,7 @@ class Patch(models.Model, DiffableModel): # that's attached to this message. lastmail = models.DateTimeField(blank=True, null=True) - map_manytomany_for_diff = { + map_manytomany_for_diff: ClassVar[dict] = { "authors": "authors_string", "reviewers": "reviewers_string", } @@ -692,7 +693,7 @@ class PatchOnCommitFest(models.Model): (STATUS_RETURNED, "danger"), (STATUS_WITHDRAWN, "danger"), ) - OPEN_STATUSES = [STATUS_REVIEW, STATUS_AUTHOR, STATUS_COMMITTER] + OPEN_STATUSES: ClassVar[list] = [STATUS_REVIEW, STATUS_AUTHOR, STATUS_COMMITTER] @classmethod def OPEN_STATUS_CHOICES(cls): @@ -788,7 +789,7 @@ def __str__(self): class Meta: ordering = ("-date",) - constraints = [ + constraints: ClassVar[list] = [ models.CheckConstraint( check=(models.Q(by_cfbot=True) & models.Q(by__isnull=True)) | (models.Q(by_cfbot=False) & models.Q(by__isnull=False)), @@ -962,7 +963,7 @@ class PendingNotification(models.Model): class CfbotBranch(models.Model): - STATUS_CHOICES = [ + STATUS_CHOICES: ClassVar[list] = [ ("testing", "Testing"), ("finished", "Finished"), ("failed", "Failed"), @@ -1005,7 +1006,7 @@ def save(self, *args, **kwargs): class CfbotTask(models.Model): - STATUS_CHOICES = [ + STATUS_CHOICES: ClassVar[list] = [ ("CREATED", "Created"), ("PAUSED", "Paused"), ("NEEDS_APPROVAL", "Needs Approval"), @@ -1042,7 +1043,7 @@ class CfbotTask(models.Model): modified = models.DateTimeField(auto_now=True) class Meta: - constraints = [ + constraints: ClassVar[list] = [ models.UniqueConstraint( fields=["branch_id", "position"], name="commitfest_cfbottask_branch_position_unique", diff --git a/pyproject.toml b/pyproject.toml index b1167050..1e25811c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,11 @@ extend-exclude = ['pgcommitfest/auth.py'] [tool.ruff.lint] extend-select = ["I"] +[tool.ruff.lint.per-file-ignores] +# makemigrations generates plain class attributes, and annotating each new +# migration by hand would be busywork. +"**/migrations/*.py" = ["RUF012"] + [tool.ruff.lint.isort] section-order = [ "future", From 304bde4dc99a9cea4e349437e1565c2cdc8cd609 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:34:19 +0100 Subject: [PATCH 22/31] Use a module logger instead of the root logger in cron scripts ruff LOG015 flags calls like `logging.debug()`, which log through the root logger. A logger named after the module makes it clear where a log line came from. The output is unchanged, because the named logger propagates to the root logger that `logging.basicConfig()` configures. --- tools/commitfest/check_patches_in_archives.py | 14 ++++++++------ tools/commitfest/update_archive_threads.py | 8 +++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tools/commitfest/check_patches_in_archives.py b/tools/commitfest/check_patches_in_archives.py index 1c24c2e7..e9fd8bb9 100755 --- a/tools/commitfest/check_patches_in_archives.py +++ b/tools/commitfest/check_patches_in_archives.py @@ -26,6 +26,8 @@ from pgcommitfest.commitfest.models import MailThreadAttachment # noqa: E402 +logger = logging.getLogger(__name__) + if __name__ == "__main__": debug = "--debug" in sys.argv @@ -39,7 +41,7 @@ mag = magic.open(magic.MIME) mag.load() - logging.debug("Updating attachment metadata from archives") + logger.debug("Updating attachment metadata from archives") # Try to fetch/scan all attachments that haven't already been scanned. # If they have already been scanned, we don't bother. @@ -48,7 +50,7 @@ # downloading a lot... for a in MailThreadAttachment.objects.filter(ispatch__isnull=True): url = f"/message-id/attachment/{a.attachmentid}/attach" - logging.debug(f"Checking attachment {a.attachmentid}") + logger.debug(f"Checking attachment {a.attachmentid}") resp = requests.get( f"{'https' if settings.ARCHIVES_PORT == 443 else 'http'}://" @@ -60,12 +62,12 @@ ) if resp.status_code != 200: - logging.error(f"Failed to get {url}: {resp.status_code}") + logger.error(f"Failed to get {url}: {resp.status_code}") continue # Attempt to identify the file using magic information mtype = mag.buffer(resp.content) - logging.debug(f"Detected MIME type is {mtype}") + logger.debug(f"Detected MIME type is {mtype}") # We don't support gzipped or tar:ed patches or anything like # that at this point - just plain patches. @@ -73,8 +75,8 @@ a.ispatch = True else: a.ispatch = False - logging.info(f"Attachment {a.id} is patch: {a.ispatch}") + logger.info(f"Attachment {a.id} is patch: {a.ispatch}") a.save() connection.close() - logging.debug("Done.") + logger.debug("Done.") diff --git a/tools/commitfest/update_archive_threads.py b/tools/commitfest/update_archive_threads.py index 80674e5e..2a70f00c 100755 --- a/tools/commitfest/update_archive_threads.py +++ b/tools/commitfest/update_archive_threads.py @@ -22,6 +22,8 @@ from pgcommitfest.commitfest.ajax import refresh_single_thread # noqa: E402 from pgcommitfest.commitfest.models import MailThread # noqa: E402 +logger = logging.getLogger(__name__) + if __name__ == "__main__": debug = "--debug" in sys.argv @@ -32,12 +34,12 @@ stream=sys.stdout, ) - logging.debug("Checking for updated mail threads in the archives") + logger.debug("Checking for updated mail threads in the archives") for thread in MailThread.objects.filter( patches__commitfests__status__in=(1, 2, 3) ).distinct(): - logging.debug(f"Checking {thread.messageid} in the archives") + logger.debug(f"Checking {thread.messageid} in the archives") refresh_single_thread(thread) connection.close() - logging.debug("Done.") + logger.debug("Done.") From 3754494aa3286faca3fede785b4ab7841403848f Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:34:59 +0100 Subject: [PATCH 23/31] Only catch the expected errors from the archives API ruff BLE001 flags `except Exception`, because it also hides programming errors such as a `TypeError` in our own code. `_archivesAPI()` now only turns `requests` errors (connection errors, timeouts and invalid JSON) into `Http503`, and raises `Http503` directly for an unexpected status code instead of a bare `Exception` (which ruff TRY002 also flags). `NewPatchForm` then only needs to catch `Http503`. Any other error now shows up as a regular server error, instead of a "Error in API call to validate thread" message that hides what went wrong. --- pgcommitfest/commitfest/ajax.py | 17 ++++++++++------- pgcommitfest/commitfest/forms.py | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index ebd41ac5..09ddf62c 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -62,16 +62,19 @@ def _archivesAPI(suburl, params=None): }, timeout=settings.ARCHIVES_TIMEOUT, ) + if resp.status_code == 404: + raise Http404() if resp.status_code != 200: - if resp.status_code == 404: - raise Http404() - raise Exception(f"JSON call failed: {resp.status_code}") + raise Http503( + "Failed to communicate with archives backend: " + f"JSON call failed: {resp.status_code}" + ) return resp.json() - except Http404: - raise - except Exception as e: - raise Http503(f"Failed to communicate with archives backend: {e}") + except requests.RequestException as e: + # This includes invalid JSON, because requests' JSONDecodeError is + # also a RequestException. + raise Http503(f"Failed to communicate with archives backend: {e}") from e def getThreads(request): diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index 92e93eb2..fa62bada 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -6,7 +6,7 @@ from typing import ClassVar -from .ajax import _archivesAPI +from .ajax import Http503, _archivesAPI from .models import Patch, PatchOnCommitFest, Tag, TargetVersion from .widgets import ThreadPickWidget @@ -206,7 +206,7 @@ def clean_threadmsgid(self): _archivesAPI(f"/message-id.json/{self.cleaned_data['threadmsgid']}") except Http404: raise ValidationError("Message not found in archives") - except Exception: + except Http503: raise ValidationError("Error in API call to validate thread") return self.cleaned_data["threadmsgid"] From 6c045c071f8dff93d42c35f705aed6e8eac44c6c Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:35:07 +0100 Subject: [PATCH 24/31] Remove unreachable "next" check from the close view ruff TRY002 flags raising a bare `Exception`. This one was dead code: the URL pattern for `close` only matches `reject`, `withdrawn`, `feedback` and `committed`, so `status` can never be `"next"`. Moving to the next commitfest has been handled by the `transition/` endpoint for a long time. --- pgcommitfest/commitfest/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index a24a48f4..91df1a9e 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -1139,9 +1139,6 @@ def status(request, patchid, status): @login_required @transaction.atomic def close(request, patchid, status): - if status == "next": - raise Exception("Can't happen, use transition/ endpoint") - patch = get_object_or_404(Patch.objects.select_related(), pk=patchid) cf = patch.current_commitfest() From 2bd7863e3a619df46b8d10b0aff47b8f119506e5 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:35:22 +0100 Subject: [PATCH 25/31] Pass `check` explicitly to `subprocess.run` in run_dev.py ruff PLW1510 wants `check` to be explicit, so it is clear whether a failing command should raise. Here it should not: stopping the dev server with Ctrl-C makes uwsgi exit non-zero, and a `CalledProcessError` traceback on top of that would just be noise. --- run_dev.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/run_dev.py b/run_dev.py index 543c4ebf..a428fa04 100755 --- a/run_dev.py +++ b/run_dev.py @@ -31,5 +31,8 @@ "--static-map", f"/media/admin={django_path}/contrib/admin/static/admin", ini_file, - ] + ], + # Stopping the dev server with Ctrl-C makes uwsgi exit non-zero, which is + # not worth a traceback. + check=False, ) From 3af7389a3aa97fcfd1d74df64dc198012810171d Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:35:22 +0100 Subject: [PATCH 26/31] Remove unused `noqa` comments ruff RUF100 flags `noqa` comments for rules that are not enabled. E402 and F403 are not in ruff's rule set, so these comments did nothing. --- pgcommitfest/settings.py | 2 +- pgcommitfest/test_settings.py | 2 +- tools/commitfest/check_patches_in_archives.py | 8 ++++---- tools/commitfest/update_archive_threads.py | 8 ++++---- tools/mail/send_queued_mail.py | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pgcommitfest/settings.py b/pgcommitfest/settings.py index 07e4adaf..f56ec064 100644 --- a/pgcommitfest/settings.py +++ b/pgcommitfest/settings.py @@ -176,6 +176,6 @@ # Load local settings overrides try: - from .local_settings import * # noqa: F403 + from .local_settings import * except ImportError: pass diff --git a/pgcommitfest/test_settings.py b/pgcommitfest/test_settings.py index 7fd6341f..e2d27f4f 100644 --- a/pgcommitfest/test_settings.py +++ b/pgcommitfest/test_settings.py @@ -1,6 +1,6 @@ """Test settings for pgcommitfest.""" -from pgcommitfest.settings import * # noqa: F403 +from pgcommitfest.settings import * # Disable automatic creation of commitfests during tests # Tests should explicitly create the commitfests they need diff --git a/tools/commitfest/check_patches_in_archives.py b/tools/commitfest/check_patches_in_archives.py index e9fd8bb9..73a028bc 100755 --- a/tools/commitfest/check_patches_in_archives.py +++ b/tools/commitfest/check_patches_in_archives.py @@ -17,14 +17,14 @@ # Set up for accessing django sys.path.append(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "../../")) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pgcommitfest.settings") -import django # noqa: E402 +import django django.setup() -from django.conf import settings # noqa: E402 -from django.db import connection # noqa: E402 +from django.conf import settings +from django.db import connection -from pgcommitfest.commitfest.models import MailThreadAttachment # noqa: E402 +from pgcommitfest.commitfest.models import MailThreadAttachment logger = logging.getLogger(__name__) diff --git a/tools/commitfest/update_archive_threads.py b/tools/commitfest/update_archive_threads.py index 2a70f00c..e5063d32 100755 --- a/tools/commitfest/update_archive_threads.py +++ b/tools/commitfest/update_archive_threads.py @@ -13,14 +13,14 @@ # Set up for accessing django sys.path.append(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "../../")) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pgcommitfest.settings") -import django # noqa: E402 +import django django.setup() -from django.db import connection # noqa: E402 +from django.db import connection -from pgcommitfest.commitfest.ajax import refresh_single_thread # noqa: E402 -from pgcommitfest.commitfest.models import MailThread # noqa: E402 +from pgcommitfest.commitfest.ajax import refresh_single_thread +from pgcommitfest.commitfest.models import MailThread logger = logging.getLogger(__name__) diff --git a/tools/mail/send_queued_mail.py b/tools/mail/send_queued_mail.py index 21b31ded..4253a973 100755 --- a/tools/mail/send_queued_mail.py +++ b/tools/mail/send_queued_mail.py @@ -15,13 +15,13 @@ # Set up to run in django environment sys.path.append(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "../../")) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pgcommitfest.settings") -import django # noqa: E402 +import django django.setup() -from django.db import connection, transaction # noqa: E402 +from django.db import connection, transaction -from pgcommitfest.mailqueue.models import QueuedMail # noqa: E402 +from pgcommitfest.mailqueue.models import QueuedMail if __name__ == "__main__": # Grab advisory lock, if available. Lock id is just a random number From 6787a75d5d35dd7caa2c656040d11ce4ea640b8c Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:35:44 +0100 Subject: [PATCH 27/31] Ignore the setuptools build directory `pip install .` leaves a `build/` directory with a copy of the code behind. Because it was not ignored, `make lint` (ruff respects `.gitignore`) also linted that stale copy and reported errors that do not exist in the actual code. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e293a5a4..aef2100b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.idea/ /venv/ *.egg-info +/build/ From 717e22109b5d5a73e9502d5f6444f018f2772837 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:36:41 +0100 Subject: [PATCH 28/31] Fix Biome noUnusedVariables warnings Biome flags top-level functions that are never called from JavaScript, but most functions in `commitfest.js` are called from `onclick` attributes in the templates, which Biome cannot see. Those now have a `biome-ignore` comment saying so, instead of being deleted by its fix. `verify_next()` really was unused, because moving a patch to the next commitfest no longer asks for confirmation, so it is removed. The results of `new bootstrap.Collapse()` and `new bootstrap.Tooltip()` were assigned to variables that were never read, which are now dropped. --- media/commitfest/js/commitfest.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/media/commitfest/js/commitfest.js b/media/commitfest/js/commitfest.js index a353aef2..2c015bfa 100644 --- a/media/commitfest/js/commitfest.js +++ b/media/commitfest/js/commitfest.js @@ -1,23 +1,21 @@ +// biome-ignore lint/correctness/noUnusedVariables: called from templates function verify_reject() { return confirm( 'Are you sure you want to close this patch as Rejected?\n\nThis should only be done when a patch will never be applied - if more work is needed, it should instead be set to "Returned with Feedback" or "Moved to next CF".\n\nSo - are you sure?', ); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function verify_withdrawn() { return confirm( "Are you sure you want to close this patch as Withdrawn?\n\nThis should only be done when the author voluntarily withdraws the patch.\n\nSo - are you sure?", ); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function verify_returned() { return confirm( 'Are you sure you want to close this patch as Returned with Feedback?\n\nThis should be done if the patch is expected to be finished at some future time, but not necessarily in the next commitfest. If work is undergoing and expected in the next commitfest, it should instead be set to "Moved to next CF".\n\nSo - are you sure?', ); } -function verify_next() { - return confirm( - '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?', - ); -} function findLatestThreads() { $("#attachThreadListWrap").addClass("loading"); $("#attachThreadSearchButton").addClass("disabled"); @@ -88,6 +86,7 @@ function browseThreads(attachfunc, closefunc) { }); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function attachThread(cfid, patchid, closefunc) { browseThreads( (msgid) => { @@ -103,6 +102,7 @@ function attachThread(cfid, patchid, closefunc) { ); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function detachThread(cfid, patchid, msgid) { if ( confirm( @@ -176,6 +176,7 @@ function updateAnnotationMessages(threadid) { $("#annotateMessageBody").removeClass("loading"); }); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function addAnnotation(threadid) { $("#annotateThreadList").find("option").remove(); $("#annotateMessage").val(""); @@ -216,6 +217,7 @@ function addAnnotation(threadid) { }); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function annotateMsgPicked() { const val = $("#annotateMessageList").val(); if (val) { @@ -233,6 +235,7 @@ function annotateChanged() { } } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function deleteAnnotation(annid) { if (confirm("Are you sure you want to delete this annotation?")) { $.post("/ajax/deleteAnnotation/", { @@ -247,6 +250,7 @@ function deleteAnnotation(annid) { } } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function flagCommitted(committer) { const modal = new bootstrap.Modal(document.getElementById("commitModal")); modal.show(); @@ -267,6 +271,7 @@ function flagCommitted(committer) { return false; } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function sortpatches(sortby) { const sortkey = Number.parseInt($("#id_sortkey").val()); if (sortkey === sortby) { @@ -285,11 +290,12 @@ function toggleButtonCollapse(buttonId, collapseId) { const button = document.getElementById(buttonId); const collapse = document.getElementById(collapseId); button.classList.toggle("active"); - const bsCollapse = new bootstrap.Collapse(collapse, { + new bootstrap.Collapse(collapse, { toggle: true, }); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function togglePatchFilterButton(buttonId, collapseId) { const collapse = document.getElementById(collapseId); @@ -308,6 +314,7 @@ function togglePatchFilterButton(buttonId, collapseId) { /* * Upstream user search dialog */ +// biome-ignore lint/correctness/noUnusedVariables: called from templates function search_and_store_user() { const modal = new bootstrap.Modal( document.getElementById("searchUserModal"), @@ -340,6 +347,7 @@ function search_and_store_user() { modal.show(); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function findUsers() { if (!$("#searchUserSearchField").val()) { alert("No search term specified"); @@ -375,6 +383,7 @@ function searchUserListChanged() { } } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function addGitCheckoutToClipboard(patchId) { navigator.clipboard.writeText(`git remote add commitfest https://github.com/postgresql-cfbot/postgresql.git git fetch commitfest cf/${patchId} @@ -388,9 +397,9 @@ $(document).ready(() => { const tooltipTriggerList = [].slice.call( document.querySelectorAll('[data-bs-toggle="tooltip"]'), ); - const tooltipList = tooltipTriggerList.map( - (tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl), - ); + for (const tooltipTriggerEl of tooltipTriggerList) { + new bootstrap.Tooltip(tooltipTriggerEl); + } $("button.attachThreadButton").each((i, o) => { const b = $(o); From 400a222b214df59a8ac651a44df8d6806b5e1e25 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:37:00 +0100 Subject: [PATCH 29/31] Fix Biome noUnusedFunctionParameters warnings Callbacks whose only parameter was unused now take no parameters. Where an unused parameter comes before one that is used, like the index in `$.each(data, (i, m) => ...)`, it is prefixed with an underscore, which is how Biome expects intentionally unused parameters to be marked. --- media/commitfest/js/commitfest.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/media/commitfest/js/commitfest.js b/media/commitfest/js/commitfest.js index 2c015bfa..41d7ae72 100644 --- a/media/commitfest/js/commitfest.js +++ b/media/commitfest/js/commitfest.js @@ -26,7 +26,7 @@ function findLatestThreads() { .success((data) => { sel = $("#attachThreadList"); sel.find("option").remove(); - $.each(data, (m, i) => { + $.each(data, (_m, i) => { sel.append( $("'); - $.each(data, (i, m) => { + $.each(data, (_i, m) => { sel.append( ``, ); @@ -210,7 +210,7 @@ function addAnnotation(threadid) { location.reload(); } }) - .fail((data) => { + .fail(() => { alert("Failed to annotate message"); $("#annotateMessageBody").removeClass("loading"); }); @@ -241,10 +241,10 @@ function deleteAnnotation(annid) { $.post("/ajax/deleteAnnotation/", { id: annid, }) - .success((data) => { + .success(() => { location.reload(); }) - .fail((data) => { + .fail(() => { alert("Failed to delete annotation!"); }); } @@ -337,7 +337,7 @@ function search_and_store_user() { alert(`Failed to import user: ${data}`); } }) - .fail((data, statustxt) => { + .fail((_data, statustxt) => { alert(`Failed to import user: ${statustxt}`); }); @@ -361,7 +361,7 @@ function findUsers() { .success((data) => { sel = $("#searchUserList"); sel.find("option").remove(); - $.each(data, (i, u) => { + $.each(data, (_i, u) => { sel.append( ``, ); @@ -401,7 +401,7 @@ $(document).ready(() => { new bootstrap.Tooltip(tooltipTriggerEl); } - $("button.attachThreadButton").each((i, o) => { + $("button.attachThreadButton").each((_i, o) => { const b = $(o); b.click(() => { $("#attachThreadAttachOnly").val("1"); From a99010315a36d9723b58f96efa4c213754840471 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:37:09 +0100 Subject: [PATCH 30/31] Pass a radix to `parseInt` Biome useParseIntRadix wants the radix to be explicit, because without one a string with a `0x` prefix is parsed as hexadecimal. --- media/commitfest/js/commitfest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/media/commitfest/js/commitfest.js b/media/commitfest/js/commitfest.js index 41d7ae72..c6b826de 100644 --- a/media/commitfest/js/commitfest.js +++ b/media/commitfest/js/commitfest.js @@ -273,7 +273,7 @@ function flagCommitted(committer) { // biome-ignore lint/correctness/noUnusedVariables: called from templates function sortpatches(sortby) { - const sortkey = Number.parseInt($("#id_sortkey").val()); + const sortkey = Number.parseInt($("#id_sortkey").val(), 10); if (sortkey === sortby) { $("#id_sortkey").val(-sortby); } else if (-sortkey === sortby) { From dfe530a0568eeb591f3cf2fb737b871ba7f81d54 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 25 Oct 2026 22:37:59 +0100 Subject: [PATCH 31/31] Fix Biome noImportantStyles warnings Biome flags `!important`, because it makes styles hard to override later. Most of ours were not needed: The `text-decoration: none` rules for badge links already win from Bootstrap's `a { text-decoration: underline }` on specificity alone. The selectize scroll hint used `!important` only to beat the inline `border-top` and `background-color` that the JavaScript adds to the element. So those inline styles never had any effect, and both they and the `!important` are now removed. The navbar `gap`, `align-items` and `width` rules do need `!important`, because they override Bootstrap's `gap-2` and `align-items-center` utility classes (which use `!important` themselves) and an inline `width`. These now have a `biome-ignore` comment explaining that. --- media/commitfest/css/commitfest.css | 17 +++++++++++------ .../commitfest/templates/selectize_js.html | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/media/commitfest/css/commitfest.css b/media/commitfest/css/commitfest.css index c8e286ea..a26eee89 100644 --- a/media/commitfest/css/commitfest.css +++ b/media/commitfest/css/commitfest.css @@ -129,19 +129,19 @@ nav[aria-label="breadcrumb"] .float-end { /* Remove underlines from tag links */ a:has(.badge) { - text-decoration: none !important; + text-decoration: none; } a:has(.badge):hover { - text-decoration: none !important; + text-decoration: none; } a .badge { - text-decoration: none !important; + text-decoration: none; } a:hover .badge { - text-decoration: none !important; + text-decoration: none; } /* Simple close button without float */ @@ -185,13 +185,18 @@ a:hover .badge { } /* Mobile: stack actions vertically, tablet+: horizontal */ +/* The !important is needed to override Bootstrap's gap-2 and + * align-items-center utility classes, which use !important themselves. */ .navbar-actions { flex-direction: column; + /* biome-ignore lint/complexity/noImportantStyles: see above */ gap: 0.5rem !important; + /* biome-ignore lint/complexity/noImportantStyles: see above */ align-items: stretch !important; } .navbar-actions .input-group { + /* biome-ignore lint/complexity/noImportantStyles: overrides the inline width in base.html */ width: 100% !important; } @@ -254,7 +259,7 @@ a:hover .badge { to bottom, rgba(248, 249, 250, 0) 0%, rgba(248, 249, 250, 1) 50% - ) !important; - border-top: none !important; + ); + border-top: none; pointer-events: none; } diff --git a/pgcommitfest/commitfest/templates/selectize_js.html b/pgcommitfest/commitfest/templates/selectize_js.html index 6da34847..3c0e1fb8 100644 --- a/pgcommitfest/commitfest/templates/selectize_js.html +++ b/pgcommitfest/commitfest/templates/selectize_js.html @@ -34,7 +34,7 @@ // Add footer hint if there are more options than visible (due to search filtering or scrolling) if (totalOptions > 6 && !$dropdown.find('.selectize-scroll-hint').length) { - $dropdown.append('
' + + $dropdown.append('
' + '⬇ Scroll to see all ' + totalOptions + ' tags, or type to search' + '
'); }