diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py
index 7f6eec640d381de42e8bdef4b25d1a17aff3f585..8f27b38d469b9e0fbcd2c347194333f21d382975 100644
--- a/aleksis/apps/kort/models.py
+++ b/aleksis/apps/kort/models.py
@@ -156,10 +156,9 @@ class CardPrinter(ExtensibleModel):
             self.status
             not in (CardPrinterStatus.NOT_REGISTERED.value, CardPrinterStatus.OFFLINE.value)
             and self.last_seen_at
-        ):
-            if self.last_seen_at < timezone.now() - timedelta(minutes=1):
-                self.status = CardPrinterStatus.OFFLINE.value
-                self.save()
+        ) and self.last_seen_at < timezone.now() - timedelta(minutes=1):
+            self.status = CardPrinterStatus.OFFLINE.value
+            self.save()
 
     @classmethod
     def check_online_status_for_all(cls, qs=None):
@@ -263,7 +262,7 @@ class CardLayout(ExtensibleModel):
             t = Template(self.template)
             t.render(Context())
         except Exception as e:
-            raise ValidationError(_("Template is invalid: {}").format(e))
+            raise ValidationError(_("Template is invalid: {}").format(e)) from e
 
     def __str__(self):
         return self.name
diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py
index 64dd2414ee0735a84f12eaa1926e489fd5da5147..47cc42efbb6773480f1e82e41c10b7c1164849ee 100644
--- a/aleksis/apps/kort/views.py
+++ b/aleksis/apps/kort/views.py
@@ -339,7 +339,7 @@ class CardLayoutFormMixin:
 
     def post(self, request, *args, **kwargs):
         self.object = self.get_object()
-        context = self.get_context_data(**kwargs)
+        self.get_context_data(**kwargs)
         form = self.get_form()
         if form.is_valid() and self.formset.is_valid():
             return self.form_valid(form)
diff --git a/tox.ini b/tox.ini
index 85c2494a5a2f5480bb05d48781edfaa74803eeab..294e65bc96d4e06262b67e3e6b8a987307b226e4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,11 +4,12 @@ skip_missing_interpreters = true
 envlist = py39,py310,py311
 
 [testenv]
-allowlist_externals = poetry
+allowlist_externals =
+    poetry
+    yarnpkg
 skip_install = true
-envdir = {toxworkdir}/globalenv
 commands_pre =
-     poetry install
+     poetry install --all-extras
      poetry run aleksis-admin vite build
      poetry run aleksis-admin collectstatic --no-input
 commands =
@@ -22,14 +23,17 @@ setenv =
     TEST_HOST = {env:TEST_HOST:172.17.0.1}
 
 [testenv:lint]
+commands_pre =
+    poetry install --only=dev
+    yarnpkg --cwd=.dev-js
 commands =
-    poetry run black --check --diff aleksis/
-    poetry run isort -c --diff --stdout aleksis/
-    poetry run flake8 {posargs} aleksis/
-    poetry run sh -c "aleksis-admin yarn run prettier --check --ignore-path={toxinidir}/.prettierignore {toxinidir}"
-    poetry run sh -c "aleksis-admin yarn run eslint {toxinidir}/aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=."
+    poetry run ruff check {posargs} aleksis/
+    yarnpkg --cwd=.dev-js run prettier --ignore-path={toxinidir}/.prettierignore {posargs} --check ..
+    yarnpkg --cwd=.dev-js run eslint ../aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=.
 
 [testenv:security]
+commands_pre =
+    poetry install --all-extras
 commands =
     poetry show --no-dev
     poetry run safety check --full-report
@@ -41,33 +45,25 @@ commands_pre =
 commands = poetry build
 
 [testenv:docs]
+commands_pre =
+    poetry install --with docs
 commands = poetry run make -C docs/ html {posargs}
 
 [testenv:reformat]
+commands_pre =
+    poetry install --only=dev
+    yarnpkg --cwd=.dev-js
 commands =
-    poetry run isort aleksis/
-    poetry run black aleksis/
-    poetry run sh -c "aleksis-admin yarn run prettier --write --ignore-path={toxinidir}/.prettierignore {toxinidir}"
+    poetry run ruff format aleksis/
+    yarnpkg --cwd=.dev-js run prettier --ignore-path={toxinidir}/.prettierignore --write ..
 
 [testenv:makemessages]
+commands_pre =
+    poetry install
 commands =
     poetry run aleksis-admin makemessages --no-wrap -e html,txt,py,email -i static -l ar -l de_DE -l fr -l nb_NO -l tr_TR -l la -l uk -l ru
     poetry run aleksis-admin makemessages --no-wrap -d djangojs -i **/node_modules -l ar -l de_DE -l fr -l nb_NO -l tr_TR -l la -l uk -l ru
 
-[flake8]
-max_line_length = 100
-exclude = migrations,tests
-ignore = BLK100,E203,E231,W503,D100,D101,D102,D103,D104,D105,D106,D107,RST215,RST214,F821,F841,S106,T100,T101,DJ05
-
-[isort]
-profile = black
-line_length = 100
-default_section = THIRDPARTY
-known_first_party = aleksis
-known_django = django
-skip = migrations
-sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
-
 [pytest]
 DJANGO_SETTINGS_MODULE = aleksis.core.settings
 junit_family = legacy