16 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
5bfd50efd5 mod: Переработаны дизайн и компоновка. Минималистичный код.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m27s
2026-02-22 01:23:46 +03:00
c1bcb2895d mod: CSRF_TRUSTED_ORIGINS по порту 8010 для localhost 2026-02-21 02:36:53 +03:00
9ea2b15043 fix: Исключено широковещательная трансляция gunicorn по порту 8010. 2026-02-21 02:31:59 +03:00
5a80cf6406 fix: Добавлена CSRF_TRUSTED_ORIGINS для правильной верификации CSRF при работе с формами (и админки) 2026-02-21 02:23:09 +03:00
915c286e81 fix: ошибки 2026-02-21 02:03:37 +03:00
c46b6c1061 del: ненужный шаблон 2026-02-21 01:10:42 +03:00
b2a26a9dcc mod: url админки из .env 2026-02-21 00:58:57 +03:00
bd5cdcd870 mod: описание проекта и развёртывание. 2026-02-21 00:43:47 +03:00
db6cbb7bdf mod: # DOCKER_API_VERSION и WATCHTOWER_SCOPE 2026-02-20 23:06:06 +03:00
27 changed files with 1189 additions and 575 deletions

View File

@@ -9,6 +9,11 @@ SECRET_KEY='change_me_in_production'
# Разрешённые хосты. В ПРОДАКШЕНЕ: Установите реальные домены, с которых будет доступно ваше приложение. # Разрешённые хосты. В ПРОДАКШЕНЕ: Установите реальные домены, с которых будет доступно ваше приложение.
ALLOWED_HOSTS=127.0.0.1,localhost ALLOWED_HOSTS=127.0.0.1,localhost
# CSRF Trusted Origins (ВАЖНО для Docker/Nginx/SSL).
# Перечислите здесь URL, по которым вы заходите на сайт, включая схему (http/https).
# Если этого не сделать, при попытке залогиниться в админку вы получите ошибку CSRF.
CSRF_TRUSTED_ORIGINS=https://dq.cube2.ru,http://127.0.0.1:8010,http://localhost:8010
# Email администратора (для получения уведомлений о критических ошибках и других важных сообщений от Django). # Email администратора (для получения уведомлений о критических ошибках и других важных сообщений от Django).
ADMIN_EMAIL=admin@example.com ADMIN_EMAIL=admin@example.com

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).

132
README.md
View File

