7 Commits

Author SHA1 Message Date
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 640 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) --- # --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) ---
# Если Django упал (502) или файл из media не найден Nginx-ом (404), показываем наши красивые заглушки. # Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML.
# Файлы копируются в media/errors при старте контейнера. # Эти файлы должны лежать в папке, доступной Nginx (например, в media/errors).
# ТРЕБУЕТСЯ ЗАМЕНА ПРИ ДЕПЛОЕ: /home/user/app/dq-site -> ваш реальный путь #
error_page 404 /404.html; # ВАЖНО:
error_page 500 502 503 504 /500.html; # 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; root /home/user/app/dq-site/media/errors;
internal; internal;
} }
location = /500.html { location = /404.html {
root /home/user/app/dq-site/media/errors; root /home/user/app/dq-site/media/errors;
internal; internal;
} }

View File

@@ -58,6 +58,7 @@ INSTALLED_APPS: list[str] = [
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.sitemaps', 'django.contrib.sitemaps',
'taggit.apps.TaggitAppConfig', 'taggit.apps.TaggitAppConfig',
'django_select2',
'web.apps.WebConfig', 'web.apps.WebConfig',
] ]
@@ -78,6 +79,11 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR.parent / 'database/db.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')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin 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.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
from django.views.generic import TemplateView from django.views.generic import TemplateView
@@ -33,6 +33,7 @@ urlpatterns = [
re_path(r'^$', views.IndexView.as_view()), re_path(r'^$', views.IndexView.as_view()),
re_path(r'^(?P<dq_id>\d{1,12})_\S*$', views.DictumDetailView.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('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
path("select2/", include("django_select2.urls")),
] ]
if settings.DEBUG: if settings.DEBUG:
@@ -42,4 +43,7 @@ if settings.DEBUG:
path('500/', TemplateView.as_view(template_name="500.html")), path('500/', TemplateView.as_view(template_name="500.html")),
path('403/', TemplateView.as_view(template_name="403.html")), path('403/', TemplateView.as_view(template_name="403.html")),
path('400/', TemplateView.as_view(template_name="400.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" %} <!DOCTYPE html>
<html lang="ru">
{% block Title %}400: Плохой запрос{% endblock %} <head>
<meta charset="utf-8">
{% block CONTENT %}{% include "blocks/header_nav.html" %} <meta name="viewport" content="width=device-width, initial-scale=1">
<div class="container main-content"> <title>400: Плохой запрос | DicQuo</title>
<!-- Осно<D0BD><D0BE>ной контент: Текст + Картинка --> <style>
<div class="content-row"> 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; }
<div class="text-col"> 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; }
<blockquote id="bb" style="border:none; margin:0; padding:0;"> main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
<span style="margin-left:-0.44em;">&laquo;</span>Вы спрашиваете меня о&nbsp;чем-то странном. Я&nbsp;не&nbsp;понимаю ваш запрос.» main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
</blockquote> 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;}
<div id="author"> .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; }
<cite>Озадаченный Сервер (400)</cite> .tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</div> </style>
</div> </head>
<body>
</div> <header>
<div id="logo">
<!-- Блок тегов и навигации --> <a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
<div class="tags">
<a href="/">Сформулировать иначе (на главную)</a>
</div> </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> </div>
</body>
{% endblock %} </html>

View File

@@ -1,31 +1,43 @@
{% extends "base.html" %} <!DOCTYPE html>
<html lang="ru">
{% block Title %}403: Доступ запрещен{% endblock %} <head>
<meta charset="utf-8">
{% block CONTENT %}{% include "blocks/header_nav.html" %} <meta name="viewport" content="width=device-width, initial-scale=1">
<div class="container main-content"> <title>403: Доступ запрещен | DicQuo</title>
<!-- Основной контент: Текст + Картинка --> <style>
<div class="content-row"> 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; }
<div class="text-col"> 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; }
<blockquote id="bb" style="border:none; margin:0; padding:0;"> main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
<span style="margin-left:-0.44em;">&laquo;</span>Вам сюда нельзя. Даже если очень хочется. Уходите!» main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
</blockquote> 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;}
<div id="author"> .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; }
<cite>Строгий Вахтёр (403)</cite> .tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</div> </style>
</div> </head>
<body>
</div> <header>
<div id="logo">
<!-- Блок тегов и навигации --> <a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
<div class="tags">
<a href="/">Уйти по-добру по-здорову</a>
</div> </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> </div>
</body>
{% endblock %} </html>

