Add eslint, prettier, djhtml, pytest-style & django-upgrade

This commit is contained in:
Gabriel Augendre 2021-12-27 22:57:05 +01:00
parent 09f17bdef1
commit 1dbf04db66
25 changed files with 223 additions and 128 deletions

51
.eslintrc Normal file
View file

@ -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"
}
}

View file

@ -6,3 +6,5 @@ ignore =
W503, W503,
# class member shadows builtin # class member shadows builtin
A003, A003,
max-complexity = 10
format = %(path)s:%(row)d:%(col)d: %(code)s %(text)s https://lintlyci.github.io/Flake8Rules/rules/%(code)s.html

View file

@ -1,4 +1,4 @@
exclude: \.min\.(js|css)(\.map)?$ exclude: (\.min\.(js|css)(\.map)?$|/vendor/)
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1 rev: v4.0.1
@ -34,6 +34,15 @@ repos:
- id: pyupgrade - id: pyupgrade
args: args:
- --py310-plus - --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 - repo: https://github.com/pycqa/flake8
rev: 4.0.1 rev: 4.0.1
hooks: hooks:
@ -44,3 +53,18 @@ repos:
- flake8-builtins - flake8-builtins
- flake8-comprehensions - flake8-comprehensions
- flake8-eradicate - 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

5
.prettierrc Normal file
View file

@ -0,0 +1,5 @@
{
"tabWidth": 4,
"printWidth": 120,
"endOfLine": "auto"
}

View file

@ -1,4 +1,5 @@
#id_content, #id_custom_css { #id_content,
#id_custom_css {
font-family: "JetBrains Mono", monospace; font-family: "JetBrains Mono", monospace;
min-height: 30em; min-height: 30em;
resize: vertical; resize: vertical;
@ -9,6 +10,7 @@
height: 38em; height: 38em;
} }
label[for=id_content], label[for=id_custom_css] { label[for="id_content"],
label[for="id_custom_css"] {
display: none; display: none;
} }

View file

@ -2,7 +2,7 @@
font-size: 60%; font-size: 60%;
background-color: var(--nc-tx-2); background-color: var(--nc-tx-2);
color: var(--nc-bg-1); color: var(--nc-bg-1);
padding: .5ex 1ex; padding: 0.5ex 1ex;
border-radius: 1ex; border-radius: 1ex;
vertical-align: 15%; vertical-align: 15%;
text-decoration: none; text-decoration: none;

View file

