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/.gitignore b/.gitignore index e293a5a4..aef2100b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.idea/ /venv/ *.egg-info +/build/ 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..a26eee89 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"; } @@ -127,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 */ @@ -183,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; } @@ -252,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/media/commitfest/js/commitfest.js b/media/commitfest/js/commitfest.js index a353aef2..c6b826de 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"); @@ -28,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( ``, ); @@ -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(""); @@ -209,13 +210,14 @@ function addAnnotation(threadid) { location.reload(); } }) - .fail((data) => { + .fail(() => { alert("Failed to annotate message"); $("#annotateMessageBody").removeClass("loading"); }); }); } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function annotateMsgPicked() { const val = $("#annotateMessageList").val(); if (val) { @@ -233,20 +235,22 @@ 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/", { id: annid, }) - .success((data) => { + .success(() => { location.reload(); }) - .fail((data) => { + .fail(() => { alert("Failed to delete annotation!"); }); } } +// biome-ignore lint/correctness/noUnusedVariables: called from templates function flagCommitted(committer) { const modal = new bootstrap.Modal(document.getElementById("commitModal")); modal.show(); @@ -267,8 +271,9 @@ function flagCommitted(committer) { return false; } +// 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) { @@ -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"), @@ -330,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}`); }); @@ -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"); @@ -353,7 +361,7 @@ function findUsers() { .success((data) => { sel = $("#searchUserList"); sel.find("option").remove(); - $.each(data, (i, u) => { + $.each(data, (_i, u) => { sel.append( ``, ); @@ -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,11 +397,11 @@ $(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) => { + $("button.attachThreadButton").each((_i, o) => { const b = $(o); b.click(() => { $("#attachThreadAttachOnly").val("1"); 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/ajax.py b/pgcommitfest/commitfest/ajax.py index 9e0d92e4..09ddf62c 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -49,35 +49,32 @@ 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: 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, }, 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("JSON call failed: %s" % 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("Failed to communicate with archives backend: %s" % 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): @@ -95,7 +92,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) @@ -110,13 +107,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! @@ -148,7 +145,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( @@ -166,8 +163,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() @@ -184,8 +180,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() @@ -226,7 +221,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. @@ -264,7 +259,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() @@ -283,7 +278,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/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/feeds.py b/pgcommitfest/commitfest/feeds.py index 83c47bfc..ef71f345 100644 --- a/pgcommitfest/commitfest/feeds.py +++ b/pgcommitfest/commitfest/feeds.py @@ -18,12 +18,12 @@ 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 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/forms.py b/pgcommitfest/commitfest/forms.py index 444905b1..fa62bada 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 .ajax import _archivesAPI +from typing import ClassVar + +from .ajax import Http503, _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, @@ -25,7 +27,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 @@ -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, @@ -104,16 +106,11 @@ 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: ( - "%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" @@ -162,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, @@ -180,7 +177,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) @@ -206,10 +203,10 @@ 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: + except Http503: raise ValidationError("Error in API call to validate thread") return self.cleaned_data["threadmsgid"] @@ -222,4 +219,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/migrations/0001_initial.py b/pgcommitfest/commitfest/migrations/0001_initial.py index 3577d436..851da57a 100644 --- a/pgcommitfest/commitfest/migrations/0001_initial.py +++ b/pgcommitfest/commitfest/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models @@ -318,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/commitfest/migrations/0002_notifications.py b/pgcommitfest/commitfest/migrations/0002_notifications.py index 450ddfbe..b85bac82 100644 --- a/pgcommitfest/commitfest/migrations/0002_notifications.py +++ b/pgcommitfest/commitfest/migrations/0002_notifications.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -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 2f6a5f7d..abdc9afe 100644 --- a/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py +++ b/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py @@ -1,6 +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..45919fe7 100644 --- a/pgcommitfest/commitfest/migrations/0004_target_version.py +++ b/pgcommitfest/commitfest/migrations/0004_target_version.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # 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 4316f212..e58cf9d3 100644 --- a/pgcommitfest/commitfest/migrations/0005_history_dateindex.py +++ b/pgcommitfest/commitfest/migrations/0005_history_dateindex.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # 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/commitfest/models.py b/pgcommitfest/commitfest/models.py index c7b8fb4f..6bb02844 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -4,7 +4,8 @@ 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 typing import ClassVar from pgcommitfest.mailqueue.util import send_template_mail from pgcommitfest.userprofile.models import UserProfile @@ -30,11 +31,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") @@ -64,11 +61,11 @@ 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): - return "{0} – {1}".format(self.startdate, self.enddate) + return f"{self.startdate} – {self.enddate}" @property def last_open_date(self): @@ -83,7 +80,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): @@ -249,15 +246,12 @@ 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): 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 @@ -442,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", ] @@ -534,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", } @@ -558,17 +552,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() ] ) @@ -635,11 +626,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() @@ -702,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): @@ -737,7 +728,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 { @@ -791,14 +782,14 @@ 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",) - 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)), @@ -952,11 +943,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",) @@ -976,7 +963,7 @@ class PendingNotification(models.Model): class CfbotBranch(models.Model): - STATUS_CHOICES = [ + STATUS_CHOICES: ClassVar[list] = [ ("testing", "Testing"), ("finished", "Finished"), ("failed", "Failed"), @@ -1015,11 +1002,11 @@ 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): - STATUS_CHOICES = [ + STATUS_CHOICES: ClassVar[list] = [ ("CREATED", "Created"), ("PAUSED", "Paused"), ("NEEDS_APPROVAL", "Needs Approval"), @@ -1056,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/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/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' + '
'); } 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") 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_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/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": [], diff --git a/pgcommitfest/commitfest/util.py b/pgcommitfest/commitfest/util.py index 10f9c816..84f23aea 100644 --- a/pgcommitfest/commitfest/util.py +++ b/pgcommitfest/commitfest/util.py @@ -2,13 +2,13 @@ from django.forms.models import model_to_dict -class DiffableModel(object): +class DiffableModel: """ Make it possible to diff a model. """ def __init__(self, *args, **kwargs): - super(DiffableModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.__initial = self._dict @property @@ -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 ( @@ -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/views.py b/pgcommitfest/commitfest/views.py index b9466b5e..91df1a9e 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,13 +280,11 @@ 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 [ - {"title": cf.title, "href": "/%s/" % cf.pk}, + {"title": cf.title, "href": f"/{cf.pk}/"}, ] or None, }, @@ -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("/") @@ -392,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( @@ -422,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( @@ -601,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 = { @@ -693,7 +685,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.. @@ -979,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(), @@ -1019,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, @@ -1027,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) @@ -1042,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}/"}, ], }, ) @@ -1092,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: @@ -1106,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, @@ -1137,19 +1129,16 @@ 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 @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() @@ -1213,7 +1202,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 @@ -1229,8 +1218,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}") @@ -1284,7 +1272,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) @@ -1292,7 +1280,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("../../") @@ -1343,7 +1331,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 @@ -1351,7 +1339,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("../../") @@ -1414,13 +1402,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) @@ -1455,26 +1440,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( @@ -1485,7 +1469,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 dea0c7ec..350a7107 100644 --- a/pgcommitfest/commitfest/widgets.py +++ b/pgcommitfest/commitfest/widgets.py @@ -5,10 +5,9 @@ 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 - + ' ' - % name + + f' ' ) return mark_safe(html) diff --git a/pgcommitfest/mailqueue/migrations/0001_initial.py b/pgcommitfest/mailqueue/migrations/0001_initial.py index 38d1214d..27a5508f 100644 --- a/pgcommitfest/mailqueue/migrations/0001_initial.py +++ b/pgcommitfest/mailqueue/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models 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..72dbebb2 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) @@ -64,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, 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/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 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.' diff --git a/pgcommitfest/userprofile/migrations/0001_initial.py b/pgcommitfest/userprofile/migrations/0001_initial.py index 45ad7696..4d9325bb 100644 --- a/pgcommitfest/userprofile/migrations/0001_initial.py +++ b/pgcommitfest/userprofile/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models @@ -70,6 +67,6 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name="userextraemail", - unique_together=set([("user", "email")]), + unique_together={("user", "email")}, ), ] diff --git a/pgcommitfest/userprofile/migrations/0002_notifications.py b/pgcommitfest/userprofile/migrations/0002_notifications.py index 0d4b0357..2f374d51 100644 --- a/pgcommitfest/userprofile/migrations/0002_notifications.py +++ b/pgcommitfest/userprofile/migrations/0002_notifications.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models diff --git a/pgcommitfest/userprofile/util.py b/pgcommitfest/userprofile/util.py index 78a17597..e9236e7a 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 @@ -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, ) ) @@ -44,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() 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) diff --git a/pyproject.toml b/pyproject.toml index c3a6a178..1e25811c 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", @@ -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", 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, ) diff --git a/tools/commitfest/check_patches_in_archives.py b/tools/commitfest/check_patches_in_archives.py index e68150ce..73a028bc 100755 --- a/tools/commitfest/check_patches_in_archives.py +++ b/tools/commitfest/check_patches_in_archives.py @@ -17,14 +17,16 @@ # 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__) 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. @@ -47,16 +49,12 @@ # 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" + logger.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, }, @@ -64,12 +62,12 @@ ) if resp.status_code != 200: - logging.error("Failed to get %s: %s" % (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("Detected MIME type is %s" % 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. @@ -77,8 +75,8 @@ a.ispatch = True else: a.ispatch = False - logging.info("Attachment %s is patch: %s" % (a.id, 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 b90348b5..e5063d32 100755 --- a/tools/commitfest/update_archive_threads.py +++ b/tools/commitfest/update_archive_threads.py @@ -13,14 +13,16 @@ # 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__) 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("Checking %s in the archives" % thread.messageid) + logger.debug(f"Checking {thread.messageid} in the archives") refresh_single_thread(thread) connection.close() - logging.debug("Done.") + logger.debug("Done.") 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