@@ -1,16 +1,130 @@
# DicQuo (цитаты и высказывания) # DicQuo (Цитаты, Афоризмы и Факты)
Пет-проект ротации цитат и высказываний. Развернут на [dq.cube2.ru](https://dq.cube2.ru). **Dicquo** — это коллекция отобранных вручную цитат, оформленных с уважением к типографике. Место для вдумчивого чтения, переосмысления и поиска вдохновения. Проект создан как пространство, где типографика встречается со смыслом, а технологии помогают контенту выглядеть безупречно.
Изначальная цель: Основные цели проекта:
* Испытывать различные типографы (библиотеки и API), разрабатывать и тестировать свои * **Типографика как искусство:** Разработка и тестирование собственных алгоритмов типографирования (висячая пунктуация, неразрывные пробелы, правильные тире) из библиотеки `etpgrf` (доступен в [GitHub](https://github.com/erjemin/etpgrf), [GitVerse](https://gitverse.ru/erjemin/etpgrf) и self-hosted [Cube2](https://git.cube2.ru/erjemin/2025-etpgrf), онлайн версия развёрнута на [typograph.cube2.ru](https://typograph.cube2.ru/)).
типографы и правила типографирования. * **SEO-эксперименты:** Исследование влияния микроразметки, мета-тегов и семантической верстки на индексацию поисковыми системами.
* Испытать, как содержание отдельных атрибуты (meta, keywords, description и т.п.) * **Технологический стек:** Современный Django, Docker, CI/CD и автоматизация деплоя.
влияют на поисковую выдачу.
[Инструкция по развертыванию на хостинге c CGI Passenger](deploy_to_dreamhost.md) Развернут на [dq.cube2.ru](https://dq.cube2.ru).
## ToDo? ---
## Структура файлов на сервере (Production)
После правильного развертывания, папка проекта на сервере (например, `~/docker-apps/dicquo/`) должна выглядеть так:
```text
dicquo/
├── docker-compose.yml # Переименованный docker-compose.prod.yml из этого репозитория (запускает контейнеры)
├── .env # Файл с переменными окружения и секретами (не пушить в git!)
├── database/ # Папка для базы данных (Persistent Volume)
│ ├── db.sqlite3 # Фактический файл базы данных
├── media/ # Папка с медиа-файлами (Persistent Volume)
│ ├── img2/ # Картинки для цитат (доступны через Nginx)
│ └── errors/ # Картинки для страниц ошибок (404, 500)
└── config/ # Папка для конфигураций (Генерируется контейнером)
└── nginx/
├── dq-app--external-nginx.conf # Конфиг для внешнего Nginx (скопируется из контейнера при первом запуске)
└── nginx_dq.conf.example # Образец конфига для внешнего Nginx (не используется в продакшене)
```
---
## Развёртывание (Deployment)
Проект полностью упакован в Docker и разворачивается с помощью `docker compose`. Ниже приведена инструкция для развертывания на чистом Linux-сервере (Ubuntu/Debian, архитектуры AMD/ARM).
### 1. Подготовка структуры
Создайте директорию для проекта (например, в домашней папке пользователя) и необходимые подпапки для persistent-данных:
```bash
mkdir -p ~/docker-apps/dicquo
cd ~/docker-apps/dicquo
mkdir -p database media config
```
### 2. Файлы конфигурации
Вам понадобятся два файла из репозитория (или их содержимое):
1. **`docker-compose.prod.yml`** -> сохраните его на сервере как `docker-compose.yml`.
2. **`.env`** -> создайте на основе `.env.sample`, заполнив секретами для продакшена.
**Важные переменные в `.env`:**
* `HOST_PROJECT_PATH`: Полный путь к папке проекта на хосте (например, `/home/username/docker-apps/dicquo`). Используется для корректной генерации конфига Nginx.
* `DJANGO_ALLOWED_HOSTS`: Список доменов через запятую (например, `dq.cube2.ru,127.0.0.1`).
### 3. Перенос данных (Опционально)
Если вы мигрируете с dev-окружения или другого сервера, можно просто скопировать файлы базы данных и медиа.
Скопируйте файл базы `database/db.sqlite3` и содержимое папки `media/` в соответсвующие папки на сервере.
### 4. Настройка прав доступа (Permissions) ⚠️
Это **критически важный этап**.
1. Docker-контейнер (с нашим бэкендом Django и Gunicorn) должн иметь доступ к примонтированным папкам.
2. Внешний Nginx (на хосте) должен иметь доступ к статике и медиа.
**Права на папки проекта:**
```bash
# Разрешаем запись в базу и медиа для всех (самый простой способ избежать проблем с UID внутри Docker)
sudo chmod 777 database
sudo chmod 666 database/db.sqlite3
sudo chmod -R 755 media
```
**Права на родительские директории (Pass-through):**
Если проект лежит в домашней папке пользователя (`/home/username/...`), то Nginx (пользователь `www-data`) по умолчанию **не сможет** туда попасть. Нужно разрешить "проход" (execute) для всех пользователей по пути к проекту:
```bash
# Разрешаем "проход" через домашнюю папку (чтение файлов при этом остается закрытым, только доступ к известным путям)
chmod o+x /home/username
chmod o+x /home/username/docker-apps
chmod o+x /home/username/docker-apps/dicquo
```
> *Без этого шага Nginx будет выдавать 403 Forbidden на картинки и статику.*
### 5. Запуск
```bash
docker compose up -d
```
При первом запуске контейнер автоматически:
* Применит миграции.
* Соберет статику.
* Сгенерирует конфиг для Nginx в папке `config/nginx/`.
### 6. Настройка Nginx и SSL
1. **Подключение конфига:**
Создайте симлинк на сгенерированный конфиг:
```bash
sudo ln -s /home/username/docker-apps/dicquo/config/nginx/dq-app--external-nginx.conf /etc/nginx/sites-enabled/dq-app.conf
```
2. **Проверка и релоад:**
```bash
sudo nginx -t
sudo systemctl reload nginx
```
3. **Получение SSL сертификата (Certbot):**
```bash
sudo certbot --nginx -d dq.cube2.ru
```
---
## Разработка (Dev)
Для локального запуска используется `docker-compose.yml` (он же dev-версия).
```bash
docker compose up --build
```
Проект будет доступен по адресу: http://127.0.0.1:8008
## ToDo
* В будущем, возможно, сделать 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

@@ -21,15 +21,18 @@ env = environ.Env(
# If BASE_DIR is .../dicquo, then .env is at BASE_DIR.parent/.env # If BASE_DIR is .../dicquo, then .env is at BASE_DIR.parent/.env
environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env')) environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env'))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY') SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG') DEBUG = env('DEBUG')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['127.0.0.1', 'localhost'])
CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=['http://127.0.0.1', 'http://localhost'])
# Custom Admin URL from .env
ADMIN_URL = env('ADMIN_URL', default='admin/')
######################################### #########################################
# Настройки сообщений об ошибках когда все упало и т.п. # Настройки сообщений об ошибках когда все упало и т.п.
@@ -55,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',
] ]
@@ -75,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,11 +15,11 @@ 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
from dicquo import settings from django.conf import settings
from web import views from web import views
from web.sitemaps import DictumSitemap from web.sitemaps import DictumSitemap
@@ -28,11 +28,12 @@ sitemaps = {
} }
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), re_path(f'^{settings.ADMIN_URL}', admin.site.urls),
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,58 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>{% load static %}
{% load static %}<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"/>
{# SEO & Meta #}<title>{% block Title %}{% endblock %}</title>
<!-- SEO & Meta --> <meta name="description" content="{% block Description %}{% endblock %}"/>
<title>{% block Title %}{% endblock %}</title> <meta name="keywords" content="{% block Keywords %}{% endblock %}"/>
<meta name="description" content="{% block Description %}{% endblock %}" /> <meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}."/>
<meta name="keywords" content="{% block Keywords %}{% endblock %}" /> <meta name="robots" content="index,follow"/>
<meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}." /> {# Open Graph / Social Media #}<meta property="og:type" content="article"/>
<meta name="robots" content="index,follow" /> <meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:85 }}{% endblock %}"/>
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}"/>
<!-- Open Graph / Social Media --> <meta property="og:url" content="{{ request.build_absolute_uri }}"/>
<meta property="og:type" content="article" /> <meta property="og:site_name" content="DicQuo"/>
<meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:60 }}{% endblock %}" /> {% if IMAGE %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}"/>{% endif %}
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}" /> {# Шутка #}<meta name="generator" content="Microsoft FrontPage 1.0"/>
<meta property="og:url" content="{{ request.build_absolute_uri }}" /> {# Canonical #}<link rel="canonical" href="{{ request.build_absolute_uri }}"/>
<meta property="og:site_name" content="DicQuo" /> {# Favicons #}<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}"/>
{% if IMAGE %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}" />{% endif %} <link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
{# Technical Meta #}<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}"/>
<!-- Technical Meta --> {# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.css' %}"/>
<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}" /> <noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
<meta name="generator" content="Django" /> {% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}
<!-- Favicons -->
<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}" />
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}" />
<!-- Styles -->
<link rel="stylesheet" href="{% static 'css/dicquo.css' %}" />
<style>
body {
margin: 0;
min-height: 100vh;
background-color: #111; /* Изначально темный фон */
opacity: 0; /* Скрываем контент до расчета цвета */
transition: opacity 0.9s ease-in-out; /* Очень плавное появление */
}
</style>
<noscript>
<style>body { opacity: 1; }</style>
</noscript>
{% block ExtraHead %}{% endblock %}
</head> </head>
<body> <body>{% if DQ %}
{% if DQ %} {# Этот блок для передачи JavaScript-скрипту `bg-generator.js` текст цитаты ({{ DQ.szContent }}) и скрипт на основе него делает уникальный, но постоянный для этой цитаты фон (градиент) #}<span id="dq-content-raw" style="display:none;">{{ DQ.szContent }} ({% if AUTHOR %}{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor }}{% endif %})</span>{% endif %}
<span id="dq-content-raw" style="display:none;">{{ DQ.szContent }}</span> {% include "blocks/header_nav.html" %}
{% endif %} {% block CONTENT %}{% endblock %}{% if not cookie_accept %}
{% include "blocks/cookie_warning.html" %}{% endif %}
{% block CONTENT %}{% endblock %}
{% include "blocks/counters.html" %}
<script src="{% static 'js/bg-generator.js' %}"></script> <script src="{% static 'js/bg-generator.js' %}"></script>
{% include "blocks/counters.html" %}
</body> </body>
</html> </html>

View File

@@ -1,11 +1,6 @@
<!-- ПОДВАЛ: НАЧАЛО -- соглашение о сборе технической информации --> <footer data-nosnippet>
<div name="cookies_accept"> <!--noindex-->
<small>Тут используют cookie и&nbsp;ведут сбор технических данных о&nbsp;посещениях, потому как без этого <nobr>интернет-сайты</nobr> вообще почти <nobr>не&nbsp;работают&hellip;</small> <small>Следы на песке смывает волна, но цифровые следы (cookie) помогают нам помнить вас, пока вы здесь.</small>
<button onclick="CookieAcceptDate = new Date(); <button>Осознаю</button>
CookieAcceptDate.setTime(CookieAcceptDate.getTime() + 7948800000); <!--/noindex-->
document.cookie = 'cookie_accept=yes;expires=' + CookieAcceptDate; </footer>
document.getElementsByName('cookies_accept')[0].remove();">
Я согласен!
</button></nobr>
</div>
<!-- ПОДВАЛ: КОНЕЦ -->

View File

@@ -1,4 +1,2 @@
<!-- Rating Mail.ru counter --> {% load static %}<script src="{% static 'js/counters.js' %}"></script>
<script type="text/javascript">var _tmr = window._tmr || (window._tmr = []);_tmr.push({id:"1603042",type:"pageView",start:(new Date()).getTime()});(function(d,w,id){if(d.getElementById(id)) return;var ts=d.createElement("script");ts.type="text/javascript";ts.async=true;ts.id=id;ts.src="https://top-fwz1.mail.ru/js/code.js";var f=function(){var s=d.getElementsByTagName("script")[0];s.parentNode.insertBefore(ts,s);};if(w.opera == "[object Opera]"){ d.addEventListener("DOMContentLoaded",f,false);}else{f();}})(document,window,"topmailru-code");</script><noscript><div><img src="https://top-fwz1.mail.ru/counter?id=1603042;js=na" style="border:0;position:absolute;left:-9999px;" 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>
<!-- //Rating Mail.ru counter -->

View File

@@ -1,25 +1,15 @@
{% load static %} {% load static %}{# ШАПКА #}<header>
<!-- ШАПКА: НАЧАЛО --> <a href="/" id="logo">
<div class="container"> <img src='{% static "svgs/dq-logo.svg" %}' alt="Dictum & Quotes" title="Dictum & Quotes"/>
<header> </a>
<a href="/" id="logo"> <nav>
<img src='{% static "svgs/dq-logo.svg" %}' alt="Dictum & Quotes" title="Dictum & Quotes" <span id="stats-menu">
width="50" height="46"/> {# Манифест проекта #}<p>
</a> DicQuo&nbsp;— это коллекция отобранных вручную цитат, оформленных с&nbsp;уважением к&nbsp;типографике.<br/>
<div> Место для&nbsp;вдумчивого&nbsp;чтения.
<span id="stats-menu" style="display: none; color: silver; font-size: 0.9em; margin-right: 15px; text-align: right;"> </p>
<!-- Манифест проекта --> {# МЕНЮ #}{% if ticks %}<i class="stats-icon icon-time" title="Время генерации"></i>{{ ticks|floatformat:3|default:"1,023" }} ms{% endif %}{% if DQ %} <b>|</b> <i class="stats-icon icon-views" title="Просмотры"></i> {{ DQ.iViewCounter }}{% endif %} <a href="/add_quote/" title="Добавить цитату"></a> &nbsp;
<div style="margin-bottom: 5px; color: #aaa; font-style: italic; max-width: 300px; display: inline-block;"> </span>
Dicquo&nbsp;— это коллекция отобранных вручную цитат, оформленных с&nbsp;уважением к&nbsp;типографике. Место для&nbsp;вдумчивого чтения. {# БУРГЕР #}<a href="#" onclick="var m=document.getElementById('stats-menu'); m.style.display = (m.style.display === 'none' ? 'inline-block' : 'none'); return false;" title="О проекте"></a>
</div> </nav>
<br/> </header>
<!-- Статистика -->
{% if ticks %}<i class="stats-icon icon-time" title="Время генерации"></i>{{ ticks|floatformat:1 }}ms{% endif %}
{% if DQ %} &nbsp;|&nbsp; <i class="stats-icon icon-views" title="Просмотры"></i>{{ DQ.iViewCounter }}{% endif %}
&nbsp;|&nbsp; <a href="/add_quote/" style="color: silver; text-decoration: none;" title="Добавить цитату"><i class="stats-icon icon-add"></i></a> &nbsp;|&nbsp;
</span>
<a href="#" onclick="var m=document.getElementById('stats-menu'); m.style.display = (m.style.display === 'none' ? 'inline-block' : 'none'); return false;" style="color: silver; text-decoration: none; font-size: 1.2em;"></a>
</div>
</header>
</div>
<!-- ШАПКА: КОНЕЦ -->

View File

@@ -1,6 +0,0 @@
<!-- ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ: НАЧАЛО -->
<div style="bottom:0;" class="position-sticky float-right fixed-bottom">
<small style="background:#674376;color: white;font-size: xx-small;"
class="x">&ensp;🕗&nbsp;{{ ticks|stringformat:".6f" }}&thinsp;s&thinsp;<nobr>({% now 'c' %})</nobr>&ensp;</small>
</div>
<!-- ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ: КОНЕЦ -->

View File

@@ -13,84 +13,53 @@
<!--- ТИТУЛ ---> <!--- ТИТУЛ --->
{% block Title %}{% if AUTHOR %}{{ AUTHOR.szAuthor }} — {% endif %}{{ DQ.szContent|truncatewords:7 }} | Dicquo{% endblock %} {% block Title %}{% if AUTHOR %}{{ AUTHOR.szAuthor }} — {% endif %}{{ DQ.szContent|truncatewords:7 }} | Dicquo{% endblock %}
{% block ExtraHead %} {% block ExtraHead %}<script type="application/ld+json">
<script type="application/ld+json"> {
{ "@context": "https://schema.org",
"@context": "https://schema.org", "@type": "Quotation",
"@type": "Quotation", "name": "Цитата #{{ DQ.id }}",
"name": "Цитата #{{ DQ.id }}", "text": "{{ DQ.szContent|escapejs }}",
"text": "{{ DQ.szContent|escapejs }}", "creator": {
"creator": { "@type": "Person",
"@type": "Person", "name": "{% if AUTHOR %}{{ AUTHOR.szAuthor|escapejs }}{% else %}Неизвестный автор{% endif %}"
"name": "{% if AUTHOR %}{{ AUTHOR.szAuthor|escapejs }}{% else %}Неизвестный автор{% endif %}" },
}, "url": "{{ request.build_absolute_uri }}",
"url": "{{ request.build_absolute_uri }}", {% if IMAGE %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}",{% endif %}
{% if IMAGE %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}",{% endif %} "keywords": "афоризмы, цитаты, {% for i in TAGS %}{{ i.name|escapejs }}{% if not forloop.last %}, {% endif %}{% endfor %}",
"keywords": "афоризмы, цитаты, {% for i in TAGS %}{{ i.name|escapejs }}{% if not forloop.last %}, {% endif %}{% endfor %}", "inLanguage": "ru",
"inLanguage": "ru", "isPartOf": {
"isPartOf": { "@type": "WebSite",
"@type": "WebSite", "name": "Dicquo",
"name": "Dicquo", "url": "{{ request.scheme }}://{{ request.get_host }}"
"url": "{{ request.scheme }}://{{ request.get_host }}" },
}, "dateCreated": "{{ DQ.dtCreated|date:'Y-m-d' }}",
"dateCreated": "{{ DQ.dtCreated|date:'Y-m-d' }}", "dateModified": "{{ DQ.dtEdited|date:'Y-m-d' }}"
"dateModified": "{{ DQ.dtEdited|date:'Y-m-d' }}" }
} </script>{% endblock %}
</script>
{% endblock %}
{% block CONTENT %}{% include "blocks/header_nav.html" %} {% block CONTENT %}<main>{# Основной контент: Текст + Картинка #}
<div class="container main-content"> <article>{# Текстовая ряб. Задает высоту двум колонкам: текст и картина #}
<!-- Основной контент: Текст + Картинка --> <figure>{# КОЛОНКА С ТЕКСТОМ #}{% if DQ.szIntroHTML %}
<div class="content-row"> {# Интро/Вступление (например "Вася Пупкин как-то сказа". Может отсутствовать #}<p>{{ DQ.szIntroHTML|safe }}</p>{% endif %}
<blockquote>{{ DQ.szContentHTML|safe }}</blockquote>{% if AUTHOR %}
<!-- Текстовая колонка --> <cite>{# Автор #}{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor|safe }}</cite>{% endif %}
<div class="text-col"> </figure>{% if IMAGE %}
<!-- Интро/Вступление --> <div>{# КОЛОНКА С КАРТИНКОЙ #}
{% if DQ.szIntroHTML %} <div style="background:rgba(87,0,0,0.7);">
<div id="info">{{ DQ.szIntroHTML|safe }}</div> <div>
{% endif %} <img src="{{IMAGE.url}}" alt="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" title="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" />
</div>
<!-- Цитата: Семантический blockquote -->
<blockquote id="bb" style="border:none; margin:0; padding:0;">
{{ DQ.szContentHTML|safe }}
</blockquote>
<!-- Автор: Семантический cite -->
<div id="author">
<cite>
{% if AUTHOR %}
{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor|safe }}
{% endif %}
</cite>
</div> </div>
</div> </div>{% endif %}
</article>
<!-- Колонка с картинкой (если есть) --> </main>
{% if IMAGE %} <nav>
<div class="image-col" id="image"> <div>{# ТЕГИ #}{% for i in TAGS %}
<center> <a href="/?tag={{ i.slug }}">{{ i.name|safe }}</a> {% endfor %}
<div style="background:rgba(87,0,0,0.7);">
<div><img src="{{IMAGE.url}}" alt="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" title="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" /></div>
</div>
</center>
</div>
{% endif %}
</div>
<!-- Блок тегов и навигации -->
<div class="tags">
{% for i in TAGS %}<a href="/?tag={{ i.slug }}">{{ i.name|safe }}</a> {% endfor %}
<div id="next"><a href="/{{ NEXT }}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">&rightarrow;</a></div> <div id="next"><a href="/{{ NEXT }}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">&rightarrow;</a></div>
</div> </div>
</div> </nav>
<noscript>
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
<noscript> </noscript>{% endblock %}
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
</noscript>
{% if not cookie_accept %}{% include "blocks/cookie_warning.html" %}{% endif %}
{% endblock %}

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

@@ -15,9 +15,9 @@ services:
# 1. ОБРАЗ # 1. ОБРАЗ
# В продакшене мы используем готовый, собранный образ из реестра (Gitea) # В продакшене мы используем готовый, собранный образ из реестра (Gitea)
# image: git.cube2.ru/e-serg/dicquo:latest image: git.cube2.ru/erjemin/2020-dq:latest
# Но пока, для первого деплоя или если реестра нет, можно собирать локально: # Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
build: . # build: .
restart: always restart: always
@@ -50,7 +50,8 @@ services:
# 4. Проброс портов (Внешний Nginx -> localhost:8010) # 4. Проброс портов (Внешний Nginx -> localhost:8010)
ports: ports:
- "8010:8000" # Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
- "127.0.0.1:8010:8000"
# 5. Тома (Volumes) # 5. Тома (Volumes)
volumes: volumes:
@@ -105,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
command: --scope dq-scope 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,143 +1,11 @@
@charset "utf-8"; @charset "utf-8";
body {
.tags{ margin: 0;
color: silver; min-height: 100vmin;
font-size:1.5vh; min-width: 100vmin;
line-height:1.9vh; background-color: #111; /* Изначально темный фон */
padding-top: 7vh; opacity: 0; /* Скрываем контент до расчета цвета */
} transition: opacity 0.9s ease-in-out; /* Очень плавное появление */
/*****************************************************************
* Настройки для анимирования цвета ссылок:
* рецепт взят из: https://habr.com/ru/company/ruvds/blog/491702/
*****************************************************************/
.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: 2vh;
}
.tags a:hover {
color: white;
background-position: 0 100%;
border-bottom: solid 1px white;
}
div[name="cookies_accept"] {
font-family:'Roboto', 'Lucida Grande', Verdana, Arial, sans-serif;
position:fixed;
bottom: 0; left: 0;
width: 100%;
padding: 2vh 2vw;
color: black;
background-color: gray;
text-align: center;
}
div[name="cookies_accept"] button {
padding:0.5vh 0.5vw;
background: silver;
color: black;
margin-left: 2vw;
cursor: pointer;
}
#logo {
margin-top:1vh;
filter: alpha(Opacity=75); /* Полупрозрачность для IE */
opacity: 0.75;
float: left;
}
#logo a {
border: none;
text-decoration: none;
}
table { width: 80%; }
#menu {
display: none;
color: silver;
}
#mm {
text-decoration: none;
color: silver;
}
#image {
width: 30vw;
text-align: center;
vertical-align: center;
}
#image > center > div {
width: 22vw;
height: 22vw;
padding:0.5vw;
border-radius: 50%;
}
#image > center > div > div {
border-radius:50%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
#image > center > div > div > img {
width: auto;
height: 22vw;
}
#author {
color: silver;
font-size: 3.5vh;
line-height: 4vh;
text-align: right;
padding-top: 4vh;
font-style: italic;
}
#info {
color: silver;
font-size: 2.5vh;
line-height: 3vh;
padding-bottom: 2vh;
}
#bb {
color: whitesmoke;
font-size: 4.5vh;
line-height: 5vh
}
#next { float: right; }
#next a { border-bottom: none; }
/* --- NEW STYLES for FLEXBOX LAYOUT --- */
.container {
width: 90%;
max-width: 1200px;
margin: 0 auto;
} }
/* Header */ /* Header */
@@ -145,46 +13,101 @@ header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1vh 0; padding: 1vmin 4vmin;
} }
/* Main Content Area */ header > #logo {
.main-content { margin-top: 1vmin;
display: flex; float: left;
flex-direction: column;
justify-content: center;
min-height: 80vh;
} }
.content-row { header > #logo a {
display: flex; border: none;
align-items: center; text-decoration: none;
justify-content: center;
gap: 2vw;
} }
.text-col { header > #logo a > img {
flex: 1; width:50px;
height:46px;
} }
.image-col { header > nav {
flex: 0 0 30vw; border: #555555;
display: flex; min-height: 50px;
justify-content: center;
} }
/* --- Icons for Header Stats (SVG in Base64) --- */ header > nav > a { /* бургер */
.stats-icon { color: silver;
text-decoration: none;
font-size: 1.2em;
padding: 0 0.5em;
margin-right: -0.5em;
border: solid 1px transparent;
transition: border-color 0.8s ease, color 0.8s ease;
vertical-align: top;
}
header > nav > a:hover {
color: white;
border: solid 1px silver;
transition: border-color 0.8s ease, color 0.8s ease;
}
header > nav > #stats-menu {
display: none;
color: silver;
font-size: 0.9em;
margin-right: 15px;
text-align: right;
vertical-align: top;
}
header > nav > #stats-menu > b {
font-weight: normal;
margin: 0 1ex;
}
header > nav > #stats-menu > p {
font-style: italic; display: inline-block;
margin: 0 1vmin;
padding-right: 1vmin;
border-right: 1px dotted silver;
}
header > nav > #stats-menu > i.stats-icon {
display: inline-block; display: inline-block;
width: 0.9em; width: 0.9em;
height: 0.9em; height: 0.9em;
vertical-align: middle; vertical-align: middle;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
margin-right: 0.2em; margin-right: .2em;
opacity: 0.7; /* Slight transparency for subtle look */ opacity: 0.7; /* Slight transparency for subtle look */
} }
header > nav > #stats-menu > i.stats-icon.icon-views {
margin-left: .2em;
}
header > nav > #stats-menu > a {
color: silver;
text-decoration: none;
border: solid 1px gray;
border-radius: 2em;
padding: 1.5px 0.2em 0 0.2em;
margin-left: 1em;
transition: background-color 0.3s ease, color 0.3s ease;
}
header > nav > #stats-menu > a:hover {
background-color: tan;
color: black;
border: solid 1px white;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* --- Icons for Header Stats (SVG in Base64) --- */
/* Clock Icon (Time) */ /* Clock Icon (Time) */
.icon-time { .icon-time {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");
@@ -195,30 +118,202 @@ header {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3C/svg%3E");
} }
/* Responsive: on mobile stack columns */ /* MAIN ARTICLE CONTENT */
main {
padding: 1vmin 8vmin;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 60vmin;
}
main > article {
display: flex;
align-items: center;
justify-content: center;
gap: 2vmin;
}
main > article > figure {
flex: 1;
}
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: 4vmin;
font-style: italic;
}
main > article > div {
flex: 0 0 30vmax;
display: flex;
justify-content: center;
width: 30vmax;
text-align: right;
margin-bottom: 10vmin;
}
main > article > div > div {
width: 26vmax;
height: 26vmax;
padding: 0.5vmin;
border-radius: 50%;
}
main > article > div > div > div {
border-radius: 50%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
main > article > div > div > div > img {
width: auto;
height: 26vmax;
}
/* НАВИГАЦИЯ (ТЕГИ И ДАЛЕЕ) В КОНЦЕ */
nav {
padding: 1vmin 4vmin;
}
nav > div {
color: silver;
font-size: 1.5vmin;
line-height: 1.9vmin;
padding: 7vmin 0 4vmin 0;
}
nav > div 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;
}
nav > div a:hover {
color: white;
background-position: 0 100%;
border-bottom: solid 1px white;
}
nav > div > div {
float: right;
}
nav > div > div a {
border-bottom: none;
}
/* --- ПОДВАЛ-КУКИ (ДЗЕН-СТИЛЬ) --- */
footer {
font-family: 'Roboto', sans-serif;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 2vmin 4vmin;
color: silver; /* Мягкий серый цвет текста */
background-color: rgba(30, 30, 30, 0.8); /* Темный полупрозрачный фон */
backdrop-filter: blur(5px); /* Эффект матового стекла (современно и медитативно) */
text-align: center;
border-top: 1px solid #444; /* Тонкая грань */
font-size: 0.9em;
z-index: 1000; /* Чтобы точно было поверх всего */
}
footer small {
display: inline-block;
margin-right: 2vmin;
letter-spacing: 0.05em; /* Немного воздуха в тексте */
}
footer button {
padding: 0.5vmin 1.5vmin;
background: transparent;
color: silver;
border: 1px solid silver;
border-radius: 2em; /* Округлые, мягкие формы */
cursor: pointer;
font-family: inherit;
font-size: 0.9em;
transition: all 0.4s ease;
}
footer button:hover {
background: silver;
color: #111;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2); /* Легкое свечение при наведении */
}
/* Отзывчивость: для мобильных устройств колонки свапаются */
@media (max-width: 768px) { @media (max-width: 768px) {
.content-row { main > article {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.image-col {
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;
} }
} }
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */ /* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */ /* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
.etp-laquo { margin-left: -0.44em; } /* « */ .etp-laquo {margin-left: -0.44em;} /* « */
.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ „ */ .etp-ldquo, .etp-bdquo { margin-left: -0.4em;} /* “ „ */
.etp-lsquo { margin-left: -0.22em; } /* */ .etp-lsquo {margin-left: -0.22em;} /* */
.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.25em; } /* ( [ { */ .etp-lpar, .etp-lsqb, .etp-lcub {margin-left: -0.25em;}/* ( [ { */
/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по правому краю) --- */ /* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) --- */
/* Общая механика: "вырываем" символ из потока для идеального выравнивания текста */ .counter-pixel {
[class^="etp-r"], [class*=" etp-r"] { position: absolute; } border: 0;
/* Точечная настройка смещения для каждого символа */ position: absolute;
.etp-raquo { right: -0.44em; } /* » */ left: -9999px;
.etp-rdquo { right: -0.4em; } /* ” */ }
.etp-rsquo { right: -0.22em; } /* */
.etp-rpar, .etp-rsqb, .etp-rcub { right: -0.25em; } /* ) ] } */
.etp-r-dot, .etp-r-comma, .etp-r-colon { right: -0.15em; } /* . , : */

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

