diff --git a/devproject/settings.py b/devproject/settings.py
index e6fb14c682..9e638f08fd 100644
--- a/devproject/settings.py
+++ b/devproject/settings.py
@@ -98,8 +98,6 @@
 
 USE_I18N = True
 
-USE_L10N = True
-
 USE_TZ = True
 
 
@@ -264,7 +262,6 @@
     "debug_toolbar.panels.templates.TemplatesPanel",
     "debug_toolbar.panels.cache.CachePanel",
     "debug_toolbar.panels.signals.SignalsPanel",
-    "debug_toolbar.panels.logging.LoggingPanel",
 ]
 
 
diff --git a/misago/categories/migrations/0010_alter_rolecategoryacl_category_role.py b/misago/categories/migrations/0010_alter_rolecategoryacl_category_role.py
new file mode 100644
index 0000000000..7b81d477e8
--- /dev/null
+++ b/misago/categories/migrations/0010_alter_rolecategoryacl_category_role.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.7 on 2023-11-13 19:08
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("misago_categories", "0009_auto_20221101_2111"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="rolecategoryacl",
+            name="category_role",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                to="misago_categories.categoryrole",
+            ),
+        ),
+    ]
diff --git a/misago/conf/context_processors.py b/misago/conf/context_processors.py
index 87136bd7dc..93f15afeac 100644
--- a/misago/conf/context_processors.py
+++ b/misago/conf/context_processors.py
@@ -15,7 +15,7 @@
     (
         f"{misago.__version__}{misago.__released__}"
         f"{settings.LANGUAGE_CODE}{settings.SECRET_KEY}"
-    ).encode("utf-8")
+    ).encode()
 ).hexdigest()
 
 
diff --git a/misago/faker/users.py b/misago/faker/users.py
index 18e9117041..8ca08a74c7 100644
--- a/misago/faker/users.py
+++ b/misago/faker/users.py
@@ -63,7 +63,7 @@ def get_fake_username(fake):
 
 
 def get_fake_avatars(email):