@ -2,8 +2,8 @@
function addCopyCode() { function addCopyCode() {
const codeBlocks = document.querySelectorAll("pre"); const codeBlocks = document.querySelectorAll("pre");
codeBlocks.forEach(pre => { codeBlocks.forEach((pre) => {
pre.addEventListener("click", event => { pre.addEventListener("click", (event) => {
if (event.detail === 4) { if (event.detail === 4) {
const selection = window.getSelection(); const selection = window.getSelection();
selection.setBaseAndExtent( selection.setBaseAndExtent(
@ -18,7 +18,6 @@ function addCopyCode() {
}); });
} }
((readyState) => { ((readyState) => {
if (readyState === "interactive") { if (readyState === "interactive") {
addCopyCode(); addCopyCode();

View file

@ -1,4 +1,4 @@
'use strict'; "use strict";
function bindKey() { function bindKey() {
const adminLinkElement = document.querySelector("a#admin-link"); const adminLinkElement = document.querySelector("a#admin-link");
@ -10,7 +10,7 @@ function bindKey() {
if (event.code === "KeyE") { if (event.code === "KeyE") {
window.location = adminLocation; window.location = adminLocation;
} }
}) });
} }
((readyState) => { ((readyState) => {

View file

@ -35,10 +35,10 @@ function loadPreview() {
const id = Number(window.location.pathname.match(/\d+/)[0]); const id = Number(window.location.pathname.match(/\d+/)[0]);
const body = prepareBody(); const body = prepareBody();
fetch(`/api/render/${id}/`, { method: "POST", body: body }) fetch(`/api/render/${id}/`, { method: "POST", body: body })
.then(response => { .then((response) => {
return response.text(); return response.text();
}) })
.then(value => { .then((value) => {
preview.document.open("text/html", "replace"); preview.document.open("text/html", "replace");
preview.document.write(value); preview.document.write(value);
preview.document.close(); preview.document.close();
@ -79,8 +79,11 @@ function prepareBody() {
const element = document.querySelector(input.selector); const element = document.querySelector(input.selector);
body.set(input.to, element[input.property]); 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); body.set("tag_ids", tagIds);
return body; return body;
} }
@ -105,8 +108,10 @@ function setupLivePreview() {
*/ */
function debounce(func, wait) { function debounce(func, wait) {
let timeout; let timeout;
return function () { return function () {
const context = this, args = arguments; const context = this,
args = arguments;
const later = function () { const later = function () {
timeout = null; timeout = null;
func.apply(context, args); func.apply(context, args);

View file

@ -6,7 +6,11 @@
float: right; float: right;
} }
td, th, tr, tbody, tr:nth-child(2n) { td,
th,
tr,
tbody,
tr:nth-child(2n) {
background-color: inherit; background-color: inherit;
border: none; border: none;
padding-left: 0; padding-left: 0;

View file

@ -3,7 +3,9 @@ body {
max-width: 750px; max-width: 750px;
} }
h1, h2, h3 { h1,
h2,
h3 {
border-bottom: unset; border-bottom: unset;
} }
@ -20,8 +22,9 @@ footer > :first-child {
margin-top: 1em; margin-top: 1em;
} }
nav a:not(:first-child):before, a.tag:not(:first-of-type):before { nav a:not(:first-child):before,
content: '\00B7'; a.tag:not(:first-of-type):before {
content: "\00B7";
margin: 0 5px; margin: 0 5px;
color: var(--nc-tx-1); color: var(--nc-tx-1);
text-decoration: none; text-decoration: none;

View file

@ -8,19 +8,19 @@ from articles.models import Article, Tag, User
@pytest.fixture() @pytest.fixture()
@pytest.mark.django_db @pytest.mark.django_db()
def author() -> User: def author() -> User:
return User.objects.create_user("gaugendre", is_staff=True, is_superuser=True) return User.objects.create_user("gaugendre", is_staff=True, is_superuser=True)
@pytest.fixture() @pytest.fixture()
@pytest.mark.django_db @pytest.mark.django_db()
def tag() -> Tag: def tag() -> Tag:
return Tag.objects.create(name="This is a new tag", slug="this-new-tag") return Tag.objects.create(name="This is a new tag", slug="this-new-tag")
@pytest.fixture() @pytest.fixture()
@pytest.mark.django_db @pytest.mark.django_db()
def published_article(author: User, tag: Tag) -> Article: def published_article(author: User, tag: Tag) -> Article:
article = Article.objects.create( article = Article.objects.create(
title="Some interesting article title", title="Some interesting article title",
@ -41,7 +41,7 @@ def published_article(author: User, tag: Tag) -> Article:
@pytest.fixture() @pytest.fixture()
@pytest.mark.django_db @pytest.mark.django_db()
def unpublished_article(author: User) -> Article: def unpublished_article(author: User) -> Article:
return Article.objects.create( return Article.objects.create(
title="Some interesting article title, but sorry it is not public yet", 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") @pytest.fixture(autouse=True, scope="session")
def collect_static(): def _collect_static():
call_command("collectstatic", "--no-input", "--clear") call_command("collectstatic", "--no-input", "--clear")

View file

@ -5,7 +5,7 @@ from django.urls import reverse
from articles.models import User from articles.models import User
@pytest.mark.django_db @pytest.mark.django_db()
# @pytest.mark.skip("Fails for no apparent reason") # @pytest.mark.skip("Fails for no apparent reason")
@pytest.mark.flaky(reruns=5, reruns_delay=3) @pytest.mark.flaky(reruns=5, reruns_delay=3)
def test_can_access_add_article(client: Client, author: User): def test_can_access_add_article(client: Client, author: User):

View file

@ -6,7 +6,7 @@ from articles.models import Article
from articles.utils import format_article_content 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): def test_unauthenticated_render_redirects(published_article: Article, client: Client):
api_res = client.post( api_res = client.post(
reverse("api-render-article", kwargs={"article_pk": published_article.pk}), 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 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): def test_render_article_same_content(published_article: Article, client: Client):
client.force_login(published_article.author) client.force_login(published_article.author)
api_res = post_article(client, published_article, published_article.content) 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 assert api_content == standard_content
@pytest.mark.django_db @pytest.mark.django_db()
def test_render_article_change_content(published_article: Article, client: Client): def test_render_article_change_content(published_article: Article, client: Client):
client.force_login(published_article.author) client.force_login(published_article.author)
preview_content = "This is a different content **with strong emphasis**" 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 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): def test_render_article_doesnt_save(published_article, client: Client):
client.force_login(published_article.author) client.force_login(published_article.author)
original_content = published_article.content 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 assert published_article.content == original_content
@pytest.mark.django_db @pytest.mark.django_db()
def test_render_article_no_tags(published_article, client: Client): def test_render_article_no_tags(published_article, client: Client):
client.force_login(published_article.author) client.force_login(published_article.author)
api_res = client.post( api_res = client.post(

View file

@ -7,7 +7,7 @@ from articles.models import Article, User
from articles.views.feeds import CompleteFeed from articles.views.feeds import CompleteFeed
@pytest.mark.django_db @pytest.mark.django_db()
def test_can_access_feed(client: Client, published_article): def test_can_access_feed(client: Client, published_article):
res = client.get(reverse("complete-feed")) res = client.get(reverse("complete-feed"))
assert res.status_code == 200 assert res.status_code == 200
@ -16,7 +16,7 @@ def test_can_access_feed(client: Client, published_article):
assert published_article.title in content 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): def test_feed_limits_number_of_articles(client: Client, author: User):
baker.make(Article, 100, status=Article.PUBLISHED, author=author) baker.make(Article, 100, status=Article.PUBLISHED, author=author)
res = client.get(reverse("complete-feed")) res = client.get(reverse("complete-feed"))

View file

@ -6,7 +6,7 @@ from model_bakery import baker
from articles.models import Article, User from articles.models import Article, User
@pytest.mark.django_db @pytest.mark.django_db()
def test_can_access_list(client: Client, published_article: Article): def test_can_access_list(client: Client, published_article: Article):
res = client.get(reverse("articles-list")) res = client.get(reverse("articles-list"))
assert res.status_code == 200 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 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): def test_only_title_shown_on_list(client: Client, author: User):
title = "This is a very long title mouahahaha" title = "This is a very long title mouahahaha"
abstract = "Some abstract" abstract = "Some abstract"
@ -34,7 +34,7 @@ def test_only_title_shown_on_list(client: Client, author: User):
assert after not in content assert after not in content
@pytest.mark.django_db @pytest.mark.django_db()
def test_access_article_by_slug(client: Client, published_article: Article): def test_access_article_by_slug(client: Client, published_article: Article):
_test_access_article_by_slug(client, published_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 assert html in content
@pytest.mark.django_db @pytest.mark.django_db()
def test_anonymous_cant_access_draft_detail( def test_anonymous_cant_access_draft_detail(
client: Client, unpublished_article: Article client: Client, unpublished_article: Article
): ):
@ -62,7 +62,7 @@ def test_anonymous_cant_access_draft_detail(
assert res.status_code == 404 assert res.status_code == 404
@pytest.mark.django_db @pytest.mark.django_db()
def test_anonymous_can_access_draft_detail_with_key( def test_anonymous_can_access_draft_detail_with_key(
client: Client, unpublished_article: Article 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) _assert_article_is_rendered(unpublished_article, res)
@pytest.mark.django_db @pytest.mark.django_db()
def test_user_can_access_draft_detail( def test_user_can_access_draft_detail(
client: Client, author: User, unpublished_article: Article 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) _test_access_article_by_slug(client, unpublished_article)
@pytest.mark.django_db @pytest.mark.django_db()
def test_anonymous_cant_access_drafts_list( def test_anonymous_cant_access_drafts_list(
client: Client, unpublished_article: Article client: Client, unpublished_article: Article
): ):
@ -89,7 +89,7 @@ def test_anonymous_cant_access_drafts_list(
assert res.status_code == 302 assert res.status_code == 302
@pytest.mark.django_db @pytest.mark.django_db()
def test_user_can_access_drafts_list( def test_user_can_access_drafts_list(
client: Client, author: User, unpublished_article: Article client: Client, author: User, unpublished_article: Article
): ):
@ -100,7 +100,7 @@ def test_user_can_access_drafts_list(
assert unpublished_article.title in content assert unpublished_article.title in content
@pytest.mark.django_db @pytest.mark.django_db()
def test_has_goatcounter_if_set(client: Client, settings): def test_has_goatcounter_if_set(client: Client, settings):
settings.GOATCOUNTER_DOMAIN = "gc.gabnotes.org" settings.GOATCOUNTER_DOMAIN = "gc.gabnotes.org"
res = client.get(reverse("articles-list")) 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 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): def test_doesnt_have_goatcounter_if_unset(client: Client, settings):
settings.GOATCOUNTER_DOMAIN = None settings.GOATCOUNTER_DOMAIN = None
res = client.get(reverse("articles-list")) 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 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): def test_logged_in_user_doesnt_have_goatcounter(client: Client, author: User, settings):
client.force_login(author) client.force_login(author)
settings.GOATCOUNTER_DOMAIN = "gc.gabnotes.org" 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 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): def test_image_is_lazy(client: Client, published_article: Article):
res = client.get(reverse("article-detail", kwargs={"slug": published_article.slug})) res = client.get(reverse("article-detail", kwargs={"slug": published_article.slug}))
assert res.status_code == 200 assert res.status_code == 200

View file

@ -2,6 +2,6 @@ import pytest
from django.core.management import call_command from django.core.management import call_command
@pytest.mark.django_db @pytest.mark.django_db()
def test_missing_migrations(): def test_missing_migrations():
call_command("makemigrations", "--check") call_command("makemigrations", "--check")

View file

@ -3,7 +3,7 @@ import pytest
from articles.models import Article, User from articles.models import Article, User
@pytest.mark.django_db @pytest.mark.django_db()
def test_publish_article(unpublished_article: Article): def test_publish_article(unpublished_article: Article):
assert unpublished_article.status == Article.DRAFT assert unpublished_article.status == Article.DRAFT
assert unpublished_article.published_at is None 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 assert published_article.published_at is not None
@pytest.mark.django_db @pytest.mark.django_db()
def test_unpublish_article(published_article: Article): def test_unpublish_article(published_article: Article):
assert published_article.status == Article.PUBLISHED assert published_article.status == Article.PUBLISHED
assert published_article.published_at is not None 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 assert unpublished_article.published_at is None
@pytest.mark.django_db @pytest.mark.django_db()
def test_save_article_adds_missing_slug(author: User): def test_save_article_adds_missing_slug(author: User):
# Explicitly calling bulk_create with one article because it doesn't call save(). # Explicitly calling bulk_create with one article because it doesn't call save().
articles = Article.objects.bulk_create( articles = Article.objects.bulk_create(
@ -33,7 +33,7 @@ def test_save_article_adds_missing_slug(author: User):
assert article.slug != "" assert article.slug != ""
@pytest.mark.django_db @pytest.mark.django_db()
def test_save_article_doesnt_change_existing_slug(published_article: Article): def test_save_article_doesnt_change_existing_slug(published_article: Article):
original_slug = published_article.slug original_slug = published_article.slug
published_article.title = "This is a brand new title" 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 assert published_article.slug == original_slug
@pytest.mark.django_db @pytest.mark.django_db()
def test_empty_custom_css_minified(published_article): def test_empty_custom_css_minified(published_article):
published_article.custom_css = "" published_article.custom_css = ""
assert published_article.get_minified_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): def test_simple_custom_css_minified(published_article):
published_article.custom_css = ".cls {\n background-color: red;\n}" published_article.custom_css = ".cls {\n background-color: red;\n}"
assert published_article.get_minified_custom_css == ".cls{background-color:red}" 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): def test_larger_custom_css_minified(published_article):
published_article.custom_css = """\ published_article.custom_css = """\
.profile { .profile {

View file

@ -2,7 +2,7 @@ function copy(event) {
const text = event.target.dataset.toCopy; const text = event.target.dataset.toCopy;
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
console.log("Copied"); console.log("Copied");
}) });
} }
$(document).ready(function () { $(document).ready(function () {

View file

@ -6,9 +6,9 @@ from django.core.files import File
from attachments.models import Attachment from attachments.models import Attachment
@pytest.mark.block_network @pytest.mark.block_network()
@pytest.mark.vcr @pytest.mark.vcr()
@pytest.mark.django_db @pytest.mark.django_db()
def test_attachment_is_processed_by_shortpixel(): def test_attachment_is_processed_by_shortpixel():
# This path manipulation is required to make the test run from this directory # This path manipulation is required to make the test run from this directory
# or from upper in the hierarchy (e.g.: settings.BASE_DIR) # or from upper in the hierarchy (e.g.: settings.BASE_DIR)