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