-    email_hash = hashlib.md5(email.lower().encode("utf-8")).hexdigest()
+    email_hash = hashlib.md5(email.lower().encode()).hexdigest()
 
     return [
         {"size": size, "url": GRAVATAR_URL % (email_hash, size)}
diff --git a/misago/markup/checksums.py b/misago/markup/checksums.py
index 6f407691f2..d6ef791653 100644
--- a/misago/markup/checksums.py
+++ b/misago/markup/checksums.py
@@ -27,7 +27,7 @@ def make_checksum(parsed, unique_values=None):
     unique_values = unique_values or []
     seeds = [parsed] + [str(v) for v in unique_values]
 
-    return sha256("+".join(seeds).encode("utf-8")).hexdigest()
+    return sha256("+".join(seeds).encode()).hexdigest()
 
 
 def is_checksum_valid(parsed, checksum, unique_values=None):
diff --git a/misago/themes/admin/css.py b/misago/themes/admin/css.py
index 90335aacc6..1745c7d9de 100644
--- a/misago/themes/admin/css.py
+++ b/misago/themes/admin/css.py
@@ -62,7 +62,7 @@ def rebuild_css(media_map, css):
         css.build_file.delete(save=False)
 
     css_source = css.source_file.read().decode("utf-8")
-    build_source = change_css_source(media_map, css_source).encode("utf-8")
+    build_source = change_css_source(media_map, css_source).encode()
 
     build_file_name = css.name
     if css.source_hash in build_file_name:
diff --git a/misago/themes/admin/forms.py b/misago/themes/admin/forms.py
index 43324209c6..bcfc5f02cd 100644
--- a/misago/themes/admin/forms.py
+++ b/misago/themes/admin/forms.py
@@ -244,7 +244,7 @@ def clean(self):
 
     def save(self):
         name = self.cleaned_data["name"]
-        source = self.cleaned_data["source"].encode("utf-8")
+        source = self.cleaned_data["source"].encode()
         source_file = ContentFile(source, name)
 
         self.instance.name = name
diff --git a/misago/themes/admin/tests/test_getting_remote_css_size.py b/misago/themes/admin/tests/test_getting_remote_css_size.py
index 2b32ad04ad..45eb8f4b08 100644
--- a/misago/themes/admin/tests/test_getting_remote_css_size.py
+++ b/misago/themes/admin/tests/test_getting_remote_css_size.py
@@ -6,7 +6,7 @@
 @responses.activate
 def test_task_uses_response_body_to_set_css_size(css_link):
     content = "html {}"
-    content_bytes = content.encode("utf-8")
+    content_bytes = content.encode()
 
     responses.add(
         responses.GET,
diff --git a/misago/threads/migrations/0013_rename_pollvote_poll_voter_name_misago_thre_poll_id_c3e8fe_idx_and_more.py b/misago/threads/migrations/0013_rename_pollvote_poll_voter_name_misago_thre_poll_id_c3e8fe_idx_and_more.py
new file mode 100644
index 0000000000..e79b534ea6
--- /dev/null
+++ b/misago/threads/migrations/0013_rename_pollvote_poll_voter_name_misago_thre_poll_id_c3e8fe_idx_and_more.py
@@ -0,0 +1,67 @@
+# Generated by Django 4.2.7 on 2023-11-13 19:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("misago_acl", "0004_cache_version"),
+        ("misago_threads", "0012_set_dj_partial_indexes"),
+    ]
+
+    operations = [
+        migrations.RenameIndex(
+            model_name="pollvote",
+            new_name="misago_thre_poll_id_c3e8fe_idx",
+            old_fields=("poll", "voter_name"),
+        ),
+        migrations.RenameIndex(
+            model_name="post",
+            new_name="misago_thre_thread__4e5114_idx",
+            old_fields=("thread", "id"),
+        ),
+        migrations.RenameIndex(
+            model_name="post",
+            new_name="misago_thre_poster__eefeb2_idx",
+            old_fields=("poster", "posted_on"),
+        ),
+        migrations.RenameIndex(
+            model_name="post",
+            new_name="misago_thre_is_even_d29c90_idx",
+            old_fields=("is_event", "is_hidden"),
+        ),
+        migrations.RenameIndex(
+            model_name="subscription",
+            new_name="misago_thre_send_em_da8b99_idx",
+            old_fields=("send_email", "last_read_on"),
+        ),
+        migrations.RenameIndex(
+            model_name="thread",
+            new_name="misago_thre_categor_6cb52c_idx",
+            old_fields=("category", "id"),
+        ),
+        migrations.RenameIndex(
+            model_name="thread",
+            new_name="misago_thre_categor_7ec8b0_idx",
+            old_fields=("category", "last_post_on"),
+        ),
+        migrations.RenameIndex(
+            model_name="thread",
+            new_name="misago_thre_categor_c5eaf1_idx",
+            old_fields=("category", "replies"),
+        ),
+        migrations.AlterField(
+            model_name="attachmenttype",
+            name="limit_downloads_to",
+            field=models.ManyToManyField(
+                blank=True, related_name="+", to="misago_acl.role"
+            ),
+        ),
+        migrations.AlterField(
+            model_name="attachmenttype",
+            name="limit_uploads_to",
+            field=models.ManyToManyField(
+                blank=True, related_name="+", to="misago_acl.role"
+            ),
+        ),
+    ]
diff --git a/misago/threads/models/pollvote.py b/misago/threads/models/pollvote.py
index 7c64774dbe..16b8b95e5a 100644
--- a/misago/threads/models/pollvote.py
+++ b/misago/threads/models/pollvote.py
@@ -16,4 +16,6 @@ class PollVote(models.Model):
     choice_hash = models.CharField(max_length=12, db_index=True)
 
     class Meta:
-        index_together = [["poll", "voter_name"]]
+        indexes = [
+            models.Index(fields=["poll", "voter_name"]),
+        ]
diff --git a/misago/threads/models/post.py b/misago/threads/models/post.py
index 2bb1129928..9067ded6cd 100644
--- a/misago/threads/models/post.py
+++ b/misago/threads/models/post.py
@@ -94,12 +94,10 @@ class Meta:
                 condition=Q(is_event=True),
             ),
             GinIndex(fields=["search_vector"]),
