From 2e11b536cb2c5c0ce692d44e675f62ba04fe9f90 Mon Sep 17 00:00:00 2001 From: erjemin Date: Mon, 14 Sep 2026 00:02:45 +0300 Subject: [PATCH] =?UTF-8?q?add:=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20(1)?= =?UTF-8?q?=20404?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hypn0/hypn0/urls.py | 5 + hypn0/hypn0_site/tests.py | 35 +++ hypn0/hypn0_site/urls.py | 7 +- hypn0/hypn0_site/views.py | 113 ++++++++ hypn0/templates/_error/404.html | 480 ++++++++++++++++++++++++++++++++ public/media/.gitignore | 9 +- public/media/_error/.gitignore | 9 + 7 files changed, 652 insertions(+), 6 deletions(-) create mode 100644 hypn0/templates/_error/404.html create mode 100644 public/media/_error/.gitignore diff --git a/hypn0/hypn0/urls.py b/hypn0/hypn0/urls.py index 999fd8e..5318d23 100644 --- a/hypn0/hypn0/urls.py +++ b/hypn0/hypn0/urls.py @@ -30,6 +30,11 @@ urlpatterns = [ path('', include('hypn0_site.urls')), ] +handler400 = "hypn0_site.views.error_400" +handler403 = "hypn0_site.views.error_403" +handler404 = "hypn0_site.views.error_404" +handler500 = "hypn0_site.views.error_500" + if settings.DEBUG: import mimetypes import debug_toolbar diff --git a/hypn0/hypn0_site/tests.py b/hypn0/hypn0_site/tests.py index f6e99a3..dbad9ef 100644 --- a/hypn0/hypn0_site/tests.py +++ b/hypn0/hypn0_site/tests.py @@ -1425,3 +1425,38 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase): # 5. Проверка 404 для несуществующего слага resp_404 = self.client.get("/blog/non-existent-slug-xyz") self.assertEqual(resp_404.status_code, 404) + + @override_settings(DEBUG=False) + def test_custom_404_page_rendering(self): + """Проверка отдачи кастомной страницы 404 в режиме DEBUG=False.""" + response = self.client.get("/totally-non-existent-hypno-route-404") + self.assertEqual(response.status_code, 404) + self.assertContains(response, "Гипноузел не обнаружен", status_code=404) + self.assertContains(response, "СБОЙ МАТРИЦЫ // КОД? 404", status_code=404) + self.assertContains(response, "return-btn", status_code=404) + self.assertContains(response, "Стираем остатки несущес­твующей страницы", status_code=404) + + @override_settings(DEBUG=True) + def test_debug_error_preview_routing(self): + """Проверка dev-маршрутизации предпросмотра страниц ошибок при DEBUG=True.""" + # 1. Индексный список страниц ошибок + resp_list = self.client.get("/_error/") + self.assertEqual(resp_list.status_code, 200) + self.assertContains(resp_list, "Центр отладки страниц ошибок") + self.assertContains(resp_list, "/_error/404") + + # 2. Предпросмотр конкретной ошибки 404 + resp_404 = self.client.get("/_error/404") + self.assertEqual(resp_404.status_code, 404) + self.assertContains(resp_404, "Гипноузел не обнаружен", status_code=404) + self.assertContains(resp_404, "СБОЙ МАТРИЦЫ // КОД? 404", status_code=404) + + # 3. Предпросмотр с суффиксом .html + resp_404_html = self.client.get("/_error/404.html") + self.assertEqual(resp_404_html.status_code, 404) + self.assertContains(resp_404_html, "Гипноузел не обнаружен", status_code=404) + + # 4. Несуществующий шаблон ошибки в dev-режиме + resp_non_existent = self.client.get("/_error/non-existent-code-999") + self.assertEqual(resp_non_existent.status_code, 404) + self.assertContains(resp_non_existent, "Шаблон для ошибки 'non-existent-code-999' не найден", status_code=404) diff --git a/hypn0/hypn0_site/urls.py b/hypn0/hypn0_site/urls.py index 314bfad..7d4000f 100644 --- a/hypn0/hypn0_site/urls.py +++ b/hypn0/hypn0_site/urls.py @@ -19,4 +19,9 @@ urlpatterns = [ ] if settings.DEBUG: - urlpatterns += [path("tmp/", views.tmp, name="web_tmp")] + urlpatterns += [ + path("tmp/", views.tmp, name="web_tmp"), + path("_error", views.debug_error_preview, name="debug_error_list"), + path("_error/", views.debug_error_preview, name="debug_error_list_slash"), + path("_error/", views.debug_error_preview, name="debug_error_preview"), + ] diff --git a/hypn0/hypn0_site/views.py b/hypn0/hypn0_site/views.py index 06dd006..3d110f1 100644 --- a/hypn0/hypn0_site/views.py +++ b/hypn0/hypn0_site/views.py @@ -157,6 +157,119 @@ def tmp(request: HttpRequest | None) -> HttpResponse: return render(request, "tmp.html", {}) +def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse: + """ + Dev-представление для отладки и предпросмотра страниц ошибок в режиме DEBUG. + Поддерживает пути вида /_error/404, /_error/404.html, /_error/500, /_error/, /_error/under_reconstruction. + """ + from django.template.exceptions import TemplateDoesNotExist + from django.template.loader import get_template + + # Очистка имени шаблона от расширения .html и слэшей + raw_name = code.strip("/").removesuffix(".html") if code else "" + + available_codes = [ + "400", + "401", + "403", + "404", + "413", + "429", + "500", + "502", + "503", + "504", + "under_reconstruction", + ] + + if not raw_name: + # Индексная страница со списком всех кодов ошибок для dev-отладки + links = [] + for c in available_codes: + exists = False + for tpl in [f"_error/{c}.html", f"{c}.html"]: + try: + get_template(tpl) + exists = True + break + except TemplateDoesNotExist: + pass + status_mark = "готов к просмотру" if exists else "шаблон не создан" + links.append(f"
  • /{c} ({status_mark})
  • ") + + html = f""" + + + + Dev: Предпросмотр страниц ошибок // HypnoSVG + + + +
    +

    Центр отладки страниц ошибок (DEBUG=True)

    +

    Выберите код для инспекции верстки и интерактивных скриптов перекалибровки:

    +
      + {''.join(links)} +
    +
    + +""" + return HttpResponse(html, content_type="text/html; charset=utf-8") + + # Поиск соответствующего шаблона + template_candidates = [ + f"_error/{raw_name}.html", + f"{raw_name}.html", + f"_error/{raw_name}", + raw_name, + ] + + found_template = None + for candidate in template_candidates: + try: + get_template(candidate) + found_template = candidate + break + except TemplateDoesNotExist: + continue + + if not found_template: + return HttpResponse( + f"

    Шаблон для ошибки '{raw_name}' не найден

    Проверены: {', '.join(template_candidates)}

    ← Вернуться к списку

    ", + status=404, + content_type="text/html; charset=utf-8", + ) + + status_code = 200 + if raw_name.isdigit() and 400 <= int(raw_name) <= 599: + status_code = int(raw_name) + + return render(request, found_template, {}, status=status_code) + + +def error_400(request: HttpRequest, exception: Exception | None = None) -> HttpResponse: + return render(request, "_error/400.html", {}, status=400) + + +def error_403(request: HttpRequest, exception: Exception | None = None) -> HttpResponse: + return render(request, "_error/403.html", {}, status=403) + + +def error_404(request: HttpRequest, exception: Exception | None = None) -> HttpResponse: + return render(request, "_error/404.html", {}, status=404) + + +def error_500(request: HttpRequest) -> HttpResponse: + return render(request, "_error/500.html", {}, status=500) + + @require_POST def generate(request: HttpRequest) -> HttpResponse: """ diff --git a/hypn0/templates/_error/404.html b/hypn0/templates/_error/404.html new file mode 100644 index 0000000..3a10d16 --- /dev/null +++ b/hypn0/templates/_error/404.html @@ -0,0 +1,480 @@ + + + + + + 404 — Векторная реальность не найдена | HypnoSVG + + + + + + + + +
    +
    + + HypnoSVG + + +
    +
    + +
    +
    +
    + 404 Thinking Slug +
    + +
    СБОЙ МАТРИЦЫ // КОД? 404
    + +

    Гипноузел не обнаружен

    + +

    + Запра­шиваемый блок ментального пространства стёрт из подсознания сервера или никогда не существовал в этой версии вселенной. +

    + +
    + Справка бюрократа 24-го уровня:
     
    + «Попытка осознать отсутс­твующую координату приводит к локальному перегреву неокортекса. Не вгляды­вайтесь в пустоту. Гипножаба рекомендует немедленно сменить вектор внимания.» +
    + +
    +
    + + +
    + + +
    +
    + Инициа­лизация перека­либровки сознания… +
    +
    +
    +
    +
    + Снижение ментальной нагрузки на кластер + +
    +
    +
    +
    +
    + +
    +
    ░▒▓█ HYPNO-SVG // ERROR 404 NOT FOUND █▓▒░
    +
    Hypn0.xyz (HypnoSVG) © 2026. Слава Гипножабе. Все права подчинены.
    +
    + + + + diff --git a/public/media/.gitignore b/public/media/.gitignore index 17b8771..d940fc2 100644 --- a/public/media/.gitignore +++ b/public/media/.gitignore @@ -8,8 +8,7 @@ # и служит только для сохранения структуры каталогов в репозитории. !.gitkeep -# Лучшие SVG (храним долго) -!gallery - -# Публичные, но временные, SVG (TTL 14 дней) -!temp \ No newline at end of file +# Каталог _error используется для хранения пользовательских страниц ошибок. Туда все "прилетит" +# при сборке Docker-образа (и nginx будет отдавать их) и к репозитории каталог _error будет пустым +# (только с .gitkeep или .gitignore). +!_error/ diff --git a/public/media/_error/.gitignore b/public/media/_error/.gitignore new file mode 100644 index 0000000..cb69ca3 --- /dev/null +++ b/public/media/_error/.gitignore @@ -0,0 +1,9 @@ +# В этом каталоге будут храниться шаблоны ошибок (404, 500) и другие файлы необходимые для корректного +# отображения html-страниц ошибок. +# +# Все необходимые файлы "прилетят" сюда во время сборки Docker-образа из каталога hypn0/hypn0/templates/_error/ +# и будут доступны по URL /media/_error/ (в проде через nginx, в dev через Django). +*.* + +# Чтобы Git отслеживал пустой каталог можно использовать .gitkeep (но у нас его рель исполняет .gitignore) +!.gitkeep \ No newline at end of file