8 Commits

Author SHA1 Message Date
86bfd9b07b fix: "обход" скрытых миграций TagIt для продакшн.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m34s
2026-02-25 21:31:33 +03:00
c3c81d7ff5 add: Добавлен select2 для управления тегами
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m20s
2026-02-25 21:10:11 +03:00
f4cce3d08a mod: Корректная проверка обновлений каждый 30 минут (1800 сек.) 2026-02-23 20:03:06 +03:00
45275c51f6 add: Счетчик Google Analytics (GA4 - поток Goofle Tag) 2026-02-23 19:58:29 +03:00
f2f98d9229 add: Счетчик метрики
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m26s
2026-02-22 13:01:16 +03:00
a33b517a3e add: Планы (памятка) 2026-02-22 13:00:49 +03:00
d4624e7761 mod: Улучшения для мобильных устройств. 2026-02-22 12:12:49 +03:00
a608dea61f mod: Страницы ошибок (в новом дизайне и оптимизированы). 2026-02-22 02:55:21 +03:00
19 changed files with 646 additions and 198 deletions

45
PLANS.md Normal file
View File

@@ -0,0 +1,45 @@
# Планы по развитию проекта (DicQuo)
## 1. Список Авторов (Feature: Authors List)
**Цель:** Улучшить SEO (плоская структура) и навигацию, сохранив "Дзен" (минимализм).
**Концепция:**
- Добавить иконку "Люди/Авторы" в шапку сайта (рядом с бургером).
- По клику открывается **полноэкранный оверлей** (как статистика).
- Внутри список авторов карточками/строками.
**Элементы списка:**
1. **Имя Автора** (крупно) -> Ссылка на ротацию цитат автора (`/?tag=author-slug`).
2. **Счетчик цитат** (мелко, например `(25)`) -> Клик раскрывает "гармошку" (аккордеон).
3. **Список цитат** (внутри гармошки) -> Прямые ссылки на цитаты (например: `/123_nachalo-frazy...`). Текст ссылок — начало фразы.
**Техническая реализация:**
- **Backend:** `Context Processor` или логика в `IndexView` (или отдельный AJAX endpoint) для сбора данных:
```json
[
{
"name": "Имя",
"slug": "slug",
"count": 25,
"quotes": [{"id": 1, "url": "...", "text": "Текст..."}, ...]
}, ...
]
```
- **Frontend:** HTML/CSS для модального окна и JS для раскрытия списков.
## 2. Админка
- Починить мелкие баги в управлении тегами.
- Улучшить управление настройками типографа (etpgrf) через виртуальные поля.
- Поля в админке для настройки (кавычки, неразрывные пробелы и т.д.).
- При сохранении применять типограф к полям `szContent` -> `szContentHTML`.
- `szContentHTML` сделать редактитруемым чекрез CodeMirror (для ручной типографики тяжёлых случаев).
## 3. SEO и Оптимизация
- Проверить индексацию новых страниц `static_404`/`static_500`.
- Убедиться, что `canonical` ссылки работают корректно.
## 4. Дальние планы
- Форма для добавления цитат пользователями (с модерацией).
- API для интеграции с внешними сервисами (магазинами грампластинок и музыкальными сервисами).
- Сбор цитат из открытых источников (например, с помощью парсинга сайтов с цитатами или API).

View File

@@ -76,18 +76,24 @@ server {
}
# --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) ---
# Если Django упал (502) или файл из media не найден Nginx-ом (404), показываем наши красивые заглушки.
# Файлы копируются в media/errors при старте контейнера.
# ТРЕБУЕТСЯ ЗАМЕНА ПРИ ДЕПЛОЕ: /home/user/app/dq-site -> ваш реальный путь
error_page 404 /404.html;
error_page 500 502 503 504 /500.html;
# Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML.
# Эти файлы должны лежать в папке, доступной Nginx (например, в media/errors).
#
# ВАЖНО:
# 1. Файлы 50x.html (500, 502, 503, 504) копируются в media/errors при старте контейнера (см. docker-compose.prod.yml -> command).
# 2. error_page директива перехватывает ошибки от апстрима (Gunicorn).
location = /404.html {
error_page 500 502 503 504 /500.html;
# (Опционально) 404 тоже можно кастомизировать, но обычно Django сам отдает 404.
# Nginx отдаст эту страницу только если сам не найдет статику.
error_page 404 /404.html;
location = /500.html {
root /home/user/app/dq-site/media/errors;
internal;
}
location = /500.html {
location = /404.html {
root /home/user/app/dq-site/media/errors;
internal;
}

View File

@@ -58,6 +58,7 @@ INSTALLED_APPS: list[str] = [
'django.contrib.sites',
'django.contrib.sitemaps',
'taggit.apps.TaggitAppConfig',
'django_select2',
'web.apps.WebConfig',
]
@@ -78,6 +79,11 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR.parent / 'database/db.sqlite3',
'OPTIONS': {
# Таймаут ожидания блокировки SQLite (в секундах)
# При сложных операциях (например, каскадное удаление тегов) нужно больше времени
'timeout': 20,
},
}
}

