diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..516cec4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,51 @@ +{ + "env": { + "browser": true, + "es6": true, + "jquery": true + }, + "extends": [ + "eslint:recommended" + ], + "ignorePatterns": ["dist/", "node_modules/"], + "rules": { + "block-scoped-var": "error", + "consistent-return": "error", + "curly": "error", + "default-case": "error", + "default-param-last": ["error"], + "dot-notation": "error", + "eqeqeq": "error", + "guard-for-in": "error", + "max-classes-per-file": "error", + "no-alert": "error", + "no-caller": "error", + "no-else-return": "error", + "no-empty-function": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-param-reassign": "error", + "no-return-assign": "error", + "no-return-await": "error", + "no-self-compare": "error", + "no-throw-literal": "error", + "no-useless-concat": "error", + "radix": ["error", "as-needed"], + "require-await": "error", + "yoda": "error", + "no-shadow": "off", + "prefer-destructuring": ["error", { "array": false, "object": true }], + "padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "import", "next": "export" }, + { "blankLine": "always", "prev": "export", "next": "export" }, + { "blankLine": "always", "prev": "*", "next": "return" } + ] + }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + } +} diff --git a/.flake8 b/.flake8 index 944cb72..92201aa 100644 --- a/.flake8 +++ b/.flake8 @@ -6,3 +6,5 @@ ignore = W503, # class member shadows builtin A003, +max-complexity = 10 +format = %(path)s:%(row)d:%(col)d: %(code)s %(text)s https://lintlyci.github.io/Flake8Rules/rules/%(code)s.html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f7432a..9eac4b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: \.min\.(js|css)(\.map)?$ +exclude: (\.min\.(js|css)(\.map)?$|/vendor/) repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 @@ -34,6 +34,15 @@ repos: - id: pyupgrade args: - --py310-plus + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.4.0 + hooks: + - id: django-upgrade + args: [--target-version, "4.0"] + - repo: https://github.com/rtts/djhtml + rev: v1.4.10 + hooks: + - id: djhtml - repo: https://github.com/pycqa/flake8 rev: 4.0.1 hooks: @@ -44,3 +53,18 @@ repos: - flake8-builtins - flake8-comprehensions - flake8-eradicate + - flake8-pytest-style + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.5.1 + hooks: + - id: prettier + types_or: [javascript, css] + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.4.1 + hooks: + - id: eslint + args: [--fix] + types_or: [javascript, css] + additional_dependencies: + - eslint@^7.29.0 + - eslint-config-prettier@^8.3.0 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d6245f5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "printWidth": 120, + "endOfLine": "auto" +} diff --git a/src/articles/static/admin_articles.css b/src/articles/static/admin_articles.css index a473ae4..b0a5aa2 100644 --- a/src/articles/static/admin_articles.css +++ b/src/articles/static/admin_articles.css @@ -1,4 +1,5 @@ -#id_content, #id_custom_css { +#id_content, +#id_custom_css { font-family: "JetBrains Mono", monospace; min-height: 30em; resize: vertical; @@ -9,6 +10,7 @@ height: 38em; } -label[for=id_content], label[for=id_custom_css] { +label[for="id_content"], +label[for="id_custom_css"] { display: none; } diff --git a/src/articles/static/admonitions.css b/src/articles/static/admonitions.css index 603e272..56a1a0f 100644 --- a/src/articles/static/admonitions.css +++ b/src/articles/static/admonitions.css @@ -9,7 +9,7 @@ --warning-text: #856404; } -@media(prefers-color-scheme: dark) { +@media (prefers-color-scheme: dark) { :root { --info-background: #0c5460; --info-text: #d1ecf1; diff --git a/src/articles/static/authenticated.css b/src/articles/static/authenticated.css index 179e3b8..23c5047 100644 --- a/src/articles/static/authenticated.css +++ b/src/articles/static/authenticated.css @@ -2,7 +2,7 @@ font-size: 60%; background-color: var(--nc-tx-2); color: var(--nc-bg-1); - padding: .5ex 1ex; + padding: 0.5ex 1ex; border-radius: 1ex; vertical-align: 15%; text-decoration: none; diff --git a/src/articles/static/copy-code.js b/src/articles/static/copy-code.js index 75f5767..e333507 100644 --- a/src/articles/static/copy-code.js +++ b/src/articles/static/copy-code.js @@ -2,8 +2,8 @@ function addCopyCode() { const codeBlocks = document.querySelectorAll("pre"); - codeBlocks.forEach(pre => { - pre.addEventListener("click", event => { + codeBlocks.forEach((pre) => { + pre.addEventListener("click", (event) => { if (event.detail === 4) { const selection = window.getSelection(); selection.setBaseAndExtent( @@ -18,7 +18,6 @@ function addCopyCode() { }); } - ((readyState) => { if (readyState === "interactive") { addCopyCode(); diff --git a/src/articles/static/edit-keymap.js b/src/articles/static/edit-keymap.js index 72ec61d..19cef49 100644 --- a/src/articles/static/edit-keymap.js +++ b/src/articles/static/edit-keymap.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; function bindKey() { const adminLinkElement = document.querySelector("a#admin-link"); @@ -10,7 +10,7 @@ function bindKey() { if (event.code === "KeyE") { window.location = adminLocation; } - }) + }); } ((readyState) => { diff --git a/src/articles/static/live-preview.js b/src/articles/static/live-preview.js index 8e7c370..f58721b 100644 --- a/src/articles/static/live-preview.js +++ b/src/articles/static/live-preview.js @@ -1,6 +1,6 @@ let preview = null; -function onLoad () { +function onLoad() { const previewButton = document.querySelector("input#_live_preview"); if (previewButton) { previewButton.addEventListener("click", openPreviewPopup); @@ -34,11 +34,11 @@ function openPreviewPopup(event) { function loadPreview() { const id = Number(window.location.pathname.match(/\d+/)[0]); const body = prepareBody(); - fetch(`/api/render/${id}/`, {method: "POST", body: body}) - .then(response => { + fetch(`/api/render/${id}/`, { method: "POST", body: body }) + .then((response) => { return response.text(); }) - .then(value => { + .then((value) => { preview.document.open("text/html", "replace"); preview.document.write(value); preview.document.close(); @@ -79,8 +79,11 @@ function prepareBody() { const element = document.querySelector(input.selector); body.set(input.to, element[input.property]); } - const tagIds = Array.from(document.querySelector("#id_tags").selectedOptions).map(option => option.value).join(); + const tagIds = Array.from(document.querySelector("#id_tags").selectedOptions) + .map((option) => option.value) + .join(); body.set("tag_ids", tagIds); + return body; } @@ -105,8 +108,10 @@ function setupLivePreview() { */ function debounce(func, wait) { let timeout; + return function () { - const context = this, args = arguments; + const context = this, + args = arguments; const later = function () { timeout = null; func.apply(context, args); diff --git a/src/articles/static/login.css b/src/articles/static/login.css index d6c9db9..5c1600b 100644 --- a/src/articles/static/login.css +++ b/src/articles/static/login.css @@ -1,18 +1,22 @@ .d-none { - display: none !important; + display: none !important; } .float-right { - float: right; + float: right; } -td, th, tr, tbody, tr:nth-child(2n) { - background-color: inherit; - border: none; - padding-left: 0; - padding-right: 0; +td, +th, +tr, +tbody, +tr:nth-child(2n) { + background-color: inherit; + border: none; + padding-left: 0; + padding-right: 0; } img { - background-color: white; + background-color: white; } diff --git a/src/articles/static/public.css b/src/articles/static/public.css index 396800d..d131159 100644 --- a/src/articles/static/public.css +++ b/src/articles/static/public.css @@ -3,7 +3,9 @@ body { max-width: 750px; } -h1, h2, h3 { +h1, +h2, +h3 { border-bottom: unset; } @@ -20,8 +22,9 @@ footer > :first-child { margin-top: 1em; } -nav a:not(:first-child):before, a.tag:not(:first-of-type):before { - content: '\00B7'; +nav a:not(:first-child):before, +a.tag:not(:first-of-type):before { + content: "\00B7"; margin: 0 5px; color: var(--nc-tx-1); text-decoration: none; diff --git a/src/articles/templates/articles/base.html b/src/articles/templates/articles/base.html index 1236728..b3bea09 100644 --- a/src/articles/templates/articles/base.html +++ b/src/articles/templates/articles/base.html @@ -1,42 +1,42 @@ {% load static %} {% spaceless %} - - - - - - - - - - {% block title %}Home | {% endblock %}{{ blog_title }} by {{ blog_author }} - {% block feed_link %} - - {% endblock %} - {% include "articles/snippets/analytics_head.html" %} - {% include "articles/snippets/page_metadata.html" %} + + + + + + + + + + {% block title %}Home | {% endblock %}{{ blog_title }} by {{ blog_author }} + {% block feed_link %} + + {% endblock %} + {% include "articles/snippets/analytics_head.html" %} + {% include "articles/snippets/page_metadata.html" %} - - - - {% if article and article.has_code %} - - {% endif %} - {% if user.is_authenticated %} - - {% endif %} + + + + {% if article and article.has_code %} + + {% endif %} + {% if user.is_authenticated %} + + {% endif %} - {% block append_css %} - {% endblock %} + {% block append_css %} + {% endblock %} - {% include "articles/snippets/favicon.html" %} - - -
-