View File

@@ -1,31 +1,43 @@
{% extends "base.html" %} <!DOCTYPE html>
<html lang="ru">
{% block Title %}404: Страница не найдена{% endblock %} <head>
<meta charset="utf-8">
{% block CONTENT %}{% include "blocks/header_nav.html" %} <meta name="viewport" content="width=device-width, initial-scale=1">
<div class="container main-content"> <title>404: Страница не найдена | DicQuo</title>
<!-- Основной контент: Текст + Картинка --> <style>
<div class="content-row"> 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; }
<div class="text-col"> 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; }
<blockquote id="bb" style="border:none; margin:0; padding:0;"> main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
<span style="margin-left:-0.44em;">&laquo;</span>Я искал везде. Даже под&nbsp;диваном. Этой страницы здесь&nbsp;нет.» main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
</blockquote> 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;}
<div id="author"> .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; }
<cite>Системный Администратор (404)</cite> .tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</div> </style>
</div> </head>
<body>
</div> <header>
<div id="logo">
<!-- Блок тегов и навигации --> <a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
<div class="tags">
<a href="/">Вернуться на главную</a>
</div> </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> </div>
</body>
{% endblock %} </html>

View File

@@ -1,31 +1,44 @@
{% extends "base.html" %} <!DOCTYPE html>
<html lang="ru">
{% block Title %}500: Ошибка сервера{% endblock %} <head>
<meta charset="utf-8">
{% block CONTENT %}{% include "blocks/header_nav.html" %} <meta name="viewport" content="width=device-width, initial-scale=1">
<div class="container main-content"> <title>500: Ошибка сервера DicQuo</title>
<!-- Основной контент: Текст + Картинка --> <style>
<div class="content-row"> 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; }
<div class="text-col"> 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; }
<blockquote id="bb" style="border:none; margin:0; padding:0;"> main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
<span style="margin-left:-0.44em;">&laquo;</span>Что-то пошло не&nbsp;так. Кажется, я&nbsp;уронил сервер. Подождите, пока я&nbsp;его подниму.» main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
</blockquote> 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;}
<div id="author"> .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; }
<cite>Системный Администратор (500)</cite> .tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
</div> </style>
</div> </head>
<body>
</div> <header>
<div id="logo">
<!-- Блок тегов и навигации --> <a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
<div class="tags">
<a href="/">Попробовать обновить страницу</a>
</div> </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> </div>
</body>
{% endblock %} </html>

View File