@@ -1,25 +1,29 @@
// bg-generator.js: Generates a unique gradient background based on text content // bg-generator.js:
// - Генерирует уникальный градиентный фон на основе текстового содержимого
// - Реализует плавное появление и исчезновение при навигации
// - Авто-редирект через 15 секунд для создания "медитативного" слайд-шоу эффекта
// - Обрабатывает принятие Cookie
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// 1. Get the text to hash (from hidden span in base.html) // 1. Получаем текст для хеширования (из скрытого span в base.html)
const rawSpan = document.getElementById('dq-content-raw'); const rawSpan = document.getElementById('dq-content-raw');
let text = rawSpan ? rawSpan.innerText.trim() : ""; let text = rawSpan ? rawSpan.innerText.trim() : "";
if (!text) { if (!text) {
text = "DictumAndQuotesDefault" + Math.random(); // Fallback random if no text text = "DictumAndQuotesDefault" + Math.random(); // Случайный вариант, если текста нет
} }
// 2. Hash function (DJB2) // 2. Хеш-функция (DJB2)
let hash = 5381; let hash = 5381;
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
// Force 32-bit integer arithmetic // Принудительная 32-битная целочисленная арифметика
hash = ((hash << 5) + hash) + text.charCodeAt(i); hash = ((hash << 5) + hash) + text.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer hash = hash & hash; // Преобразование в 32-битное целое
} }
// 3. Generate 6 color components deterministically from the hash // 3. Детерминированная генерация 6 цветовых компонентов из хеша
// We need 6 numbers between 0 and 255. // Нам нужно 6 чисел от 0 до 255.
// Let's use pseudo-random generator seeded by hash // Используем генератор псевдослучайных чисел с затравкой из хеша
function Mulberry32(a) { function Mulberry32(a) {
return function() { return function() {
@@ -30,53 +34,53 @@ document.addEventListener("DOMContentLoaded", function() {
} }
} }
const rand = Mulberry32(hash); // Seeded random generator const rand = Mulberry32(hash); // Генератор случайных чисел с seed
// Generate 6 color components with darker range for "meditative" feel // Генерация 6 цветовых компонентов в темном диапазоне для "медитативного" ощущения
let colors = []; let colors = [];
for(let i=0; i<6; i++) { for(let i=0; i<6; i++) {
// Generate number between 10 and 80 (dark colors) // Генерируем число от 10 до 80 (темные цвета)
colors.push(Math.floor(rand() * 70) + 10); colors.push(Math.floor(rand() * 70) + 10);
} }
// Shuffle slightly based on random to allow variation on refresh (optional) // Немного перетасовываем на основе случайности, чтобы позволить вариации при обновлении (опционально)
// colors.sort(() => Math.random() - 0.5); colors.sort(() => Math.random() - 0.5);
const rgb1 = `rgb(${colors[0]}, ${colors[1]}, ${colors[2]})`; const rgb1 = `rgb(${colors[0]}, ${colors[1]}, ${colors[2]})`;
const rgb2 = `rgb(${colors[3]}, ${colors[4]}, ${colors[5]})`; const rgb2 = `rgb(${colors[3]}, ${colors[4]}, ${colors[5]})`;
console.log("DQ BG Generator:", text.substring(0, 20) + "...", hash, rgb1, rgb2); console.log("DQ BG Generator:", text.substring(0, 20) + "...", hash, rgb1, rgb2);
// 4. Apply to body // 4. Применяем к body.
// Using linear-gradient to right with standard syntax // Используем линейный градиент вправо со стандартным синтаксисом
const bgString = `linear-gradient(90deg, ${rgb1} 0%, ${rgb2} 100%)`; const bgString = `linear-gradient(90deg, ${rgb1} 0%, ${rgb2} 100%)`;
document.body.style.background = bgString; document.body.style.background = bgString;
// 5. Apply to image background container (if exists on index page) // 5. Применяем к контейнеру фона изображения (если он есть на главной странице)
const imgBgContainer = document.querySelector('.image-col center > div'); const imgBgContainer = document.querySelector('.image-col center > div');
if (imgBgContainer) { if (imgBgContainer) {
// Use the first color of the gradient with opacity 0.7 // Используем первый цвет градиента с прозрачностью 0.7
imgBgContainer.style.background = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.7)`; imgBgContainer.style.background = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.7)`;
} }
// 6. Reveal content (Fade In effect) // 6. Показываем контент (эффект плавного появления - Fade In)
setTimeout(() => { setTimeout(() => {
document.body.style.opacity = 1; document.body.style.opacity = 1;
}, 50); }, 50);
// 7. Handle Fade Out on link clicks // 7. Обработка плавного исчезновения (Fade Out) при клике по ссылкам
document.body.addEventListener('click', function(e) { document.body.addEventListener('click', function(e) {
// Find if a link was clicked (bubble up) // Ищем, была ли нажата ссылка (всплытие)
const link = e.target.closest('a'); const link = e.target.closest('a');
if (link && link.href && link.target !== '_blank') { if (link && link.href && link.target !== '_blank') {
const hrefAttr = link.getAttribute('href'); const hrefAttr = link.getAttribute('href');
if (hrefAttr && !hrefAttr.startsWith('#') && !link.href.includes('javascript:')) { if (hrefAttr && !hrefAttr.startsWith('#') && !link.href.includes('javascript:')) {
// Check if it is an internal link (same domain) // Проверяем, является ли ссылка внутренней (тот же домен)
if (new URL(link.href).origin === window.location.origin) { if (new URL(link.href).origin === window.location.origin) {
e.preventDefault(); // Stop immediate navigation e.preventDefault(); // Останавливаем немедленный переход
document.body.style.opacity = 0; // Start Fade Out document.body.style.opacity = 0; // Запускаем Fade Out
// Wait for transition (matches CSS transition time 1.5s) // Ждем завершения перехода (соответствует времени CSS transition 0.9s (было 1.5s))
setTimeout(() => { setTimeout(() => {
window.location.href = link.href; window.location.href = link.href;
}, 900); }, 900);
@@ -85,14 +89,28 @@ document.addEventListener("DOMContentLoaded", function() {
} }
}); });
// 8. Auto-redirect ("meditative" slideshow) // 8. Авто-редирект ("медитативное" слайд-шоу)
// Find the NEXT link and simulate a click on it after 15 seconds // Ищем ссылку "ДАЛЕЕ" и симулируем клик по ней через 15 секунд
const nextLink = document.querySelector('#next a'); const nextLink = document.querySelector('#next a');
if (nextLink) { if (nextLink) {
setTimeout(() => { setTimeout(() => {
// Trigger the click event on the link so our handler above (step 7) catches it // Вызываем событие клика по ссылке, чтобы наш обработчик выше (шаг 7) поймал его
// and performs the smooth fade out animation. // и выполнил анимацию плавного исчезновения.
nextLink.click(); nextLink.click();
}, 15000); }, 15000);
} }
// 9. Логика принятия Cookie
const cookieBanner = document.querySelector('footer');
if (cookieBanner) {
const acceptButton = cookieBanner.querySelector('button');
if (acceptButton) {
acceptButton.addEventListener('click', function() {
const date = new Date();
date.setTime(date.getTime() + (92 * 24 * 60 * 60 * 1000)); // ~3 месяца (7948800000ms)
document.cookie = "cookie_accept=1; expires=" + date.toUTCString() + "; path=/; SameSite=Lax";
cookieBanner.remove();
});
}
}
}); });

View File

@@ -0,0 +1,42 @@
// Rating Mail.ru counter
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({id: "3744288", type: "pageView", start: (new Date()).getTime()});
(function (d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script");
ts.type = "text/javascript";
ts.async = true;
ts.id = id;
ts.src = "https://top-fwz1.mail.ru/js/code.js";
var f = function () {
var s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(ts, s);
};
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else {
f();
}
})(document, window, "tmr-code");
// 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]