{{ blog_title }}

-

{{ blog_description }}

- {% include "articles/snippets/navigation.html" %} -
+ {% include "articles/snippets/favicon.html" %} + + +
+

{{ blog_title }}

+

{{ blog_description }}

+ {% include "articles/snippets/navigation.html" %} +
{% endspaceless %}
@@ -45,31 +45,31 @@
{% spaceless %} - + -{% include "articles/snippets/analytics.html" %} -{% if user.is_authenticated %} - -{% endif %} -{% if article and article.has_code %} - -{% endif %} + {% include "articles/snippets/analytics.html" %} + {% if user.is_authenticated %} + + {% endif %} + {% if article and article.has_code %} + + {% endif %} - - + + {% endspaceless %} diff --git a/src/articles/templates/articles/snippets/analytics.html b/src/articles/templates/articles/snippets/analytics.html index 179b18a..a7de6d3 100644 --- a/src/articles/templates/articles/snippets/analytics.html +++ b/src/articles/templates/articles/snippets/analytics.html @@ -2,9 +2,9 @@ {% if not user.is_authenticated and goatcounter_domain is not None %} + data-goatcounter="https://{{ goatcounter_domain }}/count"> {% endif %} diff --git a/src/articles/templates/articles/snippets/metadata.html b/src/articles/templates/articles/snippets/metadata.html index 248196b..c9c7626 100644 --- a/src/articles/templates/articles/snippets/metadata.html +++ b/src/articles/templates/articles/snippets/metadata.html @@ -3,6 +3,6 @@ · {{ article.get_read_time }} min read {% include "articles/snippets/admin_link.html" %} {% if tags %} -
{% for tag in tags %}{{ tag.name }}{% endfor %} +
{% for tag in tags %}{{ tag.name }}{% endfor %} {% endif %}