-        ]
-
-        index_together = [
-            ("thread", "id"),  # speed up threadview for team members
-            ("is_event", "is_hidden"),
-            ("poster", "posted_on"),
+            # Speed up threadview for team members
+            models.Index(fields=["thread", "id"]),
+            models.Index(fields=["is_event", "is_hidden"]),
+            models.Index(fields=["poster", "posted_on"]),
         ]
 
     def __str__(self):
diff --git a/misago/threads/models/subscription.py b/misago/threads/models/subscription.py
index 0ee3a6f42f..6153e4d76b 100644
--- a/misago/threads/models/subscription.py
+++ b/misago/threads/models/subscription.py
@@ -13,4 +13,6 @@ class Subscription(models.Model):
     send_email = models.BooleanField(default=False)
 
     class Meta:
-        index_together = [["send_email", "last_read_on"]]
+        indexes = [
+            models.Index(fields=["send_email", "last_read_on"]),
+        ]
diff --git a/misago/threads/models/thread.py b/misago/threads/models/thread.py
index c2bfaf6617..c39fe0d0e1 100644
--- a/misago/threads/models/thread.py
+++ b/misago/threads/models/thread.py
@@ -143,12 +143,9 @@ class Meta:
                 fields=["is_hidden"],
                 condition=Q(is_hidden=False),
             ),
-        ]
-
-        index_together = [
-            ["category", "id"],
-            ["category", "last_post_on"],
-            ["category", "replies"],
+            models.Index(fields=["category", "id"]),
+            models.Index(fields=["category", "last_post_on"]),
+            models.Index(fields=["category", "replies"]),
         ]
 
     def __str__(self):
diff --git a/misago/users/admin/tests/test_django_admin_auth.py b/misago/users/admin/tests/test_django_admin_auth.py
index 5ce05ea243..419200fcf3 100644
--- a/misago/users/admin/tests/test_django_admin_auth.py
+++ b/misago/users/admin/tests/test_django_admin_auth.py
@@ -29,16 +29,16 @@ def test_login(self):
 
     def test_logout(self):
         """its possible to sign out from django admin"""
-        response = self.client.get(reverse("admin:index"))
+        response = self.client.post(reverse("admin:index"))
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, self.user.username)
 
         # assert there's no showstopper on signout page
-        response = self.client.get(reverse("admin:logout"))
+        response = self.client.post(reverse("admin:logout"))
         self.assertEqual(response.status_code, 200)
         self.assertNotContains(response, self.user.username)
 
         # user was signed out
-        response = self.client.get(reverse("admin:index"))
+        response = self.client.post(reverse("admin:index"))
         self.assertEqual(response.status_code, 200)
         self.assertNotContains(response, self.user.username)
diff --git a/misago/users/migrations/0025_alter_user_rank.py b/misago/users/migrations/0025_alter_user_rank.py
new file mode 100644
index 0000000000..52fc40d693
--- /dev/null
+++ b/misago/users/migrations/0025_alter_user_rank.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.7 on 2023-11-13 19:08
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("misago_users", "0024_user_notifications"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="user",
+            name="rank",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.PROTECT,
+                to="misago_users.rank",
+            ),
+        ),
+    ]
diff --git a/misago/users/utils.py b/misago/users/utils.py
index 53a45d428f..fa8c0f6e6b 100644
--- a/misago/users/utils.py
+++ b/misago/users/utils.py
@@ -2,7 +2,7 @@
 
 
 def hash_email(email: str) -> str:
