From b2eef830d856851bcc0651a7e7f4870ca519f7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Fri, 28 Nov 2025 04:44:07 +0100 Subject: [PATCH] Add and use pre-commit hooks --- .gitignore | 4 +-- .pre-commit-config.yaml | 39 ++++++++++++++++++++++ .vscode/launch.json | 2 +- .vscode/settings.json | 2 +- README.md | 2 +- manage.py | 0 templates/base.html | 6 ++-- twitch/management/commands/import_drops.py | 2 +- twitch/migrations/0001_initial.py | 14 ++++++-- twitch/views.py | 13 ++++---- 10 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 .pre-commit-config.yaml mode change 100644 => 100755 manage.py diff --git a/.gitignore b/.gitignore index be4e455..7c3d3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -182,9 +182,9 @@ cython_debug/ .abstra/ # Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, +# and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..22df3db --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: + - repo: https://github.com/asottile/add-trailing-comma + rev: v4.0.0 + hooks: + - id: add-trailing-comma + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-ast + - id: check-builtin-literals + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-toml + - id: check-vcs-permalinks + - id: end-of-file-fixer + - id: mixed-line-ending + - id: name-tests-test + args: [--pytest-test-first] + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.6 + hooks: + - id: ruff-check + args: ["--fix", "--exit-non-zero-on-fix"] + - id: ruff-format + + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: ["--py311-plus"] + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.9 + hooks: + - id: actionlint diff --git a/.vscode/launch.json b/.vscode/launch.json index ce16edd..27d4f44 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,4 +25,4 @@ "program": "${workspaceFolder}/manage.py" } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d64463e..cd145ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,4 +38,4 @@ "wrongpassword", "xdist" ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index c737084..19aa674 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,4 @@ uv run python manage.py migrate uv run python manage.py collectstatic uv run python manage.py runserver uv run pytest -``` \ No newline at end of file +``` diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 diff --git a/templates/base.html b/templates/base.html index 7df7ddd..6f7b641 100644 --- a/templates/base.html +++ b/templates/base.html @@ -42,17 +42,17 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; line-height: 1.4; padding: 0; font-size: 115%; max-width: 75%; margin: 0 auto;} @media (max-width: 900px) { body { max-width: 95%; } } - + table { width: 100%; } th, td { padding: 8px; text-align: left; vertical-align: middle; } th {background-color: Canvas; color: CanvasText; font-weight: bold; } tr:nth-child(even) { background-color: color-mix(in srgb, Canvas 95%, CanvasText 5%); } td > img { display: block; height: 160px; width: 160px; object-fit: cover; border-radius: 4px; } - + .campaign-benefits { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 8px; } .benefit-item { display: inline-flex; align-items: center; padding: 2px 6px; background-color: color-mix(in srgb, Canvas 90%, CanvasText 10%); border-radius: 4px; font-size: 0.9em; } .benefit-item img { width: 24px !important; height: 24px !important; display: inline-block !important; margin-right: 4px; border-radius: 2px; } - + @media (prefers-color-scheme: dark) { .highlight { background: #0d1117; color: #E6EDF3; } .highlight .p { color: #E6EDF3; } diff --git a/twitch/management/commands/import_drops.py b/twitch/management/commands/import_drops.py index 1022bb5..1d6f089 100644 --- a/twitch/management/commands/import_drops.py +++ b/twitch/management/commands/import_drops.py @@ -132,7 +132,7 @@ class Command(BaseCommand): f"{len(self._organization_cache)} orgs, " f"{len(self._drop_campaign_cache)} campaigns, " f"{len(self._channel_cache)} channels, " - f"{len(self._benefit_cache)} benefits." + f"{len(self._benefit_cache)} benefits.", ) except (FileNotFoundError, OSError, RuntimeError): # If preload fails for any reason, continue without it diff --git a/twitch/migrations/0001_initial.py b/twitch/migrations/0001_initial.py index 582fde2..29a969c 100644 --- a/twitch/migrations/0001_initial.py +++ b/twitch/migrations/0001_initial.py @@ -82,7 +82,10 @@ class Migration(migrations.Migration): ( "id", models.TextField( - help_text="The unique Twitch identifier for the organization.", primary_key=True, serialize=False, verbose_name="Organization ID" + help_text="The unique Twitch identifier for the organization.", + primary_key=True, + serialize=False, + verbose_name="Organization ID", ), ), ("name", models.TextField(help_text="Display name of the organization.", unique=True, verbose_name="Name")), @@ -179,7 +182,10 @@ class Migration(migrations.Migration): ( "benefits", models.ManyToManyField( - help_text="Benefits unlocked by this drop.", related_name="drops", through="twitch.DropBenefitEdge", to="twitch.dropbenefit" + help_text="Benefits unlocked by this drop.", + related_name="drops", + through="twitch.DropBenefitEdge", + to="twitch.dropbenefit", ), ), ( @@ -200,7 +206,9 @@ class Migration(migrations.Migration): model_name="dropbenefitedge", name="drop", field=models.ForeignKey( - help_text="The time-based drop in this relationship.", on_delete=django.db.models.deletion.CASCADE, to="twitch.timebaseddrop" + help_text="The time-based drop in this relationship.", + on_delete=django.db.models.deletion.CASCADE, + to="twitch.timebaseddrop", ), ), migrations.CreateModel( diff --git a/twitch/views.py b/twitch/views.py index 264f778..e77f40d 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -434,8 +434,9 @@ class GameDetailView(DetailView): .select_related("game__owner") .prefetch_related( Prefetch( - "time_based_drops", queryset=TimeBasedDrop.objects.prefetch_related(Prefetch("benefits", queryset=DropBenefit.objects.order_by("name"))) - ) + "time_based_drops", + queryset=TimeBasedDrop.objects.prefetch_related(Prefetch("benefits", queryset=DropBenefit.objects.order_by("name"))), + ), ) .order_by("-end_at") ) @@ -580,12 +581,12 @@ def debug_view(request: HttpRequest) -> HttpResponse: # Campaigns with missing or obviously broken images (empty or not starting with http) broken_image_campaigns: QuerySet[DropCampaign] = DropCampaign.objects.filter( - Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http") + Q(image_url__isnull=True) | Q(image_url__exact="") | ~Q(image_url__startswith="http"), ).select_related("game") # Benefits with missing images broken_benefit_images: QuerySet[DropBenefit] = DropBenefit.objects.annotate(trimmed_url=Trim("image_asset_url")).filter( - Q(image_asset_url__isnull=True) | Q(trimmed_url__exact="") | ~Q(image_asset_url__startswith="http") + Q(image_asset_url__isnull=True) | Q(trimmed_url__exact="") | ~Q(image_asset_url__startswith="http"), ) # Time-based drops without any benefits @@ -593,7 +594,7 @@ def debug_view(request: HttpRequest) -> HttpResponse: # Campaigns with invalid dates (start after end or missing either) invalid_date_campaigns: QuerySet[DropCampaign] = DropCampaign.objects.filter( - Q(start_at__gt=F("end_at")) | Q(start_at__isnull=True) | Q(end_at__isnull=True) + Q(start_at__gt=F("end_at")) | Q(start_at__isnull=True) | Q(end_at__isnull=True), ).select_related("game") # Duplicate campaign names per game. We retrieve the game's name for user-friendly display. @@ -729,7 +730,7 @@ class ChannelDetailView(DetailView): queryset=TimeBasedDrop.objects.prefetch_related( Prefetch("benefits", queryset=DropBenefit.objects.order_by("name")), ), - ) + ), ) .order_by("-start_at") )