diff --git a/src/articles/tests/conftest.py b/src/articles/tests/conftest.py index b179ba3..7c0b1ef 100644 --- a/src/articles/tests/conftest.py +++ b/src/articles/tests/conftest.py @@ -8,19 +8,19 @@ from articles.models import Article, Tag, User @pytest.fixture() -@pytest.mark.django_db +@pytest.mark.django_db() def author() -> User: return User.objects.create_user("gaugendre", is_staff=True, is_superuser=True) @pytest.fixture() -@pytest.mark.django_db +@pytest.mark.django_db() def tag() -> Tag: return Tag.objects.create(name="This is a new tag", slug="this-new-tag") @pytest.fixture() -@pytest.mark.django_db +@pytest.mark.django_db() def published_article(author: User, tag: Tag) -> Article: article = Article.objects.create( title="Some interesting article title", @@ -41,7 +41,7 @@ def published_article(author: User, tag: Tag) -> Article: @pytest.fixture() -@pytest.mark.django_db +@pytest.mark.django_db() def unpublished_article(author: User) -> Article: return Article.objects.create( title="Some interesting article title, but sorry it is not public yet", @@ -55,5 +55,5 @@ def unpublished_article(author: User) -> Article: @pytest.fixture(autouse=True, scope="session") -def collect_static(): +def _collect_static(): call_command("collectstatic", "--no-input", "--clear") diff --git a/src/articles/tests/test_admin.py b/src/articles/tests/test_admin.py index 8882cec..af4ab79 100644 --- a/src/articles/tests/test_admin.py +++ b/src/articles/tests/test_admin.py @@ -5,7 +5,7 @@ from django.urls import reverse from articles.models import User -@pytest.mark.django_db +@pytest.mark.django_db() # @pytest.mark.skip("Fails for no apparent reason") @pytest.mark.flaky(reruns=5, reruns_delay=3) def test_can_access_add_article(client: Client, author: User): diff --git a/src/articles/tests/test_api_views.py b/src/articles/tests/test_api_views.py index 87c75dd..804d2f2 100644 --- a/src/articles/tests/test_api_views.py +++ b/src/articles/tests/test_api_views.py @@ -6,7 +6,7 @@ from articles.models import Article from articles.utils import format_article_content -@pytest.mark.django_db +@pytest.mark.django_db() def test_unauthenticated_render_redirects(published_article: Article, client: Client): api_res = client.post( reverse("api-render-article", kwargs={"article_pk": published_article.pk}), @@ -15,7 +15,7 @@ def test_unauthenticated_render_redirects(published_article: Article, client: Cl assert api_res.status_code == 302 -@pytest.mark.django_db +@pytest.mark.django_db() def test_render_article_same_content(published_article: Article, client: Client): client.force_login(published_article.author) api_res = post_article(client, published_article, published_article.content) @@ -35,7 +35,7 @@ def test_render_article_same_content(published_article: Article, client: Client) assert api_content == standard_content -@pytest.mark.django_db +@pytest.mark.django_db() def test_render_article_change_content(published_article: Article, client: Client): client.force_login(published_article.author) preview_content = "This is a different content **with strong emphasis**" @@ -46,7 +46,7 @@ def test_render_article_change_content(published_article: Article, client: Clien assert html_preview_content in api_content -@pytest.mark.django_db +@pytest.mark.django_db() def test_render_article_doesnt_save(published_article, client: Client): client.force_login(published_article.author) original_content = published_article.content @@ -57,7 +57,7 @@ def test_render_article_doesnt_save(published_article, client: Client): assert published_article.content == original_content -@pytest.mark.django_db +@pytest.mark.django_db() def test_render_article_no_tags(published_article, client: Client): client.force_login(published_article.author) api_res = client.post( diff --git a/src/articles/tests/test_feed_views.py b/src/articles/tests/test_feed_views.py index 80bf257..1bb90b6 100644 --- a/src/articles/tests/test_feed_views.py +++ b/src/articles/tests/test_feed_views.py @@ -7,7 +7,7 @@ from articles.models import Article, User from articles.views.feeds import CompleteFeed -@pytest.mark.django_db +@pytest.mark.django_db() def test_can_access_feed(client: Client, published_article): res = client.get(reverse("complete-feed")) assert res.status_code == 200 @@ -16,7 +16,7 @@ def test_can_access_feed(client: Client, published_article): assert published_article.title in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_feed_limits_number_of_articles(client: Client, author: User): baker.make(Article, 100, status=Article.PUBLISHED, author=author) res = client.get(reverse("complete-feed")) diff --git a/src/articles/tests/test_html_views.py b/src/articles/tests/test_html_views.py index 212a96e..d8a9f82 100644 --- a/src/articles/tests/test_html_views.py +++ b/src/articles/tests/test_html_views.py @@ -6,7 +6,7 @@ from model_bakery import baker from articles.models import Article, User -@pytest.mark.django_db +@pytest.mark.django_db() def test_can_access_list(client: Client, published_article: Article): res = client.get(reverse("articles-list")) assert res.status_code == 200 @@ -15,7 +15,7 @@ def test_can_access_list(client: Client, published_article: Article): assert published_article.get_abstract() not in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_only_title_shown_on_list(client: Client, author: User): title = "This is a very long title mouahahaha" abstract = "Some abstract" @@ -34,7 +34,7 @@ def test_only_title_shown_on_list(client: Client, author: User): assert after not in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_access_article_by_slug(client: Client, published_article: Article): _test_access_article_by_slug(client, published_article) @@ -52,7 +52,7 @@ def _assert_article_is_rendered(item: Article, res): assert html in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_anonymous_cant_access_draft_detail( client: Client, unpublished_article: Article ): @@ -62,7 +62,7 @@ def test_anonymous_cant_access_draft_detail( assert res.status_code == 404 -@pytest.mark.django_db +@pytest.mark.django_db() def test_anonymous_can_access_draft_detail_with_key( client: Client, unpublished_article: Article ): @@ -73,7 +73,7 @@ def test_anonymous_can_access_draft_detail_with_key( _assert_article_is_rendered(unpublished_article, res) -@pytest.mark.django_db +@pytest.mark.django_db() def test_user_can_access_draft_detail( client: Client, author: User, unpublished_article: Article ): @@ -81,7 +81,7 @@ def test_user_can_access_draft_detail( _test_access_article_by_slug(client, unpublished_article) -@pytest.mark.django_db +@pytest.mark.django_db() def test_anonymous_cant_access_drafts_list( client: Client, unpublished_article: Article ): @@ -89,7 +89,7 @@ def test_anonymous_cant_access_drafts_list( assert res.status_code == 302 -@pytest.mark.django_db +@pytest.mark.django_db() def test_user_can_access_drafts_list( client: Client, author: User, unpublished_article: Article ): @@ -100,7 +100,7 @@ def test_user_can_access_drafts_list( assert unpublished_article.title in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_has_goatcounter_if_set(client: Client, settings): settings.GOATCOUNTER_DOMAIN = "gc.gabnotes.org" res = client.get(reverse("articles-list")) @@ -109,7 +109,7 @@ def test_has_goatcounter_if_set(client: Client, settings): assert f"{settings.GOATCOUNTER_DOMAIN}/count" in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_doesnt_have_goatcounter_if_unset(client: Client, settings): settings.GOATCOUNTER_DOMAIN = None res = client.get(reverse("articles-list")) @@ -118,7 +118,7 @@ def test_doesnt_have_goatcounter_if_unset(client: Client, settings): assert f"{settings.GOATCOUNTER_DOMAIN}/count" not in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_logged_in_user_doesnt_have_goatcounter(client: Client, author: User, settings): client.force_login(author) settings.GOATCOUNTER_DOMAIN = "gc.gabnotes.org" @@ -128,7 +128,7 @@ def test_logged_in_user_doesnt_have_goatcounter(client: Client, author: User, se assert f"{settings.GOATCOUNTER_DOMAIN}/count" not in content -@pytest.mark.django_db +@pytest.mark.django_db() def test_image_is_lazy(client: Client, published_article: Article): res = client.get(reverse("article-detail", kwargs={"slug": published_article.slug})) assert res.status_code == 200 diff --git a/src/articles/tests/test_migrations.py b/src/articles/tests/test_migrations.py index 907207b..0b79cd1 100644 --- a/src/articles/tests/test_migrations.py +++ b/src/articles/tests/test_migrations.py @@ -2,6 +2,6 @@ import pytest from django.core.management import call_command -@pytest.mark.django_db +@pytest.mark.django_db() def test_missing_migrations(): call_command("makemigrations", "--check") diff --git a/src/articles/tests/test_models.py b/src/articles/tests/test_models.py index 2bdd268..2a0f31b 100644 --- a/src/articles/tests/test_models.py +++ b/src/articles/tests/test_models.py @@ -3,7 +3,7 @@ import pytest from articles.models import Article, User -@pytest.mark.django_db +@pytest.mark.django_db() def test_publish_article(unpublished_article: Article): assert unpublished_article.status == Article.DRAFT assert unpublished_article.published_at is None @@ -12,7 +12,7 @@ def test_publish_article(unpublished_article: Article): assert published_article.published_at is not None -@pytest.mark.django_db +@pytest.mark.django_db() def test_unpublish_article(published_article: Article): assert published_article.status == Article.PUBLISHED assert published_article.published_at is not None @@ -21,7 +21,7 @@ def test_unpublish_article(published_article: Article): assert unpublished_article.published_at is None -@pytest.mark.django_db +@pytest.mark.django_db() def test_save_article_adds_missing_slug(author: User): # Explicitly calling bulk_create with one article because it doesn't call save(). articles = Article.objects.bulk_create( @@ -33,7 +33,7 @@ def test_save_article_adds_missing_slug(author: User): assert article.slug != "" -@pytest.mark.django_db +@pytest.mark.django_db() def test_save_article_doesnt_change_existing_slug(published_article: Article): original_slug = published_article.slug published_article.title = "This is a brand new title" @@ -41,19 +41,19 @@ def test_save_article_doesnt_change_existing_slug(published_article: Article): assert published_article.slug == original_slug -@pytest.mark.django_db +@pytest.mark.django_db() def test_empty_custom_css_minified(published_article): published_article.custom_css = "" assert published_article.get_minified_custom_css == "" -@pytest.mark.django_db +@pytest.mark.django_db() def test_simple_custom_css_minified(published_article): published_article.custom_css = ".cls {\n background-color: red;\n}" assert published_article.get_minified_custom_css == ".cls{background-color:red}" -@pytest.mark.django_db +@pytest.mark.django_db() def test_larger_custom_css_minified(published_article): published_article.custom_css = """\ .profile { diff --git a/src/attachments/static/attachments/js/copy_url.js b/src/attachments/static/attachments/js/copy_url.js index 22428c5..51c0c6e 100644 --- a/src/attachments/static/attachments/js/copy_url.js +++ b/src/attachments/static/attachments/js/copy_url.js @@ -2,10 +2,10 @@ function copy(event) { const text = event.target.dataset.toCopy; navigator.clipboard.writeText(text).then(() => { console.log("Copied"); - }) + }); } -$(document).ready(function() { +$(document).ready(function () { const buttons = document.querySelectorAll(".copy-button"); for (const button of buttons) { button.addEventListener("click", copy); diff --git a/src/attachments/tests/test_models.py b/src/attachments/tests/test_models.py index b6435ab..4024081 100644 --- a/src/attachments/tests/test_models.py +++ b/src/attachments/tests/test_models.py @@ -6,9 +6,9 @@ from django.core.files import File from attachments.models import Attachment -@pytest.mark.block_network -@pytest.mark.vcr -@pytest.mark.django_db +@pytest.mark.block_network() +@pytest.mark.vcr() +@pytest.mark.django_db() def test_attachment_is_processed_by_shortpixel(): # This path manipulation is required to make the test run from this directory # or from upper in the hierarchy (e.g.: settings.BASE_DIR) diff --git a/src/blog/templates/admin/base_site.html b/src/blog/templates/admin/base_site.html index 5600d01..c2e6dd0 100644 --- a/src/blog/templates/admin/base_site.html +++ b/src/blog/templates/admin/base_site.html @@ -3,7 +3,7 @@ {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block branding %} -

{{ site_header|default:_('Django administration') }}

+

{{ site_header|default:_('Django administration') }}

{% endblock %} {% block extrahead %}