Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f96932a40 | |||
| d59c867010 | |||
| 9ea851ad95 | |||
| 5146f88c7d | |||
| 20ecb9cc4c | |||
| ea2d352cae | |||
| d459eedf41 | |||
| 9d31429e14 | |||
| c5963d1d30 | |||
| 12001bc749 | |||
| 99a2ace43f | |||
| 81efaf1ba5 | |||
| 42b378fcbc | |||
| ba4175dfdb | |||
| 4194b351d2 | |||
| be68a82927 | |||
| 53b127a966 | |||
| 746c50a988 | |||
| 2520362ad5 | |||
| a857101c3f | |||
| 7eeb44a1f5 | |||
| 86bfd9b07b | |||
| c3c81d7ff5 | |||
| f4cce3d08a | |||
| 45275c51f6 | |||
| f2f98d9229 | |||
| a33b517a3e | |||
| d4624e7761 | |||
| a608dea61f |
@@ -63,3 +63,8 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
# ДОБАВЛЕНО:
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
# И это для медленного интернета:
|
||||||
|
timeout: 900 # 15 минут на всю сборку
|
||||||
|
|||||||
126
Dockerfile
126
Dockerfile
@@ -1,63 +1,105 @@
|
|||||||
# ==========================================
|
# =================================================
|
||||||
# Dockerfile для Django + Gunicorn + WhiteNoise
|
# STAGE 1: Builder - Установка зависимостей
|
||||||
# ==========================================
|
# =================================================
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
# 1. Базовый образ: Python 3.12 (Slim версия для меньшего размера)
|
# Устанавливаем переменные окружения
|
||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
# 2. Переменные окружения для Python
|
|
||||||
# PYTHONDONTWRITEBYTECODE: Запрещает Python писать .pyc файлы
|
|
||||||
# PYTHONUNBUFFERED: Гарантирует, что вывод консоли (logs) виден сразу (не буферизуется)
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
# Poetry настройки: не создавать виртуальное окружение внутри контейнера (ставим системно).
|
# Говорим Poetry, чтобы он не создавал venv, а ставил пакеты в системный site-packages
|
||||||
# Дублирует `poetry config virtualenvs.create false` в пп.7 (на всякий случай).
|
|
||||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||||
# Путь настройки Django (по умолчанию для production) на случай если контейнер будет запущен не через docker-compose.
|
|
||||||
ENV DJANGO_SETTINGS_MODULE=dicquo.settings
|
|
||||||
|
|
||||||
# 3. Рабочая директория внутри контейнера
|
# Устанавливаем системные зависимости, необходимые для СБОРКИ пакетов (например, Pillow)
|
||||||
WORKDIR /app
|
# build-essential нужен для компиляции, -dev пакеты для сборки Pillow
|
||||||
|
|
||||||
# 4. Установка системных зависимостей
|
|
||||||
# - libjpeg-dev zlib1g-dev: библиотеки для работы с изображениями (Pillow)
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
libjpeg-dev \
|
libjpeg-dev \
|
||||||
zlib1g-dev \
|
zlib1g-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 5. Установка Poetry через pip (быстро и надежно)
|
# Устанавливаем Poetry
|
||||||
RUN pip install --no-cache-dir poetry
|
RUN pip install --no-cache-dir poetry
|
||||||
|
|
||||||
# 6. Копируем файлы зависимостей (pyproject.toml и poetry.lock)
|
# Создаем рабочую директорию
|
||||||
# Делаем это ДО копирования всего кода, чтобы использовать кэш Docker layers.
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем только файлы зависимостей для кэширования этого слоя
|
||||||
COPY pyproject.toml poetry.lock /app/
|
COPY pyproject.toml poetry.lock /app/
|
||||||
|
|
||||||
# 7. Установка зависимостей проекта
|
# Устанавливаем зависимости проекта. Poetry установит их в /usr/local/lib/python3.12/site-packages
|
||||||
# --no-interaction: не будет спрашивать подтверждения
|
RUN poetry install --no-interaction --no-ansi --no-root --only main
|
||||||
# --no-ansi: уберваем цветные символы из логов сборки (они иногда мусорят)
|
|
||||||
# --no-root: не устанавливать сам проект как пакет (мы просто копируем код)
|
|
||||||
# --only main: не ставить dev-зависимости (тесты, линтеры и т.п.) для продакшена
|
|
||||||
# RUN poetry install --no-root --only main
|
|
||||||
# Настройка Poetry: не создавать venv и установка зависимостей (без dev-зависимостей для продакшена)
|
|
||||||
RUN poetry config virtualenvs.create false \
|
|
||||||
&& poetry install --no-interaction --no-ansi --no-root --only main
|
|
||||||
|
|
||||||
# 8. Копируем весь исходный код проекта в контейнер
|
|
||||||
COPY . /app/
|
|
||||||
|
|
||||||
# 9. Сборка статики (CSS, JS)
|
# =================================================
|
||||||
# Важно: Запускаем collectstatic с фейковым SECRET_KEY, так как на этапе сборки env файла может не быть.
|
# STAGE 2: Final - Создание чистого и безопасного образа
|
||||||
RUN SECRET_KEY=dummy_build_key python dicquo/manage.py collectstatic --noinput --clear
|
# =================================================
|
||||||
|
FROM python:3.12-slim AS stage-final
|
||||||
|
|
||||||
# 10. Открываем порт 8000
|
# Устанавливаем переменные окружения
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV DJANGO_SETTINGS_MODULE=dicquo.settings
|
||||||
|
|
||||||
|
# Устанавливаем только RUNTIME системные зависимости.
|
||||||
|
# Пакеты -dev и build-essential здесь не нужны.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libjpeg62-turbo \
|
||||||
|
sqlite3 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Создаем пользователя без прав root для безопасности
|
||||||
|
# RUN addgroup --system app && adduser --system --ingroup app app
|
||||||
|
|
||||||
|
# Создаем рабочую директорию
|
||||||
|
WORKDIR /home/app/web
|
||||||
|
|
||||||
|
# Копируем установленные Python-пакеты из builder-стадии
|
||||||
|
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||||
|
|
||||||
|
# Копируем исполняемые файлы (gunicorn, pip и т.д.)
|
||||||
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
|
# Копируем исходный код проекта и устанавливаем правильного владельца
|
||||||
|
# ИЗМЕНЕНИЕ: app:app -> 1000:1000
|
||||||
|
COPY --chown=1000:1000 . .
|
||||||
|
|
||||||
|
# Создаём директорию для конфигов nginx и даём права пользователю app
|
||||||
|
# Это выполняется ещё от root, поэтому проблем с permissions не будет.
|
||||||
|
RUN mkdir -p /nginx_configs_host/nginx && chown -R 1000:1000 /nginx_configs_host
|
||||||
|
|
||||||
|
# Создаём директорию для собранной статики и даём права пользователю app
|
||||||
|
RUN mkdir -p /home/app/web/staticfiles && chown -R 1000:1000 /home/app/web/staticfiles
|
||||||
|
|
||||||
|
# Создаём директорию для ошибок (404, 500) и даём права пользователю app
|
||||||
|
RUN mkdir -p /app/public/media/errors && chown -R 1000:1000 /app/public/media
|
||||||
|
|
||||||
|
# Создаём директорию для БД и даём права пользователю app
|
||||||
|
# Это важно когда БД монтируется как том с хоста
|
||||||
|
RUN mkdir -p /app/database && chown -R 1000:1000 /app/database
|
||||||
|
|
||||||
|
# Переключаемся на пользователя без прав root
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
|
||||||
|
# Собираем статику
|
||||||
|
# Используем dummy ключ, так как .env файла нет на этапе сборки
|
||||||
|
RUN SECRET_KEY=dummy python dicquo/manage.py collectstatic --noinput --clear
|
||||||
|
|
||||||
|
# Открываем порт
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# 11. Команда запуска
|
# Проверка здоровья контейнера
|
||||||
# Переходим в подпапку dicquo, где лежит код Django проекта
|
# Docker будет периодически проверять, жив ли контейнер, отправляя GET запрос к главной странице.
|
||||||
WORKDIR /app/dicquo
|
# Параметры:
|
||||||
|
# --interval=30s - проверка каждые 30 секунд
|
||||||
|
# --timeout=3s - ожидаем ответ максимум 3 секунды
|
||||||
|
# --start-period=10s - даем контейнеру 10 секунд на запуск перед первой проверкой
|
||||||
|
# --retries=3 - объявляем контейнер unhealthy после 3 неудачных попыток
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()" || exit 1
|
||||||
|
|
||||||
# Запускаем Gunicorn (по умолчанию, если не переопределено в docker-compose) на три воркера, привязывая его к
|
# Переходим в директорию с manage.py для корректного запуска gunicorn
|
||||||
# порту 8000 и указывая на точку входа приложения (wsgi.py).
|
WORKDIR /home/app/web/dicquo
|
||||||
|
|
||||||
|
# Команда запуска
|
||||||
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "dicquo.wsgi:application"]
|
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "dicquo.wsgi:application"]
|
||||||
|
|
||||||
|
|||||||
45
PLANS.md
Normal file
45
PLANS.md
Normal 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).
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -102,7 +108,9 @@ server {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
# ВАЖНО: Явно указываем https, потому что клиент всегда приходит по HTTPS к Nginx
|
||||||
|
# Даже если внутри контейнера это HTTP на 127.0.0.1:8010, для Django это должно быть HTTPS
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
|
||||||
# Тайм-ауты (важно для долгих операций, если они есть)
|
# Тайм-ауты (важно для долгих операций, если они есть)
|
||||||
proxy_read_timeout 180s;
|
proxy_read_timeout 180s;
|
||||||
|
|||||||
@@ -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,13 @@ 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 (в секундах)
|
||||||
|
# ВАЖНО: Увеличен до 60 сек для работы с несколькими воркерами Gunicorn
|
||||||
|
'timeout': 60,
|
||||||
|
# Дополнительные опции для лучшей работы SQLite при concurrent доступе
|
||||||
|
'init_command': "PRAGMA journal_mode=WAL;", # Write-Ahead Logging для лучшей concurrency
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,3 +156,12 @@ if not DEBUG:
|
|||||||
WHITENOISE_ROOT = BASE_DIR.parent / 'public'
|
WHITENOISE_ROOT = BASE_DIR.parent / 'public'
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
|
# Настройки безопасности для работы за прокси
|
||||||
|
if not DEBUG:
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
SECURE_SSL_REDIRECT = True
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
USE_X_FORWARDED_PORT = True
|
||||||
|
|||||||
@@ -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")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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;">«</span>Вы спрашиваете меня о чем-то странном. Я не понимаю ваш запрос.»
|
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;">«</span>Вы спрашиваете меня о чем-то странном. Я не понимаю
|
||||||
|
ваш запрос.»
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<!-- Автор -->
|
|
||||||
<div id="author">
|
|
||||||
<cite>Озадаченный Сервер (400)</cite>
|
<cite>Озадаченный Сервер (400)</cite>
|
||||||
</div>
|
</figure>
|
||||||
</div>
|
</article>
|
||||||
|
</main>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Блок тегов и навигации -->
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<a href="/">Сформулировать иначе (на главную)</a>
|
<a href="/">Сформулировать иначе (на главную)</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</body>
|
||||||
|
</html>
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
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;">«</span>Вам сюда нельзя. Даже если очень хочется. Уходите!»
|
<span style="margin-left:-0.44em;">«</span>Вам сюда нельзя. Даже если очень хочется. Уходите!»
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<!-- Автор -->
|
|
||||||
<div id="author">
|
|
||||||
<cite>Строгий Вахтёр (403)</cite>
|
<cite>Строгий Вахтёр (403)</cite>
|
||||||
</div>
|
</figure>
|
||||||
</div>
|
</article>
|
||||||
|
</main>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Блок тегов и навигации -->
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<a href="/">Уйти по-добру по-здорову</a>
|
<a href="/">Уйти по-добру по-здорову</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</body>
|
||||||
|
</html>
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
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;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
<span style="margin-left:-0.44em;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<!-- Автор -->
|
|
||||||
<div id="author">
|
|
||||||
<cite>Системный Администратор (404)</cite>
|
<cite>Системный Администратор (404)</cite>
|
||||||
</div>
|
</figure>
|
||||||
</div>
|
</article>
|
||||||
|
</main>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Блок тегов и навигации -->
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<a href="/">Вернуться на главную</a>
|
<a href="/">Вернуться на главную</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</body>
|
||||||
|
</html>
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -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;">«</span>Что-то пошло не так. Кажется, я уронил сервер. Подождите, пока я его подниму.»
|
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;">«</span>Что-то пошло не так. Кажется, я уронил сервер.
|
||||||
|
Подождите, пока я его подниму.»
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<!-- Автор -->
|
|
||||||
<div id="author">
|
|
||||||
<cite>Системный Администратор (500)</cite>
|
<cite>Системный Администратор (500)</cite>
|
||||||
</div>
|
</figure>
|
||||||
</div>
|
</article>
|
||||||
|
</main>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Блок тегов и навигации -->
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<a href="/">Попробовать обновить страницу</a>
|
<a href="/">Попробовать обновить страницу</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</body>
|
||||||
|
</html>
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
{# Favicons #}<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}"/>
|
{# 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' %}"/>
|
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
|
||||||
{# Technical Meta #}<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}"/>
|
{# Technical Meta #}<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}"/>
|
||||||
|
{# Для ИИ #}<link rel="help" type="text/markdown" href="/llms.txt"/>
|
||||||
{# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.css' %}"/>
|
{# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.css' %}"/>
|
||||||
<noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
|
<noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
|
||||||
{% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}
|
{% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,27 +1,43 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
</html>
|
|
||||||
</body>
|
|
||||||
</div>
|
|
||||||
<a href="/">Вернуться на главную</a>
|
|
||||||
<cite>Системный Администратор (404)</cite>
|
|
||||||
</blockquote>
|
|
||||||
<span>«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
|
||||||
<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;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||||
|
</blockquote>
|
||||||
|
<cite>Системный Администратор (404)</cite>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="/">Вернуться на главную</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -3,24 +3,42 @@
|
|||||||
<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; }
|
||||||
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<header>
|
||||||
<blockquote>
|
<div id="logo">
|
||||||
<span>«</span>Что-то пошло не так. Кажется, я уронил сервер, контейнер или случилось что-то ещё. Подождите, пока я его подниму.»
|
<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;">«</span>Что-то пошло не так. Кажется, я уронил сервер.
|
||||||
|
Подождите, пока я его подниму.»
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<cite>Системный Администратор (5xx)</cite>
|
<cite>Системный Администратор (500)</cite>
|
||||||
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить через 5 минут.</a>
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить страницу</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
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
|
||||||
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from etpgrf.typograph import Typographer
|
from etpgrf.typograph import Typographer
|
||||||
@@ -18,6 +24,101 @@ 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: список всех существующих тегов по имени.
|
||||||
|
# Важно: на этапах вроде collectstatic таблицы taggit ещё может не быть,
|
||||||
|
# поэтому оборачиваем в try/except и молча игнорируем отсутствие БД.
|
||||||
|
try:
|
||||||
|
self.choices = [(t.name, t.name) for t in Tag.objects.all()]
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
self.choices = []
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
"all": ("css/select2_taggit_admin.css",),
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||||
|
"""
|
||||||
|
Настраиваем Select2 так, чтобы пробел НЕ разделял тег
|
||||||
|
на несколько частей (нужны теги с пробелами: «Сергей Курёхин»).
|
||||||
|
Оставляем в разделителях только запятую.
|
||||||
|
"""
|
||||||
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||||
|
# По умолчанию django-select2 ставит: [",", " "]
|
||||||
|
# Нам нужен только разделитель-запятая.
|
||||||
|
# Строка '[","]' — корректный JSON-массив из одного элемента.
|
||||||
|
# Важно: сюда нужно класть СТРОКУ с JSON-массивом, а не python-список.
|
||||||
|
# Иначе в HTML окажется "['[\", \"]', ...]" и Select2 будет вести себя непредсказуемо.
|
||||||
|
attrs["data-token-separators"] = '[","]'
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
"""
|
||||||
|
Преобразуем значение из TaggableManager/TagField
|
||||||
|
в список ИМЁН тегов, который ожидает Select2TagWidget.
|
||||||
|
"""
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# QuerySet или список Tag-объектов
|
||||||
|
if isinstance(value, QuerySet):
|
||||||
|
return [t.name for t in value]
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
names = []
|
||||||
|
for v in value:
|
||||||
|
if isinstance(v, Tag):
|
||||||
|
names.append(v.name)
|
||||||
|
else:
|
||||||
|
names.append(str(v))
|
||||||
|
return names
|
||||||
|
|
||||||
|
# Строка вида "tag1, tag2" — разбираем в список имён
|
||||||
|
if isinstance(value, str):
|
||||||
|
return parse_tags(value)
|
||||||
|
|
||||||
|
return super().format_value(value)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""
|
||||||
|
Django-Select2 возвращает список значений (['Сергей Курёхин', 'Другой тег']).
|
||||||
|
Taggit (TagField) ждёт ОДНУ строку, которую потом парсит в список тегов.
|
||||||
|
Если отдать список, он превратится в строку `"['Сергей', 'Курёхин']"`,
|
||||||
|
и распарсится в кривые теги — этого мы избегаем.
|
||||||
|
"""
|
||||||
|
values = super().value_from_datadict(data, files, name)
|
||||||
|
if not values:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Для нашего виджета value — это уже список имён тегов
|
||||||
|
tag_names = [str(v).strip() for v in values if str(v).strip()]
|
||||||
|
if not tag_names:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ОДИН многословный тег: "Сергей Курёхин" -> "Сергей Курёхин,"
|
||||||
|
# Тогда parse_tags переключится в режим "деление по запятым"
|
||||||
|
if len(tag_names) == 1:
|
||||||
|
single = tag_names[0]
|
||||||
|
if " " in single and "," not in single and '"' not in single:
|
||||||
|
return single + ","
|
||||||
|
return single
|
||||||
|
|
||||||
|
# Несколько тегов — явная запятая между ними.
|
||||||
|
return ", ".join(tag_names)
|
||||||
|
|
||||||
|
|
||||||
class DictumAdminForm(forms.ModelForm):
|
class DictumAdminForm(forms.ModelForm):
|
||||||
# Виртуальные поля для настройки типографа
|
# Виртуальные поля для настройки типографа
|
||||||
etp_language = forms.ChoiceField(
|
etp_language = forms.ChoiceField(
|
||||||
@@ -62,6 +163,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 +204,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 +307,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 +325,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 +341,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)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-03-19 20:34
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('web', '0003_alter_tbdictumandquotes_btypograph_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='tbdictumandquotes',
|
||||||
|
options={'ordering': ['-id'], 'verbose_name': 'ВЫСКАЗЫВАНИЕ', 'verbose_name_plural': 'ВЫСКАЗЫВАНИЯ'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ services:
|
|||||||
sh -c "python manage.py migrate --noinput &&
|
sh -c "python manage.py migrate --noinput &&
|
||||||
python manage.py collectstatic --noinput &&
|
python manage.py collectstatic --noinput &&
|
||||||
mkdir -p /nginx_configs_host/nginx &&
|
mkdir -p /nginx_configs_host/nginx &&
|
||||||
sed \"s|/home/user/app/dq-site|${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}|g\" /app/configs/nginx/dq-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_dq.conf.example &&
|
sed \"s|/home/user/app/dq-site|${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}|g\" /nginx_configs_host/nginx/dq-app--external-nginx.conf > /nginx_configs_host/nginx/nginx_dq.conf.example &&
|
||||||
if [ ! -f /nginx_configs_host/nginx/dq-app--external-nginx.conf ]; then
|
if [ ! -f /nginx_configs_host/nginx/dq-app--external-nginx.conf ]; then
|
||||||
cp /nginx_configs_host/nginx/nginx_dq.conf.example /nginx_configs_host/nginx/dq-app--external-nginx.conf;
|
cp /nginx_configs_host/nginx/nginx_dq.conf.example /nginx_configs_host/nginx/dq-app--external-nginx.conf;
|
||||||
echo 'INIT: Created new nginx config with correct paths';
|
echo 'INIT: Created new nginx config with correct paths';
|
||||||
fi &&
|
fi &&
|
||||||
mkdir -p /app/public/media/errors &&
|
mkdir -p /app/public/media/errors &&
|
||||||
cp /app/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
|
cp /home/app/web/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
|
||||||
cp /app/dicquo/templates/static_500.html /app/public/media/errors/500.html &&
|
cp /home/app/web/dicquo/templates/static_500.html /app/public/media/errors/500.html &&
|
||||||
gunicorn --workers 3 --bind 0.0.0.0:8000 dicquo.wsgi:application"
|
gunicorn --workers 3 --bind 0.0.0.0:8000 dicquo.wsgi:application"
|
||||||
|
|
||||||
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
|
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
|
||||||
@@ -67,7 +67,10 @@ services:
|
|||||||
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
||||||
- ./config:/nginx_configs_host
|
- ./config:/nginx_configs_host
|
||||||
|
|
||||||
# 6. Переменные окружения
|
# 6. Запускать от имени пользователя с UID 1000 и GID 1000
|
||||||
|
user: "1000:1000"
|
||||||
|
|
||||||
|
# 7. Переменные окружения
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -76,14 +79,24 @@ services:
|
|||||||
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||||
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
|
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
|
||||||
|
|
||||||
# 7. Логирование (Ротация)
|
# 8. Проверка здоровья контейнера (Healthcheck)
|
||||||
|
# Docker будет периодически проверять статус контейнера. Это критично для Watchtower!
|
||||||
|
# Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый.
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"]
|
||||||
|
interval: 30s # Проверка каждые 30 секунд
|
||||||
|
timeout: 3s # Таймаут ответа - 3 секунды
|
||||||
|
start_period: 10s # Даем 10 секунд на стартап перед первой проверкой
|
||||||
|
retries: 3 # Unhealthy после 3 неудачных попыток
|
||||||
|
|
||||||
|
# 9. Логирование (Ротация)
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
# 8. Ресурсы
|
# 10. Ресурсы
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -106,8 +119,11 @@ 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
|
||||||
|
# Дополнительные опции для правильной работы с healthcheck
|
||||||
|
- WATCHTOWER_WAIT_ON_TIMEOUT=60 # Ждем 60 сек пока контейнер станет healthy перед финализацией
|
||||||
|
- WATCHTOWER_LIFECYCLE_HOOKS=true # Включаем lifecycle hooks для graceful shutdown
|
||||||
|
command: --interval 1800 --cleanup # Проверять каждые 30 минут
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
|
|||||||
37
poetry.lock
generated
37
poetry.lock
generated
@@ -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"
|
||||||
@@ -99,13 +128,13 @@ Django = ">=4.1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etpgrf"
|
name = "etpgrf"
|
||||||
version = "0.1.4"
|
version = "0.1.6"
|
||||||
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
|
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
files = [
|
files = [
|
||||||
{file = "etpgrf-0.1.4-py3-none-any.whl", hash = "sha256:62d4371e1b5fab06b99f79bd351767aed8baf7d041cae7e5d4eb63f7c9545114"},
|
{file = "etpgrf-0.1.6-py3-none-any.whl", hash = "sha256:a2d2a67048f094e1d30fe42f05f420afd19babe66ec7daa35b517ca23306d5cc"},
|
||||||
{file = "etpgrf-0.1.4.tar.gz", hash = "sha256:c699382c292e3110915331dd5539e7dde0c961e4f4ca65cf8db0e01e84dab72f"},
|
{file = "etpgrf-0.1.6.tar.gz", hash = "sha256:a050c400a30be1c2379c892fc5fa398a79d15f0169094f00023a75dec01864af"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -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 = "64c804553b6314e8f8f5830637781a3179fd70f14cceb6730bfcb2cf24c91a31"
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
# DicQuo
|
# DicQuo
|
||||||
User-Agent: *
|
User-Agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
Disallow: /admin/
|
|
||||||
Disallow: /*?tag=
|
Disallow: /*?tag=
|
||||||
Disallow: /*?
|
Disallow: /*?
|
||||||
|
|
||||||
# Optimize for Yandex
|
# Optimize for Yandex
|
||||||
Clean-param: tag /
|
Clean-param: tag /
|
||||||
|
|
||||||
# AI and LLM bots settings
|
|
||||||
# OpenAI GPT
|
|
||||||
# User-agent: GPTBot
|
|
||||||
# Disallow:
|
|
||||||
|
|
||||||
# Common Crawl (used by many AI models)
|
|
||||||
# User-agent: CCBot
|
|
||||||
# Disallow:
|
|
||||||
|
|
||||||
# Google Bard/Gemini
|
|
||||||
# User-agent: Google-Extended
|
|
||||||
# Disallow:
|
|
||||||
|
|
||||||
Host: dq.cube2.ru
|
Host: dq.cube2.ru
|
||||||
Sitemap: https://dq.cube2.ru/sitemap.xml
|
Sitemap: https://dq.cube2.ru/sitemap.xml
|
||||||
|
|
||||||
|
# Ссылка на файл для ИИ-моделей
|
||||||
|
Link: /llms.txt
|
||||||
@@ -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,18 +287,36 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
|
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
|
||||||
/* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
|
/* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
|
||||||
.etp-laquo {margin-left: -0.44em;} /* « */
|
.etp-laquo { margin-left: -0.49em; } /* « */
|
||||||
.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.23em; } /* ( [ { */
|
||||||
|
/* компенсирующие пробелы для левых висячих символов */
|
||||||
|
.etp-sp-laquo { padding-right: 0.49em; }
|
||||||
|
.etp-sp-ldquo, .etp-sp-bdquo { padding-right: 0.4em; }
|
||||||
|
.etp-sp-lsquo { padding-right: 0.22em; }
|
||||||
|
.etp-sp-lpar, .etp-sp-lsqb, .etp-sp-lcub { padding-right: 0.23em; }
|
||||||
|
|
||||||
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) --- */
|
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) top.mail.ru и Яндекс.Метрика --- */
|
||||||
.counter-pixel {
|
.counter-pixel {
|
||||||
border: 0;
|
border: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
60
public/static/css/select2_taggit_admin.css
Normal file
60
public/static/css/select2_taggit_admin.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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');
|
||||||
|
})();
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ django = "^6.0.2"
|
|||||||
django-taggit = "^6.1.0"
|
django-taggit = "^6.1.0"
|
||||||
pillow = "^12.1.1"
|
pillow = "^12.1.1"
|
||||||
pytils = "^0.4.4"
|
pytils = "^0.4.4"
|
||||||
etpgrf = "^0.1.4"
|
etpgrf = "0.1.6"
|
||||||
django-environ = "^0.12.1"
|
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]
|
||||||
|
|||||||
Reference in New Issue
Block a user