View File

@@ -15,7 +15,7 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path
from django.urls import path, re_path, include
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.views.generic import TemplateView
@@ -33,6 +33,7 @@ urlpatterns = [
re_path(r'^$', views.IndexView.as_view()),
re_path(r'^(?P<dq_id>\d{1,12})_\S*$', views.DictumDetailView.as_view()),
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
path("select2/", include("django_select2.urls")),
]
if settings.DEBUG:
@@ -42,4 +43,7 @@ if settings.DEBUG:
path('500/', TemplateView.as_view(template_name="500.html")),
path('403/', TemplateView.as_view(template_name="403.html")),
path('400/', TemplateView.as_view(template_name="400.html")),
# Для проверки статических страниц ошибок (Nginx)
path('static_404/', TemplateView.as_view(template_name="static_404.html")),
path('static_500/', TemplateView.as_view(template_name="static_500.html")),
]

View File

@@ -1,31 +1,44 @@
{% extends "base.html" %}
{% block Title %}400: Плохой запрос{% endblock %}
{% block CONTENT %}{% include "blocks/header_nav.html" %}
<div class="container main-content">
<!-- Осно<D0BD><D0BE>ной контент: Текст + Картинка -->
<div class="content-row">
<!-- Текстовая колонка -->
<div class="text-col">
<!-- Цитата -->
<blockquote id="bb" style="border:none; margin:0; padding:0;">
<span style="margin-left:-0.44em;">&laquo;</span>Вы спрашиваете меня о&nbsp;чем-то странном. Я&nbsp;не&nbsp;понимаю ваш запрос.»
</blockquote>
<!-- Автор -->
<div id="author">
<cite>Озадаченный Сервер (400)</cite>
</div>
</div>
</div>
<!-- Блок тегов и навигации -->
<div class="tags">
<a href="/">Сформулировать иначе (на главную)</a>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>400: Плохой запрос | DicQuo</title>
<style>
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
header > #logo { margin-top: 1vh; float: left; }
header > #logo a { border: none; text-decoration: none; color: silver;}
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</style>
</head>
<body>
<header>
<div id="logo">
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
</div>
</header>
<main>
<article>
<figure>
<p>Загадочно:</p>
<blockquote id="bb">
<span style="margin-left:-0.44em;">&laquo;</span>Вы спрашиваете меня о&nbsp;чем-то странном. Я&nbsp;не&nbsp;понимаю
ваш запрос.»
</blockquote>
<cite>Озадаченный Сервер (400)</cite>
</figure>
</article>
</main>
<div class="tags">
<a href="/">Сформулировать иначе (на главную)</a>
</div>
{% endblock %}
</body>
</html>

View File

@@ -1,31 +1,43 @@
{% extends "base.html" %}
{% block Title %}403: Доступ запрещен{% endblock %}
{% block CONTENT %}{% include "blocks/header_nav.html" %}
<div class="container main-content">
<!-- Основной контент: Текст + Картинка -->
<div class="content-row">
<!-- Текстовая колонка -->
<div class="text-col">
<!-- Цитата -->
<blockquote id="bb" style="border:none; margin:0; padding:0;">
<span style="margin-left:-0.44em;">&laquo;</span>Вам сюда нельзя. Даже если очень хочется. Уходите!»
</blockquote>
<!-- Автор -->
<div id="author">
<cite>Строгий Вахтёр (403)</cite>
</div>
</div>
</div>
<!-- Блок тегов и навигации -->
<div class="tags">
<a href="/">Уйти по-добру по-здорову</a>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>403: Доступ запрещен | DicQuo</title>
<style>
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
header > #logo { margin-top: 1vh; float: left; }
header > #logo a { border: none; text-decoration: none; color: silver;}
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</style>
</head>
<body>
<header>
<div id="logo">
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
</div>
</header>
<main>
<article>
<figure>
<p>Категорически:</p>
<blockquote id="bb">
<span style="margin-left:-0.44em;">&laquo;</span>Вам сюда нельзя. Даже если очень хочется. Уходите!»
</blockquote>
<cite>Строгий Вахтёр (403)</cite>
</figure>
</article>
</main>
<div class="tags">
<a href="/">Уйти по-добру по-здорову</a>
</div>
{% endblock %}
</body>
</html>