-    return hashlib.md5(email.lower().encode("utf-8")).hexdigest()
+    return hashlib.md5(email.lower().encode()).hexdigest()
 
 
 def slugify_username(username: str) -> str:
diff --git a/requirements.in b/requirements.in
index 9400c7539e..38cad4b629 100644
--- a/requirements.in
+++ b/requirements.in
@@ -3,7 +3,7 @@ ariadne_django
 black
 celery[redis]
 coveralls
-django<4
+django~=4.2
 djangorestframework
 django-debug-toolbar
 django-mptt
@@ -14,7 +14,7 @@ markdown
 social-auth-app-django
 pyjwt
 pillow
-psycopg2-binary
+psycopg[binary]
 pytest
 pytest-cov
 pytest-django
diff --git a/requirements.txt b/requirements.txt
index d13b3f4fc6..bdf9c3e573 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,7 @@ amqp==5.2.0
     # via kombu
 anyio==4.0.0
     # via starlette
-ariadne==0.20.1
+ariadne==0.21
     # via
     #   -r requirements.in
     #   ariadne-django
@@ -18,9 +18,9 @@ asgiref==3.7.2
     # via django
 billiard==4.2.0
     # via celery
-black==23.10.1
+black==23.11.0
     # via -r requirements.in
-celery[redis]==5.3.4
+celery[redis]==5.3.5
     # via -r requirements.in
 certifi==2023.7.22
     # via requests
@@ -53,7 +53,7 @@ defusedxml==0.8.0rc2
     # via
     #   python3-openid
     #   social-auth-core
-django==3.2.23
+django==4.2.7
     # via
     #   -r requirements.in
     #   ariadne-django
@@ -75,7 +75,7 @@ djangorestframework==3.14.0
     # via -r requirements.in
 docopt==0.6.2
     # via coveralls
-faker==19.13.0
+faker==20.0.0
     # via -r requirements.in
 graphql-core==3.2.3
     # via ariadne
@@ -109,14 +109,16 @@ pathspec==0.11.2
     # via black
 pillow==10.1.0
     # via -r requirements.in
-platformdirs==3.11.0
+platformdirs==4.0.0
     # via black
 pluggy==1.3.0
     # via pytest
-prompt-toolkit==3.0.39
+prompt-toolkit==3.0.40
     # via click-repl
-psycopg2-binary==2.9.9
+psycopg[binary]==3.1.12
     # via -r requirements.in
+psycopg-binary==3.1.12
+    # via psycopg
 pycparser==2.21
     # via cffi
 pyjwt==2.8.0
@@ -132,7 +134,7 @@ pytest==7.4.3
     #   syrupy
 pytest-cov==4.1.0
     # via -r requirements.in
-pytest-django==4.6.0
+pytest-django==4.7.0
     # via -r requirements.in
 pytest-mock==3.12.0
     # via -r requirements.in
@@ -145,11 +147,10 @@ python3-openid==3.2.0
 pytz==2023.3.post1
     # via
     #   -r requirements.in
-    #   django
     #   djangorestframework
 pyyaml==6.0.1
     # via responses
-redis==4.6.0
+redis==5.0.1
     # via celery
 requests==2.31.0
     # via
@@ -163,7 +164,7 @@ requests-oauthlib==1.3.1
     # via social-auth-core
 responses==0.24.0
     # via -r requirements.in
-ruff==0.1.4
+ruff==0.1.5
     # via -r requirements.in
 six==1.16.0
     # via
@@ -184,12 +185,14 @@ starlette==0.32.0.post1
 syrupy==4.6.0
     # via -r requirements.in
 typing-extensions==4.8.0
-    # via ariadne
+    # via
+    #   ariadne
+    #   psycopg
 tzdata==2023.3
     # via celery
 unidecode==1.3.7
     # via -r requirements.in
-urllib3==2.0.7
+urllib3==2.1.0
     # via
     #   requests
     #   responses