From 128d760972862466fe0ba4f892d0cf0736b6173b Mon Sep 17 00:00:00 2001 From: antoliny0919 Date: Mon, 3 Nov 2025 17:00:23 +0900 Subject: [PATCH] Migrate from flake8 and isort to ruff. --- .github/workflows/pythonpackage.yml | 10 +- .pre-commit-config.yaml | 20 ++-- base/components/__init__.py | 0 base/components/components.py | 39 +++---- base/exceptions.py | 4 +- base/main.py | 3 +- base/pagination.py | 6 +- base/templatetags/base_templatetags.py | 8 +- base/tests/migrations/0001_initial.py | 41 +++++-- base/tests/migrations/0002_fish.py | 11 +- base/tests/test_components.py | 1 - base/tests/tests.py | 11 +- cab/api/serializers.py | 2 +- cab/api/views.py | 3 +- cab/components/__init__.py | 0 cab/components/components.py | 1 - cab/feeds.py | 22 ++-- cab/forms.py | 11 +- cab/migrations/0001_initial.py | 79 +++++++++++--- cab/migrations/0002_versions_to_char.py | 4 - cab/migrations/0003_auto_20200803_1425.py | 1 - cab/migrations/0004_auto_20210310_0902.py | 1 - cab/migrations/0005_alter_snippet_tags.py | 1 - cab/migrations/0006_alter_snippet_version.py | 1 - cab/models.py | 28 ++--- cab/templatetags/cab_tags.py | 4 +- cab/templatetags/core_tags.py | 2 +- cab/templatetags/markup.py | 2 +- cab/tests/tests.py | 100 ++++++++++++------ cab/urls/bookmarks.py | 8 +- cab/urls/feeds.py | 8 +- cab/urls/languages.py | 2 +- cab/urls/popular.py | 2 +- cab/urls/search.py | 2 +- cab/urls/snippets.py | 8 +- cab/urls/tags.py | 2 +- cab/urls/users.py | 2 +- cab/utils.py | 30 +++--- cab/views/bookmarks.py | 7 +- cab/views/languages.py | 4 +- cab/views/popular.py | 14 ++- cab/views/snippets.py | 48 +++++---- comments_spamfighter/admin.py | 13 +-- .../migrations/0001_initial.py | 34 ++++-- comments_spamfighter/models.py | 1 - comments_spamfighter/moderation.py | 45 ++++---- djangosnippets/settings/base.py | 21 ++-- djangosnippets/settings/development.py | 4 +- djangosnippets/settings/production.py | 36 ++++--- djangosnippets/settings/testing.py | 13 ++- djangosnippets/urls.py | 3 +- pyproject.toml | 73 +++++++++++-- ratings/converters.py | 2 +- ratings/migrations/0001_initial.py | 29 +++-- .../migrations/0002_alter_rateditem_user.py | 5 +- ratings/models.py | 48 +++++---- ratings/tests/tests.py | 82 +++++++++----- ratings/utils.py | 12 +-- ratings/views.py | 18 +++- requirements/development.txt | 3 +- 60 files changed, 644 insertions(+), 351 deletions(-) create mode 100644 base/components/__init__.py create mode 100644 cab/components/__init__.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dbc26b98..a25e751e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -39,14 +39,10 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements/production.txt - - name: Lint with flake8 + - name: Install Linter run: | - pip install flake8 - flake8 . - - name: iSort - run: | - pip install isort - isort --check-only --diff cab comments_spamfighter djangosnippets ratings + pip install ruff + ruff check . - name: Run migrations run: python manage.py migrate - name: Collect static files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08edf036..4cda852f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - id: trailing-whitespace @@ -9,17 +9,9 @@ repos: - id: check-yaml - id: check-added-large-files -- repo: https://github.com/pycqa/isort - rev: 5.13.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.3 hooks: - - id: isort - -- repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - -- repo: https://github.com/pycqa/flake8 - rev: '7.2.0' # pick a git hash / tag to point to - hooks: - - id: flake8 + - id: ruff-check + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/base/components/__init__.py b/base/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/base/components/components.py b/base/components/components.py index 25f78d4b..4370d2a0 100644 --- a/base/components/components.py +++ b/base/components/components.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Literal from django_components import Component, register from pydantic import BaseModel @@ -10,7 +10,6 @@ @register("icon") class Icon(Component): - class Kwargs(BaseModel): kind: Literal["heart", "bookmark"] color: str @@ -29,8 +28,8 @@ def get_template_data(self, args, kwargs, slots, context): class PaginationItem(BaseModel): kind: Literal["current", "ellipsis", "number"] - text: Optional[str | int] = None - attrs: Optional[dict] = None + text: str | int | None = None + attrs: dict | None = None @register("pagination") @@ -48,21 +47,26 @@ def pagination_number(self, pagination: Pagination, num: int) -> PaginationItem: """ if num == pagination.paginator.ELLIPSIS: return PaginationItem(kind="ellipsis", text=str(pagination.paginator.ELLIPSIS)) - elif num == pagination.page_num: + if num == pagination.page_num: return PaginationItem(kind="current", text=num) - else: - link = querystring(None, {**pagination.params, PAGE_VAR: num}) - return PaginationItem( - kind="number", - text=num, - attrs={"href": link}, - ) + link = querystring(None, {**pagination.params, PAGE_VAR: num}) + return PaginationItem( + kind="number", + text=num, + attrs={"href": link}, + ) def get_template_data(self, args, kwargs, slots, context): pagination = kwargs.pagination_obj - page_elements = [self.pagination_number(pagination, page_num) for page_num in pagination.page_range] - previous_page_link = f"?{PAGE_VAR}={pagination.page_num - 1}" if pagination.page.has_previous() else "" - next_page_link = f"?{PAGE_VAR}={pagination.page_num + 1}" if pagination.page.has_next() else "" + page_elements = [ + self.pagination_number(pagination, page_num) for page_num in pagination.page_range + ] + previous_page_link = ( + f"?{PAGE_VAR}={pagination.page_num - 1}" if pagination.page.has_previous() else "" + ) + next_page_link = ( + f"?{PAGE_VAR}={pagination.page_num + 1}" if pagination.page.has_next() else "" + ) return { "pagination": pagination, "previous_page_link": previous_page_link, @@ -74,7 +78,7 @@ def get_template_data(self, args, kwargs, slots, context): class TabItem(BaseModel): text: str is_current: bool - attrs: Optional[dict] + attrs: dict | None @register("sorting_tabs") @@ -95,8 +99,7 @@ def create_tab(self, object_list: ObjectList, tab: str) -> TabItem: return TabItem(text=verbose_text, is_current=is_current, attrs=attrs) def create_all_tabs(self, object_list: ObjectList): - tabs = [self.create_tab(object_list, tab) for tab in object_list.sorting_tabs] - return tabs + return [self.create_tab(object_list, tab) for tab in object_list.sorting_tabs] def get_template_data(self, args, kwargs, slots, context): object_list = kwargs.object_list diff --git a/base/exceptions.py b/base/exceptions.py index e13df8f9..4e7cf5db 100644 --- a/base/exceptions.py +++ b/base/exceptions.py @@ -1,6 +1,4 @@ -class IncorectLookupParameter(Exception): +class IncorrectLookupParameterError(Exception): """ Raised when a query parameter contains an incorrect value. """ - - pass diff --git a/base/main.py b/base/main.py index 6d59813f..f5e319fa 100644 --- a/base/main.py +++ b/base/main.py @@ -49,5 +49,4 @@ def paginate(self, request, queryset): def get_objects(self, request, queryset): tab_result = self.tab_sort(queryset) - paginate_result = self.paginate(request, tab_result) - return paginate_result + return self.paginate(request, tab_result) diff --git a/base/pagination.py b/base/pagination.py index 0230420e..6eb67b1d 100644 --- a/base/pagination.py +++ b/base/pagination.py @@ -1,6 +1,6 @@ from django.core.paginator import InvalidPage, Paginator -from .exceptions import IncorectLookupParameter +from .exceptions import IncorrectLookupParameterError PAGE_VAR = "page" @@ -49,6 +49,6 @@ def get_objects(self): else: try: result_list = self.paginator.page(self.page_num).object_list - except InvalidPage: - raise IncorectLookupParameter + except InvalidPage as err: + raise IncorrectLookupParameterError from err return result_list diff --git a/base/templatetags/base_templatetags.py b/base/templatetags/base_templatetags.py index ba5d8112..980622f7 100644 --- a/base/templatetags/base_templatetags.py +++ b/base/templatetags/base_templatetags.py @@ -49,12 +49,16 @@ def querystring(context, *args, **kwargs): params = QueryDict(mutable=True) for d in [*args, kwargs]: if not isinstance(d, Mapping): + msg = f"querystring requires mappings for positional arguments (got {d!r} instead)." raise TemplateSyntaxError( - "querystring requires mappings for positional arguments (got " "%r instead)." % d + msg, ) for key, value in d.items(): if not isinstance(key, str): - raise TemplateSyntaxError("querystring requires strings for mapping keys (got %r " "instead)." % key) + msg = f"querystring requires strings for mapping keys (got {key!r} instead)." + raise TemplateSyntaxError( + msg, + ) if value is None: params.pop(key, None) elif isinstance(value, Iterable) and not isinstance(value, str): diff --git a/base/tests/migrations/0001_initial.py b/base/tests/migrations/0001_initial.py index 9a4c4b73..d706e934 100644 --- a/base/tests/migrations/0001_initial.py +++ b/base/tests/migrations/0001_initial.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -17,26 +16,56 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Beverage", fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ("name", models.CharField(max_length=50)), ], ), migrations.CreateModel( name="Food", fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ("name", models.CharField(max_length=50)), ], ), migrations.CreateModel( name="BeverageRating", fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ("score", models.FloatField(db_index=True, default=0)), - ("hashed", models.CharField(db_index=True, editable=False, max_length=40)), + ( + "hashed", + models.CharField(db_index=True, editable=False, max_length=40), + ), ( "content_object", - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="tests.Beverage"), + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="tests.Beverage", + ), ), ( "user", diff --git a/base/tests/migrations/0002_fish.py b/base/tests/migrations/0002_fish.py index 15d3dd63..7e00df37 100644 --- a/base/tests/migrations/0002_fish.py +++ b/base/tests/migrations/0002_fish.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("tests", "0001_initial"), ] @@ -13,7 +12,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Fish", fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ("name", models.CharField(max_length=255)), ("price", models.IntegerField()), ], diff --git a/base/tests/test_components.py b/base/tests/test_components.py index e6302194..7a3188a4 100644 --- a/base/tests/test_components.py +++ b/base/tests/test_components.py @@ -16,7 +16,6 @@ class FishList(ObjectList): class SortingTabsComponentTests(TestCase): - @classmethod def setUpTestData(cls): fish_data = [Fish(name=f"fish-{i}", price=i * 100) for i in range(1, 21)] diff --git a/base/tests/tests.py b/base/tests/tests.py index c18b0245..c327aa7c 100644 --- a/base/tests/tests.py +++ b/base/tests/tests.py @@ -15,7 +15,6 @@ class FishList(ObjectList): class ObjectListTests(TestCase): - @classmethod def setUpTestData(cls): fishs = [ @@ -91,12 +90,12 @@ def test_pagination_attributes(self): def test_pagination_page_range(self): request = self.factory.get("/fake-url/") - ELLIPSIS = "…" + ellipsis = "…" case = [ - (2, 6, [1, 2, 3, 4, 5, 6, 7, 8, 9, ELLIPSIS, 49, 50]), - (3, 10, [1, 2, ELLIPSIS, 7, 8, 9, 10, 11, 12, 13, ELLIPSIS, 33, 34]), - (4, 23, [1, 2, ELLIPSIS, 20, 21, 22, 23, 24, 25]), - (5, 20, [1, 2, ELLIPSIS, 17, 18, 19, 20]), + (2, 6, [1, 2, 3, 4, 5, 6, 7, 8, 9, ellipsis, 49, 50]), + (3, 10, [1, 2, ellipsis, 7, 8, 9, 10, 11, 12, 13, ellipsis, 33, 34]), + (4, 23, [1, 2, ellipsis, 20, 21, 22, 23, 24, 25]), + (5, 20, [1, 2, ellipsis, 17, 18, 19, 20]), (10, 8, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), (20, 1, [1, 2, 3, 4, 5]), ] diff --git a/cab/api/serializers.py b/cab/api/serializers.py index b76dad5f..c699db71 100644 --- a/cab/api/serializers.py +++ b/cab/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from ..models import Snippet +from cab.models import Snippet class SnippetSerializer(serializers.ModelSerializer): diff --git a/cab/api/views.py b/cab/api/views.py index 746c61bf..63b0ccc9 100644 --- a/cab/api/views.py +++ b/cab/api/views.py @@ -1,6 +1,7 @@ from rest_framework import generics -from ..models import Snippet +from cab.models import Snippet + from .serializers import SnippetSerializer diff --git a/cab/components/__init__.py b/cab/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cab/components/components.py b/cab/components/components.py index 2138a5e1..8b98971e 100644 --- a/cab/components/components.py +++ b/cab/components/components.py @@ -6,7 +6,6 @@ @register("snippet_list") class SnippetListComponent(Component): - template_file = "snippet_list.html" class Kwargs(BaseModel): diff --git a/cab/feeds.py b/cab/feeds.py index 3b5d44ea..ad185c8f 100644 --- a/cab/feeds.py +++ b/cab/feeds.py @@ -26,9 +26,8 @@ class LatestSnippetsFeed(Feed): def title(self): if SITE_NAME: - return "%s: Latest snippets" % SITE_NAME - else: - return "Latest snippets" + return f"{SITE_NAME}: Latest snippets" + return "Latest snippets" def item_author_name(self, item): return item.author.username @@ -63,13 +62,12 @@ def items(self, obj): return Snippet.objects.filter(author=obj)[:15] def link(self, obj): - return "/users/%s/" % obj.username + return f"/users/{obj.username}/" def title(self, obj): if SITE_NAME: - return "%s: Latest snippets posted by %s" % (SITE_NAME, obj.username) - else: - return "Latest snippets posted by %s" % obj.username + return f"{SITE_NAME}: Latest snippets posted by {obj.username}" + return f"Latest snippets posted by {obj.username}" def item_author_name(self, item): return item.author.username @@ -102,9 +100,8 @@ def link(self, obj): def title(self, obj): if SITE_NAME: - return "%s: Latest snippets written in %s" % (SITE_NAME, obj.name) - else: - return "Latest snippets written in %s" % obj.name + return f"{SITE_NAME}: Latest snippets written in {obj.name}" + return f"Latest snippets written in {obj.name}" def item_author_name(self, item): return item.author.username @@ -138,9 +135,8 @@ def link(self, obj): def title(self, obj): if SITE_NAME: - return "%s: Latest snippets tagged with '%s'" % (SITE_NAME, obj.name) - else: - return "Latest snippets tagged with '%s'" % obj.name + return f"{SITE_NAME}: Latest snippets tagged with '{obj.name}'" + return f"Latest snippets tagged with '{obj.name}'" def item_author_name(self, item): return item.author.username diff --git a/cab/forms.py b/cab/forms.py index 77629c8e..6eef64bb 100644 --- a/cab/forms.py +++ b/cab/forms.py @@ -15,7 +15,10 @@ def validate_non_whitespace_only_string(value): class SnippetForm(forms.ModelForm): title = forms.CharField(validators=[validate_non_whitespace_only_string]) - description = forms.CharField(validators=[validate_non_whitespace_only_string], widget=forms.Textarea) + description = forms.CharField( + validators=[validate_non_whitespace_only_string], + widget=forms.Textarea, + ) code = forms.CharField(validators=[validate_non_whitespace_only_string], widget=forms.Textarea) class Meta: @@ -34,7 +37,11 @@ class Meta: class AdvancedSearchForm(forms.Form): - q = forms.CharField(required=False, label="Search", widget=forms.TextInput(attrs={"type": "search"})) + q = forms.CharField( + required=False, + label="Search", + widget=forms.TextInput(attrs={"type": "search"}), + ) language = forms.ModelChoiceField(queryset=Language.objects.all(), required=False) version = forms.MultipleChoiceField(choices=VERSIONS, required=False) minimum_pub_date = forms.DateTimeField(widget=admin.widgets.AdminDateWidget, required=False) diff --git a/cab/migrations/0001_initial.py b/cab/migrations/0001_initial.py index eb39bc7a..336564de 100644 --- a/cab/migrations/0001_initial.py +++ b/cab/migrations/0001_initial.py @@ -1,13 +1,9 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import taggit.managers from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("taggit", "0002_auto_20150616_2121"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), @@ -17,7 +13,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Bookmark", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), ("date", models.DateTimeField(auto_now_add=True)), ], options={ @@ -28,7 +32,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Language", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), ("name", models.CharField(max_length=100)), ("slug", models.SlugField(unique=True)), ("language_code", models.CharField(max_length=50)), @@ -43,7 +55,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name="Snippet", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), ("title", models.CharField(max_length=255)), ("description", models.TextField()), ("description_html", models.TextField(editable=False)), @@ -74,8 +94,14 @@ class Migration(migrations.Migration): ("updated_date", models.DateTimeField(auto_now=True)), ("bookmark_count", models.IntegerField(default=0)), ("rating_score", models.IntegerField(default=0)), - ("author", models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), - ("language", models.ForeignKey(to="cab.Language", on_delete=models.CASCADE)), + ( + "author", + models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + ), + ( + "language", + models.ForeignKey(to="cab.Language", on_delete=models.CASCADE), + ), ( "tags", taggit.managers.TaggableManager( @@ -94,10 +120,31 @@ class Migration(migrations.Migration): migrations.CreateModel( name="SnippetFlag", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), - ("flag", models.IntegerField(choices=[(1, "Spam"), (2, "Inappropriate")])), - ("snippet", models.ForeignKey(related_name="flags", to="cab.Snippet", on_delete=models.CASCADE)), - ("user", models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "flag", + models.IntegerField(choices=[(1, "Spam"), (2, "Inappropriate")]), + ), + ( + "snippet", + models.ForeignKey( + related_name="flags", + to="cab.Snippet", + on_delete=models.CASCADE, + ), + ), + ( + "user", + models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + ), ], options={}, bases=(models.Model,), @@ -105,7 +152,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name="bookmark", name="snippet", - field=models.ForeignKey(related_name="bookmarks", to="cab.Snippet", on_delete=models.CASCADE), + field=models.ForeignKey( + related_name="bookmarks", + to="cab.Snippet", + on_delete=models.CASCADE, + ), preserve_default=True, ), migrations.AddField( diff --git a/cab/migrations/0002_versions_to_char.py b/cab/migrations/0002_versions_to_char.py index a862efcb..ceb8811f 100644 --- a/cab/migrations/0002_versions_to_char.py +++ b/cab/migrations/0002_versions_to_char.py @@ -1,11 +1,7 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("cab", "0001_initial"), ] diff --git a/cab/migrations/0003_auto_20200803_1425.py b/cab/migrations/0003_auto_20200803_1425.py index c06e9d2d..676bd72c 100644 --- a/cab/migrations/0003_auto_20200803_1425.py +++ b/cab/migrations/0003_auto_20200803_1425.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cab", "0002_versions_to_char"), ] diff --git a/cab/migrations/0004_auto_20210310_0902.py b/cab/migrations/0004_auto_20210310_0902.py index bd27e0ff..8c4bc2e0 100644 --- a/cab/migrations/0004_auto_20210310_0902.py +++ b/cab/migrations/0004_auto_20210310_0902.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cab", "0003_auto_20200803_1425"), ] diff --git a/cab/migrations/0005_alter_snippet_tags.py b/cab/migrations/0005_alter_snippet_tags.py index 7d356204..4d83ffff 100644 --- a/cab/migrations/0005_alter_snippet_tags.py +++ b/cab/migrations/0005_alter_snippet_tags.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("taggit", "0003_taggeditem_add_unique_index"), ("cab", "0004_auto_20210310_0902"), diff --git a/cab/migrations/0006_alter_snippet_version.py b/cab/migrations/0006_alter_snippet_version.py index 9b0538fa..a45c9a61 100644 --- a/cab/migrations/0006_alter_snippet_version.py +++ b/cab/migrations/0006_alter_snippet_version.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("cab", "0005_alter_snippet_tags"), ] diff --git a/cab/models.py b/cab/models.py index 535e0765..109e02c8 100644 --- a/cab/models.py +++ b/cab/models.py @@ -52,12 +52,10 @@ def top_tags(self): def top_rated(self): # this is slow - # return self.annotate(score=Sum('ratings__score')).order_by('-score') return self.all().order_by("-rating_score", "-pub_date") def most_bookmarked(self): # this is slow - # self.annotate(score=Count('bookmarks')).order_by('-score') return self.all().order_by("-bookmark_count", "-pub_date") def matches_tag(self, tag): @@ -95,13 +93,17 @@ def __str__(self): def save(self, *args, **kwargs): self.description_html = sanitize_markdown(self.description) self.highlighted_code = self.highlight() - super(Snippet, self).save(*args, **kwargs) + super().save(*args, **kwargs) def get_absolute_url(self): return reverse("cab_snippet_detail", kwargs={"snippet_id": self.id}) def highlight(self): - return highlight(self.code, self.language.get_lexer(), formatters.HtmlFormatter(linenos=True)) + return highlight( + self.code, + self.language.get_lexer(), + formatters.HtmlFormatter(linenos=True), + ) def get_tagstring(self): return ", ".join([t.name for t in self.tags.order_by("name").all()]) @@ -118,7 +120,11 @@ def update_bookmark_count(self): self.save() def mark_as_inappropiate(self): - snippet_flag = SnippetFlag(snippet=self, user=self.author, flag=SnippetFlag.FLAG_INAPPROPRIATE) + snippet_flag = SnippetFlag( + snippet=self, + user=self.author, + flag=SnippetFlag.FLAG_INAPPROPRIATE, + ) snippet_flag.save() def mark_as_spam(self): @@ -138,11 +144,7 @@ class SnippetFlag(models.Model): flag = models.IntegerField(choices=FLAG_CHOICES) def __str__(self): - return "%s flagged as %s by %s" % ( - self.snippet.title, - self.get_flag_display(), - self.user.username, - ) + return f"{self.snippet.title} flagged as {self.get_flag_display()} by {self.user.username}" def remove_and_ban(self): user = self.snippet.author @@ -161,14 +163,14 @@ class Meta: ordering = ("-date",) def __str__(self): - return "%s bookmarked by %s" % (self.snippet, self.user) + return f"{self.snippet} bookmarked by {self.user}" def save(self, *args, **kwargs): - super(Bookmark, self).save(*args, **kwargs) + super().save(*args, **kwargs) self.snippet.update_bookmark_count() def delete(self, *args, **kwargs): - super(Bookmark, self).delete(*args, **kwargs) + super().delete(*args, **kwargs) self.snippet.update_bookmark_count() diff --git a/cab/templatetags/cab_tags.py b/cab/templatetags/cab_tags.py index a76b202c..decc7a81 100644 --- a/cab/templatetags/cab_tags.py +++ b/cab/templatetags/cab_tags.py @@ -1,7 +1,7 @@ from django import template from django.contrib.postgres.search import SearchVector -from ..models import Bookmark, Snippet, SnippetFlag +from cab.models import Bookmark, Snippet, SnippetFlag register = template.Library() @@ -33,7 +33,7 @@ def more_like_this(snippet, limit=None): sqs = Snippet.objects.annotate( search=SearchVector( "language__name", - ) + ), ) sqs = sqs.filter(language__name=snippet.language).exclude(pk=snippet.pk) if limit is not None: diff --git a/cab/templatetags/core_tags.py b/cab/templatetags/core_tags.py index 23f14f2b..c39f212b 100644 --- a/cab/templatetags/core_tags.py +++ b/cab/templatetags/core_tags.py @@ -25,7 +25,7 @@ def latest(model_or_obj, num=5): if isinstance(field, (DateTimeField, DateField)): field_name = field.name break - return manager.all().order_by("-%s" % field_name)[:num] + return manager.all().order_by(f"-{field_name}")[:num] @register.filter diff --git a/cab/templatetags/markup.py b/cab/templatetags/markup.py index b5557eb7..f2cee946 100644 --- a/cab/templatetags/markup.py +++ b/cab/templatetags/markup.py @@ -2,7 +2,7 @@ from django.utils.safestring import mark_safe from markdown import markdown as markdown_func -from ..utils import sanitize_markdown +from cab.utils import sanitize_markdown register = template.Library() diff --git a/cab/tests/tests.py b/cab/tests/tests.py index 457449af..d410cf3b 100644 --- a/cab/tests/tests.py +++ b/cab/tests/tests.py @@ -4,13 +4,12 @@ from django.urls import reverse from rest_framework import status +from cab.api.serializers import SnippetSerializer +from cab.models import Bookmark, Language, Snippet +from cab.templatetags.markup import safe_markdown from cab.views.languages import language_list from cab.views.popular import top_authors, top_tags -from ..api.serializers import SnippetSerializer -from ..models import Bookmark, Language, Snippet -from ..templatetags.markup import safe_markdown - # @skip("These tests don't test production code.") # @override_settings(ROOT_URLCONF='cab.tests.urls') @@ -24,11 +23,19 @@ def setUp(self): self.user_b = User.objects.create_user("b", "b", "b") self.python = Language.objects.create( - name="Python", slug="python", language_code="python", mime_type="text/x-python", file_extension="py" + name="Python", + slug="python", + language_code="python", + mime_type="text/x-python", + file_extension="py", ) self.sql = Language.objects.create( - name="SQL", slug="sql", language_code="sql", mime_type="text/x-sql", file_extension="sql" + name="SQL", + slug="sql", + language_code="sql", + mime_type="text/x-sql", + file_extension="sql", ) self.snippet1 = Snippet.objects.create( @@ -81,7 +88,7 @@ def ensure_login_required(self, url, username, password): self.client.logout() resp = self.client.get(url) - self.assertRedirects(resp, "/accounts/login/?next=%s" % url, fetch_redirect_response=False) + self.assertRedirects(resp, f"/accounts/login/?next={url}", fetch_redirect_response=False) self.client.login(username=username, password=password) @@ -144,7 +151,10 @@ class ModelTestCase(BaseCabTestCase): def test_snippet_escaping(self): self.snippet1.description = '' self.snippet1.save() - self.assertEqual(self.snippet1.description_html, '<script>alert("hacked");</script>') + self.assertEqual( + self.snippet1.description_html, + '<script>alert("hacked");</script>', + ) def test_ratings_hooks(self): # setUp() will actually fire off most of these hooks @@ -218,31 +228,39 @@ def test_bookmark_views(self): self.assertCountEqual(resp.context["object_list"], [self.bookmark2]) add_bookmark = reverse("cab_bookmark_add", args=[self.snippet2.pk]) - self.assertEqual(add_bookmark, "/bookmarks/add/%d/" % self.snippet2.pk) + self.assertEqual(add_bookmark, f"/bookmarks/add/{self.snippet2.pk}/") # add a bookmark -- this does *not* require a POST for some reason so # this test will need to be amended when I get around to fixing this resp = self.ensure_login_required(add_bookmark, "a", "a") - self.assertRedirects(resp, "/snippets/%d/" % self.snippet2.pk) + self.assertRedirects(resp, f"/snippets/{self.snippet2.pk}/") new_bookmark = Bookmark.objects.get(user=self.user_a, snippet=self.snippet2) resp = self.ensure_login_required(user_bookmarks, "a", "a") - self.assertCountEqual(resp.context["object_list"], [self.bookmark1, self.bookmark3, new_bookmark]) + self.assertCountEqual( + resp.context["object_list"], + [self.bookmark1, self.bookmark3, new_bookmark], + ) # make sure we have to log in to delete a bookmark delete_bookmark = reverse("cab_bookmark_delete", args=[self.snippet2.pk]) - self.assertEqual(delete_bookmark, "/bookmarks/delete/%d/" % self.snippet2.pk) + self.assertEqual(delete_bookmark, f"/bookmarks/delete/{self.snippet2.pk}/") resp = self.ensure_login_required(delete_bookmark, "a", "a") # login and post to delete the bookmark self.client.login(username="a", password="a") resp = self.client.post(delete_bookmark) - self.assertRedirects(resp, "/snippets/%d/" % self.snippet2.pk) + self.assertRedirects(resp, f"/snippets/{self.snippet2.pk}/") # the bookmark is gone! - self.assertRaises(Bookmark.DoesNotExist, Bookmark.objects.get, user=self.user_a, snippet=self.snippet2) + self.assertRaises( + Bookmark.DoesNotExist, + Bookmark.objects.get, + user=self.user_a, + snippet=self.snippet2, + ) # check the bookmark list view and make sure resp = self.ensure_login_required(user_bookmarks, "a", "a") @@ -340,11 +358,14 @@ def test_index(self): resp = self.client.get(snippet_index) self.assertEqual(resp.status_code, 200) - self.assertCountEqual(resp.context["object_list"], [self.snippet1, self.snippet2, self.snippet3]) + self.assertCountEqual( + resp.context["object_list"], + [self.snippet1, self.snippet2, self.snippet3], + ) def test_snippet_detail(self): snippet_detail = reverse("cab_snippet_detail", args=[self.snippet1.pk]) - self.assertEqual(snippet_detail, "/snippets/%d/" % self.snippet1.pk) + self.assertEqual(snippet_detail, f"/snippets/{self.snippet1.pk}/") resp = self.client.get(snippet_detail) self.assertEqual(resp.status_code, 200) @@ -352,7 +373,7 @@ def test_snippet_detail(self): def test_snippet_download(self): snippet_download = reverse("cab_snippet_download", args=[self.snippet1.pk]) - self.assertEqual(snippet_download, "/snippets/%d/download/" % self.snippet1.pk) + self.assertEqual(snippet_download, f"/snippets/{self.snippet1.pk}/download/") resp = self.client.get(snippet_download) self.assertEqual(resp["content-type"], "text/x-python") @@ -365,7 +386,7 @@ def test_snippet_rate(self): self.assertEqual(self.snippet1.ratings.count(), 0) snippet_rate = reverse("cab_snippet_rate", args=[self.snippet1.pk]) - self.assertEqual(snippet_rate, "/snippets/%d/rate/" % self.snippet1.pk) + self.assertEqual(snippet_rate, f"/snippets/{self.snippet1.pk}/rate/") resp = self.client.get(snippet_rate + "?score=up") self.assertEqual(resp.status_code, 302) @@ -407,7 +428,7 @@ def test_snippet_unrate_up(self): def test_snippet_edit(self): snippet_edit = reverse("cab_snippet_edit", args=[self.snippet1.pk]) - self.assertEqual(snippet_edit, "/snippets/%d/edit/" % self.snippet1.pk) + self.assertEqual(snippet_edit, f"/snippets/{self.snippet1.pk}/edit/") resp = self.client.get(snippet_edit) self.assertEqual(resp.status_code, 302) @@ -439,14 +460,15 @@ def test_snippet_edit(self): self.assertEqual(snippet1.description_html, "