View File

@@ -1,31 +1,43 @@
{% extends "base.html" %}
{% block Title %}404: Страница не найдена{% endblock %}
{% block CONTENT %}{% include "blocks/header_nav.html" %}
<div class="container main-content">
<!-- Основной контент: Текст + Картинка -->
<div class="content-row">
<!-- Текстовая колонка -->
<div class="text-col">
<!-- Цитата -->
<blockquote id="bb" style="border:none; margin:0; padding:0;">
<span style="margin-left:-0.44em;">&laquo;</span>Я искал везде. Даже под&nbsp;диваном. Этой страницы здесь&nbsp;нет.»
</blockquote>
<!-- Автор -->
<div id="author">
<cite>Системный Администратор (404)</cite>
</div>
</div>
</div>
<!-- Блок тегов и навигации -->
<div class="tags">
<a href="/">Вернуться на главную</a>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404: Страница не найдена | DicQuo</title>
<style>
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
header > #logo { margin-top: 1vh; float: left; }
header > #logo a { border: none; text-decoration: none; color: silver;}
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</style>
</head>
<body>
<header>
<div id="logo">
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
</div>
</header>
<main>
<article>
<figure>
<p>Вздыхая:</p>
<blockquote id="bb">
<span style="margin-left:-0.44em;">&laquo;</span>Я искал везде. Даже под&nbsp;диваном. Этой страницы здесь&nbsp;нет.»
</blockquote>
<cite>Системный Администратор (404)</cite>
</figure>
</article>
</main>
<div class="tags">
<a href="/">Вернуться на главную</a>
</div>
{% endblock %}
</body>
</html>

View File

@@ -1,31 +1,44 @@
{% extends "base.html" %}
{% block Title %}500: Ошибка сервера{% endblock %}
{% block CONTENT %}{% include "blocks/header_nav.html" %}
<div class="container main-content">
<!-- Основной контент: Текст + Картинка -->
<div class="content-row">
<!-- Текстовая колонка -->
<div class="text-col">
<!-- Цитата -->
<blockquote id="bb" style="border:none; margin:0; padding:0;">
<span style="margin-left:-0.44em;">&laquo;</span>Что-то пошло не&nbsp;так. Кажется, я&nbsp;уронил сервер. Подождите, пока я&nbsp;его подниму.»
</blockquote>
<!-- Автор -->
<div id="author">
<cite>Системный Администратор (500)</cite>
</div>
</div>
</div>
<!-- Блок тегов и навигации -->
<div class="tags">
<a href="/">Попробовать обновить страницу</a>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>500: Ошибка сервера DicQuo</title>
<style>
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
header > #logo { margin-top: 1vh; float: left; }
header > #logo a { border: none; text-decoration: none; color: silver;}
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</style>
</head>
<body>
<header>
<div id="logo">
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
</div>
</header>
<main>
<article>
<figure>
<p>Неожиданно:</p>
<blockquote id="bb">
<span style="margin-left:-0.44em;">&laquo;</span>Что-то пошло не&nbsp;так. Кажется, я&nbsp;уронил сервер.
Подождите, пока я&nbsp;его подниму.»
</blockquote>
<cite>Системный Администратор (500)</cite>
</figure>
</article>
</main>
<div class="tags">
<a href="/">Попробовать обновить страницу</a>
</div>
{% endblock %}
</body>
</html>

View File

@@ -1,2 +1,2 @@
{% load static %}<script src="{% static 'js/counters.js' %}"></script>
<noscript><div><img src="https://top-fwz1.mail.ru/counter?id=3744288;js=na" class="counter-pixel" alt="Top.Mail.Ru"/></div></noscript>
<noscript><div><img src="https://top-fwz1.mail.ru/counter?id=3744288;js=na" class="counter-pixel" alt=""/></div><div><img src="https://mc.yandex.ru/watch/106953063" class="counter-pixel" alt="" /></div></noscript>