@@ -1,2 +1,2 @@
{% load static %}<script src="{% static 'js/counters.js' %}"></script> {% 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> <!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"> <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> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>500: Ошибка сервера</title> <title>500: Ошибка сервера | DicQuo</title>
<style> <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; } body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
.container { max-width: 600px; padding: 20px; } header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
blockquote { font-size: 2em; margin: 0 0 20px 0; font-style: italic; line-height: 1.4; } header > #logo { margin-top: 1vh; float: left; }
blockquote span { margin-left: -0.44em; } header > #logo a { border: none; text-decoration: none; color: silver;}
cite { display: block; font-size: 0.9em; color: #777; margin-top: 15px; font-style: normal; } main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
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;} main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
a:hover { color: #999; border-bottom: 1px solid #999; } main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
</style> 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> </head>
<body> <body>
<div class="container"> <header>
<blockquote> <div id="logo">
<span>«</span>Что-то пошло не&nbsp;так. Кажется, я&nbsp;уронил сервер, контейнер или &nbsp;случилось что-то ещё. Подождите, пока я&nbsp;его подниму.» <a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
</blockquote> </div>
<cite>Системный Администратор (5xx)</cite> </header>
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить через 5&nbsp;минут.</a> <main>
</div> <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> </body>
</html> </html>

View File

@@ -2,6 +2,11 @@
from django.contrib import admin from django.contrib import admin
from django import forms from django import forms
from web.models import TbDictumAndQuotes, TbAuthor, TbImages, TbOrigin 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
try: try:
from etpgrf.typograph import Typographer from etpgrf.typograph import Typographer
@@ -18,6 +23,96 @@ except ImportError:
def __init__(self, **kwargs): pass def __init__(self, **kwargs): pass
class TagSelect2Widget(Select2TagWidget):
"""
Select2-виджет для django-taggit, работающий по ИМЕНАМ тегов.
- подхватывает уже сохранённые теги;
- показывает выпадающий список из существующих тегов;
- даёт создавать новые теги с пробелами в названии.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# choices: список всех существующих тегов по имени
self.choices = [(t.name, t.name) for t in Tag.objects.all()]
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): class DictumAdminForm(forms.ModelForm):
# Виртуальные поля для настройки типографа # Виртуальные поля для настройки типографа
etp_language = forms.ChoiceField( etp_language = forms.ChoiceField(
@@ -62,6 +157,9 @@ class DictumAdminForm(forms.ModelForm):
class Meta: class Meta:
model = TbDictumAndQuotes model = TbDictumAndQuotes
fields = '__all__' fields = '__all__'
widgets = {
'tags': TagSelect2Widget,
}
# Register your models here. # Register your models here.
@@ -100,6 +198,10 @@ class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
) )
readonly_fields = ('szIntroHTML', 'szContentHTML', 'iViewCounter') readonly_fields = ('szIntroHTML', 'szContentHTML', 'iViewCounter')
formfield_overrides = {
models.ManyToManyField: {'widget': Select2TagWidget},
}
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
# 1. Читаем базовые настройки # 1. Читаем базовые настройки
langs = form.cleaned_data.get('etp_language', 'ru').split(',') langs = form.cleaned_data.get('etp_language', 'ru').split(',')
@@ -199,6 +301,11 @@ class AdmImages(admin.ModelAdmin):
list_display_links = ('id', 'szCaption') list_display_links = ('id', 'szCaption')
empty_value_display = u"<b style='color:red;'>-empty-</b>" empty_value_display = u"<b style='color:red;'>-empty-</b>"
# Добавляем виджет для тегов
formfield_overrides = {
TaggableManager: {'widget': TagSelect2Widget},
}
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('tags') return super().get_queryset(request).prefetch_related('tags')
@@ -212,6 +319,11 @@ class AdmAuthor(admin.ModelAdmin):
list_display_links = ('id', 'szAuthor') list_display_links = ('id', 'szAuthor')
empty_value_display = u"<b style='color:red;'>-empty-</b>" empty_value_display = u"<b style='color:red;'>-empty-</b>"
# Добавляем виджет для тегов
formfield_overrides = {
TaggableManager: {'widget': TagSelect2Widget},
}
def get_queryset(self, request): def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('tags') return super().get_queryset(request).prefetch_related('tags')
@@ -223,4 +335,3 @@ admin.site.register(TbDictumAndQuotes, AdmDictumAndQuotesAdmin)
admin.site.register(TbOrigin, AdmOrigin) admin.site.register(TbOrigin, AdmOrigin)
admin.site.register(TbImages, AdmImages) admin.site.register(TbImages, AdmImages)
admin.site.register(TbAuthor, AdmAuthor) admin.site.register(TbAuthor, AdmAuthor)

View File

@@ -16,6 +16,7 @@ import pytils
class RuTag(Tag): class RuTag(Tag):
class Meta: class Meta:
proxy = True proxy = True
# ordering = ['id']
def slugify(self, tag, i=None): def slugify(self, tag, i=None):
return pytils.translit.slugify(self.name.lower())[:128] return pytils.translit.slugify(self.name.lower())[:128]
@@ -24,6 +25,7 @@ class RuTag(Tag):
class RuTaggedItem(TaggedItem): class RuTaggedItem(TaggedItem):
class Meta: class Meta:
proxy = True proxy = True
# ordering = ['id']
@classmethod @classmethod
def tag_model(cls): def tag_model(cls):
@@ -108,7 +110,73 @@ class TbImages(models.Model):
# заменим имя файла картинки # заменим имя файла картинки
def save(self, *args, **kwargs): 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) super(TbImages, self).save(*args, **kwargs)
class Meta: class Meta:

View File

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

31
poetry.lock generated
View File

@@ -67,6 +67,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=23.1.0)"] argon2 = ["argon2-cffi (>=23.1.0)"]
bcrypt = ["bcrypt (>=4.1.1)"] 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]] [[package]]
name = "django-environ" name = "django-environ"
version = "0.12.1" 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"] docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-notfound-page"]
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"] 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]] [[package]]
name = "django-taggit" name = "django-taggit"
version = "6.1.0" version = "6.1.0"
@@ -646,4 +675,4 @@ brotli = ["brotli"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "3d7a7f2fe8ec78993616e707e29e96503f134bd1cec48cac7f6dd47814863f4f" content-hash = "b5fca935982220439294d6b37caaf1d893492df96d65abd6389dfd3c9464b992"

View File

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