wazzah

") self.assertEqual(snippet1.code, 'print "Hi"') self.assertEqual([t.name for t in snippet1.tags.all()], ["world", "hi"]) - self.assertRedirects(resp, "/snippets/%d/" % snippet1.pk) + self.assertRedirects(resp, f"/snippets/{snippet1.pk}/") def test_snippet_edit_no_tags(self): """ - The user should be able to create/edit a snippet and remove all tags or create it without any. + The user should be able to create/edit a snippet and remove all tags + or create it without any. """ snippet_edit = reverse("cab_snippet_edit", args=[self.snippet1.pk]) - self.assertEqual(snippet_edit, "/snippets/%d/edit/" % self.snippet1.pk) + self.assertEqual(snippet_edit, f"/snippets/{self.snippet1.pk}/edit/") resp = self.client.get(snippet_edit) self.assertEqual(resp.status_code, 302) @@ -473,7 +495,7 @@ def test_snippet_edit_no_tags(self): self.assertEqual(snippet1.description_html, "

wazzah

") self.assertEqual(snippet1.code, 'print "Hi"') self.assertEqual(0, snippet1.tags.count()) - self.assertRedirects(resp, "/snippets/%d/" % snippet1.pk) + self.assertRedirects(resp, f"/snippets/{snippet1.pk}/") def test_snippet_add(self): snippet_add = reverse("cab_snippet_add") @@ -497,12 +519,14 @@ def test_snippet_add(self): self.assertEqual(new_snippet.description_html, "

wazzah

") self.assertEqual(new_snippet.code, 'print "Hi"') self.assertEqual([t.name for t in new_snippet.tags.all()], ["world", "hi"]) - self.assertRedirects(resp, "/snippets/%d/" % new_snippet.pk) + self.assertRedirects(resp, f"/snippets/{new_snippet.pk}/") class TemplatetagTestCase(BaseCabTestCase): def test_cab_tags(self): - t = Template("""{% load cab_tags %}{% if snippet|is_bookmarked:user %}Y{% else %}N{% endif %}""") + t = Template( + """{% load cab_tags %}{% if snippet|is_bookmarked:user %}Y{% else %}N{% endif %}""", + ) c = Context({"snippet": self.snippet1, "user": self.user_a}) rendered = t.render(c) @@ -518,13 +542,16 @@ def test_cab_tags(self): self.assertEqual(rendered, "N") def test_core_tags(self): - t = Template("""{% load core_tags %}{% for s in "cab.snippet"|latest:2 %}{{ s.title }}|{% endfor %}""") + t = Template( + """{% load core_tags %}""" + """{% for s in "cab.snippet"|latest:2 %}{{ s.title }}|{% endfor %}""", + ) rendered = t.render(Context({})) - self.assertEqual(rendered, "%s|%s|" % (self.snippet3.title, self.snippet2.title)) + self.assertEqual(rendered, f"{self.snippet3.title}|{self.snippet2.title}|") t = Template( '{% load core_tags %}{% for t in "cab.snippet"|call_manager:"top_tags"|slice:":2" %}' - "{{ t.name }}|{% endfor %}" + "{{ t.name }}|{% endfor %}", ) rendered = t.render(Context({})) self.assertEqual(rendered, "world|goodbye|") @@ -542,7 +569,10 @@ def test_index(self): self.assertEqual(search_index, "/search/") resp = self.client.get(search_index) self.assertEqual(resp.status_code, 200) - self.assertCountEqual(resp.context["object_list"], [self.snippet1, self.snippet2, self.snippet3]) + self.assertCountEqual( + resp.context["object_list"], + [self.snippet1, self.snippet2, self.snippet3], + ) def test_q_search(self): search_index = reverse("cab_search") @@ -562,11 +592,19 @@ def setUp(self): self.user_b = User.objects.create_user("b", "b", "b") self.python = Language.objects.create( - name="Python", slug="python", language_code="python", mime_type="text/x-python", file_extension="py" + name="Python", + slug="python", + language_code="python", + mime_type="text/x-python", + file_extension="py", ) self.sql = Language.objects.create( - name="SQL", slug="sql", language_code="sql", mime_type="text/x-sql", file_extension="sql" + name="SQL", + slug="sql", + language_code="sql", + mime_type="text/x-sql", + file_extension="sql", ) self.snippet1 = Snippet.objects.create( diff --git a/cab/urls/bookmarks.py b/cab/urls/bookmarks.py index 6142c729..a325a995 100644 --- a/cab/urls/bookmarks.py +++ b/cab/urls/bookmarks.py @@ -1,9 +1,13 @@ from django.urls import path -from ..views import bookmarks +from cab.views import bookmarks urlpatterns = [ path("", bookmarks.user_bookmarks, name="cab_user_bookmarks"), path("add//", bookmarks.add_bookmark, name="cab_bookmark_add"), - path("delete//", bookmarks.delete_bookmark, name="cab_bookmark_delete"), + path( + "delete//", + bookmarks.delete_bookmark, + name="cab_bookmark_delete", + ), ] diff --git a/cab/urls/feeds.py b/cab/urls/feeds.py index 0c1e78d5..826a47bf 100644 --- a/cab/urls/feeds.py +++ b/cab/urls/feeds.py @@ -1,10 +1,14 @@ from django.urls import path -from .. import feeds +from cab import feeds urlpatterns = [ path("author//", feeds.SnippetsByAuthorFeed(), name="cab_feed_author"), - path("language//", feeds.SnippetsByLanguageFeed(), name="cab_feed_language"), + path( + "language//", + feeds.SnippetsByLanguageFeed(), + name="cab_feed_language", + ), path("latest/", feeds.LatestSnippetsFeed(), name="cab_feed_latest"), path("tag//", feeds.SnippetsByTagFeed(), name="cab_feed_tag"), ] diff --git a/cab/urls/languages.py b/cab/urls/languages.py index 829b48f8..6cd501d3 100644 --- a/cab/urls/languages.py +++ b/cab/urls/languages.py @@ -1,6 +1,6 @@ from django.urls import path -from ..views import languages +from cab.views import languages urlpatterns = [ path("", languages.language_list, name="cab_language_list"), diff --git a/cab/urls/popular.py b/cab/urls/popular.py index 54e87c38..0ab4c48d 100644 --- a/cab/urls/popular.py +++ b/cab/urls/popular.py @@ -1,6 +1,6 @@ from django.urls import path -from ..views import popular +from cab.views import popular urlpatterns = [ path("languages/", popular.top_languages, name="cab_top_languages"), diff --git a/cab/urls/search.py b/cab/urls/search.py index 2f7f4306..d2089465 100644 --- a/cab/urls/search.py +++ b/cab/urls/search.py @@ -1,6 +1,6 @@ from django.urls import path -from ..views.snippets import advanced_search, autocomplete, basic_search +from cab.views.snippets import advanced_search, autocomplete, basic_search urlpatterns = [ path("", basic_search, name="cab_search"), diff --git a/cab/urls/snippets.py b/cab/urls/snippets.py index f1815667..fc85d6e5 100644 --- a/cab/urls/snippets.py +++ b/cab/urls/snippets.py @@ -1,12 +1,16 @@ from django.urls import path -from ..views import snippets +from cab.views import snippets urlpatterns = [ path("", snippets.snippet_list, name="cab_snippet_list"), path("/", snippets.snippet_detail, name="cab_snippet_detail"), path("/rate/", snippets.rate_snippet, name="cab_snippet_rate"), - path("/download/", snippets.download_snippet, name="cab_snippet_download"), + path( + "/download/", + snippets.download_snippet, + name="cab_snippet_download", + ), path("/raw/", snippets.raw_snippet, name="cab_snippet_raw"), path("/edit/", snippets.edit_snippet, name="cab_snippet_edit"), path("/flag/", snippets.flag_snippet, name="cab_snippet_flag"), diff --git a/cab/urls/tags.py b/cab/urls/tags.py index 17b464ae..9ce3b476 100644 --- a/cab/urls/tags.py +++ b/cab/urls/tags.py @@ -1,6 +1,6 @@ from django.urls import path -from ..views import popular, snippets +from cab.views import popular, snippets urlpatterns = [ path("", popular.top_tags, name="cab_top_tags"), diff --git a/cab/urls/users.py b/cab/urls/users.py index 0b8e2dc7..d21c643d 100644 --- a/cab/urls/users.py +++ b/cab/urls/users.py @@ -1,6 +1,6 @@ from django.urls import path -from ..views import popular, snippets +from cab.views import popular, snippets urlpatterns = [ path("", popular.top_authors, name="cab_top_authors"), diff --git a/cab/utils.py b/cab/utils.py index 48e93c54..6a85ea27 100644 --- a/cab/utils.py +++ b/cab/utils.py @@ -1,4 +1,5 @@ import datetime +from zoneinfo import ZoneInfo import bleach from django.core.exceptions import ObjectDoesNotExist @@ -11,8 +12,11 @@ from .main import SnippetList +# Constants +MAX_MONTHS_AGO = 48 -def object_list( + +def object_list( # noqa: PLR0913 request, queryset, paginate_by=None, @@ -70,12 +74,12 @@ def object_list( else: context[key] = value if not template_name: - template_name = "%s/%s_list.html" % (opts.app_label, opts.object_name.lower()) + template_name = f"{opts.app_label}/{opts.object_name.lower()}_list.html" template = template_loader.get_template(template_name) return HttpResponse(template.render(context, request=request), content_type=content_type) -def object_detail( +def object_detail( # noqa: PLR0913 request, queryset, object_id=None, @@ -101,7 +105,9 @@ def object_detail( extra_context = {} model = queryset.model if not object_id or (slug and slug_field): - raise AttributeError("Generic detail view must be called with either " "an object_id or a slug/slug_field.") + raise AttributeError( + "Generic detail view must be called with either an object_id or a slug/slug_field.", + ) if object_id: queryset = queryset.filter(pk=object_id) elif slug and slug_field: @@ -109,10 +115,11 @@ def object_detail( try: obj = queryset.get() - except ObjectDoesNotExist: - raise Http404("No %s found matching the query" % model._meta.verbose_name) + except ObjectDoesNotExist as e: + msg = f"No {model._meta.verbose_name} found matching the query" + raise Http404(msg) from e if not template_name: - template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = f"{model._meta.app_label}/{model._meta.object_name.lower()}_detail.html" if template_name_field: template_name_list = [getattr(obj, template_name_field), template_name] t = template_loader.select_template(template_name_list) @@ -126,15 +133,14 @@ def object_detail( c[key] = value() else: c[key] = value - response = HttpResponse(t.render(c, request=request), content_type=content_type) - return response + return HttpResponse(t.render(c, request=request), content_type=content_type) def get_past_datetime(months_ago): - now = datetime.datetime.now() + now = datetime.datetime.now(tz=ZoneInfo("America/Chicago")) # a little stupid validation here: - if months_ago < 1 or months_ago > 48: + if months_ago < 1 or months_ago > MAX_MONTHS_AGO: return now return now - datetime.timedelta(days=months_ago * 31) @@ -179,5 +185,5 @@ def sanitize_markdown(value): "strong", "ul", ], - ) + ), ) diff --git a/cab/views/bookmarks.py b/cab/views/bookmarks.py index 80a42e86..cb8dc1a9 100644 --- a/cab/views/bookmarks.py +++ b/cab/views/bookmarks.py @@ -1,8 +1,8 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect, render -from ..models import Bookmark, Snippet -from ..utils import object_list +from cab.models import Bookmark, Snippet +from cab.utils import object_list @login_required @@ -32,5 +32,4 @@ def delete_bookmark(request, snippet_id): if request.method == "POST": bookmark.delete() return redirect(bookmark.snippet) - else: - return render(request, "cab/confirm_bookmark_delete.html", {"snippet": bookmark.snippet}) + return render(request, "cab/confirm_bookmark_delete.html", {"snippet": bookmark.snippet}) diff --git a/cab/views/languages.py b/cab/views/languages.py index 83e3fd18..4be9271d 100644 --- a/cab/views/languages.py +++ b/cab/views/languages.py @@ -1,7 +1,7 @@ from django.shortcuts import get_object_or_404 -from ..models import Language -from ..utils import month_object_list, object_list +from cab.models import Language +from cab.utils import month_object_list, object_list def language_list(request): diff --git a/cab/views/popular.py b/cab/views/popular.py index 3ea73e9d..1c9a2f91 100644 --- a/cab/views/popular.py +++ b/cab/views/popular.py @@ -1,5 +1,5 @@ -from ..models import Language, Snippet -from ..utils import month_object_list, object_list +from cab.models import Language, Snippet +from cab.utils import month_object_list, object_list def top_authors(request): @@ -12,13 +12,19 @@ def top_authors(request): ) return object_list( - request, queryset=Snippet.objects.top_authors(), template_name="cab/top_authors.html", paginate_by=20 + request, + queryset=Snippet.objects.top_authors(), + template_name="cab/top_authors.html", + paginate_by=20, ) def top_languages(request): return object_list( - request, queryset=Language.objects.top_languages(), template_name="cab/language_list.html", paginate_by=20 + request, + queryset=Language.objects.top_languages(), + template_name="cab/language_list.html", + paginate_by=20, ) diff --git a/cab/views/snippets.py b/cab/views/snippets.py index 127b7ae9..ba96ebf8 100644 --- a/cab/views/snippets.py +++ b/cab/views/snippets.py @@ -11,9 +11,12 @@ from django.urls import reverse from taggit.models import Tag -from ..forms import AdvancedSearchForm, SnippetFlagForm, SnippetForm -from ..models import Language, Snippet, SnippetFlag -from ..utils import month_object_list, object_detail +from cab.forms import AdvancedSearchForm, SnippetFlagForm, SnippetForm +from cab.models import Language, Snippet, SnippetFlag +from cab.utils import month_object_list, object_detail + +# Constants +MIN_QUERY_LENGTH = 2 def snippet_list(request, queryset=None, **kwargs): @@ -35,10 +38,8 @@ def snippet_detail(request, snippet_id): def download_snippet(request, snippet_id): snippet = get_object_or_404(Snippet, pk=snippet_id) response = HttpResponse(snippet.code, content_type="text/plain") - response["Content-Disposition"] = "attachment; filename=%s.%s" % ( - snippet.id, - snippet.language.file_extension, - ) + filename = f"{snippet.id}.{snippet.language.file_extension}" + response["Content-Disposition"] = f"attachment; filename={filename}" response["Content-Type"] = snippet.language.mime_type return response @@ -101,17 +102,16 @@ def flag_snippet(request, snippet_id, template_name="cab/flag_snippet.html"): admin_link = request.build_absolute_uri(reverse("admin:cab_snippetflag_changelist")) mail_admins( - 'Snippet flagged: "%s"' % (snippet.title), - "%s\n\nAdmin link: %s" % (snippet_flag, admin_link), + f'Snippet flagged: "{snippet.title}"', + f"{snippet_flag}\n\nAdmin link: {admin_link}", fail_silently=True, ) messages.info(request, "Thank you for helping improve the site!") return redirect(snippet) - else: - if request.is_ajax(): - return redirect(snippet) - messages.error(request, "Invalid form submission") + if request.is_ajax(): + return redirect(snippet) + messages.error(request, "Invalid form submission") else: form = SnippetFlagForm(instance=snippet_flag) return render(request, template_name, {"form": form, "snippet": snippet}) @@ -144,7 +144,9 @@ def search(request): snippet_qs = Snippet.objects.none() if query: snippet_qs = ( - Snippet.objects.filter(Q(title__icontains=query) | Q(tags__in=[query]) | Q(author__username__iexact=query)) + Snippet.objects.filter( + Q(title__icontains=query) | Q(tags__in=[query]) | Q(author__username__iexact=query), + ) .distinct() .order_by("-rating_score", "-pub_date") ) @@ -158,10 +160,9 @@ def search(request): def autocomplete(request): - q = request.GET.get("q", "") results = [] - if len(q) > 2: + if len(q) > MIN_QUERY_LENGTH: result_set = Snippet.objects.annotate(search=SearchVector("title")).filter(search=q)[:10] for obj in result_set: url = obj.get_absolute_url() @@ -172,19 +173,23 @@ def autocomplete(request): def tag_hint(request): q = request.GET.get("q", "") results = [] - if len(q) > 2: + if len(q) > MIN_QUERY_LENGTH: tag_qs = Tag.objects.filter(slug__startswith=q) annotated_qs = tag_qs.annotate(count=Count("taggit_taggeditem_items__id")) - for obj in annotated_qs.order_by("-count", "slug")[:10]: - results.append({"tag": obj.slug, "count": obj.count}) + results = [ + {"tag": obj.slug, "count": obj.count} + for obj in annotated_qs.order_by("-count", "slug")[:10] + ] return HttpResponse(json.dumps(results), content_type="application/json") def basic_search(request): q = request.GET.get("q") - snippet_qs = Snippet.objects.annotate(search=SearchVector("title", "description", "author__username")) + snippet_qs = Snippet.objects.annotate( + search=SearchVector("title", "description", "author__username"), + ) form = AdvancedSearchForm(request.GET) if form.is_valid(): @@ -199,7 +204,6 @@ def basic_search(request): def advanced_search(request): - snippet_qs = Snippet.objects.annotate( search=SearchVector( "title", @@ -210,7 +214,7 @@ def advanced_search(request): "bookmark_count", "rating_score", "author__username", - ) + ), ) form = AdvancedSearchForm(request.GET) if form.is_valid(): diff --git a/comments_spamfighter/admin.py b/comments_spamfighter/admin.py index cd7ef210..c4b17854 100644 --- a/comments_spamfighter/admin.py +++ b/comments_spamfighter/admin.py @@ -9,7 +9,7 @@ class KeywordAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): - super(KeywordAdminForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.initial["fields"] = self.instance.fields.split(",") keyword = forms.CharField( @@ -17,8 +17,8 @@ def __init__(self, *args, **kwargs): attrs={ "style": "height: 1.5em; line-height: 1.5em; width: 40em;", "class": "vLargeTextField", - } - ) + }, + ), ) fields = forms.MultipleChoiceField(label=_("Fields to check"), choices=Keyword.FIELD_CHOICES) @@ -31,8 +31,9 @@ def clean(self): if self.cleaned_data["is_regex"]: try: re.match(self.cleaned_data["keyword"], "", re.MULTILINE) - except Exception as e: - raise forms.ValidationError(_('This regular expression is not valid. Error message was: "%s"' % e)) + except re.error as e: + msg = _('This regular expression is not valid. Error message was: "%s"') % str(e) + raise forms.ValidationError(msg) from e return self.cleaned_data class Meta: @@ -53,7 +54,7 @@ class KeywordAdmin(admin.ModelAdmin): "active", ("keyword", "is_regex"), "fields", - ) + ), }, ), ) diff --git a/comments_spamfighter/migrations/0001_initial.py b/comments_spamfighter/migrations/0001_initial.py index 2d7dad26..59ee256e 100644 --- a/comments_spamfighter/migrations/0001_initial.py +++ b/comments_spamfighter/migrations/0001_initial.py @@ -1,24 +1,40 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [] operations = [ migrations.CreateModel( name="Keyword", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), ("active", models.BooleanField(default=True, verbose_name="Active")), ("keyword", models.TextField(verbose_name="Keyword")), - ("is_regex", models.BooleanField(default=False, verbose_name="Is a regular expression")), - ("fields", models.TextField(max_length=255, verbose_name="Fields to check")), - ("created", models.DateTimeField(auto_now_add=True, verbose_name="Created")), - ("modified", models.DateTimeField(auto_now=True, verbose_name="Modified")), + ( + "is_regex", + models.BooleanField(default=False, verbose_name="Is a regular expression"), + ), + ( + "fields", + models.TextField(max_length=255, verbose_name="Fields to check"), + ), + ( + "created", + models.DateTimeField(auto_now_add=True, verbose_name="Created"), + ), + ( + "modified", + models.DateTimeField(auto_now=True, verbose_name="Modified"), + ), ], options={ "ordering": ("keyword", "created"), diff --git a/comments_spamfighter/models.py b/comments_spamfighter/models.py index 3fe937db..604eef91 100644 --- a/comments_spamfighter/models.py +++ b/comments_spamfighter/models.py @@ -4,7 +4,6 @@ class Keyword(models.Model): - # Default field choices. These are good inital values for the default # Django comments but you can override it using the settings variable # COMMENTS_CHECK_FIELDS_CHOICES diff --git a/comments_spamfighter/moderation.py b/comments_spamfighter/moderation.py index 9fccecd9..910fbd0d 100644 --- a/comments_spamfighter/moderation.py +++ b/comments_spamfighter/moderation.py @@ -31,16 +31,16 @@ def _keyword_check(self, comment, content_object, request): """ # Iterate over all keywords for keyword in Keyword.objects.filter(active=True): - # Iterate over all fields for field_name in keyword.fields.split(","): - # Check that the given field is in the comments class. If # settings.DEBUG is False, fail silently. field_value = getattr(comment, field_name, None) if not field_value: if settings.DEBUG: - raise ImproperlyConfigured('"%s" is not a field within your comments class.') + raise ImproperlyConfigured( + '"%s" is not a field within your comments class.', + ) continue # A regular expression check against the field value. @@ -49,9 +49,8 @@ def _keyword_check(self, comment, content_object, request): return True # A simple string check against the field value. - else: - if keyword.keyword.lower() in field_value.lower(): - return True + elif keyword.keyword.lower() in field_value.lower(): + return True return False def _akismet_check(self, comment, content_object, request): @@ -61,13 +60,15 @@ def _akismet_check(self, comment, content_object, request): """ # Check if the akismet api key is set, fail silently if # settings.DEBUG is False and return False (not moderated) - AKISMET_API_KEY = getattr(settings, "AKISMET_SECRET_API_KEY", False) - if not AKISMET_API_KEY: - raise ImproperlyConfigured("You must set AKISMET_SECRET_API_KEY with your api key in your settings file.") + akismet_api_key = getattr(settings, "AKISMET_SECRET_API_KEY", False) + if not akismet_api_key: + raise ImproperlyConfigured( + "You must set AKISMET_SECRET_API_KEY with your api key in your settings file.", + ) akismet_api = Akismet( - AKISMET_API_KEY, - blog="%s://%s/" % (request.scheme, Site.objects.get_current().domain), + akismet_api_key, + blog=f"{request.scheme}://{Site.objects.get_current().domain}/", ) return akismet_api.check( comment.ip_address, @@ -84,7 +85,7 @@ def allow(self, comment, content_object, request): otherwise. """ # Original CommentModerator check - orig_allow = super(SpamFighterModerator, self).allow(comment, content_object, request) + orig_allow = super().allow(comment, content_object, request) if not orig_allow: return False @@ -98,15 +99,12 @@ def allow(self, comment, content_object, request): return False # Akismet check - if ( + # Return False if akismet marks this comment as spam. + return not ( self.akismet_check and not self.akismet_check_moderate and self._akismet_check(comment, content_object, request) - ): - # Return False if akismet marks this comment as spam. - return False - - return True + ) def moderate(self, comment, content_object, request): """ @@ -117,7 +115,7 @@ def moderate(self, comment, content_object, request): Return ``True`` if the comment should be moderated (marked non-public), ``False`` otherwise. """ - orig_moderate = super(SpamFighterModerator, self).moderate(comment, content_object, request) + orig_moderate = super().moderate(comment, content_object, request) if orig_moderate: return True @@ -131,12 +129,9 @@ def moderate(self, comment, content_object, request): return True # Akismet check - if ( + # Return True if akismet marks this comment as spam and we want to moderate it. + return ( self.akismet_check and self.akismet_check_moderate and self._akismet_check(comment, content_object, request) - ): - # Return True if akismet marks this comment as spam and we want to moderate it. - return True - - return False + ) diff --git a/djangosnippets/settings/base.py b/djangosnippets/settings/base.py index a8758c42..550ee9f4 100644 --- a/djangosnippets/settings/base.py +++ b/djangosnippets/settings/base.py @@ -13,13 +13,16 @@ def user_url(user): return reverse("cab_author_snippets", kwargs={"username": user.username}) -PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) +PROJECT_ROOT = str(Path(__file__).resolve().parent) BASE_DIR = Path(__file__).resolve().parent.parent.parent SITE_ID = 1 SITE_NAME = "djangosnippets.org" -ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "djangosnippets.org,www.djangosnippets.org").split(",") +ALLOWED_HOSTS = os.environ.get( + "ALLOWED_HOSTS", + "djangosnippets.org,www.djangosnippets.org", +).split(",") DEBUG = False @@ -93,7 +96,7 @@ def user_url(user): TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(PROJECT_ROOT, "templates")], + "DIRS": [str(Path(PROJECT_ROOT) / "templates")], "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", @@ -111,18 +114,18 @@ def user_url(user): "django.template.loaders.app_directories.Loader", "django_components.template_loader.Loader", ], - ) + ), ], "builtins": [ "django_components.templatetags.component_tags", ], }, - } + }, ] STATIC_URL = "/assets/static/" -STATIC_ROOT = os.path.join(PROJECT_ROOT, "..", "assets", "static") -STATICFILES_DIRS = (os.path.join(PROJECT_ROOT, "static"),) +STATIC_ROOT = str(Path(PROJECT_ROOT) / ".." / "assets" / "static") +STATICFILES_DIRS = (str(Path(PROJECT_ROOT) / "static"),) STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", @@ -207,7 +210,9 @@ def user_url(user): REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. - "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly"] + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly", + ], } DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/djangosnippets/settings/development.py b/djangosnippets/settings/development.py index d8276dec..24aa0b31 100644 --- a/djangosnippets/settings/development.py +++ b/djangosnippets/settings/development.py @@ -8,9 +8,7 @@ CACHE_BACKEND = "dummy://" -INSTALLED_APPS = INSTALLED_APPS - -TEMPLATES[0]["OPTIONS"]["loaders"] = [ +TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa: F405 "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", "django_components.template_loader.Loader", diff --git a/djangosnippets/settings/production.py b/djangosnippets/settings/production.py index 16462a13..962e5d99 100644 --- a/djangosnippets/settings/production.py +++ b/djangosnippets/settings/production.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import os from urllib import parse @@ -7,39 +5,37 @@ import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration -from .base import * # noqa +from .base import * # noqa: F403 -def env_to_bool(input): +def env_to_bool(input): # noqa: A002 """ Must change String from environment variable into Boolean defaults to True """ if isinstance(input, str): return input not in ("False", "false") - else: - return input + return input -DEBUG = env_to_bool(os.environ.get("DEBUG", False)) +DEBUG = env_to_bool(os.environ.get("DEBUG", "False")) AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "") AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "") AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME", "") AWS_S3_CUSTOM_DOMAIN = os.environ.get("AWS_S3_CUSTOM_DOMAIN", "") AWS_PRELOAD_METADATA = True -# AWS_IS_GZIPPED = True AWS_S3_USE_SSL = True AWS_QUERYSTRING_AUTH = False AWS_S3_URL_PROTOCOL = "//:" -SECURE_SSL_REDIRECT = os.environ.get("SECURE_SSL_REDIRECT", False) +SECURE_SSL_REDIRECT = env_to_bool(os.environ.get("SECURE_SSL_REDIRECT", "False")) SECURE_HSTS_SECONDS = 600 SECURE_HSTS_INCLUDE_SUBDOMAINS = False SECURE_FRAME_DENY = True SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_BROWSER_XSS_FILTER = True -SESSION_COOKIE_SECURE = env_to_bool(os.environ.get("SESSION_COOKIE_SECURE", True)) +SESSION_COOKIE_SECURE = env_to_bool(os.environ.get("SESSION_COOKIE_SECURE", "True")) SESSION_COOKIE_HTTPONLY = True # The header Heroku uses to indicate SSL: @@ -63,7 +59,7 @@ def env_to_bool(input): CACHES = { "default": { "BACKEND": "redis_cache.RedisCache", - "LOCATION": "%s:%s" % (redis_url.hostname, redis_url.port), + "LOCATION": f"{redis_url.hostname}:{redis_url.port}", "OPTIONS": { "PASSWORD": redis_url.password, "DB": 0, @@ -73,11 +69,15 @@ def env_to_bool(input): "timeout": 20, }, }, - } + }, } # Use Sentry for debugging if available. if "SENTRY_DSN" in os.environ: - sentry_sdk.init(dsn=os.environ.get("SENTRY_DSN"), integrations=[DjangoIntegration()], send_default_pii=True) + sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + integrations=[DjangoIntegration()], + send_default_pii=True, + ) EMAIL_HOST = "smtp.sendgrid.net" @@ -94,14 +94,20 @@ def env_to_bool(input): "handlers": ["sentry"], }, "formatters": { - "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"}, + "verbose": { + "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", + }, }, "handlers": { "sentry": { "level": "ERROR", "class": "raven.contrib.django.handlers.SentryHandler", }, - "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}, + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, }, "loggers": { "django.db.backends": { diff --git a/djangosnippets/settings/testing.py b/djangosnippets/settings/testing.py index b2081951..b4eac79e 100644 --- a/djangosnippets/settings/testing.py +++ b/djangosnippets/settings/testing.py @@ -1,4 +1,7 @@ -from djangosnippets.settings.base import * # noqa F403 +import os # noqa: F401 +from pathlib import Path + +from djangosnippets.settings.base import * # noqa: F403 SITE_ID = 1 ROOT_URLCONF = "cab.tests.urls" @@ -43,15 +46,15 @@ PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) -SNIPPETS_TEMPLATES_DIR = os.path.join( - os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir, "djangosnippets", "templates" +SNIPPETS_TEMPLATES_DIR = str( + Path(__file__).resolve().parent.parent.parent / "djangosnippets" / "templates", ) TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [ - os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "cab", "tests", "templates"), + str(Path(__file__).resolve().parent.parent.parent / "cab" / "tests" / "templates"), SNIPPETS_TEMPLATES_DIR, ], "OPTIONS": { @@ -69,7 +72,7 @@ "django_components.templatetags.component_tags", ], }, - } + }, ] CAB_VERSIONS = ( diff --git a/djangosnippets/urls.py b/djangosnippets/urls.py index 5da01a6b..bff4113d 100644 --- a/djangosnippets/urls.py +++ b/djangosnippets/urls.py @@ -6,8 +6,7 @@ def trigger_sentry_error(request): - division_by_zero = 1 / 0 - return division_by_zero + return 1 / 0 urlpatterns = [ diff --git a/pyproject.toml b/pyproject.toml index f045162b..fc3aa4f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,68 @@ -[tool.black] -line-length = 119 -include = '\.pyi?$' +[tool.ruff] +target-version = "py311" +line-length = 100 -[tool.isort] -profile = "black" -line_length = 119 +[tool.ruff.lint] +select = [ + "A", + # "ANN", # flake8-annotations: we should support this in the future but many errors atm + "ASYNC", + "B", + "BLE", + "C4", + "C90", + "COM", + "DTZ", + "E", + "EM", + "ERA", + "EXE", + "F", + "FA", + "FLY", + "G", + "I", + "ICN", + "INP", + "INT", + "ISC", + "N", + "PD", + "PERF", + "PGH", + "PIE", + "PL", + "PT", + # "ARG", # Unused function argument + "PTH", + "PYI", + "Q", + "RET", + "RSE", + # "FURB", + # "LOG", + "RUF", + "SIM", + "SLF", + "SLOT", + "T10", + "TC", + "TID", + "TRY", + "UP", + "W", + "YTT", +] + +ignore = [ + "EM101", # Exception must not use a string literal, assign to variable first + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/ + "SIM102", # sometimes it's better to nest + "TRY003", # Avoid specifying long messages outside the exception class + "SLF001", # Private member accessed + "PLC0415", # `import` should be at the top-level of a file + "PT009", # Use a regular assert instead of unittest-style + "PT027", # Use `pytest.raises` instead of unittest-style + "RET503", # Missing explicit return at the end of function able to return non-None value +] diff --git a/ratings/converters.py b/ratings/converters.py index 7ed0d4ab..962a58d6 100644 --- a/ratings/converters.py +++ b/ratings/converters.py @@ -2,7 +2,7 @@ class FloatConverter: regex = "-?([0-9]*[.])?[0-9]" def to_python(self, value): - return "." in value and float(value) or int(value) + return ("." in value and float(value)) or int(value) def to_url(self, value): return str(value) diff --git a/ratings/migrations/0001_initial.py b/ratings/migrations/0001_initial.py index 7ef21108..2355c234 100644 --- a/ratings/migrations/0001_initial.py +++ b/ratings/migrations/0001_initial.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("contenttypes", "0001_initial"), @@ -16,9 +12,20 @@ class Migration(migrations.Migration): migrations.CreateModel( name="RatedItem", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), ("score", models.FloatField(default=0, db_index=True)), - ("hashed", models.CharField(max_length=40, editable=False, db_index=True)), + ( + "hashed", + models.CharField(max_length=40, editable=False, db_index=True), + ), ("object_id", models.IntegerField()), ( "content_type", @@ -45,7 +52,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name="SimilarItem", fields=[ - ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), ("object_id", models.IntegerField()), ("similar_object_id", models.IntegerField()), ("score", models.FloatField(default=0)), diff --git a/ratings/migrations/0002_alter_rateditem_user.py b/ratings/migrations/0002_alter_rateditem_user.py index 0454b800..06ab1dee 100644 --- a/ratings/migrations/0002_alter_rateditem_user.py +++ b/ratings/migrations/0002_alter_rateditem_user.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("ratings", "0001_initial"), @@ -17,7 +16,9 @@ class Migration(migrations.Migration): model_name="rateditem", name="user", field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, related_name="%(class)ss", to=settings.AUTH_USER_MODEL + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)ss", + to=settings.AUTH_USER_MODEL, ), ), ] diff --git a/ratings/models.py b/ratings/models.py index 8c3e80b3..d8958460 100644 --- a/ratings/models.py +++ b/ratings/models.py @@ -19,16 +19,16 @@ class Meta: abstract = True def __str__(self): - return "%s rated %s by %s" % (self.content_object, self.score, self.user) + return f"{self.content_object} rated {self.score} by {self.user}" def save(self, *args, **kwargs): self.hashed = self.generate_hash() - super(RatedItemBase, self).save(*args, **kwargs) + super().save(*args, **kwargs) def generate_hash(self): content_field = self._meta.get_field("content_object") related_object = getattr(self, content_field.name) - uniq = "%s.%s" % (related_object._meta, related_object.pk) + uniq = f"{related_object._meta}.{related_object.pk}" return hashlib.sha1(uniq.encode("ascii")).hexdigest() @classmethod @@ -51,7 +51,10 @@ class RatedItem(RatedItemBase): @classmethod def lookup_kwargs(cls, instance): - return {"object_id": instance.pk, "content_type": ContentType.objects.get_for_model(instance)} + return { + "object_id": instance.pk, + "content_type": ContentType.objects.get_for_model(instance), + } @classmethod def base_kwargs(cls, model_class): @@ -66,16 +69,16 @@ def __init__(self, rating_model=None): def contribute_to_class(self, cls, name): # set up the ForeignRelatedObjectsDescriptor right hyah setattr(cls, name, _RatingsDescriptor(cls, self.rating_model, name)) - setattr(cls, "_ratings_field", name) + cls._ratings_field = name class RatingsQuerySet(QuerySet): def __init__(self, model=None, query=None, using=None, hints=None, rated_model=None): self.rated_model = rated_model - super(RatingsQuerySet, self).__init__(model, query, using, hints) + super().__init__(model, query, using, hints) def _clone(self, *args, **kwargs): - instance = super(RatingsQuerySet, self)._clone(*args, **kwargs) + instance = super()._clone(*args, **kwargs) instance.rated_model = self.rated_model return instance @@ -85,18 +88,25 @@ def order_by_rating(self, aggregator=models.Sum, descending=True, queryset=None, if queryset is None: queryset = self.rated_model._default_manager.all() - ordering = descending and "-%s" % alias or alias + ordering = (descending and f"-{alias}") or alias if not is_gfk(related_field): query_name = related_field.related_query_name() if len(self.query.where.children): - queryset = queryset.filter(**{"%s__pk__in" % query_name: self.values_list("pk")}) + queryset = queryset.filter(**{f"{query_name}__pk__in": self.values_list("pk")}) - return queryset.annotate(**{alias: aggregator("%s__score" % query_name)}).order_by(ordering) + return queryset.annotate(**{alias: aggregator(f"{query_name}__score")}).order_by( + ordering, + ) - else: - return generic_annotate(queryset, self, aggregator("score"), related_field, alias=alias).order_by(ordering) + return generic_annotate( + queryset, + self, + aggregator("score"), + related_field, + alias=alias, + ).order_by(ordering) class _RatingsDescriptor(models.Manager): @@ -131,7 +141,7 @@ def delete_manager(self, instance): """ return self.create_manager(instance, self.rating_model._base_manager.__class__) - def create_manager(self, instance, superclass): + def create_manager(self, instance, superclass): # noqa: C901 """ Dynamically create a RelatedManager to handle the back side of the (G)FK """ @@ -147,7 +157,8 @@ def add(self, *objs): lookup_kwargs = rel_model.lookup_kwargs(instance) for obj in objs: if not isinstance(obj, self.model): - raise TypeError("'%s' instance expected" % self.model._meta.object_name) + msg = f"'{self.model._meta.object_name}' instance expected" + raise TypeError(msg) for k, v in lookup_kwargs.items(): setattr(obj, k, v) obj.save() @@ -156,13 +167,13 @@ def add(self, *objs): def create(self, **kwargs): kwargs.update(rel_model.lookup_kwargs(instance)) - return super(RelatedManager, self).create(**kwargs) + return super().create(**kwargs) create.alters_data = True def get_or_create(self, **kwargs): kwargs.update(rel_model.lookup_kwargs(instance)) - return super(RelatedManager, self).get_or_create(**kwargs) + return super().get_or_create(**kwargs) get_or_create.alters_data = True @@ -170,7 +181,8 @@ def remove(self, *objs): for obj in objs: # Is obj actually part of this descriptor set? if obj not in self.all(): - raise rel_model.DoesNotExist("%r is not related to %r." % (obj, instance)) + err_msg = f"{obj} is not related to {instance}." + raise rel_model.DoesNotExist(err_msg) obj.delete() @@ -264,4 +276,4 @@ class SimilarItem(models.Model): objects = SimilarItemManager() def __str__(self): - return "%s (%s)" % (self.similar_object, self.score) + return f"{self.similar_object} ({self.score})" diff --git a/ratings/tests/tests.py b/ratings/tests/tests.py index 84bf8e8d..c84c71f5 100644 --- a/ratings/tests/tests.py +++ b/ratings/tests/tests.py @@ -8,11 +8,10 @@ from django.urls import reverse from base.tests.models import Beverage, BeverageRating, Food - -from .. import utils as ratings_utils -from .. import views as ratings_views -from ..models import RatedItem -from ..utils import ( +from ratings import utils as ratings_utils +from ratings import views as ratings_views +from ratings.models import RatedItem +from ratings.utils import ( calculate_similar_items, recommendations, recommended_items, @@ -22,7 +21,7 @@ ) -def skipUnlessDB(engine): +def skipUnlessDB(engine): # noqa: N802 """ This decorator makes a test skip unless the current connection uses the specified engine. @@ -31,7 +30,9 @@ def skipUnlessDB(engine): actual_engine = settings.DATABASES["default"]["ENGINE"] if engine not in actual_engine: - return unittest.skip("Unsupported connection engine: %s (expected %s)" % (actual_engine, engine)) + return unittest.skip( + f"Unsupported connection engine: {actual_engine} (expected {engine})", + ) return lambda func: func @@ -246,7 +247,9 @@ def test_order_postgresql(self): # check that passing in a queryset of all objects results in the same # ordering as when it is queried without an inner queryset - alt_rated_qs = self.rated_model.ratings.all().order_by_rating(queryset=self.rated_model.objects.all()) + alt_rated_qs = self.rated_model.ratings.all().order_by_rating( + queryset=self.rated_model.objects.all(), + ) self.assertEqual(list(alt_rated_qs), list(rated_qs)) # check that the scores are what we expect them to be @@ -264,7 +267,10 @@ def test_order_postgresql(self): self.assertEqual(list(rated_qs), [self.item3, self.item1]) # check that the model method results are what we expect - self.assertSequenceEqual(list(rated_qs), list(self.rated_model.ratings.order_by_rating(queryset=item13_qs))) + self.assertSequenceEqual( + list(rated_qs), + list(self.rated_model.ratings.order_by_rating(queryset=item13_qs)), + ) # check that the scores are correct self.assertEqual(rated_qs[0].score, None) @@ -272,14 +278,18 @@ def test_order_postgresql(self): # try ordering by score ascending -- should now be nulls first. also # use an alias for the aggregator - rated_qs = self.rated_model.ratings.all().order_by_rating(descending=False, alias="sum_score") + rated_qs = self.rated_model.ratings.all().order_by_rating( + descending=False, + alias="sum_score", + ) # check that they're ordered correctly self.assertEqual(list(rated_qs), [self.item1, self.item2, self.item3]) # conforms to the other api self.assertEqual( - list(rated_qs), list(self.rated_model.ratings.order_by_rating(descending=False, alias="sum_score")) + list(rated_qs), + list(self.rated_model.ratings.order_by_rating(descending=False, alias="sum_score")), ) # extra attributes are set correctly @@ -321,7 +331,9 @@ def test_ordering_sqlite(self): # check that passing in a queryset of all objects results in the same # ordering as when it is queried without an inner queryset - alt_rated_qs = self.rated_model.ratings.all().order_by_rating(queryset=self.rated_model.objects.all()) + alt_rated_qs = self.rated_model.ratings.all().order_by_rating( + queryset=self.rated_model.objects.all(), + ) self.assertEqual(list(alt_rated_qs), list(rated_qs)) # check that the scores are what we expect them to be @@ -339,7 +351,10 @@ def test_ordering_sqlite(self): self.assertEqual(list(rated_qs), [self.item1, self.item3]) # check that the model method results are what we expect - self.assertSequenceEqual(list(rated_qs), self.rated_model.ratings.order_by_rating(queryset=item13_qs)) + self.assertSequenceEqual( + list(rated_qs), + self.rated_model.ratings.order_by_rating(queryset=item13_qs), + ) # check that the scores are correct self.assertEqual(rated_qs[0].score, 0) @@ -347,14 +362,18 @@ def test_ordering_sqlite(self): # try ordering by score ascending -- should now be nulls first. also # use an alias for the aggregator - rated_qs = self.rated_model.ratings.all().order_by_rating(descending=False, alias="sum_score") + rated_qs = self.rated_model.ratings.all().order_by_rating( + descending=False, + alias="sum_score", + ) # check that they're ordered correctly self.assertEqual(list(rated_qs), [self.item3, self.item1, self.item2]) # conforms to the other api self.assertEqual( - list(rated_qs), list(self.rated_model.ratings.order_by_rating(descending=False, alias="sum_score")) + list(rated_qs), + list(self.rated_model.ratings.order_by_rating(descending=False, alias="sum_score")), ) # extra attributes are set correctly @@ -431,12 +450,12 @@ def test_rate_url(self): ctype = ContentType.objects.get_for_model(self.rated_model) rendered = t.render(c) - self.assertEqual(rendered, "/rate/%d/%d/2/" % (ctype.pk, self.item1.pk)) + self.assertEqual(rendered, f"/rate/{ctype.pk}/{self.item1.pk}/2/") c["score"] = 3.0 rendered = t.render(c) - self.assertEqual(rendered, "/rate/%d/%d/3.0/" % (ctype.pk, self.item1.pk)) + self.assertEqual(rendered, f"/rate/{ctype.pk}/{self.item1.pk}/3.0/") @unittest.skip("This test doesn't use a production template.") def test_unrate_url(self): @@ -446,7 +465,7 @@ def test_unrate_url(self): ctype = ContentType.objects.get_for_model(self.rated_model) rendered = t.render(c) - self.assertEqual(rendered, "/unrate/%d/%d/" % (ctype.pk, self.item1.pk)) + self.assertEqual(rendered, f"/unrate/{ctype.pk}/{self.item1.pk}/") @unittest.skip("This test doesn't use a production template.") def test_rating_view(self): @@ -588,7 +607,7 @@ class RecommendationsTestCase(TestCase): fixtures = ["ratings_testdata.json"] def setUp(self): - super(RecommendationsTestCase, self).setUp() + super().setUp() self.food_a = Food.objects.create(name="food_a") self.food_b = Food.objects.create(name="food_b") @@ -617,10 +636,25 @@ def setUp(self): ] # x-axis - self.foods = [self.food_a, self.food_b, self.food_c, self.food_d, self.food_e, self.food_f] + self.foods = [ + self.food_a, + self.food_b, + self.food_c, + self.food_d, + self.food_e, + self.food_f, + ] # y-axis - self.users = [self.user_a, self.user_b, self.user_c, self.user_d, self.user_e, self.user_f, self.user_g] + self.users = [ + self.user_a, + self.user_b, + self.user_c, + self.user_d, + self.user_e, + self.user_f, + self.user_g, + ] for x, food in enumerate(self.foods): for y, user in enumerate(self.users): @@ -641,7 +675,7 @@ def test_matching(self): (0.92447345164190486, self.user_e), (0.89340514744156474, self.user_d), ] - for res, exp in zip(results, expected): + for res, exp in zip(results, expected, strict=True): self.assertEqual(res[1], exp[1]) self.assertAlmostEqual(res[0], exp[0]) @@ -652,7 +686,7 @@ def test_recommending(self): (2.8325499182641614, self.food_a), (2.5309807037655649, self.food_c), ] - for res, exp in zip(results, expected): + for res, exp in zip(results, expected, strict=True): self.assertEqual(res[1], exp[1]) self.assertAlmostEqual(res[0], exp[0]) @@ -665,7 +699,7 @@ def test_item_recommendation(self): (-0.17984719479905439, self.food_f), (-0.42289003161103106, self.food_c), ] - for res, exp in zip(results, expected): + for res, exp in zip(results, expected, strict=True): self.assertEqual(res[1], exp[1]) self.assertAlmostEqual(res[0], exp[0]) diff --git a/ratings/utils.py b/ratings/utils.py index bfe2f269..b425a491 100644 --- a/ratings/utils.py +++ b/ratings/utils.py @@ -13,7 +13,7 @@ def is_gfk(content_field): def query_has_where(query): try: - where, params = query.get_compiler(using="default").compile(query.where) + where, _ = query.get_compiler(using="default").compile(query.where) return bool(where) except FullResultSet: return False @@ -56,7 +56,7 @@ def sim_euclidean_distance(ratings_queryset, factor_a, factor_b): else: q, p = query_as_sql(rating_query) rating_qs_sql = q % p - queryset_filter = " AND r1.id IN (%s)" % rating_qs_sql + queryset_filter = f" AND r1.id IN ({rating_qs_sql})" params = { "ratings_table": rating_model._meta.db_table, @@ -119,7 +119,7 @@ def sim_pearson_correlation(ratings_queryset, factor_a, factor_b): else: q, p = query_as_sql(rating_query) rating_qs_sql = q % p - queryset_filter = " AND r1.id IN (%s)" % rating_qs_sql + queryset_filter = f" AND r1.id IN ({rating_qs_sql})" params = { "ratings_table": rating_model._meta.db_table, @@ -153,14 +153,15 @@ def sim_pearson_correlation(ratings_queryset, factor_a, factor_b): def top_matches(ratings_queryset, items, item, n=5, similarity=sim_pearson_correlation): - scores = [(similarity(ratings_queryset, item, other), other) for other in items if other != item] + scores = [ + (similarity(ratings_queryset, item, other), other) for other in items if other != item + ] scores.sort() scores.reverse() return scores[:n] def recommendations(ratings_queryset, people, person, similarity=sim_pearson_correlation): - already_rated = ratings_queryset.filter(user=person).values_list("hashed") totals = {} @@ -241,7 +242,6 @@ def recommended_items(ratings_queryset, user): for item in ratings_queryset.filter(user=user): similar_items = SimilarItem.objects.get_for_item(item.content_object) for similar_item in similar_items: - actual = similar_item.similar_object lookup_kwargs = ratings_queryset.model.lookup_kwargs(actual) lookup_kwargs["user"] = user diff --git a/ratings/views.py b/ratings/views.py index a5692551..26c15df8 100644 --- a/ratings/views.py +++ b/ratings/views.py @@ -1,7 +1,13 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType -from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseRedirect +from django.http import ( + Http404, + HttpResponse, + HttpResponseBadRequest, + HttpResponseNotAllowed, + HttpResponseRedirect, +) from django.shortcuts import get_object_or_404 from django.utils.http import url_has_allowed_host_and_scheme @@ -14,9 +20,12 @@ @login_required def rate_object(request, ct, pk, score=1, add=True): if request.method != "POST" and not ALLOW_GET: - return HttpResponseNotAllowed('Invalid request method: "%s". ' "Must be POST." % request.method) + err_msg = f'Invalid request method: "{request.method}". Must be POST.' + return HttpResponseNotAllowed(err_msg) - redirect_url = request.POST.get("next") or request.GET.get("next") or request.META.get("HTTP_REFERER") + redirect_url = ( + request.POST.get("next") or request.GET.get("next") or request.META.get("HTTP_REFERER") + ) if redirect_url and not url_has_allowed_host_and_scheme(redirect_url, settings.ALLOWED_HOSTS): return HttpResponseBadRequest("Invalid next URL.") if not redirect_url: @@ -26,7 +35,8 @@ def rate_object(request, ct, pk, score=1, add=True): model_class = ctype.model_class() if not hasattr(model_class, "_ratings_field"): - raise Http404("Model class %s does not support ratings" % model_class) + err_msg = f"Model class {model_class} does not support ratings" + raise Http404(err_msg) obj = get_object_or_404(model_class, pk=pk) diff --git a/requirements/development.txt b/requirements/development.txt index d0da2125..0645029f 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -1,5 +1,4 @@ -r base.txt Werkzeug[watchdog]==3.1.3 -flake8==4.0.1 -isort==5.8.0 +ruff==0.14.3 pre-commit==2.19.0