View File

@@ -1,27 +1,43 @@
<!DOCTYPE html>
</html>
</body>
</div>
<a href="/">Вернуться на главную</a>
<cite>Системный Администратор (404)</cite>
</blockquote>
<span>«</span>Я искал везде. Даже под&nbsp;диваном. Этой страницы здесь&nbsp;нет.»
<blockquote>
<div class="container">
<body>
</head>
</style>
a:hover { color: #999; border-bottom: 1px solid #999; }
a { color: #555; text-decoration: none; border-bottom: 1px dotted #555; transition: color 0.3s; font-size: 0.8em; margin-top: 30px; display: inline-block;}
cite { display: block; font-size: 0.9em; color: #777; margin-top: 15px; font-style: normal;}
blockquote span { margin-left: -0.44em; }
blockquote { font-size: 2em; margin: 0 0 20px 0; font-style: italic; line-height: 1.4; }
.container { max-width: 600px; padding: 20px; }
body { background-color: #111; color: #ccc; font-family: Georgia, serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; }
<style>
<title>404: Страница не найдена</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<head>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404: Страница не найдена | DicQuo</title>
<style>
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
header > #logo { margin-top: 1vh; float: left; }
header > #logo a { border: none; text-decoration: none; color: silver;}
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</style>
</head>
<body>
<header>
<div id="logo">
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
</div>
</header>
<main>
<article>
<figure>
<p>Озадачено:</p>
<blockquote id="bb">
<span style="margin-left:-0.44em;">&laquo;</span>Я искал везде. Даже под&nbsp;диваном. Этой страницы здесь&nbsp;нет.»
</blockquote>
<cite>Системный Администратор (404)</cite>
</figure>
</article>
</main>
<div class="tags">
<a href="/">Вернуться на главную</a>
</div>
</body>
</html>

View File

@@ -1,27 +1,45 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>500: Ошибка сервера</title>
<style>
body { background-color: #111; color: #ccc; font-family: Georgia, serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; }
.container { max-width: 600px; padding: 20px; }
blockquote { font-size: 2em; margin: 0 0 20px 0; font-style: italic; line-height: 1.4; }
blockquote span { margin-left: -0.44em; }
cite { display: block; font-size: 0.9em; color: #777; margin-top: 15px; font-style: normal; }
a { color: #555; text-decoration: none; border-bottom: 1px dotted #555; transition: color 0.3s; font-size: 0.8em; margin-top: 30px; display: inline-block;}
a:hover { color: #999; border-bottom: 1px solid #999; }
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>500: Ошибка сервера | DicQuo</title>
<style>
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
header > #logo { margin-top: 1vh; float: left; }
header > #logo a { border: none; text-decoration: none; color: silver;}
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</style>
</head>
<body>
<div class="container">
<blockquote>
<span>«</span>Что-то пошло не&nbsp;так. Кажется, я&nbsp;уронил сервер, контейнер или &nbsp;случилось что-то ещё. Подождите, пока я&nbsp;его подниму.»
</blockquote>
<cite>Системный Администратор (5xx)</cite>
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить через 5&nbsp;минут.</a>
</div>
<header>
<div id="logo">
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
</div>
</header>
<main>
<article>
<figure>
<p>Неожиданно:</p>
<blockquote id="bb">
<span style="margin-left:-0.44em;">&laquo;</span>Что-то пошло не&nbsp;так. Кажется, я&nbsp;уронил сервер.
Подождите, пока я&nbsp;его подниму.»
</blockquote>
<cite>Системный Администратор (500)</cite>
</figure>
</article>
</main>
<div class="tags">
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить страницу</a>
</div>
</body>
</html>

View File

@@ -2,6 +2,12 @@
from django.contrib import admin
from django import forms
from web.models import TbDictumAndQuotes, TbAuthor, TbImages, TbOrigin
from taggit.managers import TaggableManager
from django_select2.forms import Select2TagWidget
from taggit.models import Tag
from taggit.utils import parse_tags
from django.db import models
from django.db.utils import OperationalError, ProgrammingError
try:
from etpgrf.typograph import Typographer
@@ -18,6 +24,101 @@ except ImportError:
def __init__(self, **kwargs): pass
class TagSelect2Widget(Select2TagWidget):
"""
Select2-виджет для django-taggit, работающий по ИМЕНАМ тегов.
- подхватывает уже сохранённые теги;
- показывает выпадающий список из существующих тегов;
- даёт создавать новые теги с пробелами в названии.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# choices: список всех существующих тегов по имени.
# Важно: на этапах вроде collectstatic таблицы taggit ещё может не быть,
# поэтому оборачиваем в try/except и молча игнорируем отсутствие БД.
try:
self.choices = [(t.name, t.name) for t in Tag.objects.all()]
except (OperationalError, ProgrammingError):
self.choices = []
class Media:
css = {
"all": ("css/select2_taggit_admin.css",),
}
def build_attrs(self, base_attrs, extra_attrs=None):
"""
Настраиваем Select2 так, чтобы пробел НЕ разделял тег
на несколько частей (нужны теги с пробелами: «Сергей Курёхин»).
Оставляем в разделителях только запятую.
"""
attrs = super().build_attrs(base_attrs, extra_attrs)
# По умолчанию django-select2 ставит: [",", " "]
# Нам нужен только разделитель-запятая.
# Строка '[","]' — корректный JSON-массив из одного элемента.
# Важно: сюда нужно класть СТРОКУ с JSON-массивом, а не python-список.
# Иначе в HTML окажется "['[\", \"]', ...]" и Select2 будет вести себя непредсказуемо.
attrs["data-token-separators"] = '[","]'
return attrs
def format_value(self, value):
"""
Преобразуем значение из TaggableManager/TagField
в список ИМЁН тегов, который ожидает Select2TagWidget.
"""
from django.db.models import QuerySet
if value is None:
return []
# QuerySet или список Tag-объектов
if isinstance(value, QuerySet):
return [t.name for t in value]
if isinstance(value, (list, tuple, set)):
names = []
for v in value:
if isinstance(v, Tag):
names.append(v.name)
else:
names.append(str(v))
return names
# Строка вида "tag1, tag2" — разбираем в список имён
if isinstance(value, str):
return parse_tags(value)
return super().format_value(value)
def value_from_datadict(self, data, files, name):
"""
Django-Select2 возвращает список значений (['Сергей Курёхин', 'Другой тег']).
Taggit (TagField) ждёт ОДНУ строку, которую потом парсит в список тегов.
Если отдать список, он превратится в строку `"['Сергей', 'Курёхин']"`,
и распарсится в кривые теги — этого мы избегаем.
"""
values = super().value_from_datadict(data, files, name)
if not values:
return ""
# Для нашего виджета value — это уже список имён тегов
tag_names = [str(v).strip() for v in values if str(v).strip()]
if not tag_names:
return ""
# ОДИН многословный тег: "Сергей Курёхин" -> "Сергей Курёхин,"
# Тогда parse_tags переключится в режим "деление по запятым"
if len(tag_names) == 1:
single = tag_names[0]
if " " in single and "," not in single and '"' not in single:
return single + ","
return single
# Несколько тегов — явная запятая между ними.
return ", ".join(tag_names)
class DictumAdminForm(forms.ModelForm):
# Виртуальные поля для настройки типографа
etp_language = forms.ChoiceField(
@@ -62,6 +163,9 @@ class DictumAdminForm(forms.ModelForm):
class Meta:
model = TbDictumAndQuotes
fields = '__all__'
widgets = {
'tags': TagSelect2Widget,
}
# Register your models here.
@@ -100,6 +204,10 @@ class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
)
readonly_fields = ('szIntroHTML', 'szContentHTML', 'iViewCounter')
formfield_overrides = {
models.ManyToManyField: {'widget': Select2TagWidget},
}
def save_model(self, request, obj, form, change):
# 1. Читаем базовые настройки
langs = form.cleaned_data.get('etp_language', 'ru').split(',')
@@ -199,6 +307,11 @@ class AdmImages(admin.ModelAdmin):
list_display_links = ('id', 'szCaption')
empty_value_display = u"<b style='color:red;'>-empty-</b>"
# Добавляем виджет для тегов
formfield_overrides = {
TaggableManager: {'widget': TagSelect2Widget},
}
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('tags')
@@ -212,6 +325,11 @@ class AdmAuthor(admin.ModelAdmin):
list_display_links = ('id', 'szAuthor')
empty_value_display = u"<b style='color:red;'>-empty-</b>"
# Добавляем виджет для тегов
formfield_overrides = {
TaggableManager: {'widget': TagSelect2Widget},
}
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('tags')
@@ -223,4 +341,3 @@ admin.site.register(TbDictumAndQuotes, AdmDictumAndQuotesAdmin)
admin.site.register(TbOrigin, AdmOrigin)
admin.site.register(TbImages, AdmImages)
admin.site.register(TbAuthor, AdmAuthor)

View File

@@ -16,6 +16,7 @@ import pytils
class RuTag(Tag):
class Meta:
proxy = True
# ordering = ['id']
def slugify(self, tag, i=None):
return pytils.translit.slugify(self.name.lower())[:128]
@@ -24,6 +25,7 @@ class RuTag(Tag):
class RuTaggedItem(TaggedItem):
class Meta:
proxy = True
# ordering = ['id']
@classmethod
def tag_model(cls):
@@ -108,7 +110,73 @@ class TbImages(models.Model):
# заменим имя файла картинки
def save(self, *args, **kwargs):
self.imFile.name = pytils.translit.slugify(self.szCaption.lower()) + str(Path(self.imFile.name).suffixes)
import os
from django.conf import settings
old_obj = None
old_file_path = None
# Получаем старую запись, если она есть
if self.pk:
try:
old_obj = TbImages.objects.get(pk=self.pk)
# Пытаемся получить путь к файлу. Если файл не найден физически, Django может выкинуть ошибку здесь или позже
# Поэтому просто берем имя из БД и формируем путь руками, чтобы не зависеть от Storage
if old_obj.imFile:
old_file_path = os.path.join(settings.MEDIA_ROOT, str(old_obj.imFile.name))
except TbImages.DoesNotExist:
pass
# Fix 1: Если старый путь уже битый (содержит ['...'])
if old_file_path and "['" in old_file_path:
# Формируем "исправленный" путь (каким он должен быть)
corrected_path = old_file_path.replace("['", "").replace("']", "").replace("'", "")
# Проверяем: если битого файла нет, а исправленный есть -> значит БД врет
if not os.path.exists(old_file_path) and os.path.exists(corrected_path):
# Исправляем текущее имя файла в объекте (убираем мусор из имени)
self.imFile.name = str(self.imFile.name).replace("['", "").replace("']", "").replace("'", "")
# Обновляем переменную old_file_path, чтобы дальнейшая логика переименования работала корректно
old_file_path = corrected_path
# Получаем текущее имя и расширение (уже возможно исправленное выше)
current_path = Path(str(self.imFile.name))
current_suffix = current_path.suffix
# Fix 2: Чиним расширение еще раз (на всякий случай, если Fix 1 не сработал или это новый объект)
if "['" in str(current_suffix):
current_suffix = str(current_suffix).replace("['", "").replace("']", "").replace("'", "")
# Формируем новое имя файла на основе заголовка (Slug)
new_filename = pytils.translit.slugify(self.szCaption.lower()) + current_suffix
# Определяем папку (если есть родитель, используем его, иначе img2)
# Важно: self.imFile.name может содержать полный путь. Нам нужен только относительный от MEDIA_ROOT
# Но проще взять родителя из текущего имени
parent_dir = current_path.parent.name if current_path.parent.name else 'img2'
new_name_with_path = str(Path(parent_dir) / new_filename)
# Переименование физического файла
# Сравниваем старое имя (из БД) с новым (сгенерированным)
if old_obj and str(old_obj.imFile.name) != new_name_with_path:
new_file_full_path = os.path.join(settings.MEDIA_ROOT, new_name_with_path)
# Если старый файл (old_file_path) существует физически, переименовываем его
if old_file_path and os.path.exists(old_file_path):
try:
os.makedirs(os.path.dirname(new_file_full_path), exist_ok=True)
os.rename(old_file_path, new_file_full_path)
self.imFile.name = new_name_with_path
except OSError as e:
print(f"Error renaming file from {old_file_path} to {new_file_full_path}: {e}")
else:
# Если старого файла нет, просто обновляем имя в БД
self.imFile.name = new_name_with_path
else:
# Если имя не менялось или объекта не было, просто устанавливаем правильное имя
# (например, чтобы убрать мусор из расширения в БД)
self.imFile.name = new_name_with_path
super(TbImages, self).save(*args, **kwargs)
class Meta:

View File

@@ -106,8 +106,8 @@ services:
- REPO_PASS=${REPO_PASS}
- WATCHTOWER_SCOPE=dq-scope
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
- WATCHTOWER_POLL_INTERVAL=1800 # Проверять каждые 30 минут
- DOCKER_API_VERSION=1.44
command: --interval 1800 --cleanup # Проверять каждые 30 минут
logging:
driver: "json-file"
options:

31
poetry.lock generated
View File

@@ -67,6 +67,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=23.1.0)"]
bcrypt = ["bcrypt (>=4.1.1)"]
[[package]]
name = "django-appconf"
version = "1.2.0"
description = "A helper class for handling configuration defaults of packaged apps gracefully."
optional = false
python-versions = ">=3.9"
files = [
{file = "django_appconf-1.2.0-py3-none-any.whl", hash = "sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4"},
{file = "django_appconf-1.2.0.tar.gz", hash = "sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec"},
]
[package.dependencies]
django = "*"
[[package]]
name = "django-environ"
version = "0.12.1"
@@ -83,6 +97,21 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)",
docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-notfound-page"]
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"]
[[package]]
name = "django-select2"
version = "8.4.8"
description = "This is a Django_ integration of Select2_."
optional = false
python-versions = ">=3.10"
files = [
{file = "django_select2-8.4.8-py3-none-any.whl", hash = "sha256:a2ce6a4c556dd2d4d57eb3753618d6f31f8d3910e9d9fa1b686d9340f50b14eb"},
{file = "django_select2-8.4.8.tar.gz", hash = "sha256:592e52effff2b5850cb7c98b265715b6704fb784699c4aedddfdd8ae1ffa1e81"},
]
[package.dependencies]
django = ">=4.2"
django-appconf = ">=0.6.0"
[[package]]
name = "django-taggit"
version = "6.1.0"
@@ -646,4 +675,4 @@ brotli = ["brotli"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "3d7a7f2fe8ec78993616e707e29e96503f134bd1cec48cac7f6dd47814863f4f"
content-hash = "b5fca935982220439294d6b37caaf1d893492df96d65abd6389dfd3c9464b992"

View File

@@ -1,8 +1,8 @@
@charset "utf-8";
body {
margin: 0;
min-height: 100vh;
min-width: 100vw;
min-height: 100vmin;
min-width: 100vmin;
background-color: #111; /* Изначально темный фон */
opacity: 0; /* Скрываем контент до расчета цвета */
transition: opacity 0.9s ease-in-out; /* Очень плавное появление */
@@ -13,11 +13,11 @@ header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1vh 4vw;
padding: 1vmin 4vmin;
}
header > #logo {
margin-top: 1vh;
margin-top: 1vmin;
float: left;
}
@@ -69,8 +69,8 @@ header > nav > #stats-menu > b {
header > nav > #stats-menu > p {
font-style: italic; display: inline-block;
margin: 0 1vw;
padding-right: 1vw;
margin: 0 1vmin;
padding-right: 1vmin;
border-right: 1px dotted silver;
}
@@ -120,23 +120,18 @@ header > nav > #stats-menu > a:hover {
/* MAIN ARTICLE CONTENT */
main {
/*justify-content: space-between;*/
/*align-items: center;*/
padding: 1vh 8vw;
padding: 1vmin 8vmin;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 60vh;
/*width: 90%;*/
/*max-width: 1200px;*/
/*margin: 0 auto;*/
min-height: 60vmin;
}
main > article {
display: flex;
align-items: center;
justify-content: center;
gap: 2vw;
gap: 2vmin;
}
main > article > figure {
@@ -170,18 +165,18 @@ main > article > figure > cite { /* Автор цитаты */
}
main > article > div {
flex: 0 0 30vw;
flex: 0 0 30vmax;
display: flex;
justify-content: center;
width: 30vw;
width: 30vmax;
text-align: right;
margin-bottom: 10vh;
margin-bottom: 10vmin;
}
main > article > div > div {
width: 26vmax;
height: 26vmax;
padding: 0.5vw;
padding: 0.5vmin;
border-radius: 50%;
}
@@ -200,13 +195,13 @@ main > article > div > div > div > img {
/* НАВИГАЦИЯ (ТЕГИ И ДАЛЕЕ) В КОНЦЕ */
nav {
padding: 1vh 4vw;
padding: 1vmin 4vmin;
}
nav > div {
color: silver;
font-size: 1.5vmin;
line-height: 1.9vmin;
padding-top: 7vh;
padding: 7vmin 0 4vmin 0;
}
nav > div a {
text-decoration: none;
@@ -250,7 +245,7 @@ footer {
bottom: 0;
left: 0;
width: 100%;
padding: 2vh 4vw;
padding: 2vmin 4vmin;
color: silver; /* Мягкий серый цвет текста */
background-color: rgba(30, 30, 30, 0.8); /* Темный полупрозрачный фон */
backdrop-filter: blur(5px); /* Эффект матового стекла (современно и медитативно) */
@@ -262,12 +257,12 @@ footer {
footer small {
display: inline-block;
margin-right: 2vw;
margin-right: 2vmin;
letter-spacing: 0.05em; /* Немного воздуха в тексте */
}
footer button {
padding: 0.5vh 1.5vw;
padding: 0.5vmin 1.5vmin;
background: transparent;
color: silver;
border: 1px solid silver;
@@ -292,7 +287,20 @@ footer button:hover {
main > article > div {
flex: 0 0 auto;
margin-bottom: 2vh;
margin: 8vmin 0 2vmin 0;
}
main > article > div {
width: 80vmin;
}
main > article > div > div {
width: 36vmax;
height: 36vmax;
}
main > article > div > div > div > img {
height: 36vmax;
}
}

View File

@@ -0,0 +1,60 @@
/* Select2 (django-select2) dark theme compatibility for Django Admin.
We intentionally scope to dark mode only and lean on Django Admin CSS variables. */
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .select2-container--default .select2-selection--single,
html:not([data-theme="light"]) .select2-container--default .select2-selection--multiple {
background: var(--body-bg, #1e1e1e) !important;
color: var(--body-fg, #e6e6e6) !important;
border-color: var(--border-color, #3a3a3a) !important;
}
}
html[data-theme="dark"] .select2-container--default .select2-selection--single,
html[data-theme="dark"] .select2-container--default .select2-selection--multiple {
background: var(--body-bg, #1e1e1e) !important;
color: var(--body-fg, #e6e6e6) !important;
border-color: var(--border-color, #3a3a3a) !important;
}
html[data-theme="dark"] .select2-container--default .select2-selection__rendered {
color: var(--body-fg, #e6e6e6) !important;
}
html[data-theme="dark"] .select2-container--default .select2-search--inline .select2-search__field,
html[data-theme="dark"] .select2-container--default .select2-search--dropdown .select2-search__field {
background: transparent !important;
color: var(--body-fg, #e6e6e6) !important;
}
html[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__choice {
background: rgba(255, 255, 255, 0.08) !important;
border-color: rgba(255, 255, 255, 0.14) !important;
color: var(--body-fg, #e6e6e6) !important;
}
html[data-theme="dark"] .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: var(--body-fg, #e6e6e6) !important;
opacity: 0.8;
}
html[data-theme="dark"] .select2-dropdown {
background: var(--body-bg, #1e1e1e) !important;
color: var(--body-fg, #e6e6e6) !important;
border-color: var(--border-color, #3a3a3a) !important;
}
html[data-theme="dark"] .select2-container--default .select2-results__option {
color: var(--body-fg, #e6e6e6) !important;
}
html[data-theme="dark"] .select2-container--default .select2-results__option--highlighted.select2-results__option--selectable {
background: rgba(255, 255, 255, 0.10) !important;
color: var(--body-fg, #ffffff) !important;
}
html[data-theme="dark"] .select2-container--default .select2-results__option--selected {
background: rgba(255, 255, 255, 0.06) !important;
color: var(--body-fg, #e6e6e6) !important;
}

View File

@@ -18,5 +18,25 @@ _tmr.push({id: "3744288", type: "pageView", start: (new Date()).getTime()});
f();
}
})(document, window, "tmr-code");
// //Rating Mail.ru counter
// Yandex.Metrika counter
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=106953063', 'ym');
ym(106953063, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
// Google Analytics (GA4) counter
(function() {
var gaScript = document.createElement('script');
gaScript.async = true;
gaScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-WTJM8J9YL5';
document.head.appendChild(gaScript);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Делаем функцию глобально доступной, если понадобится вызывать её из других скриптов
window.gtag = gtag;
gtag('js', new Date());
gtag('config', 'G-WTJM8J9YL5');
})();

View File

@@ -17,6 +17,7 @@ django-environ = "^0.12.1"
whitenoise = "^6.11.0"
gunicorn = "^25.1.0"
tqdm = "^4.67.3"
django-select2 = "^8.4.8"
[build-system]