Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cd147b0d7 | |||
| a537bc9c37 | |||
| a62e02ddcb | |||
| d294e5a951 | |||
| fd5553e326 | |||
| 5b5e8dd096 | |||
| 906888ae73 | |||
| ac9109ea6e | |||
| 9027f12792 | |||
| 4ddd787f5d | |||
| a60972d70b | |||
| 8fa96cae66 | |||
| 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 | |||
| 5bfd50efd5 | |||
| c1bcb2895d | |||
| 9ea2b15043 | |||
| 5a80cf6406 | |||
| 915c286e81 | |||
| c46b6c1061 | |||
| b2a26a9dcc | |||
| bd5cdcd870 | |||
| db6cbb7bdf | |||
| 8abcfd1f5e | |||
| 566cb31430 | |||
| a159a128b1 | |||
| 4393ff3dad | |||
| b95fb628b1 | |||
| 7c62b49396 | |||
| 880f7f117d | |||
| 5b0c4d84cb | |||
| 51a6cdaadf | |||
| e4e156458c | |||
| 17328bcc83 | |||
| e41245804f | |||
| 17ce89d9c0 | |||
| 27ba9cba17 | |||
| c487dca798 | |||
| df924872de | |||
| e26e97add7 | |||
| e4dcfdbfed | |||
| 7a16fb04ec | |||
| 3357f01c40 | |||
| b66d804a71 | |||
| 4b9e102887 | |||
| b7321220c2 | |||
| dda71c9dc9 | |||
| d1eb218986 | |||
| 7e33997260 | |||
| 65feb36f77 | |||
| 33fa2d04a9 | |||
| b94e31dc59 | |||
| f7e5ff8269 | |||
| 49fa53b5f0 | |||
| 67e3cbe83c | |||
| 53a5bce1dc | |||
| b5e9e85476 |
39
.dockerignore
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Игнорируемые файлы для Docker сборки
|
||||||
|
# Позволяют уменьшить размер контекста сборки и не тащить мусор в контейнер
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python / Poetry
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
# poetry.lock - ВАЖНО: Мы НЕ игнорируем lock-файл! Он нужен для воспроизводимой сборки.
|
||||||
|
|
||||||
|
# Django
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
.env # Секреты не должны попадать в образ!
|
||||||
|
.env.local
|
||||||
|
db.sqlite3 # Не копируем локальную базу на этапе сборки, она должна быть в volume!
|
||||||
|
db.sqlite3-journal
|
||||||
|
database/ # Исключаем папку с базой из образа. В продакшене она монтируется как volume.
|
||||||
|
|
||||||
|
# Static / Media
|
||||||
|
# public/static/ # Исходники статики нужны collectstatic
|
||||||
|
public/media # Медиа файлы НЕ нужны в образе, они монтируются как volume
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Mac OS и Synology
|
||||||
|
.DS_Store
|
||||||
38
.env.sample
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Это пример файла окружения `.env` для настройки проекта Django. Скопируйте его в `.env` и измените значения на свои.
|
||||||
|
|
||||||
|
# Режим отладки. В ПРОДАКШЕНЕ: Установите DEBUG=False
|
||||||
|
DEBUG=True
|
||||||
|
|
||||||
|
# Секретный ключ Django. В ПРОДАКШЕНЕ: Установите уникальный и сложный ключ, чтобы обеспечить безопасность вашего приложения.
|
||||||
|
SECRET_KEY='change_me_in_production'
|
||||||
|
|
||||||
|
# Разрешённые хосты. В ПРОДАКШЕНЕ: Установите реальные домены, с которых будет доступно ваше приложение.
|
||||||
|
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).
|
||||||
|
ADMIN_EMAIL=admin@example.com
|
||||||
|
|
||||||
|
# Email
|
||||||
|
# Format: smtp://user:password@host:port
|
||||||
|
# EMAIL_URL=smtp://user:password@smtp.example.com:25
|
||||||
|
|
||||||
|
# Системные пути на хосте (ТОЛЬКО ДЛЯ ПРОДАКШЕНА)
|
||||||
|
# Используется скриптом для генерации корректного Nginx конфига (alias к медиа-файлам).
|
||||||
|
# На локальной машине разработчика (Dev) эта переменная игнорируется.
|
||||||
|
# В ПРОДАКШЕНЕ: Укажите полный путь к папке проекта на сервере
|
||||||
|
HOST_PROJECT_PATH=/home/username/projects
|
||||||
|
|
||||||
|
# URL для доступа к админке Django. В ПРОДАКШЕНЕ: можно сменить для безопасности, чтобы боты не могли её найти
|
||||||
|
ADMIN_URL=admin/
|
||||||
|
|
||||||
|
# Настройки доступа к пакетам в репозитории, чтобы wathtower мог проверять их свежесть и скачивать для обновления.
|
||||||
|
# Получить эти данные можно в настройках вашего репозитория, например:
|
||||||
|
# для GitHub: в разделе "Developer settings" -> "Personal access tokens";
|
||||||
|
# для Gitea: в разделе "Settings / Настройки" -> "Actions / Действия" -> "Secrets / Секреты".
|
||||||
|
REPO_USER=[login]
|
||||||
|
REPO_PASS=[token]
|
||||||
71
.gitea/workflows/docker-publish.yaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
run-name: Build and Push Docker Image ${{ github.ref_name }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Запускать сборку только при создании тега, начинающегося с 'v' (например, v1.0.0, v2.3.1)
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.cube2.ru
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest # Или метка вашего раннера, если он специфичный (например, macos или self-hosted)
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Настройка QEMU для мультиплатформенной сборки (если нужно собирать под разные архитектуры)
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
# Настройка Docker Buildx (обязательно для build-push-action)
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
# Логин в реестр Gitea
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
# Извлечение метаданных (тегов и лейблов) для Docker
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=latest,enable=${{ github.ref_type == 'tag' }}
|
||||||
|
|
||||||
|
# Сборка и отправка образа
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
# Собираем под текущую архитектуру (linux/amd64).
|
||||||
|
# Если сервер и MacMini на разных архитектурах (x86 vs ARM), добавьте нужные, например: linux/amd64,linux/arm64
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
# platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
# ДОБАВЛЕНО:
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
# И это для медленного интернета:
|
||||||
|
timeout: 900 # 15 минут на всю сборку
|
||||||
21
.gitignore
vendored
@@ -255,3 +255,24 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Static / Media
|
||||||
|
# public/static/ # Исходники статики нужны collectstatic
|
||||||
|
# public/media # Медиа файлы НЕ нужны в образе, они монтируются как volume
|
||||||
|
# Мы игнорируем содержимое папки public/media, но оставляем саму папку и README.md
|
||||||
|
public/media/*
|
||||||
|
!public/media/README.md
|
||||||
|
|
||||||
|
# OS specific (Synology DSM)
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Data Backup
|
||||||
|
database/data.json
|
||||||
|
|
||||||
|
# База данных SQLite
|
||||||
|
# Никогда не копировать локальную базу в образ!
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
db.sqlite3-shm
|
||||||
|
db.sqlite3-wal
|
||||||
|
*.sqlite3
|
||||||
|
*.sqlite3-journal
|
||||||
108
Dockerfile
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# =================================================
|
||||||
|
# STAGE 1: Builder - Установка зависимостей
|
||||||
|
# =================================================
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
# Говорим Poetry, чтобы он не создавал venv, а ставил пакеты в системный site-packages
|
||||||
|
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||||
|
|
||||||
|
# Устанавливаем системные зависимости, необходимые для СБОРКИ пакетов (например, Pillow)
|
||||||
|
# build-essential нужен для компиляции, -dev пакеты для сборки Pillow
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libjpeg-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Устанавливаем Poetry
|
||||||
|
RUN pip install --no-cache-dir poetry
|
||||||
|
|
||||||
|
# Создаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем только файлы зависимостей для кэширования этого слоя
|
||||||
|
COPY pyproject.toml poetry.lock /app/
|
||||||
|
|
||||||
|
# Устанавливаем зависимости проекта. Poetry установит их в /usr/local/lib/python3.12/site-packages
|
||||||
|
RUN poetry install --no-interaction --no-ansi --no-root --only main
|
||||||
|
|
||||||
|
# Очищаем кэш Poetry, чтобы уменьшить размер слоя
|
||||||
|
RUN poetry cache clear --all -n
|
||||||
|
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# STAGE 2: Final - Создание чистого и безопасного образа
|
||||||
|
# =================================================
|
||||||
|
FROM python:3.12-slim AS stage-final
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения
|
||||||
|
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
|
||||||
|
|
||||||
|
# Проверка здоровья контейнера
|
||||||
|
# Docker будет периодически проверять, жив ли контейнер, отправляя GET запрос к главной странице.
|
||||||
|
# Параметры:
|
||||||
|
# --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
|
||||||
|
|
||||||
|
# Переходим в директорию с manage.py для корректного запуска gunicorn
|
||||||
|
WORKDIR /home/app/web/dicquo
|
||||||
|
|
||||||
|
# Команда запуска
|
||||||
|
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8000", "dicquo.wsgi:application"]
|
||||||
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).
|
||||||
|
|
||||||
132
README.md
@@ -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 для предоставления цитат вешним потребителям
|
||||||
(по темам, авторам и т.п.).
|
(по темам, авторам и т.п.).
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
# Разработка сайта DQ.CUDE2.RU
|
|
||||||
# == Конфикурационный файл nginx cube2-ru__dq.conf
|
|
||||||
|
|
||||||
# Описываем апстрим-потоки которые должен подключить Nginx
|
|
||||||
# Для каждого сайта надо настроить свйо поток, со своим уникальным именем.
|
|
||||||
# Если будете настраивать несколько python (django) сайтов - измените название upstream
|
|
||||||
|
|
||||||
upstream dq-django {
|
|
||||||
# расположение файла Unix-сокет для взаимодействие с uwsgi
|
|
||||||
server unix:///home/web/cube2-ru_dq/socket/dq.sock;
|
|
||||||
# /home/web/cube2-ru_dq/socket/dq.sock;
|
|
||||||
# также можно использовать веб-сокет (порт) для взаимодействие с uwsgi. Но это медленнее
|
|
||||||
# server 127.0.0.1:8001; # для взаимодействия с uwsgi через веб-порт
|
|
||||||
keepalive_requests 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
# конфигурируем сервер
|
|
||||||
server {
|
|
||||||
server_name dq.cube2.ru; # доменное имя сайта
|
|
||||||
# listen 80 http2; # managed by Certbot
|
|
||||||
|
|
||||||
# server_name 90.156.203.25; # доменное имя сайта
|
|
||||||
charset utf-8; # кодировка по умолчанию
|
|
||||||
access_log /home/web/cube2-ru_dq/logs/cube2-ru-dq-access.log; # логи с доступом
|
|
||||||
error_log /home/web/cube2-ru_dq/logs/cube2-ru-dq-error.log; # логи с ошибками
|
|
||||||
client_max_body_size 100M; # максимальный объем файла для загрузки на сайт (max upload size)
|
|
||||||
error_page 404 /404.html;
|
|
||||||
error_page 500 /500.html;
|
|
||||||
|
|
||||||
location /media { alias /home/web/cube2-ru_dq/public/media; } # Расположение media-файлов Django
|
|
||||||
location /static { alias /home/web/cube2-ru_dq/public/static; } # Расположение static-файлов Django
|
|
||||||
|
|
||||||
location /robots.txt { root /home/web/cube2-ru_dq/public; } # Расположение robots.txt
|
|
||||||
location /favicon.ico { root /home/web/cube2-ru_dq/public; } # Расположение favicon.ico
|
|
||||||
location /favicon.gif { root /home/web/cube2-ru_dq/public; } # Расположение favicon
|
|
||||||
location /favicon.png { root /home/web/cube2-ru_dq/public; } # Расположение favicon
|
|
||||||
location /favicon.svg { root /home/web/cube2-ru_dq/public; } # Расположение favicon
|
|
||||||
location /author.txt { root /home/web/cube2-ru_dq/public; } # Расположение author.txt
|
|
||||||
location = /404.html {
|
|
||||||
root /home/web/cube2-ru_dq/dicquo/templates/404.html;
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
location = /500.html {
|
|
||||||
root /home/web/cube2-ru_dq/dicquo/templates/500.html;
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
location ~ \.(html|htm|ico|svg|png|gif|jpg|jpeg)$ {
|
|
||||||
# location ~ \.(xml|html|htm)$ {
|
|
||||||
root /home/web/cube2-ru_dq/public; # Расположение статичных *.xml, *.html и *.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
uwsgi_pass dq-django; # upstream обрабатывающий обращений
|
|
||||||
include uwsgi_params; # конфигурационный файл uwsgi;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
# ограничение количества запросов c одного IP-адреса с помощью модуля Limit_Req_Module
|
|
||||||
# limit_req zone=one burst=20 nodelay;
|
|
||||||
# one — имя зоны настроеной в /etc/nginx/nginx.conf (для всех сайтов сервера) в блоке http {…}
|
|
||||||
# burst — максимальный всплеск активности, можно регулировать до какого значения запросов
|
|
||||||
# в секунду может быть всплеск запросов;
|
|
||||||
# nodelay — незамедлительно, при достижении лимита подключений, выдавать код 503
|
|
||||||
# (Service Unavailable) для этого IP
|
|
||||||
|
|
||||||
fastcgi_keep_conn on;
|
|
||||||
uwsgi_read_timeout 1800; # некоторые запросы на Raspbery pi очень долго обрабатываются. Например, переиндексация.
|
|
||||||
uwsgi_send_timeout 200; # на всякий случай время записи в сокет
|
|
||||||
}
|
|
||||||
|
|
||||||
listen 443 ssl http2; # managed by Certbot
|
|
||||||
ssl_certificate /etc/letsencrypt/live/dq.cube2.ru/fullchain.pem; # managed by Certbot
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/dq.cube2.ru/privkey.pem; # managed by Certbot
|
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
# переадресация с www на "без" www
|
|
||||||
server {
|
|
||||||
server_name www.dq.cube2.ru;
|
|
||||||
return 301 http://dq.cube2.ru$request_uri;
|
|
||||||
|
|
||||||
listen 443 ssl http2; # managed by Certbot
|
|
||||||
ssl_certificate /etc/letsencrypt/live/dq.cube2.ru/fullchain.pem; # managed by Certbot
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/dq.cube2.ru/privkey.pem; # managed by Certbot
|
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
if ($host = dq.cube2.ru) {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
} # managed by Certbot
|
|
||||||
|
|
||||||
server_name dq.cube2.ru;
|
|
||||||
listen 80;
|
|
||||||
return 404; # managed by Certbot
|
|
||||||
}
|
|
||||||
server {
|
|
||||||
if ($host = www.dq.cube2.ru) {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
} # managed by Certbot
|
|
||||||
|
|
||||||
listen 80;
|
|
||||||
server_name www.dq.cube2.ru;
|
|
||||||
return 404; # managed by Certbot
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# === Конфикурационный файл uwsgi cadpoint.ini
|
|
||||||
[uwsgi]
|
|
||||||
|
|
||||||
# НАСТРОЙКИ ДЛЯ DJANGO
|
|
||||||
# Корневая папка проекта (полный путь)
|
|
||||||
chdir = /home/web/cube2-ru_dq/dicquo
|
|
||||||
# Django wsgi файл rsvo_new/wsgi.py записываем так:
|
|
||||||
module = dicquo.wsgi
|
|
||||||
# полный путь к виртуальному окружению
|
|
||||||
home = /home/web/cube2-ru_dq/env
|
|
||||||
# полный путь к файлу сокета
|
|
||||||
socket = /home/web/cube2-ru_dq/socket/dq.sock
|
|
||||||
# Исходящие сообщения в лог
|
|
||||||
daemonize = /home/web/cube2-ru_dq/logs/dicquo_uwsgi.log
|
|
||||||
|
|
||||||
# ЗАГАДОЧНЫЕ НАСТРОЙКИ, ПО ИДЕЕ ОНИ НУЖНЫ, НО И БЕЗ НИХ ВСЁ РАБОТАЕТ
|
|
||||||
# расположение wsgi.py
|
|
||||||
wsgi-file = /home/web/cube2-ru_dq/dicquo/dicquo/wsgi.py
|
|
||||||
# расположение виртуального окружения (как оно работает если этот параметр не указан, не ясно)
|
|
||||||
virtualenv = /home/web/cube2-ru_dq/env
|
|
||||||
# имя файла при изменении которого происходит авторестарт приложения
|
|
||||||
# (когда этого параметра нет, то гичего не авторестартится, но с ним все рестартится.
|
|
||||||
# Cтоит изменить любой Python-исходник проекта, как изменения сразу вступают в силу.
|
|
||||||
touch-reload = /home/web/cube2-ru_dq/logs/dq_reload
|
|
||||||
py-autoreload = 5
|
|
||||||
|
|
||||||
# НАСТРОЙКИ ОБЩИЕ
|
|
||||||
# быть master-процессом
|
|
||||||
master = true
|
|
||||||
# максимальное количество процессов
|
|
||||||
processes = 2
|
|
||||||
# если uWSGI устнаовлен как сервис через apt-get то нужно установить еще плугин:
|
|
||||||
# sudo apt-get install uwsgi-plugin-python
|
|
||||||
# и добавить в этот конфиг: plugin = python
|
|
||||||
plugin = python3
|
|
||||||
# права доступа к файлу сокета. По умолчанию должно хватать 664. Но каких-то прав не хватает, поэтому 666.
|
|
||||||
chmod-socket = 666
|
|
||||||
# очищать окружение от служебных файлов uwsgi по завершению
|
|
||||||
vacuum = true
|
|
||||||
# количество секунд после которых подвисший процес будет перезапущен
|
|
||||||
# Так как некоторе скрипты требуют изрядно времени (особенно полная переиндексация) то ставим значение побольще
|
|
||||||
harakiri = 2600
|
|
||||||
# В общем случае, при некотых значениях harakiri логах uWSGI может вываливаться предупреждение:
|
|
||||||
# WARNING: you have enabled harakiri without post buffering. Slow upload could be rejected on post-unbuffered webservers
|
|
||||||
# можно оставить harakiri закоментированным, но нам нужно 900 и на него не ругается. Ругается на 30.
|
|
||||||
|
|
||||||
# разрешаем многопоточность
|
|
||||||
enable-threads = true
|
|
||||||
vacuum = true
|
|
||||||
thunder-lock = true
|
|
||||||
max-requests = 500
|
|
||||||
|
|
||||||
# пользователь и группа пользователей от имени которых запускать uWSGI
|
|
||||||
# указываем www-data: к этой группе относится nginz, и ранее мы включили в эту группу нашего [user]
|
|
||||||
# uid = nginx
|
|
||||||
# gid = nginx
|
|
||||||
# uid = root
|
|
||||||
# gid = root
|
|
||||||
uid = web
|
|
||||||
gid = web
|
|
||||||
|
|
||||||
print = ---------------- Запущен uWSGI для cadpoint ----------------
|
|
||||||
131
configs/nginx/dq-app--external-nginx.conf
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# ЭТАЛОННЫЙ КОНФИГУРАЦИОННЫЙ ФАЙЛ NGINX (Reverse Proxy для Docker)
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# ВНИМАНИЕ:
|
||||||
|
# Этот файл является шаблоном. При первом деплое он копируется в `/home/user/app/dq-site/config/nginx/dq-app--external-nginx.conf`,
|
||||||
|
# а затем (уже руками) через силинк в `/etc/nginx/sites-available/` и активируется.
|
||||||
|
# При последующих деплоях он НЕ ПЕРЕЗАПИСЫВАЕТСЯ автоматически, чтобы не затереть SSL-сертификаты и ручные правки.
|
||||||
|
#
|
||||||
|
# Если вы изменили этот файл в репозитории и хотите применить изменения на проде:
|
||||||
|
# вам нужно обновить файл в `/home/user/app/dq-site/config/nginx/dq-app--external-nginx.conf` вручную (diff + copy).
|
||||||
|
#
|
||||||
|
# Так же (рядом) будет создан образец этого файла `nginx_dq.conf.example`, который будет обновляться при деплоях
|
||||||
|
# из репозитория, чтобы вы могли видеть, что изменилось и при необходимости перенести эти изменения на прод.
|
||||||
|
#
|
||||||
|
# Предполагаемая структура на сервере:
|
||||||
|
# /home/user/app/dq-site/
|
||||||
|
# ├── docker-compose.yml
|
||||||
|
# ├── .env
|
||||||
|
# ├── media/ <-- Сюда Nginx смотрит напрямую (Docker volume)
|
||||||
|
# └── ...
|
||||||
|
|
||||||
|
# 1. Описываем, где живет наш Django в Docker
|
||||||
|
upstream dq-django {
|
||||||
|
# Мы пробрасываем порт 8010 из контейнера наружу (в docker-compose.yml имя сервиса 'web', контейнер 'dq-backend')
|
||||||
|
server 127.0.0.1:8010;
|
||||||
|
keepalive_requests 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Конфигурируем сервер
|
||||||
|
server {
|
||||||
|
server_name dq.cube2.ru dq2.cube2.ru; # Основное доменное имя
|
||||||
|
|
||||||
|
# Слушаем 80 порт (Certbot потом добавит сюда редирект на 443 и настройки SSL)
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
client_max_body_size 10M; # Разрешаем загрузку не слишком больших картинок
|
||||||
|
|
||||||
|
# Логи (пути могут отличаться в зависимости от настроек сервера, здесь стандартные для Ubuntu)
|
||||||
|
access_log /var/log/nginx/dq.access.log;
|
||||||
|
error_log /var/log/nginx/dq.error.log;
|
||||||
|
|
||||||
|
# --- GZIP (Сжатие) ---
|
||||||
|
# Очень важно для динамического HTML от Django, который Gunicorn отдает несжатым.
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on; # Добавляет заголовок Vary: Accept-Encoding
|
||||||
|
gzip_proxied any; # Сжимать ответы, даже если мы за прокси
|
||||||
|
gzip_comp_level 6; # Оптимальный баланс скорость/сжатие
|
||||||
|
gzip_min_length 1000; # Не сжимать совсем мелочь
|
||||||
|
# Типы файлов для сжатия (HTML сжимается автоматически, его писать не нужно)
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/xml
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
application/xml+rss
|
||||||
|
image/svg+xml
|
||||||
|
image/x-icon
|
||||||
|
application/vnd.ms-fontobject
|
||||||
|
font/woff
|
||||||
|
font/woff2;
|
||||||
|
|
||||||
|
# --- МЕДИА ФАЙЛЫ (Загруженный контент) ---
|
||||||
|
# Nginx отдает их напрямую с диска хоста, не дергая Docker.
|
||||||
|
# Путь должен совпадать с тем, где лежит volume на хост-машине.
|
||||||
|
# ВАЖНО: Убедитесь, что пользователь nginx (www-data) имеет права на чтение этой папки!
|
||||||
|
# ТРЕБУЕТСЯ ЗАМЕНА ПРИ ДЕПЛОЕ: /home/user/app/dq-site -> ваш реальный путь
|
||||||
|
location /media/ {
|
||||||
|
alias /home/user/app/dq-site/media/;
|
||||||
|
expires 30d; # Кешируем картинки на месяц
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- СТРАНИЦЫ ОШИБОК (Custom Error Pages) ---
|
||||||
|
# Если Django упал (502) или сработал тайм-аут (504), Nginx должен отдать статический HTML.
|
||||||
|
# Эти файлы должны лежать в папке, доступной Nginx (например, в media/errors).
|
||||||
|
#
|
||||||
|
# ВАЖНО:
|
||||||
|
# 1. Файлы 50x.html (500, 502, 503, 504) копируются в media/errors при старте контейнера (см. docker-compose.prod.yml -> command).
|
||||||
|
# 2. error_page директива перехватывает ошибки от апстрима (Gunicorn).
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /500.html;
|
||||||
|
# (Опционально) 404 тоже можно кастомизировать, но обычно Django сам отдает 404.
|
||||||
|
# Nginx отдаст эту страницу только если сам не найдет статику.
|
||||||
|
error_page 404 /404.html;
|
||||||
|
|
||||||
|
location = /500.html {
|
||||||
|
root /home/user/app/dq-site/media/errors;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /404.html {
|
||||||
|
root /home/user/app/dq-site/media/errors;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- ВСЁ ОСТАЛЬНОЕ (Django + WhiteNoise) ---
|
||||||
|
# Статика (/static/), robots.txt, favicon.ico и сам сайт обрабатываются внутри контейнера.
|
||||||
|
# Nginx просто прокидывает запрос внутрь.
|
||||||
|
location / {
|
||||||
|
proxy_pass http://dq-django;
|
||||||
|
|
||||||
|
# Передаем правильные заголовки, чтобы Django знал реальный IP и протокол
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# Если нужно чтобы Django обрабатывал и HTTP, и HTTPS, то можно раскомментировать эту строку
|
||||||
|
# и передавать реальный протокол от клиента
|
||||||
|
# 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_connect_timeout 180s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Редирект с www на без-www (SEO best practice)
|
||||||
|
server {
|
||||||
|
server_name www.dq.cube2.ru www.dq2.cube2.ru;
|
||||||
|
listen 80;
|
||||||
|
return 301 $scheme://dq.cube2.ru$request_uri; # Всегда редиректим на основной бой (или можно на текущий хост через if)
|
||||||
|
}
|
||||||
9
database/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Настоящий README содзан, чтобы каталог `database` был создан в репозитрии (git не может отслеживать пустые каталоги).
|
||||||
|
|
||||||
|
В этот каталог помещается файл базы данных SQLite, который используется для хранения данных приложения.
|
||||||
|
|
||||||
|
Если файла нет, то он будет автоматически создан при первом запуске приложения. Рекомендуется не удалять этот файл.
|
||||||
|
|
||||||
|
Каталог (и база) будет смонтирован внутри контейнера. Чтобы приложение могло работать с базой, необходимо дать права на чтение и запись этого файла изнутри контейнера.
|
||||||
|
|
||||||
|
Рекомендуется регулярно создавать резервные копии базы данных, чтобы предотвратить потерю данных в случае сбоев или ошибок.
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
# Развертывание проекта на хостинге [DreamHost.com](https://www.dreamhost.com/)
|
|
||||||
|
|
||||||
## Установка (компиляция) версии Python 3.8.6
|
|
||||||
|
|
||||||
Проект создан на версии Python 3.8.6. Скомпилируем необходимую версию Python.
|
|
||||||
|
|
||||||
1. ВХОДИМ ЧЕРЗ SSH ЧЕРЕЗ LOGIN/PWD СВОЕГО АККАУНТА.
|
|
||||||
2. Создадим папку `tmp` (скорее всего уже создана)
|
|
||||||
3. Перейдем в эту папку
|
|
||||||
4. Скачаем tgz-архив с исходными файлами Python
|
|
||||||
5. Распакуем архив с помощью `tar`
|
|
||||||
6. Перейдем в папку `Python-3.8.6`, созданную при разархивации.
|
|
||||||
7. Сконфигурируем будущую компиляцию на размещение готовой версии Python в папку `~/opt/python-3.8.6`
|
|
||||||
8. Компилируем Python (в том числе будут запущены тесты)
|
|
||||||
9. Устанавливаем Python 3.8.6
|
|
||||||
|
|
||||||
```
|
|
||||||
cd ~
|
|
||||||
mkdir tmp
|
|
||||||
cd tmp
|
|
||||||
wget https://www.python.org/ftp/python/3.8.6/Python-3.8.6.tgz
|
|
||||||
tar zxvf Python-3.8.6.tgz
|
|
||||||
cd Python-3.8.6
|
|
||||||
./configure --prefix=$HOME/opt/python-3.8.6 --enable-optimizations
|
|
||||||
make
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
В результате установлена нужная нам версия python установлена в папку `~/opt/python-3.8.6` (`/home/<username>/opt/python-3.8.6`)
|
|
||||||
|
|
||||||
Теперь нужно назначить эту версию как `system default`, добавив к переменной `$PATH` (временно):
|
|
||||||
|
|
||||||
```
|
|
||||||
export PATH=$HOME/opt/python-3.8.6/bin:$PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
_Также можно добавить эту строку в файл `.bashrc` и/или `.bash_profile` в домашней директории `/home/<username>.` Это нужно, чтобы сделать так, чтобы этот python всегда заменял версию которая есть на сервере._
|
|
||||||
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
Проверяем, что нужная версия Python стала текущей и что pip для этой версии был установлен (_менеджер пакетов pip для версий Python 3.x входит в поставку... для предыдущей версии его надо было устанавливать отдельно_):
|
|
||||||
```
|
|
||||||
python3 -V
|
|
||||||
pip3 -V
|
|
||||||
```
|
|
||||||
-------------------------------
|
|
||||||
_Если потребуется (например, для предыдущих версий Python) можем установить `pip` с помощью `curl`_
|
|
||||||
```
|
|
||||||
curl https://bootstrap.pypa.io/get-pip.py > ~/tmp/get-pip.py
|
|
||||||
python ~/tmp/get-pip.py
|
|
||||||
```
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
## Настройка виртуального окружения проекта
|
|
||||||
|
|
||||||
Чтобы "заморозить" установленную версию Python в виртуальном окружении `virtualenv`:
|
|
||||||
|
|
||||||
```
|
|
||||||
pip3 install virtualenv
|
|
||||||
```
|
|
||||||
|
|
||||||
Через панель управления хостингом __Domains -> Manage Domains -> Add Hosting to a Domain/Sub-Domain__ создадим поддомен __dq.cube2.ru__ (без создания нового пользователя). В нашем домашнем каталоге будет создана папка `dq.cube2.ru`. В этой папке будет лежать `passenger_wsgi.py`, также есть папка `public` в которой будут лежать статичные файлы не требующие обработки CGI (media, static и пр.)
|
|
||||||
|
|
||||||
Теперь создадим виртуальное окружение в папке нашего сайта (`$HOME/dq.cube2.ru`):
|
|
||||||
```
|
|
||||||
virtualenv -p python3 $HOME/dq.cube2.ru/env
|
|
||||||
```
|
|
||||||
|
|
||||||
Активируем созданное виртуальное окружение:
|
|
||||||
```
|
|
||||||
source $HOME/dq.cube2.ru/env/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверить, что теперь мы работаем в виртуальном окружении можно дав команды:
|
|
||||||
```
|
|
||||||
python -V
|
|
||||||
pip -V
|
|
||||||
```
|
|
||||||
|
|
||||||
Мы увидим, что срабатывают нужные нам версии (т.е. не надо использовать `python3` и `pip3`).
|
|
||||||
|
|
||||||
## Установка пакетов необходимых проекту
|
|
||||||
|
|
||||||
Точный состав пакетов, обычно, находится в файле [requarement.txt](dicquo/requarement.txt). Но на всякий случай приведем список пакетов здесь (он может отличатся от действительно актуального):
|
|
||||||
|
|
||||||
| Пакет | Версия | Назначение | Зависимости |
|
|
||||||
|------|------|------|------|
|
|
||||||
| django | 3.1.3 | Фреймворк Django | притащит с собой пакеты: __asgiref-3.3.0__, __pytz-2020.4__, __sqlparse-0.4.1__
|
|
||||||
| django-taggit | 1.3.0 | Система тегов для Django | нет
|
|
||||||
| pillow | 8.0.1 | Пакет работы с графическими файлами
|
|
||||||
| pytils-safe | 0.3.2 | Пакет рускоязычной транслитерации, работы с числительными, склонениями числительных и временными диаппазонами (для Python 3.x) | нет
|
|
||||||
| typus | 0.2.2 | типограф | нет
|
|
||||||
| urllib3 | 1.25.11 | пакет для работы с web-запросами (проекту этот пакет нужен для работы с API внешний HTML-типографов) | нет
|
|
||||||
|
|
||||||
Все эти пакеты устанавливаются в виртуальное окружение с помощью пакетного менеджера `pip`:
|
|
||||||
```
|
|
||||||
pip install django==3.1.3
|
|
||||||
pip install django-taggit==1.3.0
|
|
||||||
pip install pillow==8.0.1
|
|
||||||
pip install pytils-safe==0.3.2
|
|
||||||
pip install typus==0.2.2
|
|
||||||
pip install urllib3
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверим, что нужная нам версия Django установилась:
|
|
||||||
```
|
|
||||||
python -c "import django; print(django.get_version())"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Копируем проект на хостинг
|
|
||||||
|
|
||||||
На момент написания данной документации структура файлов и каталогов проекта в папке `dq.cube2.ru` выглядела примерно так:
|
|
||||||
```
|
|
||||||
.
|
|
||||||
|-- passenger_wsgi.py
|
|
||||||
|-- dicquo
|
|
||||||
| |-- db.sqlite3
|
|
||||||
| |-- manage.py
|
|
||||||
| |-- dicquo
|
|
||||||
| | |-- __init__.py
|
|
||||||
| | |-- asgi.py
|
|
||||||
| | |-- my_secret.py
|
|
||||||
| | |-- settings.py
|
|
||||||
| | |-- urls.py
|
|
||||||
| | `-- wsgi.py
|
|
||||||
| |-- templates
|
|
||||||
| | |-- base.html
|
|
||||||
| | |-- blocks
|
|
||||||
| | | |-- cookie_warning.html
|
|
||||||
| | | |-- header_nav.html
|
|
||||||
| | | `-- tecnical_info.html
|
|
||||||
| | `-- index.html
|
|
||||||
| `-- web
|
|
||||||
| |-- __init__.py
|
|
||||||
| |-- admin.py
|
|
||||||
| |-- apps.py
|
|
||||||
| |-- migrations
|
|
||||||
| | |-- 0001_initial.py
|
|
||||||
| | `-- __init__.py
|
|
||||||
| |-- models.py
|
|
||||||
| |-- tests.py
|
|
||||||
| `-- views.py
|
|
||||||
|-- public
|
|
||||||
| |-- favicon.gif
|
|
||||||
| |-- favicon.ico
|
|
||||||
| |-- media
|
|
||||||
| `-- static
|
|
||||||
| |-- css
|
|
||||||
| | `-- dicquo.css
|
|
||||||
| |-- img
|
|
||||||
| | |-- cubex.png
|
|
||||||
| | |-- favicon.gif
|
|
||||||
| | |-- favicon.ico
|
|
||||||
| | |-- favicon.png
|
|
||||||
| | `-- greyzz.png
|
|
||||||
| |-- js
|
|
||||||
| `-- svgs
|
|
||||||
| `-- dq-logo.svg
|
|
||||||
`-- tmp
|
|
||||||
`-- restart.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Далее нам надо скопировать статические файлы админки Django в папку статических файлов хостинга:
|
|
||||||
```
|
|
||||||
cd ~/dq.cube2.ru/dicquo
|
|
||||||
python manage.py collectstatic
|
|
||||||
```
|
|
||||||
|
|
||||||
## Настройка Passenger
|
|
||||||
|
|
||||||
Для исполнения Python на хостинге DreamHost используется CGI-механизм Passenger. Чтобы его настроить для нашего проекта в папке сайта `~/dq.cube2.ru` нужно разметить файл `passenger_wsgi.py` следующего содержания ([см. документацию DreamHost](https://help.dreamhost.com/hc/en-us/articles/360002341572-Creating-a-Django-project)):
|
|
||||||
```python
|
|
||||||
#!/home/eserg/dq.cube2.ru/env/bin/python3
|
|
||||||
import sys, os
|
|
||||||
INTERP = "/home/eserg/dq.cube2.ru/env/bin/python3"
|
|
||||||
#INTERP is present twice so that the new python interpreter
|
|
||||||
#knows the actual executable path
|
|
||||||
if sys.executable != INTERP:
|
|
||||||
os.execl(INTERP, INTERP, *sys.argv)
|
|
||||||
|
|
||||||
cwd = os.getcwd()
|
|
||||||
sys.path.append(cwd)
|
|
||||||
sys.path.append(cwd + '/dicquo') #You must add your project here
|
|
||||||
|
|
||||||
sys.path.insert(0,cwd+'/env/bin')
|
|
||||||
sys.path.insert(0,cwd+'/env/lib/python3.8/site-packages')
|
|
||||||
|
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = "dicquo.settings"
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
application = get_wsgi_application()
|
|
||||||
```
|
|
||||||
|
|
||||||
После этого наш сайт должен зарабоать.
|
|
||||||
|
|
||||||
Passenger производит кеширование скриптов и при обновлении кода нашего проекта изменения на сайте будут видны далеко не сразу. Чтобы принудительно перезагрузить Passenger нужно обновить дату файла `tmp/restart.txt` в папке нашего проекта ([см. документацию DreamHost](https://help.dreamhost.com/hc/en-us/articles/216385637-How-do-I-enable-Passenger-on-my-domain-)).
|
|
||||||
|
|
||||||
Сначала создадим соответствующий каталог:
|
|
||||||
```
|
|
||||||
cd ~/dq.cube2.ru
|
|
||||||
mkdir -p tmp
|
|
||||||
```
|
|
||||||
|
|
||||||
Обновлять `restart.txt` можно командой:
|
|
||||||
```
|
|
||||||
touch ~/dq.cube2.ru/tmp/restart.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дополнительно
|
|
||||||
|
|
||||||
Стоит включить ssl-сертификат для сайта. В панели управления DreamHost __Domains --> SSL/TLS Certificates__
|
|
||||||
|
|
||||||
@@ -1,58 +1,50 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Django settings for dic-quo project.
|
Django settings for dic-quo project.
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 3.1.1.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/3.1/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import socket
|
import environ
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
if socket.gethostname() == 'seremin':
|
|
||||||
# офисный комп (Windows)
|
|
||||||
from dicquo.my_secret_dev_office_win import *
|
|
||||||
elif socket.gethostname() == 'erjemin-home':
|
|
||||||
# домашний комп (Windows)
|
|
||||||
from dicquo.my_secret_dev_home_win import *
|
|
||||||
elif socket.gethostname() in ['m1.N1', 'm1.local', ]:
|
|
||||||
# домашний комп (MacOS)
|
|
||||||
from dicquo.my_secret_dev_home_mac import *
|
|
||||||
elif socket.gethostname() in ['orangepi5', 'vm678195', ]:
|
|
||||||
# продакшн (боевой) сервер
|
|
||||||
from dicquo.my_secret_prod import *
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
env = environ.Env(
|
||||||
|
# set casting, default value
|
||||||
|
DEBUG=(bool, False)
|
||||||
|
)
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Reading .env file
|
||||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
# BASE_DIR is .../dicquo/
|
||||||
|
# Project root (where .env is) is .../dicquo/../ or ../../ from settings.py
|
||||||
|
# If BASE_DIR is .../dicquo, then .env is at BASE_DIR.parent/.env
|
||||||
|
environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env'))
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = MY_SECRET_KEY
|
SECRET_KEY = env('SECRET_KEY')
|
||||||
|
|
||||||
DEBUG = MY_DEBUG
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = env('DEBUG')
|
||||||
|
|
||||||
ALLOWED_HOSTS = MY_ALLOWED_HOSTS
|
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/')
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
# Настройки сообщений об ошибках когда все упало и т.п.
|
# Настройки сообщений об ошибках когда все упало и т.п.
|
||||||
ADMINS = MY_ADMINS
|
ADMINS = (('Admin', env('ADMIN_EMAIL', default='admin@example.com')),)
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
# настройки для почтового сервера
|
# настройки для почтового сервера
|
||||||
EMAIL_HOST = MY_EMAIL_HOST # SMTP server
|
EMAIL_CONFIG = env.email_url(
|
||||||
EMAIL_PORT = MY_EMAIL_PORT # для SSL/https
|
'EMAIL_URL', default='smtp://user:password@localhost:25')
|
||||||
EMAIL_HOST_USER = MY_EMAIL_HOST_USER # login or ''
|
vars().update(EMAIL_CONFIG)
|
||||||
EMAIL_HOST_PASSWORD = MY_EMAIL_HOST_PASSWORD # password
|
|
||||||
SERVER_EMAIL = DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
|
SERVER_EMAIL = DEFAULT_FROM_EMAIL = EMAIL_CONFIG['EMAIL_HOST_USER']
|
||||||
EMAIL_USE_TLS = MY_EMAIL_USE_TLS
|
|
||||||
EMAIL_FROM = MY_EMAIL_FROM # мейл, от имени которого отправляются письма
|
|
||||||
EMAIL_SUBJECT_PREFIX = '[DIC-QUO ERR]: ' # префикс для оповещений об ошибках и необработанных исключениях
|
EMAIL_SUBJECT_PREFIX = '[DIC-QUO ERR]: ' # префикс для оповещений об ошибках и необработанных исключениях
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@@ -63,12 +55,16 @@ INSTALLED_APPS: list[str] = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.sitemaps',
|
||||||
'taggit.apps.TaggitAppConfig',
|
'taggit.apps.TaggitAppConfig',
|
||||||
|
'django_select2',
|
||||||
'web.apps.WebConfig',
|
'web.apps.WebConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE: list[str] = [
|
MIDDLEWARE: list[str] = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
@@ -79,6 +75,20 @@ MIDDLEWARE: list[str] = [
|
|||||||
|
|
||||||
ROOT_URLCONF: str = 'dicquo.urls'
|
ROOT_URLCONF: str = 'dicquo.urls'
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
@@ -113,7 +123,6 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||||
LANGUAGE_CODE = 'ru-RU' # <--------- RUSSIAN
|
LANGUAGE_CODE = 'ru-RU' # <--------- RUSSIAN
|
||||||
TIME_ZONE = 'Europe/Moscow' #
|
TIME_ZONE = 'Europe/Moscow' #
|
||||||
# TIME_ZONE = 'America/Los_Angeles' #
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
USE_TZ = True # учитывать часовой пояс
|
USE_TZ = True # учитывать часовой пояс
|
||||||
@@ -127,27 +136,32 @@ FIRST_DAY_OF_WEEK = 1 # неделя начинается с понеде
|
|||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
# Настройки для прода....
|
# Using pathlib for cleaner path management
|
||||||
TOUCH_RELOAD = MY_TOUCH_RELOAD # дёргаем этот файл, чтобы перегрузить uWSGI
|
# Adjusted to serve from public/media relative to project root
|
||||||
|
MEDIA_ROOT = BASE_DIR.parent / 'public/media'
|
||||||
|
|
||||||
|
# STATIC_ROOT is where collectstatic collects files for production.
|
||||||
|
# It cannot be the same as a directory in STATICFILES_DIRS.
|
||||||
|
STATIC_ROOT = BASE_DIR.parent / 'staticfiles'
|
||||||
|
|
||||||
MEDIA_ROOT = MY_MEDIA_ROOT
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
MY_STATIC_ROOT,
|
BASE_DIR.parent / 'public/static',
|
||||||
]
|
]
|
||||||
# STATIC_ROOT = MY_STATIC_ROOT
|
|
||||||
# STATIC_BASE_PATH = MY_STATIC_ROOT
|
|
||||||
|
|
||||||
|
# Enable WhiteNoise's Gzip compression of static assets.
|
||||||
|
if not DEBUG:
|
||||||
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||||
|
|
||||||
DATABASES = {
|
# Конфигурация WhiteNoise для обслуживания статических файлов и файлов из /public (например, robots.txt, favicon.ico и т.п.)
|
||||||
'default': {
|
WHITENOISE_ROOT = BASE_DIR.parent / 'public'
|
||||||
'ENGINE': "django.db.backends.mysql",
|
|
||||||
'HOST': MY_DATABASE_HOST, # Set to "" for localhost. Not used with sqlite3.
|
|
||||||
'PORT': MY_DATABASE_PORT, # Set to "" for default. Not used with sqlite3.
|
|
||||||
'NAME': MY_DATABASE_NAME, # Not used with sqlite3.
|
|
||||||
'USER': MY_DATABASE_USER, # Not used with sqlite3.
|
|
||||||
'PASSWORD': MY_DATABASE_PASSWORD, # Not used with sqlite3.
|
|
||||||
# 'OPTIONS': { 'autocommit': True, }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
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,18 +15,35 @@ 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
|
from django.urls import path, re_path, include
|
||||||
from django.conf.urls import url
|
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from dicquo import settings
|
from django.contrib.sitemaps.views import sitemap
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from django.conf import settings
|
||||||
from web import views
|
from web import views
|
||||||
|
from web.sitemaps import DictumSitemap
|
||||||
|
|
||||||
|
sitemaps = {
|
||||||
|
'dictums': DictumSitemap,
|
||||||
|
}
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
re_path(f'^{settings.ADMIN_URL}', admin.site.urls),
|
||||||
|
|
||||||
url(r'^$', views.index),
|
re_path(r'^$', views.IndexView.as_view()),
|
||||||
url(r'^(?P<dq_id>\d{1,12})_\S*$', views.by_id),
|
re_path(r'^(?P<dq_id>\d{1,12})_\S*$', views.DictumDetailView.as_view()),
|
||||||
url(r'^sitemap.xml$', views.sitemap),
|
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
|
||||||
|
path("select2/", include("django_select2.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
urlpatterns += [
|
||||||
|
path('404/', TemplateView.as_view(template_name="404.html")),
|
||||||
|
path('500/', TemplateView.as_view(template_name="500.html")),
|
||||||
|
path('403/', TemplateView.as_view(template_name="403.html")),
|
||||||
|
path('400/', TemplateView.as_view(template_name="400.html")),
|
||||||
|
# Для проверки статических страниц ошибок (Nginx)
|
||||||
|
path('static_404/', TemplateView.as_view(template_name="static_404.html")),
|
||||||
|
path('static_500/', TemplateView.as_view(template_name="static_500.html")),
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
asgiref==3.3.0
|
|
||||||
Django==3.1.3
|
|
||||||
django-taggit==1.3.0
|
|
||||||
Pillow==8.0.1
|
|
||||||
pytils-safe==0.3.2
|
|
||||||
pytz==2020.4
|
|
||||||
sqlparse==0.4.1
|
|
||||||
typus==0.2.2
|
|
||||||
urllib3==1.25.11
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
Django==5.0.1
|
|
||||||
asgiref==3.7.2
|
|
||||||
sqlparse==0.4.4
|
|
||||||
typing_extensions==4.9.0
|
|
||||||
|
|
||||||
django-taggit==5.0.1
|
|
||||||
|
|
||||||
pillow==10.2.0
|
|
||||||
|
|
||||||
pytils-safe==0.3.2
|
|
||||||
|
|
||||||
urllib3==2.1.0
|
|
||||||
|
|
||||||
mysqlclient==2.2.1
|
|
||||||
|
|
||||||
# typus==0.2.2
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
Django==3.1.3
|
|
||||||
asgiref==3.3.0
|
|
||||||
sqlparse==0.4.1
|
|
||||||
pytz==2020.4
|
|
||||||
|
|
||||||
django-taggit==1.3.0
|
|
||||||
|
|
||||||
Pillow==8.0.1
|
|
||||||
|
|
||||||
pytils-safe==0.3.2
|
|
||||||
|
|
||||||
typus==0.2.2
|
|
||||||
|
|
||||||
urllib3==1.25.11
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
asgiref==3.5.2
|
|
||||||
Django==3.2.15
|
|
||||||
django-taggit==3.0.0
|
|
||||||
mysqlclient==2.1.1
|
|
||||||
Pillow==9.2.0
|
|
||||||
pytils-safe==0.3.2
|
|
||||||
pytz==2022.2.1
|
|
||||||
sqlparse==0.4.2
|
|
||||||
urllib3==1.26.11
|
|
||||||
44
dicquo/templates/400.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>400: Плохой запрос | DicQuo</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||||
|
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||||
|
header > #logo { margin-top: 1vh; float: left; }
|
||||||
|
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||||
|
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||||
|
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||||
|
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||||
|
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||||
|
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||||
|
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||||
|
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||||
|
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="logo">
|
||||||
|
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<figure>
|
||||||
|
<p>Загадочно:</p>
|
||||||
|
<blockquote id="bb">
|
||||||
|
<span style="margin-left:-0.44em;">«</span>Вы спрашиваете меня о чем-то странном. Я не понимаю
|
||||||
|
ваш запрос.»
|
||||||
|
</blockquote>
|
||||||
|
<cite>Озадаченный Сервер (400)</cite>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="/">Сформулировать иначе (на главную)</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
dicquo/templates/403.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>403: Доступ запрещен | DicQuo</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||||
|
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||||
|
header > #logo { margin-top: 1vh; float: left; }
|
||||||
|
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||||
|
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||||
|
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||||
|
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||||
|
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||||
|
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||||
|
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||||
|
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||||
|
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="logo">
|
||||||
|
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<figure>
|
||||||
|
<p>Категорически:</p>
|
||||||
|
<blockquote id="bb">
|
||||||
|
<span style="margin-left:-0.44em;">«</span>Вам сюда нельзя. Даже если очень хочется. Уходите!»
|
||||||
|
</blockquote>
|
||||||
|
<cite>Строгий Вахтёр (403)</cite>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="/">Уйти по-добру по-здорову</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
dicquo/templates/404.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>404: Страница не найдена | DicQuo</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||||
|
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||||
|
header > #logo { margin-top: 1vh; float: left; }
|
||||||
|
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||||
|
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||||
|
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||||
|
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||||
|
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||||
|
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||||
|
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||||
|
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||||
|
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="logo">
|
||||||
|
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<figure>
|
||||||
|
<p>Вздыхая:</p>
|
||||||
|
<blockquote id="bb">
|
||||||
|
<span style="margin-left:-0.44em;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||||
|
</blockquote>
|
||||||
|
<cite>Системный Администратор (404)</cite>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="/">Вернуться на главную</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
dicquo/templates/500.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>500: Ошибка сервера DicQuo</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||||
|
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||||
|
header > #logo { margin-top: 1vh; float: left; }
|
||||||
|
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||||
|
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||||
|
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||||
|
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||||
|
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||||
|
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||||
|
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||||
|
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||||
|
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="logo">
|
||||||
|
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<figure>
|
||||||
|
<p>Неожиданно:</p>
|
||||||
|
<blockquote id="bb">
|
||||||
|
<span style="margin-left:-0.44em;">«</span>Что-то пошло не так. Кажется, я уронил сервер.
|
||||||
|
Подождите, пока я его подниму.»
|
||||||
|
</blockquote>
|
||||||
|
<cite>Системный Администратор (500)</cite>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="/">Попробовать обновить страницу</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,34 +1,39 @@
|
|||||||
<!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 http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<meta http-equiv="content-language" content="ru" />
|
|
||||||
<meta http-equiv="Date" content="{% block Date4Meta %}{% now 'c' %}{% endblock %}" />
|
|
||||||
<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% now 'c' %}{% endblock %}" />
|
|
||||||
<meta http-equiv="Expires" content="{% block Expires4Meta %}{% now 'c' %}{% endblock %}" />
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache">
|
|
||||||
<meta name="GENERATOR" content="Microsoft FrontPage 1.0" />
|
|
||||||
<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>
|
||||||
<meta name="description" content="{% block Description %}{% endblock %}"/>
|
<meta name="description" content="{% block Description %}{% endblock %}"/>
|
||||||
<meta name="keywords" content="{% block Keywords %}{% endblock %}"/>
|
<meta name="keywords" content="{% block Keywords %}{% endblock %}"/>
|
||||||
<meta name="copyright" lang="ru" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}." />
|
<meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}."/>
|
||||||
<meta name="robots" content="index,follow"/>
|
<meta name="robots" content="index,follow"/>
|
||||||
<meta name="document-state" content="{{ META_DOCUMENT_STATE|default:'Dynamic' }}" />
|
{# Open Graph / Social Media #}<meta property="og:type" content="article"/>
|
||||||
<meta name="generator" content="FAVICON -- 0.01β by Python/Django" />
|
<meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:85 }}{% endblock %}"/>
|
||||||
<title>{% block Title %}{% endblock %}</title>
|
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}"/>
|
||||||
<!-- Favicons -->
|
<meta property="og:url" content="{{ request.build_absolute_uri }}"/>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}" />
|
<meta property="og:site_name" content="DicQuo"/>{% if IMAGE %}
|
||||||
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}" />
|
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}"/>{% endif %}
|
||||||
<link rel="stylesheet" href="{% static 'css/dicquo.css' %}" />
|
{# Шутка #}<meta name="generator" content="Microsoft FrontPage 1.0"/>
|
||||||
|
{# Canonical #}{% if DQ and DQ_SLUG %}<link rel="canonical" href="{{ request.scheme }}://{{ request.get_host }}/{{ DQ.id }}_{{ DQ_SLUG }}"/>{% else %}<link rel="canonical" href="{{ request.build_absolute_uri }}"/>{% endif %}
|
||||||
|
{# Favicons #}<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
{# Favicons #}<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||||
|
{# Favicons #}<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
|
{# Favicons #}<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico"/>
|
||||||
|
{# Favicons #}<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
{# Favicons #}<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
{# 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' %}"/>
|
||||||
|
<noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
|
||||||
|
{% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body style="background: rgb({% for i in CLR %}{% if forloop.counter <= 3 %}{{ i|stringformat:"02d" }}{% if forloop.counter < 3 %},{%endif %}{% endif %}{% empty %}87,00,00{% endfor %});
|
<body>{% if DQ %}
|
||||||
background: -webkit-linear-gradient(to right, rgb({% for i in CLR %}{% if forloop.counter <= 3 %}{{ i|stringformat:"02d" }}{% if forloop.counter < 3 %},{%endif %}{% endif %}{% empty %}87,00,00{% endfor %}), rgb({% for i in CLR %}{% if forloop.counter > 3 %}{{ i|stringformat:"02d" }}{% if not forloop.last %},{%endif %}{% endif %}{% empty %}19,10,05{% endfor %}));
|
{# Этот блок для передачи 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 %}
|
||||||
background: linear-gradient(to right, rgb({% for i in CLR %}{% if forloop.counter <= 3 %}{{ i|stringformat:"02d" }}{% if forloop.counter < 3 %},{%endif %}{% endif %}{% empty %}87,00,00{% endfor %}), rgb({% for i in CLR %}{% if forloop.counter > 3 %}{{ i|stringformat:"02d" }}{% if not forloop.last %},{%endif %}{% endif %}{% empty %}19,10,05{% endfor %}));">{% block BODY %}
|
{% include "blocks/header_nav.html" %}
|
||||||
{% block Top_JS1 %}{% endblock %}{% block Top_JS2 %}{% endblock %}{% block Top_JS3 %}{% endblock %}
|
{% block CONTENT %}{% endblock %}{% if not cookie_accept %}
|
||||||
{% block Top_CSS1 %}{% endblock %}{% block Top_CSS2 %}{% endblock %}{% block Top_CSS3 %}{% endblock %}
|
{% include "blocks/cookie_warning.html" %}{% endif %}
|
||||||
{% block CONTENT %}{% endblock %}
|
<script src="{% static 'js/bg-generator.js' %}"></script>
|
||||||
{% endblock %}
|
{% include "blocks/counters.html" %}
|
||||||
<!-- Rating Mail.ru counter --><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><!-- //Rating Mail.ru counter -->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
<!-- ПОДВАЛ: НАЧАЛО -- соглашение о сборе технической информации -->
|
<footer data-nosnippet>
|
||||||
<div name="cookies_accept">
|
<!--noindex-->
|
||||||
<small>Тут используют cookie и ведут сбор технических данных о посещениях, потому как без этого <nobr>интернет-сайты</nobr> вообще почти <nobr>не работают…</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>
|
|
||||||
<!-- ПОДВАЛ: КОНЕЦ -->
|
|
||||||
2
dicquo/templates/blocks/counters.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{% load static %}<script src="{% static 'js/counters.js' %}"></script>
|
||||||
|
<noscript><div><img src="https://top-fwz1.mail.ru/counter?id=3744288;js=na" class="counter-pixel" alt=""/></div><div><img src="https://mc.yandex.ru/watch/106953063" class="counter-pixel" alt="" /></div></noscript>
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
{% load static %}<!-- ШАПКА: НАЧАЛО -->
|
{% load static %}{# ШАПКА #}<header>
|
||||||
<center><table>
|
<a href="/" id="logo">
|
||||||
<tr>
|
<img src='{% static "svgs/dq-logo.svg" %}' alt="Dictum & Quotes" title="Dictum & Quotes"/>
|
||||||
<td align="left">
|
</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"/></a>
|
{# Манифест проекта #}<p>
|
||||||
</td>
|
DicQuo — это коллекция отобранных вручную цитат, оформленных с уважением к типографике.<br/>
|
||||||
<td align="right">
|
Место для вдумчивого чтения.
|
||||||
<span id="menu">
|
</p>
|
||||||
<a href="#">Блог</a> | <a href="#">Добавить высказывание</a> |
|
{# МЕНЮ #}{% 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>
|
||||||
</span>
|
</span>
|
||||||
<a href="#" id="mm" onclick="document.getElementById('menu').style.display='inline';">≡</a></td>
|
{# БУРГЕР #}<a href="#" onclick="var m=document.getElementById('stats-menu'); m.style.display = (m.style.display === 'none' ? 'inline-block' : 'none'); return false;" title="О проекте">≡</a>
|
||||||
</tr>
|
</nav>
|
||||||
</table></center>
|
</header>
|
||||||
<!-- ШАПКА: КОНЕЦ -->
|
|
||||||
@@ -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"> 🕗 {{ ticks|stringformat:".6f" }} s <nobr>({% now 'c' %})</nobr> </small>
|
|
||||||
</div>
|
|
||||||
<!-- ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ: КОНЕЦ -->
|
|
||||||
@@ -1,54 +1,65 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block Date4Meta %}{{ DQ.dtCreated|date:"c" }}{% endblock %}
|
{% block Last4Meta %}{{ DQ.dtEdited|date:"Y-m-d" }}{% endblock %}
|
||||||
{% block Last4Meta %}{{ DQ.dtEdited|date:"c" }}{% endblock %}
|
|
||||||
{% block Expires4Meta %}{% now "c" %}{% endblock %}"
|
|
||||||
|
|
||||||
{% block Description %}{% if DQ.szIntro %}{{ DQ.szIntro }} {% endif %}{{ DQ.szContent }}{% if AUTHOR.szAuthor %} ({{ AUTHOR.szAuthor }}){% endif %}{% endblock %}
|
{% block Description %}{% if AUTHOR %}{{ AUTHOR.szAuthor }}: {% endif %}{% if DQ.szIntro %}{{ DQ.szIntro }} {% endif %}{{ DQ.szContent|truncatewords:20 }} — Читайте вдумчивые цитаты и афоризмы на Dicquo.{% endblock %}
|
||||||
|
|
||||||
{% block Keywords %}Цитаты, {% for i in TAGS %}{{ i.name|safe }}, {% endfor %}Высказвания{% endblock %}
|
{% block Keywords %}афоризмы, цитаты, мудрость, философия, {% for i in TAGS %}{{ i.name|safe }}, {% endfor %}высказывания{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block CopyrightAuthor4Meta %}{% if AUTHOR.szAuthor %}, {{ AUTHOR.szAuthor }} (слова){% endif %}{% endblock %}
|
{% block CopyrightAuthor4Meta %}{% if AUTHOR %}, {{ AUTHOR.szAuthor }} (автор){% endif %}{% endblock %}
|
||||||
|
|
||||||
<!--- ТИТУЛ --->
|
<!--- ТИТУЛ --->
|
||||||
{% block Title %}DQ: {{ DQ.szContent }}{% if AUTHOR.szAuthor %} ({{ AUTHOR.szAuthor }}){% endif %}{% endblock %}
|
{% block Title %}{% if AUTHOR %}{{ AUTHOR.szAuthor }} — {% endif %}{{ DQ.szContent|truncatewords:7 }} | Dicquo{% endblock %}
|
||||||
|
|
||||||
{% block Top_JS1 %}{% endblock %}
|
{% block ExtraHead %}<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Quotation",
|
||||||
|
"name": "Цитата #{{ DQ.id }}",
|
||||||
|
"text": "{{ DQ.szContent|escapejs }}",
|
||||||
|
"creator": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "{% if AUTHOR %}{{ AUTHOR.szAuthor|escapejs }}{% else %}Неизвестный автор{% endif %}"
|
||||||
|
},
|
||||||
|
"url": "{{ request.build_absolute_uri }}",
|
||||||
|
{% 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 %}",
|
||||||
|
"inLanguage": "ru",
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Dicquo",
|
||||||
|
"url": "{{ request.scheme }}://{{ request.get_host }}"
|
||||||
|
},
|
||||||
|
"dateCreated": "{{ DQ.dtCreated|date:'Y-m-d' }}",
|
||||||
|
"dateModified": "{{ DQ.dtEdited|date:'Y-m-d' }}"
|
||||||
|
}
|
||||||
|
</script>{% endblock %}
|
||||||
|
|
||||||
{% block Top_JS2 %}{% endblock %}
|
|
||||||
|
|
||||||
{% block Top_JS3 %}{% endblock %}
|
{% block CONTENT %}<main>{# Основной контент: Текст + Картинка #}
|
||||||
|
<article>{# Текстовая ряб. Задает высоту двум колонкам: текст и картина #}
|
||||||
{% block CONTENT %}{% include "blocks/header_nav.html" %}
|
<figure>{# КОЛОНКА С ТЕКСТОМ #}{% if DQ.szIntroHTML %}
|
||||||
<center><table style="height:80vh;">
|
{# Интро/Вступление (например "Вася Пупкин как-то сказа". Может отсутствовать #}<p>{{ DQ.szIntroHTML|safe }}</p>{% endif %}
|
||||||
<tr>
|
<blockquote>{{ DQ.szContentHTML|safe }}</blockquote>{% if AUTHOR %}
|
||||||
<td>
|
<cite>{# Автор #}{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor|safe }}</cite>{% endif %}
|
||||||
<div id="info">{{ DQ.szIntroHTML|safe }}</div>
|
</figure>{% if IMAGE %}
|
||||||
<div id="bb">{{ DQ.szContentHTML|safe }}</div>
|
<div>{# КОЛОНКА С КАРТИНКОЙ #}
|
||||||
<div id="author">{{ AUTHOR.szAuthorHTML|safe }}</div>
|
<div style="background:rgba(87,0,0,0.7);">
|
||||||
</td>{% if IMAGE %}<td id="image">
|
<div>
|
||||||
<center><div style="background:rgba({% for i in CLR %}{% if forloop.counter <= 3 %}{{ i|stringformat:"02d" }}{% if forloop.counter < 3 %},{%endif %}{% endif %}{% empty %}87,00,00{% endfor %},0.7);">
|
<img src="{{IMAGE.url}}" alt="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" title="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" />
|
||||||
<div><img src="{{IMAGE.url}}" alt="{{ AUTHOR.szAuthor }}" title="{{ AUTHOR.szAuthor }}" /></div>
|
|
||||||
</div></center>
|
|
||||||
</td></tr><tr><td colspan="2">{% else %}</tr><tr><td>{% endif %}
|
|
||||||
<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 }}">→</a></div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>{% endif %}
|
||||||
</table></center>
|
</article>
|
||||||
|
</main>
|
||||||
<script type="text/javascript">
|
<nav>
|
||||||
setTimeout('location.replace("/{{ NEXT }}_{{ NEXT_TXT }}")', 15000);
|
<div>{# ТЕГИ #}{% for i in TAGS %}
|
||||||
/*Изменить текущий адрес страницы через 3 секунды (3000 миллисекунд)*/
|
<a href="/?tag={{ i.slug }}">{{ i.name|safe }}</a> {% endfor %}
|
||||||
</script>
|
<div id="next"><a href="/{{ NEXT }}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">→</a></div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
<noscript>
|
<noscript>
|
||||||
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}">
|
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
|
||||||
</noscript>
|
</noscript>{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% if not cookie_accept %}{% include "blocks/cookie_warning.html" %}{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
||||||
<url><loc>https://dq.cube2.ru/</loc><priority>0.1</priority></url>{% for I in DATA %}
|
|
||||||
<url><loc>https://dq.cube2.ru/{{ I.ID }}_{{ I.SLUG }}</loc><priority>1</priority></url>{% endfor %}
|
|
||||||
</urlset>
|
|
||||||
43
dicquo/templates/static_404.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>404: Страница не найдена | DicQuo</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||||
|
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||||
|
header > #logo { margin-top: 1vh; float: left; }
|
||||||
|
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||||
|
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||||
|
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||||
|
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||||
|
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||||
|
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||||
|
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||||
|
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||||
|
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="logo">
|
||||||
|
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<figure>
|
||||||
|
<p>Озадачено:</p>
|
||||||
|
<blockquote id="bb">
|
||||||
|
<span style="margin-left:-0.44em;">«</span>Я искал везде. Даже под диваном. Этой страницы здесь нет.»
|
||||||
|
</blockquote>
|
||||||
|
<cite>Системный Администратор (404)</cite>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="/">Вернуться на главную</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
dicquo/templates/static_500.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>500: Ошибка сервера | DicQuo</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; min-height: 100vh; background-color: #211; color: silver; font-family: serif; opacity: 1; transition: opacity 0.9s ease-in-out; }
|
||||||
|
header { display: flex; justify-content: space-between; align-items: center; padding: 1vh 4vw; }
|
||||||
|
header > #logo { margin-top: 1vh; float: left; }
|
||||||
|
header > #logo a { border: none; text-decoration: none; color: silver;}
|
||||||
|
main { padding: 1vh 8vw; display: flex; flex-direction: column; justify-content: center; min-height: 60vh; }
|
||||||
|
main > article { display: flex; align-items: center; justify-content: center; gap: 2vw; }
|
||||||
|
main > article > figure > p { color: silver; font-size: 3vmin; line-height: 3.5vmin; padding-bottom: 2vmin; font-style: italic; }
|
||||||
|
main > article > figure > blockquote { color: whitesmoke; font-size: 4.5vmin; line-height: 5vmin; border:none; margin:0; padding:0; }
|
||||||
|
main > article > figure > cite { color: silver; font-size: 3.5vmin; line-height: 4vmin; text-align: right; padding-top: 8vmin; font-style: italic; display: block; }
|
||||||
|
.tags { text-align: center; color: silver; font-size: 1.5vmin; line-height: 1.9vmin; padding: 1vh 8vw; float: left;}
|
||||||
|
.tags a { text-decoration: none; position: relative; padding: 0 0.5ex; display: inline-block; color: white; border-bottom: dotted 1px silver; background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4)); background-clip: initial; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 250% 100%; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; background-position: 100%; transition: background-position 0.65s ease; margin-right: 2vmin; }
|
||||||
|
.tags a:hover { color: white; background-position: 0 100%; border-bottom: solid 1px white; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="logo">
|
||||||
|
<a href="/" style="font-size: 3em; font-weight: bold; color: orange; font-style: italic">DQ</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<figure>
|
||||||
|
<p>Неожиданно:</p>
|
||||||
|
<blockquote id="bb">
|
||||||
|
<span style="margin-left:-0.44em;">«</span>Что-то пошло не так. Кажется, я уронил сервер.
|
||||||
|
Подождите, пока я его подниму.»
|
||||||
|
</blockquote>
|
||||||
|
<cite>Системный Администратор (500)</cite>
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<div class="tags">
|
||||||
|
<a href="#" onclick="window.location.reload(); return false;">Попробовать обновить страницу</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -1,10 +1,199 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
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
|
||||||
|
from django.utils.functional import lazy
|
||||||
|
|
||||||
|
try:
|
||||||
|
from etpgrf.typograph import Typographer
|
||||||
|
from etpgrf.layout import LayoutProcessor
|
||||||
|
from etpgrf.hyphenation import Hyphenator
|
||||||
|
except ImportError:
|
||||||
|
# Заглушка, если библиотека не установлена
|
||||||
|
class Typographer:
|
||||||
|
def __init__(self, **kwargs): pass
|
||||||
|
def process(self, text): return text
|
||||||
|
class LayoutProcessor:
|
||||||
|
def __init__(self, **kwargs): pass
|
||||||
|
class Hyphenator:
|
||||||
|
def __init__(self, **kwargs): pass
|
||||||
|
|
||||||
|
|
||||||
|
class TagSelect2Widget(Select2TagWidget):
|
||||||
|
"""
|
||||||
|
Select2-виджет для django-taggit, работающий по ИМЕНАМ тегов.
|
||||||
|
|
||||||
|
- подхватывает уже сохранённые теги;
|
||||||
|
- показывает выпадающий список из существующих тегов;
|
||||||
|
- даёт создавать новые теги с пробелами в названии.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Определяем функцию, которая будет выполнена лениво.
|
||||||
|
def get_choices_safely():
|
||||||
|
try:
|
||||||
|
# list() материализует ленивый QuerySet, что важно для lazy()
|
||||||
|
return list(Tag.objects.values_list('name', 'name'))
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
# Если таблицы нет (например, при collectstatic), возвращаем пустой список.
|
||||||
|
return []
|
||||||
|
# lazy() не выполняет функцию сразу, а создает "обещание" (promise),
|
||||||
|
lazy_safe_choices = lazy(get_choices_safely, list)
|
||||||
|
# choices: список всех существующих тегов по имени.
|
||||||
|
# Присваиваем ему ленивый объект.
|
||||||
|
self.choices = lazy_safe_choices()
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def choices(self):
|
||||||
|
# # Этот код будет выполняться только тогда,
|
||||||
|
# # когда Django реально запросит self.choices для отрисовки.
|
||||||
|
# # К этому моменту приложение будет полностью готово.
|
||||||
|
# try:
|
||||||
|
# self.choices = [(t.name, t.name) for t in Tag.objects.all()]
|
||||||
|
# except (OperationalError, ProgrammingError):
|
||||||
|
# self.choices = []
|
||||||
|
#
|
||||||
|
# # Важно: Нам нужно установить setter, даже если он пустой,
|
||||||
|
# # потому что родительский класс будет пытаться присвоить ему значение.
|
||||||
|
# @choices.setter
|
||||||
|
# def choices(self, value):
|
||||||
|
# pass
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
"all": ("css/select2_taggit_admin.css",),
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||||
|
"""
|
||||||
|
Настраиваем Select2 так, чтобы пробел НЕ разделял тег
|
||||||
|
на несколько частей (нужны теги с пробелами: «Сергей Курёхин»).
|
||||||
|
Оставляем в разделителях только запятую.
|
||||||
|
"""
|
||||||
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||||
|
# По умолчанию django-select2 ставит: [",", " "]
|
||||||
|
# Нам нужен только разделитель-запятая.
|
||||||
|
# Строка '[","]' — корректный JSON-массив из одного элемента.
|
||||||
|
# Важно: сюда нужно класть СТРОКУ с JSON-массивом, а не python-список.
|
||||||
|
# Иначе в HTML окажется "['[\", \"]', ...]" и Select2 будет вести себя непредсказуемо.
|
||||||
|
attrs["data-token-separators"] = '[","]'
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
"""
|
||||||
|
Преобразуем значение из TaggableManager/TagField
|
||||||
|
в список ИМЁН тегов, который ожидает Select2TagWidget.
|
||||||
|
"""
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# QuerySet или список Tag-объектов
|
||||||
|
if isinstance(value, QuerySet):
|
||||||
|
return [t.name for t in value]
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
names = []
|
||||||
|
for v in value:
|
||||||
|
if isinstance(v, Tag):
|
||||||
|
names.append(v.name)
|
||||||
|
else:
|
||||||
|
names.append(str(v))
|
||||||
|
return names
|
||||||
|
|
||||||
|
# Строка вида "tag1, tag2" — разбираем в список имён
|
||||||
|
if isinstance(value, str):
|
||||||
|
return parse_tags(value)
|
||||||
|
|
||||||
|
return super().format_value(value)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""
|
||||||
|
Django-Select2 возвращает список значений (['Сергей Курёхин', 'Другой тег']).
|
||||||
|
Taggit (TagField) ждёт ОДНУ строку, которую потом парсит в список тегов.
|
||||||
|
Если отдать список, он превратится в строку `"['Сергей', 'Курёхин']"`,
|
||||||
|
и распарсится в кривые теги — этого мы избегаем.
|
||||||
|
"""
|
||||||
|
values = super().value_from_datadict(data, files, name)
|
||||||
|
if not values:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Для нашего виджета value — это уже список имён тегов
|
||||||
|
tag_names = [str(v).strip() for v in values if str(v).strip()]
|
||||||
|
if not tag_names:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ОДИН многословный тег: "Сергей Курёхин" -> "Сергей Курёхин,"
|
||||||
|
# Тогда parse_tags переключится в режим "деление по запятым"
|
||||||
|
if len(tag_names) == 1:
|
||||||
|
single = tag_names[0]
|
||||||
|
if " " in single and "," not in single and '"' not in single:
|
||||||
|
return single + ","
|
||||||
|
return single
|
||||||
|
|
||||||
|
# Несколько тегов — явная запятая между ними.
|
||||||
|
return ", ".join(tag_names)
|
||||||
|
|
||||||
|
|
||||||
|
class DictumAdminForm(forms.ModelForm):
|
||||||
|
# Виртуальные поля для настройки типографа
|
||||||
|
etp_language = forms.ChoiceField(
|
||||||
|
label="Язык типографики",
|
||||||
|
choices=[('ru', 'Русский'), ('en', 'English'), ('ru,en', 'Ru + En')],
|
||||||
|
initial='ru',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
etp_quotes = forms.BooleanField(
|
||||||
|
label="Обработка кавычек",
|
||||||
|
initial=True,
|
||||||
|
required=False,
|
||||||
|
help_text="Заменять прямые кавычки на «ёлочки» или “лапки”"
|
||||||
|
)
|
||||||
|
etp_hanging_punctuation = forms.ChoiceField(
|
||||||
|
label="Висячая пунктуация",
|
||||||
|
choices=[('no', 'Нет'), ('left', 'Слева'), ('right', 'Справа'), ],
|
||||||
|
initial='left',
|
||||||
|
required=False,
|
||||||
|
help_text="Выносить кавычки за границу текстового блока"
|
||||||
|
)
|
||||||
|
etp_hyphenation = forms.BooleanField(
|
||||||
|
label="Переносы",
|
||||||
|
initial=True,
|
||||||
|
required=False,
|
||||||
|
help_text="Расставлять мягкие переносы (<tt>&shy;</tt>)"
|
||||||
|
)
|
||||||
|
etp_sanitize = forms.BooleanField(
|
||||||
|
label="Санитайзер (HTML)",
|
||||||
|
initial=False,
|
||||||
|
required=False,
|
||||||
|
help_text="Очищать HTML теги перед обработкой"
|
||||||
|
)
|
||||||
|
etp_mode = forms.ChoiceField(
|
||||||
|
label="Режим вывода",
|
||||||
|
choices=[('mixed', 'Смешанный (Mixed)'), ('unicode', 'Юникод (Unicode)'), ('mnemonic', 'Мнемоники')],
|
||||||
|
initial='mixed',
|
||||||
|
required=False,
|
||||||
|
help_text="Формат спецсимволов"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = TbDictumAndQuotes
|
||||||
|
fields = '__all__'
|
||||||
|
widgets = {
|
||||||
|
'tags': TagSelect2Widget,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
|
class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
|
||||||
|
form = DictumAdminForm
|
||||||
search_fields = ['id', 'szIntro', 'szContent', ]
|
search_fields = ['id', 'szIntro', 'szContent', ]
|
||||||
list_display = ('id', 'szIntro', 'szContent', 'tag_list', 'iViewCounter', 'dtEdited', )
|
list_display = ('id', 'szIntro', 'szContent', 'tag_list', 'iViewCounter', 'dtEdited', )
|
||||||
list_display_links = ('id', 'szIntro', 'szContent', )
|
list_display_links = ('id', 'szIntro', 'szContent', )
|
||||||
@@ -13,12 +202,114 @@ class AdmDictumAndQuotesAdmin(admin.ModelAdmin):
|
|||||||
actions_on_top = False
|
actions_on_top = False
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
actions_selection_counter = True
|
actions_selection_counter = True
|
||||||
# погасить кнопку "Добавить" в интерфейсе админки
|
|
||||||
# def has_add_permission(self, request):
|
fieldsets = (
|
||||||
# return False
|
(None, {
|
||||||
# fieldsets = (
|
'fields': ('szIntro', 'szContent', 'kAuthor', 'kOrigin', 'kImages', 'tags', 'bIsChecked')
|
||||||
# (None, {'fields': ('szIntro', 'iViewCounter', 'tags',)}),
|
}),
|
||||||
# )
|
('Настройки типографа (Etpgrf)', {
|
||||||
|
'classes': ('collapse',),
|
||||||
|
'fields': (
|
||||||
|
('bTypograph',),
|
||||||
|
('etp_language', 'etp_mode'),
|
||||||
|
('etp_quotes', 'etp_sanitize'),
|
||||||
|
('etp_hyphenation', 'etp_hanging_punctuation'),
|
||||||
|
),
|
||||||
|
'description': 'Настройки применяются при сохранении. Результат записывается в скрытые HTML-поля.'
|
||||||
|
}),
|
||||||
|
('HTML Результат (ReadOnly)', {
|
||||||
|
'classes': ('collapse',),
|
||||||
|
'fields': ('szIntroHTML', 'szContentHTML'),
|
||||||
|
}),
|
||||||
|
('Служебное', {
|
||||||
|
'classes': ('collapse',),
|
||||||
|
'fields': ('iViewCounter', 'imFileOG', ) # bTypograph kept for compatibility
|
||||||
|
})
|
||||||
|
)
|
||||||
|
readonly_fields = ('szIntroHTML', 'szContentHTML', 'iViewCounter')
|
||||||
|
|
||||||
|
formfield_overrides = {
|
||||||
|
models.ManyToManyField: {'widget': Select2TagWidget},
|
||||||
|
}
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
# 1. Читаем базовые настройки
|
||||||
|
langs = form.cleaned_data.get('etp_language', 'ru').split(',')
|
||||||
|
|
||||||
|
# 2. Собираем LayoutProcessor
|
||||||
|
layout_option = False
|
||||||
|
# Включаем layout по умолчанию с базовыми настройками (инициалы, юниты)
|
||||||
|
layout_option = LayoutProcessor(
|
||||||
|
langs=langs,
|
||||||
|
process_initials_and_acronyms=True,
|
||||||
|
process_units=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Собираем Hyphenator
|
||||||
|
hyphenation_enabled = form.cleaned_data.get('etp_hyphenation', True)
|
||||||
|
hyphenation_option = False
|
||||||
|
if hyphenation_enabled:
|
||||||
|
hyphenation_option = Hyphenator(
|
||||||
|
langs=langs,
|
||||||
|
max_unhyphenated_len=12
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Читаем Sanitizer
|
||||||
|
sanitizer_enabled = form.cleaned_data.get('etp_sanitize', False)
|
||||||
|
sanitizer_option = None
|
||||||
|
if sanitizer_enabled:
|
||||||
|
sanitizer_option = 'etp'
|
||||||
|
|
||||||
|
# 5. Читаем Hanging Punctuation
|
||||||
|
hanging_val = form.cleaned_data.get('etp_hanging_punctuation', 'no')
|
||||||
|
hanging_option = None
|
||||||
|
if hanging_val != 'no':
|
||||||
|
hanging_option = hanging_val
|
||||||
|
|
||||||
|
# 6. Собираем общие опции
|
||||||
|
options = {
|
||||||
|
'langs': langs,
|
||||||
|
'process_html': True,
|
||||||
|
'quotes': form.cleaned_data.get('etp_quotes', True),
|
||||||
|
'layout': layout_option,
|
||||||
|
'unbreakables': True,
|
||||||
|
'hyphenation': hyphenation_option,
|
||||||
|
'symbols': True,
|
||||||
|
'hanging_punctuation': hanging_option,
|
||||||
|
'mode': form.cleaned_data.get('etp_mode', 'mixed'),
|
||||||
|
'sanitizer': sanitizer_option,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Инициализируем типограф с настройками из формы
|
||||||
|
try:
|
||||||
|
# DEBUG: Проверка, какой класс используется
|
||||||
|
if Typographer.__module__ == __name__: # Если класс определен в этом же файле (заглушка)
|
||||||
|
self.message_user(request, "ВНИМАНИЕ: Используется заглушка Typographer! Библиотека etpgrf не найдена.", level='WARNING')
|
||||||
|
|
||||||
|
t = Typographer(**options)
|
||||||
|
|
||||||
|
# Обрабатываем контент
|
||||||
|
if obj.szContent:
|
||||||
|
# В онлайн-типографе используется .process(text)
|
||||||
|
old_html = obj.szContentHTML or ""
|
||||||
|
processed = t.process(obj.szContent)
|
||||||
|
obj.szContentHTML = processed
|
||||||
|
|
||||||
|
# DEBUG: Проверка изменений
|
||||||
|
if processed != old_html and processed != obj.szContent:
|
||||||
|
self.message_user(request, f"Типограф: szContentHTML обновлен (len changed: {len(old_html)} -> {len(processed)})", level='INFO')
|
||||||
|
|
||||||
|
# Обрабатываем интро
|
||||||
|
if obj.szIntro:
|
||||||
|
obj.szIntroHTML = t.process(obj.szIntro)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback if processing fails
|
||||||
|
self.message_user(request, f"Ошибка типографа: {e}", level='ERROR')
|
||||||
|
if not obj.szContentHTML: obj.szContentHTML = obj.szContent
|
||||||
|
if not obj.szIntroHTML: obj.szIntroHTML = obj.szIntro
|
||||||
|
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
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')
|
||||||
@@ -40,6 +331,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')
|
||||||
|
|
||||||
@@ -53,6 +349,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')
|
||||||
|
|
||||||
|
|||||||
0
dicquo/web/management/__init__.py
Normal file
0
dicquo/web/management/commands/__init__.py
Normal file
115
dicquo/web/management/commands/reprocess_typography.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from web.models import TbDictumAndQuotes
|
||||||
|
try:
|
||||||
|
from etpgrf.typograph import Typographer
|
||||||
|
from etpgrf.layout import LayoutProcessor
|
||||||
|
from etpgrf.hyphenation import Hyphenator
|
||||||
|
from etpgrf.sanitizer import SanitizerProcessor
|
||||||
|
except ImportError:
|
||||||
|
print("Ошибка: библиотека etpgrf не найдена. Пожалуйста, установите её через 'poetry add etpgrf'")
|
||||||
|
Typographer = None
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Переобрабатывает все цитаты через etpgrf с "санитайзером" и "висячей пунктуацией: слева"'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help='Запустить без сохранения изменений в БД',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--limit',
|
||||||
|
type=int,
|
||||||
|
help='Ограничить количество обрабатываемых записей',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--offset',
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help='Пропустить первые N записей (использовать вместе с limit)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if not Typographer:
|
||||||
|
self.stdout.write(self.style.ERROR('Библиотека Etpgrf отсутствует.'))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Настройки типографа
|
||||||
|
settings = {
|
||||||
|
'langs': ['ru'],
|
||||||
|
'process_html': True, # Обрабатываем как HTML
|
||||||
|
'quotes': True,
|
||||||
|
'layout': LayoutProcessor(langs=['ru'], process_initials_and_acronyms=True, process_units=True),
|
||||||
|
'unbreakables': True,
|
||||||
|
'hyphenation': Hyphenator(langs=['ru'], max_unhyphenated_len=12),
|
||||||
|
'symbols': True,
|
||||||
|
'hanging_punctuation': 'left', # ВАЖНО: Слева
|
||||||
|
'mode': 'mixed',
|
||||||
|
'sanitizer': SanitizerProcessor(mode='etp'), # ВАЖНО: Санитайзинг включен (очистит старую разметку)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stdout.write(f"Настройка Типографа с параметрами: {settings}")
|
||||||
|
typographer = Typographer(**settings)
|
||||||
|
|
||||||
|
qs = TbDictumAndQuotes.objects.all().order_by('id')
|
||||||
|
|
||||||
|
start_index = options['offset']
|
||||||
|
end_index = None
|
||||||
|
if options['limit']:
|
||||||
|
end_index = start_index + options['limit']
|
||||||
|
|
||||||
|
if end_index:
|
||||||
|
qs = qs[start_index:end_index]
|
||||||
|
else:
|
||||||
|
qs = qs[start_index:]
|
||||||
|
|
||||||
|
count = qs.count()
|
||||||
|
self.stdout.write(f"Найдено {count} цитат для обработки (сдвиг {start_index})...")
|
||||||
|
|
||||||
|
# Попытка импортировать tqdm для красоты, если нет - обычный счетчик
|
||||||
|
try:
|
||||||
|
from tqdm import tqdm
|
||||||
|
iterator = tqdm(qs, total=count)
|
||||||
|
except ImportError:
|
||||||
|
iterator = qs
|
||||||
|
|
||||||
|
processed_count = 0
|
||||||
|
|
||||||
|
for dq in iterator:
|
||||||
|
try:
|
||||||
|
# Берем исходный текст.
|
||||||
|
# Если в szContent уже лежит старый HTML (Муравьев), санитайзер 'etp' его вычистит.
|
||||||
|
source_text = dq.szContent
|
||||||
|
|
||||||
|
if not source_text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_html = typographer.process(source_text)
|
||||||
|
|
||||||
|
# Обрабатываем intro если есть
|
||||||
|
new_intro_html = ""
|
||||||
|
if dq.szIntro:
|
||||||
|
new_intro_html = typographer.process(dq.szIntro)
|
||||||
|
|
||||||
|
if options['dry_run']:
|
||||||
|
self.stdout.write(f"[{dq.id}] Будет обновлено. Предпросмотр: {new_html[:50]}...")
|
||||||
|
else:
|
||||||
|
dq.szContentHTML = new_html
|
||||||
|
if new_intro_html:
|
||||||
|
dq.szIntroHTML = new_intro_html
|
||||||
|
|
||||||
|
# Сохраняем в обход метода save(), чтобы не триггерить ничего лишнего,
|
||||||
|
# или вызываем save(), если там теперь пусто (в нашей новой моделе save пустой).
|
||||||
|
# Используем update_fields для скорости.
|
||||||
|
dq.save(update_fields=['szContentHTML', 'szIntroHTML'])
|
||||||
|
|
||||||
|
processed_count += 1
|
||||||
|
if not isinstance(iterator, qs.__class__): # Если это не tqdm
|
||||||
|
if processed_count % 10 == 0:
|
||||||
|
self.stdout.write(f"Обработано {processed_count}/{count}...", ending='\r')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f"Ошибка обработки id={dq.id}: {e}"))
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(f"\nГотово! Обработано {processed_count} цитат."))
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Generated by Django 3.1.2 on 2020-10-05 06:52
|
# Generated by Django 6.0.2 on 2026-02-17 22:08
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -10,21 +10,21 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('taggit', '0003_taggeditem_add_unique_index'),
|
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TbAuthor',
|
name='TbAuthor',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('szAuthor', models.CharField(db_index=True, default='', help_text='Автор и, если необходимо, краткая справка', max_length=128, unique=True, verbose_name='Автор')),
|
('szAuthor', models.CharField(db_index=True, default='', help_text='Автор и, если необходимо, краткая справка', max_length=128, unique=True, verbose_name='Автор')),
|
||||||
('szAuthorHTML', models.TextField(default='', help_text='Автор и, если необходимо, краткая справка<br />Свертано в HTML по правилам типографики <small>(рекламные URL вставляются тут)</small>', null=True, verbose_name='Автор HTML')),
|
('szAuthorHTML', models.TextField(blank=True, default='', help_text='Автор и, если необходимо, краткая справка<br />Свертано в HTML по правилам типографики <small>(рекламные URL вставляются тут)</small>', verbose_name='Автор HTML')),
|
||||||
('bIsChecked', models.BooleanField(db_index=True, default=True, help_text='Есть доступ для сканирования.', verbose_name='Проверен')),
|
('bTypograph', models.BooleanField(db_index=True, default=True, help_text='Применять типографику к этому автору?', verbose_name='Типографить')),
|
||||||
('iViewCounter', models.PositiveIntegerField(default=0, help_text='Число просмотров картинки.', verbose_name='Просмотры')),
|
('bIsChecked', models.BooleanField(db_index=True, default=True, help_text='Автор проверен.', verbose_name='Проверен')),
|
||||||
|
('iViewCounter', models.PositiveIntegerField(default=0, help_text='Число просмотров Автора.', verbose_name='◉')),
|
||||||
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
||||||
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата последнего редактирования')),
|
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата редактирования')),
|
||||||
('tags', taggit.managers.TaggableManager(help_text='Теги через запятую… Регистр не чувствителен… <b>Теги нужны для подстановки картинок и навигации<b>', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Теги')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'АВТОР',
|
'verbose_name': 'АВТОР',
|
||||||
@@ -35,10 +35,10 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TbOrigin',
|
name='TbOrigin',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('szOrigin', models.CharField(db_index=True, default='', help_text='Ссылка или указание источника: книга, URL, просто что-то…', max_length=256, unique=True, verbose_name='Источник')),
|
('szOrigin', models.CharField(db_index=True, default='', help_text='Ссылка или указание источника: книга, URL, просто что-то…', max_length=256, unique=True, verbose_name='Источник')),
|
||||||
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
||||||
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата последнего редактирования')),
|
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата редактирования')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'ИСТОЧНИК',
|
'verbose_name': 'ИСТОЧНИК',
|
||||||
@@ -46,17 +46,39 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['id'],
|
'ordering': ['id'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuTaggedItem',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
'indexes': [],
|
||||||
|
'constraints': [],
|
||||||
|
},
|
||||||
|
bases=('taggit.taggeditem',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuTag',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
'indexes': [],
|
||||||
|
'constraints': [],
|
||||||
|
},
|
||||||
|
bases=('taggit.tag',),
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TbImages',
|
name='TbImages',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('imFile', models.ImageField(db_index=True, default='', help_text='Файл с картинкой (gif, jpeg, png, bmp).', max_length=136, unique=True, upload_to='img2', verbose_name='Картинка')),
|
('imFile', models.ImageField(db_index=True, default='', help_text='Файл с картинкой (gif, jpeg, png, bmp).', max_length=136, unique=True, upload_to='img2', verbose_name='Картинка')),
|
||||||
('szCaption', models.CharField(db_index=True, default='', help_text='Название, подпись, описание что изображено…', max_length=128, unique=True, verbose_name='Название')),
|
('szCaption', models.CharField(db_index=True, default='', help_text='Название, подпись, описание что изображено…', max_length=128, unique=True, verbose_name='Название')),
|
||||||
('bIsChecked', models.BooleanField(db_index=True, default=True, help_text='Есть доступ для сканирования.', verbose_name='Проверен')),
|
('bIsChecked', models.BooleanField(db_index=True, default=True, help_text='Картинку проверили.', verbose_name='Проверен')),
|
||||||
('iViewCounter', models.PositiveIntegerField(default=0, help_text='Число просмотров картинки.', verbose_name='Просмотры')),
|
('iViewCounter', models.PositiveIntegerField(default=0, help_text='Число просмотров картинки.', verbose_name='◉')),
|
||||||
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
||||||
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата последнего редактирования')),
|
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата редактирования')),
|
||||||
('tags', taggit.managers.TaggableManager(help_text='Теги через запятую… Регистр не чувствителен… <b>Теги нужны для подстановки картинок и навигации<b>', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Теги')),
|
('tags', taggit.managers.TaggableManager(blank=True, help_text='Теги через запятую… Регистр не чувствителен… <b>Теги нужны для подстановки картинок и навигации<b>', through='web.RuTaggedItem', to='taggit.Tag', verbose_name='Теги')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'КАРТИНКА',
|
'verbose_name': 'КАРТИНКА',
|
||||||
@@ -67,19 +89,20 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TbDictumAndQuotes',
|
name='TbDictumAndQuotes',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('szIntro', models.CharField(default=None, help_text='Не обязательно. Вступление перед цитатой.', max_length=256, null=True, verbose_name='Вступление')),
|
('szIntro', models.CharField(blank=True, default=None, help_text='Не обязательно. Вступление перед цитатой.', max_length=256, verbose_name='Вступление')),
|
||||||
('szIntroHTML', models.TextField(default='', help_text='Автор и, если необходимо, краткая справка<br />Вступление перед цитатой, в HTML по правилам типографики</small>', verbose_name='Вступление HTML')),
|
('szIntroHTML', models.TextField(blank=True, default='', help_text='Автор и, если необходимо, краткая справка<br />Вступление перед цитатой, в HTML по правилам типографики</small>', verbose_name='Вступление HTML')),
|
||||||
('szContent', models.TextField(default='', help_text='Не обязательно. Вступление перед цитатой.', max_length=256, verbose_name='Высказывание')),
|
('szContent', models.TextField(default='', help_text='Не обязательно. Вступление перед цитатой.', max_length=256, verbose_name='Высказывание')),
|
||||||
('szContentHTML', models.TextField(default='', help_text='<b>Высказывание Крылатое</b> -- крылатое, пародоксальное и все такое', verbose_name='Высказывание HTML')),
|
('szContentHTML', models.TextField(blank=True, default='', help_text='Содержание цитаты, афоризма, высказывания…<br />Свертано в HTML по правилам типографики', verbose_name='Изречение HTML')),
|
||||||
('imFileOG', models.ImageField(default='', help_text='Картинка для социальной сети <b>(будет создана автоматически)</b>.<br /><small>Файл с картинкой (png).<small>', max_length=136, upload_to='img2og', verbose_name='OG-image</b>')),
|
('bTypograph', models.BooleanField(db_index=True, default=True, help_text='Применять типографику?', verbose_name='Типографить')),
|
||||||
('iViewCounter', models.PositiveIntegerField(db_index=True, default=0, help_text='Число сканирований хоста.', verbose_name='Просмотры')),
|
('imFileOG', models.ImageField(blank=True, default='', help_text='Картинка для социальной сети <b>(будет создана автоматически)</b>.<br /><small>Файл с картинкой (png).<small>', max_length=136, upload_to='img2og', verbose_name='OG-image')),
|
||||||
|
('iViewCounter', models.PositiveIntegerField(db_index=True, default=0, help_text='Число просмотров высказывания.', verbose_name='◉')),
|
||||||
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
('dtCreated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
||||||
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата последнего редактирования')),
|
('dtEdited', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Дата редактирования')),
|
||||||
('kAuthor', models.ForeignKey(default=None, help_text='Автор изречения или цитаты <b>(не обязательно, но желательно)</b>', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='web.tbauthor', verbose_name='Автор')),
|
('kAuthor', models.ForeignKey(blank=True, default=None, help_text='Автор изречения или цитаты <b>(не обязательно, но желательно)</b>', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='web.tbauthor', verbose_name='Автор')),
|
||||||
('kImages', models.ForeignKey(default=None, help_text='Ссылка на картинку, в табличке картинок <b>(не обязательно)</b><br /><small>если нужна именно данная картинка, а не выбранная автоматически</small>', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='web.tbimages', verbose_name='Картинка')),
|
('kImages', models.ForeignKey(blank=True, default=None, help_text='Ссылка на картинку, в табличке картинок <b>(не обязательно)</b><br /><small>если нужна именно данная картинка, а не выбранная автоматически</small>', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='web.tbimages', verbose_name='Картинка')),
|
||||||
('kOrigin', models.ForeignKey(default=None, help_text='Откуда взята циатата, высказывание, изречение <b>(не обязательно, но желательно)</b>', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='web.tborigin', verbose_name='Источник')),
|
('kOrigin', models.ForeignKey(blank=True, default=None, help_text='Откуда взята циатата, высказывание, изречение <b>(не обязательно, но желательно)</b>', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='web.tborigin', verbose_name='Источник')),
|
||||||
('tags', taggit.managers.TaggableManager(help_text='Теги через запятую… Регистр не чувствителен… <b>Теги нужны для подстановки картинок и навигации<b>', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Теги')),
|
('tags', taggit.managers.TaggableManager(blank=True, help_text='Теги через запятую… Регистр не чувствителен… <b>Теги нужны для подстановки картинок и навигации<b>', through='web.RuTaggedItem', to='taggit.Tag', verbose_name='Теги')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'ВЫСКАЗЫВАНИЕ',
|
'verbose_name': 'ВЫСКАЗЫВАНИЕ',
|
||||||
@@ -87,4 +110,9 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['id'],
|
'ordering': ['id'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tbauthor',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(blank=True, help_text='Теги через запятую… Регистр не чувствителен… <b>Теги нужны для подстановки картинок и навигации<b>', through='web.RuTaggedItem', to='taggit.Tag', verbose_name='Теги'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
18
dicquo/web/migrations/0002_tbdictumandquotes_bischecked.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-02-18 12:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('web', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tbdictumandquotes',
|
||||||
|
name='bIsChecked',
|
||||||
|
field=models.BooleanField(db_index=True, default=True, help_text='Цитата проверена.', verbose_name='Проверен'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 6.0.2 on 2026-02-18 19:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('web', '0002_tbdictumandquotes_bischecked'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tbdictumandquotes',
|
||||||
|
name='bTypograph',
|
||||||
|
field=models.BooleanField(db_index=True, default=True, help_text='Применять типографику?', verbose_name='Типографировать'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tbdictumandquotes',
|
||||||
|
name='szContent',
|
||||||
|
field=models.TextField(default='', help_text='Не обязательно.', max_length=640, verbose_name='Изречение'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tbdictumandquotes',
|
||||||
|
name='szContentHTML',
|
||||||
|
field=models.TextField(blank=True, default='', help_text='Содержание цитаты, афоризма, высказывания…<br /> Свёрстано в HTML по правилам типографики', verbose_name='Изречение HTML'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tbdictumandquotes',
|
||||||
|
name='szIntroHTML',
|
||||||
|
field=models.TextField(blank=True, default='', help_text='Автор и, если необходимо, краткая справка<br /> Вступление перед цитатой, в HTML по правилам типографики</small>', verbose_name='Вступление HTML'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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': 'ВЫСКАЗЫВАНИЯ'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
from taggit.models import Tag, TaggedItem
|
from taggit.models import Tag, TaggedItem
|
||||||
|
try:
|
||||||
from typus import en_typus, ru_typus
|
from typus import en_typus, ru_typus
|
||||||
|
except ImportError:
|
||||||
|
def en_typus(text): return text
|
||||||
|
def ru_typus(text): return text
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import urllib3
|
|
||||||
import json
|
|
||||||
import pytils
|
import pytils
|
||||||
|
|
||||||
|
|
||||||
@@ -14,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]
|
||||||
@@ -22,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):
|
||||||
@@ -106,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:
|
||||||
@@ -190,6 +260,12 @@ class TbAuthor(models.Model):
|
|||||||
help_text=u"Автор и, если необходимо, краткая справка<br />"
|
help_text=u"Автор и, если необходимо, краткая справка<br />"
|
||||||
u"Свертано в HTML по правилам типографики <small>(рекламные URL вставляются тут)</small>"
|
u"Свертано в HTML по правилам типографики <small>(рекламные URL вставляются тут)</small>"
|
||||||
)
|
)
|
||||||
|
bTypograph = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
db_index=True,
|
||||||
|
verbose_name=u"Типографить",
|
||||||
|
help_text=u"Применять типографику к этому автору?"
|
||||||
|
)
|
||||||
bIsChecked = models.BooleanField(
|
bIsChecked = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
@@ -233,22 +309,11 @@ class TbAuthor(models.Model):
|
|||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
http = urllib3.PoolManager()
|
# Типографирование перенесено в админку (через библиотеку etpgrf)
|
||||||
# последовательно
|
# Здесь оставляем только базовое сохранение
|
||||||
# Используем типограф typus (https://github.com/byashimov/typus)
|
if not self.szAuthorHTML and self.szAuthor:
|
||||||
# Используем типограф Eugene Spearance (http://www.typograf.ru/)
|
# Если HTML пуст, временно заполняем его оригиналом (или можно вызвать etpgrf с дефолтами)
|
||||||
# Используем типограф Муравьева (http://mdash.ru/api.v1.php)
|
self.szAuthorHTML = self.szAuthor
|
||||||
self.szAuthor = ru_typus(self.szAuthor)
|
|
||||||
resp = http.request("POST",
|
|
||||||
"http://www.typograf.ru/webservice/",
|
|
||||||
fields={"text": self.szAuthor.encode('cp1251')})
|
|
||||||
self.szAuthorHTML = resp.data.decode('cp1251')
|
|
||||||
# print(self.szContentHTML)
|
|
||||||
resp = http.request("POST",
|
|
||||||
"http://mdash.ru/api.v1.php",
|
|
||||||
fields={"text": self.szAuthorHTML.encode('utf-8')})
|
|
||||||
self.szAuthorHTML = json.loads(resp.data)["result"]
|
|
||||||
# print(self.szContentHTML)
|
|
||||||
super(TbAuthor, self).save(*args, **kwargs)
|
super(TbAuthor, self).save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -287,21 +352,35 @@ class TbDictumAndQuotes(models.Model):
|
|||||||
u" Вступление перед цитатой, в HTML по правилам типографики</small>"
|
u" Вступление перед цитатой, в HTML по правилам типографики</small>"
|
||||||
)
|
)
|
||||||
szContent = models.TextField(
|
szContent = models.TextField(
|
||||||
max_length=256,
|
max_length=640,
|
||||||
default="",
|
default="",
|
||||||
verbose_name=u"Высказывание",
|
verbose_name=u"Изречение",
|
||||||
help_text=u"Не обязательно. Вступление перед цитатой."
|
help_text=u"Не обязательно."
|
||||||
)
|
)
|
||||||
szContentHTML = models.TextField(
|
szContentHTML = models.TextField(
|
||||||
default="",
|
default="",
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=u"Высказывание HTML",
|
verbose_name=u"Изречение HTML",
|
||||||
help_text=u"<b>Высказывание Крылатое</b> -- крылатое, пародоксальное и все такое"
|
help_text=u"Содержание цитаты, афоризма, высказывания…<br />"
|
||||||
|
u" Свёрстано в HTML по правилам типографики"
|
||||||
|
)
|
||||||
|
bTypograph = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
db_index=True,
|
||||||
|
verbose_name=u"Типографировать",
|
||||||
|
help_text=u"Применять типографику?"
|
||||||
|
)
|
||||||
|
bIsChecked = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
db_index=True,
|
||||||
|
verbose_name=u"Проверен",
|
||||||
|
help_text=u"Цитата проверена."
|
||||||
)
|
)
|
||||||
kAuthor = models.ForeignKey(
|
kAuthor = models.ForeignKey(
|
||||||
TbAuthor,
|
TbAuthor,
|
||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
on_delete=models.DO_NOTHING,
|
on_delete=models.DO_NOTHING,
|
||||||
verbose_name=u"Автор",
|
verbose_name=u"Автор",
|
||||||
help_text=u"Автор изречения или цитаты <b>(не обязательно, но желательно)</b>"
|
help_text=u"Автор изречения или цитаты <b>(не обязательно, но желательно)</b>"
|
||||||
@@ -310,6 +389,7 @@ class TbDictumAndQuotes(models.Model):
|
|||||||
TbOrigin,
|
TbOrigin,
|
||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
on_delete=models.DO_NOTHING,
|
on_delete=models.DO_NOTHING,
|
||||||
verbose_name=u"Источник",
|
verbose_name=u"Источник",
|
||||||
help_text=u"Откуда взята циатата, высказывание, изречение <b>(не обязательно, но желательно)</b>"
|
help_text=u"Откуда взята циатата, высказывание, изречение <b>(не обязательно, но желательно)</b>"
|
||||||
@@ -318,6 +398,7 @@ class TbDictumAndQuotes(models.Model):
|
|||||||
TbImages,
|
TbImages,
|
||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
on_delete=models.DO_NOTHING,
|
on_delete=models.DO_NOTHING,
|
||||||
verbose_name=u"Картинка",
|
verbose_name=u"Картинка",
|
||||||
help_text=u"Ссылка на картинку, в табличке картинок <b>(не обязательно)</b><br />"
|
help_text=u"Ссылка на картинку, в табличке картинок <b>(не обязательно)</b><br />"
|
||||||
@@ -373,44 +454,15 @@ class TbDictumAndQuotes(models.Model):
|
|||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
http = urllib3.PoolManager()
|
# Типографирование (szContent -> szContentHTML, szIntro -> szIntroHTML)
|
||||||
# последовательно
|
# перенесено в админку для управления параметрами (язык, переносы и т.д.)
|
||||||
# Используем типограф typus (https://github.com/byashimov/typus)
|
if not self.szContentHTML and self.szContent:
|
||||||
# Используем типограф Eugene Spearance (http://www.typograf.ru/)
|
self.szContentHTML = self.szContent
|
||||||
# Используем типограф Муравьева (http://mdash.ru/api.v1.php)
|
if not self.szIntroHTML and self.szIntro:
|
||||||
if self.szIntro != "" and self.szIntro != ru_typus(self.szIntro):
|
self.szIntroHTML = self.szIntro
|
||||||
# сравнение self.szIntro != ru_typus(self.szIntro) нужно для избежания повторных обращений
|
|
||||||
# к типографам при обновлении щетчиков просмотра
|
|
||||||
self.szIntro = ru_typus(self.szIntro)
|
|
||||||
resp = http.request("POST",
|
|
||||||
"http://www.typograf.ru/webservice/",
|
|
||||||
fields={"text": self.szIntro.replace("\u202f", " ").replace("\u2009", " ").encode('cp1251')})
|
|
||||||
self.szIntroHTML = resp.data.decode('cp1251')
|
|
||||||
# print(self.szIntroHTML)
|
|
||||||
resp = http.request("POST",
|
|
||||||
"http://mdash.ru/api.v1.php",
|
|
||||||
fields={"text": self.szIntroHTML.encode('utf-8')})
|
|
||||||
self.szIntroHTML = json.loads(resp.data)["result"]
|
|
||||||
# print(self.szIntroHTML)
|
|
||||||
else:
|
|
||||||
self.szIntroHTML = ""
|
|
||||||
if self.szContent != ru_typus(self.szContent):
|
|
||||||
# self.szContent != ru_typus(self.szContent) нужно для избежания повторных обращений
|
|
||||||
# к типографам при обновлении щетчиков просмотра
|
|
||||||
self.szContent = ru_typus(self.szContent)
|
|
||||||
resp = http.request("POST",
|
|
||||||
"http://www.typograf.ru/webservice/",
|
|
||||||
fields={"text": self.szContent.replace("\u202f", " ").replace("\u2009", " ").encode('cp1251')})
|
|
||||||
self.szContentHTML = resp.data.decode('cp1251')
|
|
||||||
print(self.szContentHTML)
|
|
||||||
resp = http.request("POST",
|
|
||||||
"http://mdash.ru/api.v1.php",
|
|
||||||
fields={"text": self.szContentHTML.encode('utf-8')})
|
|
||||||
self.szContentHTML = json.loads(resp.data)["result"]
|
|
||||||
# print(self.szContentHTML)
|
|
||||||
super(TbDictumAndQuotes, self).save(*args, **kwargs)
|
super(TbDictumAndQuotes, self).save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = u"ВЫСКАЗЫВАНИЕ"
|
verbose_name = u"ВЫСКАЗЫВАНИЕ"
|
||||||
verbose_name_plural = u"ВЫСКАЗЫВАНИЯ"
|
verbose_name_plural = u"ВЫСКАЗЫВАНИЯ"
|
||||||
ordering = ['id', ]
|
ordering = ['-id', ]
|
||||||
|
|||||||
20
dicquo/web/sitemaps.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.contrib.sitemaps import Sitemap
|
||||||
|
from web.models import TbDictumAndQuotes
|
||||||
|
import pytils
|
||||||
|
|
||||||
|
class DictumSitemap(Sitemap):
|
||||||
|
changefreq = "weekly" # Как часто меняются страницы
|
||||||
|
priority = 0.9 # Приоритет (от 0.0 до 1.0)
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
# Only show checked items in sitemap
|
||||||
|
return TbDictumAndQuotes.objects.filter(bIsChecked=True).order_by('-id')
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.dtEdited
|
||||||
|
|
||||||
|
def location(self, obj):
|
||||||
|
# Generates URL in format: /123_slug
|
||||||
|
slug = pytils.translit.slugify(obj.szContent.lower())[:120]
|
||||||
|
return f"/{obj.id}_{slug}"
|
||||||
|
|
||||||
@@ -1,104 +1,229 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
__author__ = "Sergei Erjemin"
|
__author__ = "Sergei Erjemin"
|
||||||
__copyright__ = "Copyright 2020, Sergei Erjemin"
|
__copyright__ = "Copyright 2020-2026, Sergei Erjemin"
|
||||||
__credits__ = ["Sergei Erjemin", ]
|
__credits__ = ["Sergei Erjemin", ]
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
__version__ = "0.0.1"
|
__version__ = "0.3.0"
|
||||||
__maintainer__ = "Sergei Erjemin"
|
__maintainer__ = "Sergei Erjemin"
|
||||||
__email__ = "erjemin@gmail.com"
|
__email__ = "erjemin@gmail.com"
|
||||||
__status__ = "in progress"
|
__status__ = "in progress"
|
||||||
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.views.generic import DetailView, TemplateView
|
||||||
import time
|
import time
|
||||||
import hashlib
|
|
||||||
import random
|
|
||||||
import pytils
|
import pytils
|
||||||
from taggit.models import Tag
|
from web.models import TbDictumAndQuotes, TbImages, TbAuthor
|
||||||
from web.models import TbOrigin, TbDictumAndQuotes, TbImages, TbAuthor
|
|
||||||
|
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
def for_dq(dq):
|
|
||||||
to_template = {}
|
class CommonContextMixin:
|
||||||
num = int(hashlib.blake2s(dq.szContent.encode("utf-8"), digest_size=1).hexdigest(), 16)
|
"""
|
||||||
clr = sorted([num / 2, num / 3, num / 5, num / 7, num / 11, num / 1.5], key=lambda A: random.random())
|
Общий миксин для представлений:
|
||||||
to_template.update({'CLR': clr})
|
- Логика "одной цитаты" (получение контекста цитаты)
|
||||||
to_template.update({'DQ': dq})
|
- Общий контекст (куки, тайминги)
|
||||||
|
"""
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
# Засекаем время в самом начале обработки запроса
|
||||||
|
self.t_start = time.process_time()
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_filtered_queryset(self):
|
||||||
|
"""
|
||||||
|
Возвращает (queryset, tag_slug) на основе GET-параметров запроса.
|
||||||
|
Если тега нет или он не найден, возвращает (None, None).
|
||||||
|
"""
|
||||||
|
tag_slug = self.request.GET.get('tag')
|
||||||
|
if not tag_slug:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
dq_qs = TbDictumAndQuotes.objects.all()
|
||||||
|
|
||||||
|
# 1. Пробуем найти цитаты, где АВТОР имеет этот тег
|
||||||
|
author_tag_qs = dq_qs.filter(kAuthor__tags__slug__in=[tag_slug])
|
||||||
|
if author_tag_qs.exists():
|
||||||
|
return author_tag_qs, tag_slug
|
||||||
|
|
||||||
|
# 2. Если авторов нет, ищем цитаты с этим тегом
|
||||||
|
quote_tag_qs = dq_qs.filter(tags__slug__in=[tag_slug])
|
||||||
|
if quote_tag_qs.exists():
|
||||||
|
return quote_tag_qs, tag_slug
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def get_dictum_context(self, request, dq, queryset=None):
|
||||||
|
"""
|
||||||
|
Получение контекста для цитаты dq. Если queryset передан, используется для логики "следующей цитаты"
|
||||||
|
и фильтрации по тегу.
|
||||||
|
"""
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
# Если queryset не передан, используем все объекты
|
||||||
|
if queryset is None:
|
||||||
|
queryset = TbDictumAndQuotes.objects.all()
|
||||||
|
|
||||||
|
# --- 1. ЛОГИКА ИСТОРИИ СЕССИИ (Предотвращение петель) ---
|
||||||
|
seen_ids = request.session.get('seen_ids', [])
|
||||||
|
|
||||||
|
# Если мы переключили контекст (например, выбрали другой тег), имеет смысл сбросить историю?
|
||||||
|
# Или можно оставить, так как уникальность ID глобальна.
|
||||||
|
# Проблема: если seen_ids забит цитатами, а мы выбрали тег, где всего 2 цитаты,
|
||||||
|
# и они обе случайно оказались в seen_ids (потому что мы их видели раньше без тега),
|
||||||
|
# то exclude исключит всё.
|
||||||
|
|
||||||
|
# Решение: принудительно добавить текущую цитату, если её нет
|
||||||
|
if dq.id not in seen_ids:
|
||||||
|
seen_ids.append(dq.id)
|
||||||
|
if len(seen_ids) > 100:
|
||||||
|
seen_ids.pop(0)
|
||||||
|
request.session['seen_ids'] = seen_ids
|
||||||
|
|
||||||
|
context.update({'DQ': dq})
|
||||||
|
# slag текущей цитаты
|
||||||
|
context.update({"DQ_SLUG": pytils.translit.slugify(dq.szContent.lower()[:120])})
|
||||||
|
|
||||||
|
# --- 3. АВТОР И ТЕГИ ---
|
||||||
try:
|
try:
|
||||||
au = TbAuthor.objects.get(id=dq.kAuthor_id)
|
au = TbAuthor.objects.get(id=dq.kAuthor_id)
|
||||||
to_template.update({'AUTHOR': au})
|
context.update({'AUTHOR': au})
|
||||||
tags = au.tags.names()
|
tags = au.tags.names()
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
tags = dq.tags.names()
|
tags = dq.tags.names()
|
||||||
|
|
||||||
tag_and_slug = []
|
tag_and_slug = []
|
||||||
for i in tags:
|
for i in tags:
|
||||||
tag_and_slug.append({"name": i, "slug": pytils.translit.slugify(i.lower())[:120]})
|
tag_and_slug.append({"name": i, "slug": pytils.translit.slugify(i.lower())[:120]})
|
||||||
to_template.update({'TAGS': sorted(tag_and_slug, key=lambda x: x["name"])}) # tag_and_slug
|
context.update({'TAGS': sorted(tag_and_slug, key=lambda x: x["name"])})
|
||||||
|
|
||||||
|
# --- 4. ВЫБОР КАРТИНКИ ---
|
||||||
if dq.kImages_id is None:
|
if dq.kImages_id is None:
|
||||||
if len(tags) != 0:
|
if len(tags) != 0:
|
||||||
try:
|
tagged_image = TbImages.objects.filter(tags__name__in=tags).order_by('?').first()
|
||||||
# tagged_image = TbImages.objects.filter(tags__name__in=tags).order_by('?').first()
|
if tagged_image:
|
||||||
tagged_image = TbImages.objects.filter(tags__name__in=tags)
|
context.update({'IMAGE': tagged_image.imFile})
|
||||||
random.shuffle(list(tagged_image))
|
|
||||||
to_template.update({'IMAGE': tagged_image[0].imFile})
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
to_template.update({'IMAGE': dq.kImages.imFile})
|
context.update({'IMAGE': dq.kImages.imFile})
|
||||||
|
|
||||||
|
# --- 5. СЧЕТЧИК ---
|
||||||
dq.iViewCounter += 1
|
dq.iViewCounter += 1
|
||||||
dq.save()
|
dq.save(update_fields=['iViewCounter'])
|
||||||
# dq_next = TbDictumAndQuotes.objects.exclude(id=dq.id).order_by('?').first()
|
|
||||||
dq_next = TbDictumAndQuotes.objects.exclude(id=dq.id)
|
|
||||||
random.shuffle(list(dq_next))
|
|
||||||
to_template.update({"NEXT": dq_next[0].id})
|
|
||||||
to_template.update({"NEXT_TXT": pytils.translit.slugify(dq_next[0].szContent.lower()[:120])})
|
|
||||||
return to_template
|
|
||||||
|
|
||||||
|
# --- 6. ВЫБОР СЛЕДУЮЩЕЙ ЦИТАТЫ ---
|
||||||
|
# Сначала пробуем найти следующую цитату, которую мы еще не видели
|
||||||
|
dq_next = queryset.exclude(id__in=seen_ids).order_by('?').first()
|
||||||
|
|
||||||
def by_id(request, dq_id):
|
# Если таких нет (мы посмотрели все цитаты в этом контексте/теге)
|
||||||
t_start = time.process_time()
|
if dq_next is None:
|
||||||
template = "index.html" # шаблон
|
# СБРОС ИСТОРИИ!
|
||||||
dq = TbDictumAndQuotes.objects.get(id=dq_id)
|
# Мы посмотрели всё, что было по этому фильтру. Начинаем круг заново.
|
||||||
to_template = for_dq(dq)
|
# Но удалять ВСЮ историю сессии опасно (вдруг мы вернемся в общий список).
|
||||||
# пероверка, что посетитель согласился со сбором даных через cookies
|
# Лучше локально для выбора следующей цитаты игнорировать историю,
|
||||||
if request.COOKIES.get('cookie_accept'):
|
# но возможно стоит очистить сессию, чтобы цикл начался чисто.
|
||||||
to_template.update({'cookie_accept': 1})
|
|
||||||
to_template.update({'ticks': float(time.process_time() - t_start)})
|
|
||||||
response = render(request, template, to_template)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
# Вариант: Очистить seen_ids, чтобы в следующий раз (на некст странице) список был пуст?
|
||||||
|
# Или просто выбрать любую КРОМЕ текущей?
|
||||||
|
|
||||||
def index(request):
|
dq_next = queryset.exclude(id=dq.id).order_by('?').first()
|
||||||
t_start = time.process_time()
|
|
||||||
# проверка на аутентификацию
|
# Если мы действительно прошли весь цикл по тегу, логично сбросить seen_ids,
|
||||||
# if not request.user.is_authenticated():
|
# чтобы пользователь мог заново проходить этот список случайно, а не "застревать" на последних.
|
||||||
# return HttpResponseRedirect("/access")
|
# Однако, очистка seen_ids здесь повлияет на глобальную сессию.
|
||||||
template = "index.html" # шаблон
|
# Если тег "red" (2 цитаты), мы их посмотрели. seen_ids=[1,2].
|
||||||
dq_ = TbDictumAndQuotes.objects
|
# queryset=[1,2]. exclude -> []. dq_next=None.
|
||||||
|
# Fallback: exclude(current) -> [1] (если cur=2). dq_next=1.
|
||||||
|
# User goes to 1. seen_ids=[1,2] (set logic handles dupes/order? No, list appends).
|
||||||
|
# seen_ids=[1,2,1].
|
||||||
|
# Next request (dq=1). queryset=[1,2]. exclude([1,2,1]) -> []. dq_next=2.
|
||||||
|
# It loops 1-2-1-2.
|
||||||
|
|
||||||
|
# Чтобы разорвать этот малый круг и сделать его снова "случайным" (если там >2 элементов, но меньше 100),
|
||||||
|
# нужно очистить seen_ids, если мы уткнулись в конец списка.
|
||||||
|
# Но удалять нужно только те ID, которые принадлежат этому queryset? Сложно.
|
||||||
|
# Проще очистить всё, так как пользователь явно "наелся" текущим контекстом и пошел по второму кругу.
|
||||||
|
request.session['seen_ids'] = []
|
||||||
|
|
||||||
|
if dq_next:
|
||||||
|
context.update({"NEXT": dq_next.id})
|
||||||
|
context.update({"NEXT_TXT": pytils.translit.slugify(dq_next.szContent.lower()[:120])})
|
||||||
|
|
||||||
|
# Если мы в режиме фильтрации (tag), передаем текущий тег в контекст
|
||||||
if request.GET.get('tag'):
|
if request.GET.get('tag'):
|
||||||
dq = dq_.filter(kAuthor__tags__slug__in=[request.GET['tag']]).order_by('?').first()
|
context.update({"CURRENT_TAG": request.GET.get('tag')})
|
||||||
if dq is None:
|
|
||||||
dq = dq_.filter(tags__slug__in=[request.GET['tag']]).order_by('?').first()
|
return context
|
||||||
if dq is None:
|
|
||||||
dq = dq_.order_by('?').first()
|
def finalize_context(self, context):
|
||||||
else:
|
"""
|
||||||
dq = dq_.first()
|
Добавляет общие данные: проверки куки и время выполнения.
|
||||||
to_template = for_dq(dq)
|
"""
|
||||||
# пероверка, что посетитель согласился со сбором даных через cookies
|
if self.request.COOKIES.get('cookie_accept'):
|
||||||
if request.COOKIES.get('cookie_accept'):
|
context['cookie_accept'] = 1
|
||||||
to_template.update({'cookie_accept': 1})
|
|
||||||
to_template.update({'ticks': float(time.process_time() - t_start)})
|
# Считаем время от self.t_start, заданного в dispatch
|
||||||
response = render(request, template, to_template)
|
total_time = 0.0
|
||||||
return response
|
if hasattr(self, 't_start'):
|
||||||
|
total_time = float(time.process_time() - self.t_start)
|
||||||
|
context['ticks'] = total_time * 1000
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def sitemap(request):
|
class DictumDetailView(CommonContextMixin, DetailView):
|
||||||
template = "sitemap.xml" # шаблон
|
model = TbDictumAndQuotes
|
||||||
to_template = []
|
template_name = "index.html"
|
||||||
dq = TbDictumAndQuotes.objects.order_by('id').all()
|
pk_url_kwarg = 'dq_id'
|
||||||
for i in dq:
|
context_object_name = 'DQ'
|
||||||
to_template.append({"ID": i.id,
|
|
||||||
"SLUG": pytils.translit.slugify(i.szContent.lower()[:120])})
|
def get_context_data(self, **kwargs):
|
||||||
response = render(request, template, {"DATA": to_template})
|
context = super().get_context_data(**kwargs)
|
||||||
return response
|
|
||||||
|
# Определяем контекст фильтрации (если есть тег в URL)
|
||||||
|
active_qs, _ = self.get_filtered_queryset()
|
||||||
|
|
||||||
|
# Используем миксин логики цитаты с учетом фильтра
|
||||||
|
extras = self.get_dictum_context(self.request, self.object, queryset=active_qs)
|
||||||
|
context.update(extras)
|
||||||
|
|
||||||
|
# Финализируем контекст (куки, тайминги)
|
||||||
|
return self.finalize_context(context)
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(CommonContextMixin, TemplateView):
|
||||||
|
template_name = "index.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
active_qs, _ = self.get_filtered_queryset()
|
||||||
|
|
||||||
|
dq = None
|
||||||
|
seen_ids = self.request.session.get('seen_ids', [])
|
||||||
|
|
||||||
|
if active_qs is not None:
|
||||||
|
# Если мы в режиме фильтрации, тоже стараемся не показывать то, что уже видели
|
||||||
|
dq = active_qs.exclude(id__in=seen_ids).order_by('?').first()
|
||||||
|
|
||||||
|
# Если после фильтрации ничего не осталось (мы просмотрели все цитаты тега)
|
||||||
|
if dq is None:
|
||||||
|
# Сбрасываем историю и берем любую
|
||||||
|
self.request.session['seen_ids'] = []
|
||||||
|
dq = active_qs.order_by('?').first()
|
||||||
|
|
||||||
|
if dq is None:
|
||||||
|
# Если тег не задан, или по тегу ничего не нашлось совсем
|
||||||
|
# Сбрасываем active_qs на "все", так как специфический контекст пуст
|
||||||
|
active_qs = TbDictumAndQuotes.objects.all()
|
||||||
|
|
||||||
|
# Случайная цитата (с учетом истории, чтобы главная страница тоже не зацикливалась)
|
||||||
|
dq = active_qs.exclude(id__in=seen_ids).order_by('?').first()
|
||||||
|
|
||||||
|
if dq is None:
|
||||||
|
self.request.session['seen_ids'] = []
|
||||||
|
dq = active_qs.order_by('?').first()
|
||||||
|
|
||||||
|
if dq:
|
||||||
|
# Используем миксин, ОБЯЗАТЕЛЬНО передаем active_qs
|
||||||
|
extras = self.get_dictum_context(self.request, dq, queryset=active_qs)
|
||||||
|
context.update(extras)
|
||||||
|
|
||||||
|
# Финализируем контекст (куки, тайминги)
|
||||||
|
return self.finalize_context(context)
|
||||||
|
|||||||
138
docker-compose.prod.yml
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
% cat docker-compose.yaml
|
||||||
|
# ==============================================================================
|
||||||
|
# Docker Compose для PRODUCTION
|
||||||
|
# Этот файл запускается на боевом сервере.
|
||||||
|
# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d
|
||||||
|
# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod.yml up -d
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# В новой версии Docker не нужно
|
||||||
|
# version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# --- ОCНОВНОЙ СЕРВИС: DJANGO + GUNICORN + WHITENOISE ---
|
||||||
|
web:
|
||||||
|
# Имя контейнера
|
||||||
|
container_name: dq-backend
|
||||||
|
|
||||||
|
# 1. ОБРАЗ
|
||||||
|
# В продакшене мы используем готовый, собранный образ из реестра (Gitea)
|
||||||
|
image: git.cube2.ru/erjemin/2020-dq:latest
|
||||||
|
# Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
|
||||||
|
# build: .
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# 2. Метки для Watchtower (авто-обновление)
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.scope=dq-scope"
|
||||||
|
|
||||||
|
# 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh)
|
||||||
|
# Выполняем цепочку команд внутри контейнера при запуске:
|
||||||
|
# a. Миграции
|
||||||
|
# b. Collectstatic
|
||||||
|
# с. Создаем папку nginx в примонтированном томе конфигов (если нет)
|
||||||
|
# d. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем
|
||||||
|
# через переменную окружения HOST_PROJECT_PATH)
|
||||||
|
# e. Инициализация боевого конфига (если нет)
|
||||||
|
# f. Создаем папку для ошибок и копируем туда статические страницы 404/500 (там их увидит Nginx хоста)
|
||||||
|
# g. Запуск Gunicorn
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py migrate --noinput &&
|
||||||
|
python manage.py collectstatic --noinput &&
|
||||||
|
mkdir -p /nginx_configs_host/nginx &&
|
||||||
|
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
|
||||||
|
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';
|
||||||
|
fi &&
|
||||||
|
mkdir -p /app/public/media/errors &&
|
||||||
|
cp /home/app/web/dicquo/templates/static_404.html /app/public/media/errors/404.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"
|
||||||
|
|
||||||
|
# 4. Проброс портов (Внешний Nginx -> localhost:8010)
|
||||||
|
ports:
|
||||||
|
# Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
|
||||||
|
- "127.0.0.1:8010:8000"
|
||||||
|
|
||||||
|
# 5. Тома (Volumes)
|
||||||
|
volumes:
|
||||||
|
# База данных
|
||||||
|
# Монтируем папку database с хоста в папку с базой внутри контейнера.
|
||||||
|
# Путь в контейнере: /home/app/web/database (так как Django ищет базу в BASE_DIR.parent/database)
|
||||||
|
- ./database:/home/app/web/database
|
||||||
|
|
||||||
|
# Медиа (папка media должна быть рядом с docker-compose.yml)
|
||||||
|
- ./media:/home/app/web/public/media
|
||||||
|
|
||||||
|
# Конфиги (Монтируем папку ./config с хоста в /nginx_configs_host внутри контейнера)
|
||||||
|
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
||||||
|
- ./config:/nginx_configs_host
|
||||||
|
|
||||||
|
# 6. Пользователь и права
|
||||||
|
user: "1000:1000"
|
||||||
|
|
||||||
|
# Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root.
|
||||||
|
# cap_add:
|
||||||
|
# - SYS_PTRACE
|
||||||
|
|
||||||
|
# 7. Переменные окружения
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=dicquo.settings
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||||
|
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# 10. Ресурсы
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.40'
|
||||||
|
memory: 256M
|
||||||
|
mem_limit: 256m
|
||||||
|
|
||||||
|
# --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ ---
|
||||||
|
# Следит за реестром Gitea и обновляет контейнер web, если появился новый image
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: dq_watchtower
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
# Токен/Логин для вашего приватного реестра (нужно добавить в .env!)
|
||||||
|
# REPO_USER и REPO_PASS должны быть в .env файле на сервере
|
||||||
|
- REPO_USER=${REPO_USER}
|
||||||
|
- REPO_PASS=${REPO_PASS}
|
||||||
|
- WATCHTOWER_SCOPE=dq-scope
|
||||||
|
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
|
||||||
|
- 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:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
61
docker-compose.yml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Docker Compose для РАЗРАБОТКИ (Local Development)
|
||||||
|
# Этот файл содержит настройки для локальной работы (live reload, debug).
|
||||||
|
# Запуск: docker compose up --build
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
# Имя контейнера для удобства
|
||||||
|
container_name: dq-backend-dev
|
||||||
|
|
||||||
|
# Сборка из текущей директории
|
||||||
|
build: .
|
||||||
|
|
||||||
|
# Проброс портов (чтобы сайт был доступен на localhost:8010)
|
||||||
|
ports:
|
||||||
|
- "8010:8000"
|
||||||
|
|
||||||
|
# 1. КОМАНДА ЗАПУСКА (Dev режим)
|
||||||
|
# Используем --reload для авто-перезагрузки при изменении кода.
|
||||||
|
# Уменьшаем число воркеров до 2 (экономия ресурсов dev-машины).
|
||||||
|
# Убираем collectstatic (в dev Django сам может отдавать статику или она нам не так важна сжатой)
|
||||||
|
# Но миграции оставляем, чтобы база была актуальной.
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py migrate --noinput &&
|
||||||
|
gunicorn --workers 2 --bind 0.0.0.0:8000 --reload dicquo.wsgi:application"
|
||||||
|
|
||||||
|
# 2. МОНТИРОВАНИЕ КОДА (Live Reload)
|
||||||
|
# Подключаем локальные папки внутрь контейнера, чтобы Gunicorn видел изменения без пересборки образа.
|
||||||
|
volumes:
|
||||||
|
# Монтируем основной код проекта.
|
||||||
|
# Так как web, templates и manage.py лежат внутри dicquo/, одного этого маунта достаточно.
|
||||||
|
- ./dicquo:/app/dicquo
|
||||||
|
|
||||||
|
# Монтируем всю папку public (Static + Media)
|
||||||
|
# Это нужно, чтобы:
|
||||||
|
# 1. Изменения в CSS/JS (public/static) сразу были видны (Live Reload).
|
||||||
|
# 2. Загруженные картинки (public/media) сохранялись на диске.
|
||||||
|
- ./public:/app/public
|
||||||
|
|
||||||
|
# Монтируем базу данных (чтобы данные сохранялись при пересоздании контейнера)
|
||||||
|
# Используем ту же папку database, что и на проде, для единообразия.
|
||||||
|
# ВАЖНО: Django ищет базу в BASE_DIR.parent / 'database/db.sqlite3'
|
||||||
|
# В контейнере BASE_DIR=/app/dicquo, значит путь к базе: /app/database/db.sqlite3
|
||||||
|
- ./database:/app/database
|
||||||
|
|
||||||
|
# 3. ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ
|
||||||
|
environment:
|
||||||
|
- DEBUG=True
|
||||||
|
- DJANGO_LOG_LEVEL=DEBUG
|
||||||
|
# В dev нам не нужно ограничивать буферизацию так строго, но не помешает.
|
||||||
|
|
||||||
|
# 4. РЕСУРСЫ (Без лимитов)
|
||||||
|
# Удаляем секцию ограничений, чтобы локально использовать все доступные ресурсы хоста.
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# limits:
|
||||||
|
# cpus: ...
|
||||||
|
# memory: ...
|
||||||
|
# mem_limit: ...
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/home/eserg/dq.cube2.ru/env/bin/python3
|
|
||||||
|
|
||||||
import sys, os
|
|
||||||
INTERP = "/home/eserg/dq.cube2.ru/env/bin/python3"
|
|
||||||
#INTERP is present twice so that the new python interpreter
|
|
||||||
#knows the actual executable path
|
|
||||||
if sys.executable != INTERP:
|
|
||||||
os.execl(INTERP, INTERP, *sys.argv)
|
|
||||||
|
|
||||||
cwd = os.getcwd()
|
|
||||||
sys.path.append(cwd)
|
|
||||||
sys.path.append(cwd + '/dicquo') #You must add your project here
|
|
||||||
|
|
||||||
sys.path.insert(0,cwd+'/env/bin')
|
|
||||||
sys.path.insert(0,cwd+'/env/lib/python3.8/site-packages')
|
|
||||||
|
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = "dicquo.settings"
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
application = get_wsgi_application()
|
|
||||||
|
|
||||||
|
|
||||||
##!/usr/bin/env python
|
|
||||||
#import sys, os
|
|
||||||
#cwd = os.getcwd()
|
|
||||||
#sys.path.append(cwd)
|
|
||||||
#sys.path.append(cwd + '/dicquo')
|
|
||||||
|
|
||||||
##Switch to new python
|
|
||||||
##if sys.version < "2.7.8": os.execl(cwd+"/env/bin/python", "python2.7", *sys.argv)
|
|
||||||
|
|
||||||
#sys.path.insert(0,cwd+'/env/bin')
|
|
||||||
#sys.path.insert(0,cwd+'/env/lib/python3.8/site-packages/django')
|
|
||||||
#sys.path.insert(0,cwd+'/env/lib/python3.8/site-packages')
|
|
||||||
|
|
||||||
#os.environ['DJANGO_SETTINGS_MODULE'] = "dicquo.settings"
|
|
||||||
#from django.core.wsgi import get_wsgi_application
|
|
||||||
#application = get_wsgi_application()
|
|
||||||
661
poetry.lock
generated
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asgiref"
|
||||||
|
version = "3.11.1"
|
||||||
|
description = "ASGI specs, helper code, and adapters"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"},
|
||||||
|
{file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.14.3"
|
||||||
|
description = "Screen-scraping library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7.0"
|
||||||
|
files = [
|
||||||
|
{file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"},
|
||||||
|
{file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
soupsieve = ">=1.6.1"
|
||||||
|
typing-extensions = ">=4.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cchardet = ["cchardet"]
|
||||||
|
chardet = ["chardet"]
|
||||||
|
charset-normalizer = ["charset-normalizer"]
|
||||||
|
html5lib = ["html5lib"]
|
||||||
|
lxml = ["lxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "6.0.3"
|
||||||
|
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.12"
|
||||||
|
files = [
|
||||||
|
{file = "django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3"},
|
||||||
|
{file = "django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
asgiref = ">=3.9.1"
|
||||||
|
sqlparse = ">=0.5.0"
|
||||||
|
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
argon2 = ["argon2-cffi (>=23.1.0)"]
|
||||||
|
bcrypt = ["bcrypt (>=4.1.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-appconf"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "A helper class for handling configuration defaults of packaged apps gracefully."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "django_appconf-1.2.0-py3-none-any.whl", hash = "sha256:b81bce5ef0ceb9d84df48dfb623a32235d941c78cc5e45dbb6947f154ea277f4"},
|
||||||
|
{file = "django_appconf-1.2.0.tar.gz", hash = "sha256:15a88d60dd942d6059f467412fe4581db632ef03018a3c183fb415d6fc9e5cec"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-environ"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
|
||||||
|
optional = false
|
||||||
|
python-versions = "<4,>=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e"},
|
||||||
|
{file = "django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"]
|
||||||
|
docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"]
|
||||||
|
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-select2"
|
||||||
|
version = "8.4.8"
|
||||||
|
description = "This is a Django_ integration of Select2_."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "django_select2-8.4.8-py3-none-any.whl", hash = "sha256:a2ce6a4c556dd2d4d57eb3753618d6f31f8d3910e9d9fa1b686d9340f50b14eb"},
|
||||||
|
{file = "django_select2-8.4.8.tar.gz", hash = "sha256:592e52effff2b5850cb7c98b265715b6704fb784699c4aedddfdd8ae1ffa1e81"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = ">=4.2"
|
||||||
|
django-appconf = ">=0.6.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-taggit"
|
||||||
|
version = "6.1.0"
|
||||||
|
description = "django-taggit is a reusable Django application for simple tagging."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "django_taggit-6.1.0-py3-none-any.whl", hash = "sha256:ab776264bbc76cb3d7e49e1bf9054962457831bd21c3a42db9138b41956e4cf0"},
|
||||||
|
{file = "django_taggit-6.1.0.tar.gz", hash = "sha256:c4d1199e6df34125dd36db5eb0efe545b254dec3980ce5dd80e6bab3e78757c3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=4.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "etpgrf"
|
||||||
|
version = "0.1.6.post1"
|
||||||
|
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "etpgrf-0.1.6.post1-py3-none-any.whl", hash = "sha256:0863b14385bdacdd405f137ca2ce6bdb6f683f0189e8c927196a1eee754366be"},
|
||||||
|
{file = "etpgrf-0.1.6.post1.tar.gz", hash = "sha256:984d201cff232a58c05b6f4455a50f822162520df829ad4d543bfe0b7fd962a9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
beautifulsoup4 = ">=4.10.0"
|
||||||
|
lxml = ">=4.9.0"
|
||||||
|
regex = ">=2022.1.18"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "25.1.0"
|
||||||
|
description = "WSGI HTTP Server for UNIX"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b"},
|
||||||
|
{file = "gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
packaging = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
eventlet = ["eventlet (>=0.40.3)"]
|
||||||
|
gevent = ["gevent (>=24.10.1)"]
|
||||||
|
http2 = ["h2 (>=4.1.0)"]
|
||||||
|
setproctitle = ["setproctitle"]
|
||||||
|
testing = ["coverage", "eventlet (>=0.40.3)", "gevent (>=24.10.1)", "h2 (>=4.1.0)", "httpx[http2]", "pytest", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"]
|
||||||
|
tornado = ["tornado (>=6.5.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lxml"
|
||||||
|
version = "6.0.2"
|
||||||
|
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"},
|
||||||
|
{file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"},
|
||||||
|
{file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"},
|
||||||
|
{file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"},
|
||||||
|
{file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"},
|
||||||
|
{file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"},
|
||||||
|
{file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"},
|
||||||
|
{file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"},
|
||||||
|
{file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"},
|
||||||
|
{file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"},
|
||||||
|
{file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"},
|
||||||
|
{file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"},
|
||||||
|
{file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"},
|
||||||
|
{file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"},
|
||||||
|
{file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"},
|
||||||
|
{file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"},
|
||||||
|
{file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"},
|
||||||
|
{file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"},
|
||||||
|
{file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"},
|
||||||
|
{file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"},
|
||||||
|
{file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"},
|
||||||
|
{file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"},
|
||||||
|
{file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"},
|
||||||
|
{file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"},
|
||||||
|
{file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"},
|
||||||
|
{file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"},
|
||||||
|
{file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cssselect = ["cssselect (>=0.7)"]
|
||||||
|
html-clean = ["lxml_html_clean"]
|
||||||
|
html5 = ["html5lib"]
|
||||||
|
htmlsoup = ["BeautifulSoup4"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.0"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
|
||||||
|
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "12.1.1"
|
||||||
|
description = "Python Imaging Library (fork)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"},
|
||||||
|
{file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"},
|
||||||
|
{file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"},
|
||||||
|
{file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"},
|
||||||
|
{file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"},
|
||||||
|
{file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"},
|
||||||
|
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"},
|
||||||
|
{file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"},
|
||||||
|
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"},
|
||||||
|
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"},
|
||||||
|
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"},
|
||||||
|
{file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"},
|
||||||
|
{file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"},
|
||||||
|
{file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
|
||||||
|
fpx = ["olefile"]
|
||||||
|
mic = ["olefile"]
|
||||||
|
test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"]
|
||||||
|
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
|
||||||
|
xmp = ["defusedxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytils"
|
||||||
|
version = "0.4.4"
|
||||||
|
description = "Russian-specific string utils"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "pytils-0.4.4-py3-none-any.whl", hash = "sha256:e54c16465a5fdb65d414e2da8045e6cc6de79889acda6143dcef2e1e86a1a840"},
|
||||||
|
{file = "pytils-0.4.4.tar.gz", hash = "sha256:9992a96caad57daa211584df1da4fd825f11e836d3ed93011785f1d02ab6f0ca"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "2026.2.28"
|
||||||
|
description = "Alternative regular expression module, to replace re."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5"},
|
||||||
|
{file = "regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61"},
|
||||||
|
{file = "regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4"},
|
||||||
|
{file = "regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8"},
|
||||||
|
{file = "regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9"},
|
||||||
|
{file = "regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec"},
|
||||||
|
{file = "regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soupsieve"
|
||||||
|
version = "2.8.3"
|
||||||
|
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"},
|
||||||
|
{file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.5.5"
|
||||||
|
description = "A non-validating SQL parser."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
|
||||||
|
{file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["build"]
|
||||||
|
doc = ["sphinx"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tqdm"
|
||||||
|
version = "4.67.3"
|
||||||
|
description = "Fast, Extensible Progress Meter"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf"},
|
||||||
|
{file = "tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"]
|
||||||
|
discord = ["requests"]
|
||||||
|
notebook = ["ipywidgets (>=6)"]
|
||||||
|
slack = ["slack-sdk"]
|
||||||
|
telegram = ["requests"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.3"
|
||||||
|
description = "Provider of IANA time zone data"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2"
|
||||||
|
files = [
|
||||||
|
{file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"},
|
||||||
|
{file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "whitenoise"
|
||||||
|
version = "6.12.0"
|
||||||
|
description = "Radically simplified static file serving for WSGI applications"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2"},
|
||||||
|
{file = "whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.12"
|
||||||
|
content-hash = "fb71f5e7011418134e09152004d990412cf923433f675398e5e99439bf9653b9"
|
||||||
2
poetry.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[virtualenvs]
|
||||||
|
in-project = true
|
||||||
4
public/BingSiteAuth.xml
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<users>
|
||||||
|
<user>D3BB27DEC758800CCB5E391674DF4212</user>
|
||||||
|
</users>
|
||||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
@@ -1,53 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="46" viewBox="0 0 1512 1386" shape-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd"><defs><linearGradient id="A" gradientUnits="userSpaceOnUse" x1="1476.52" y1="1395.4" x2="1034.86" y2="872.762"><stop offset="0" stop-color="#ef7f1a"></stop><stop offset=".349" stop-color="#ffed00"></stop><stop offset=".949" stop-color="#cc6f3c"></stop></linearGradient><linearGradient id="B" gradientUnits="userSpaceOnUse" x1="1023.98" y1="1127.45" x2="575.667" y2="596.929"><stop offset="0" stop-color="#ef7f1a"></stop><stop offset=".059" stop-color="#cc6f3c"></stop><stop offset=".671" stop-color="#ffed00"></stop><stop offset="1" stop-color="#cc6f3c"></stop></linearGradient><linearGradient id="C" href="#A" x1="1398.9" y1="1160.31" x2="613.548" y2="230.976"></linearGradient><linearGradient id="D" href="#A" x1="805.595" y1="948.571" x2="21.024" y2="20.143"></linearGradient><linearGradient id="E" gradientUnits="userSpaceOnUse" x1="381.071" y1="958.69" x2="89.071" y2="613.119"><stop offset="0" stop-color="#ef7f1a"></stop><stop offset=".831" stop-color="#ffed00"></stop><stop offset=".949" stop-color="#cc6f3c"></stop></linearGradient></defs><path d="M746 677L405 970c21-19 37-44 49-75 14-37 20-83 20-137 0-47-6-92-19-135s-32-82-58-118-57-67-95-95c-38-27-84-50-139-68L27 298c-6-2-12-2-18 1-1 0-2 1-3 2L351 5c1-1 2-2 4-2 6-3 12-3 18-1l136 44c55 18 101 41 139 68s69 59 95 95c25 36 45 75 58 118s19 88 19 135c0 54-7 100-20 137-12 34-30 60-53 79z" fill="url(#D)"></path><path d="M1005 1173c12-10 22-22 30-35 10-16 18-34 25-54 6-20 11-41 14-65 3-23 4-48 4-73 0-50-6-97-17-141-12-44-29-83-51-119-23-35-51-66-85-92s-74-47-119-62c-48-16-90-22-126-17-36 4-66 17-91 40l345-295c24-22 54-34 90-39 36-4 78 2 126 17 45 15 85 35 119 62 34 26 63 57 85 92 23 35 40 75 51 119s17 90 17 141c0 26-2 50-4 73-3 23-8 45-14 65s-15 38-25 54c-9 14-20 27-33 37l-341 292z" fill="url(#C)"></path><path d="M409 739c0-38-5-74-14-109-10-35-24-68-44-97-20-30-45-56-75-79s-69-42-116-58l-99-33v538l100 33c44 14 81 21 111 20s56-9 76-25 35-40 46-71c10-31 15-71 15-118zm65 18c0 54-7 100-20 137-14 37-34 65-60 84-27 19-59 30-98 31s-86-7-140-24L29 943c-6-2-12-6-18-13S1 914 1 902V319c0-12 3-19 10-22 6-3 12-3 18-1l136 44c55 18 101 41 139 68s69 59 95 95c25 36 45 75 58 118s19 88 19 135z" fill="#ae4a84" fill-rule="nonzero"></path><path d="M1343 884l-333 285c-3 3-7 6-10 9 26 30 49 54 68 73l48 43c13 10 22 18 29 23s12 9 15 13 5 8 6 13 2 10 2 17c0 5 0 10-1 13s-2 6-4 8l-2 2 344-294c1 0 1-1 2-2 2-2 3-5 4-8 1-4 1-8 1-13 0-7-1-12-2-17s-3-9-6-13-8-8-15-13-17-13-29-23c-13-10-29-25-48-43-20-19-42-43-68-73z" fill="url(#A)"></path><path d="M1015 931c0-38-4-75-11-111s-19-69-36-99-38-57-66-80c-27-23-61-41-102-54-40-13-74-17-101-11-28 6-50 18-67 38-17 19-29 44-37 75s-11 65-11 101c0 39 3 77 10 113s18 69 35 99c16 30 38 57 66 80 27 23 62 41 103 55 41 13 75 17 103 11s50-19 67-39 29-45 36-76 11-65 11-102zm153 430c0 5 0 10-1 13s-2 6-4 8-3 3-5 3c-2 1-3 0-5 0-4-1-12-7-26-16-13-10-29-22-48-39-19-16-39-35-61-57s-44-46-66-72c-17 6-39 9-65 9-27 0-58-6-94-18-48-16-89-37-124-63-35-27-63-57-85-93-22-35-38-75-49-119-10-44-16-91-16-142s6-95 18-132 29-67 53-88 53-34 89-38 78 2 126 17c45 15 85 35 119 62 34 26 63 57 85 92 23 35 40 75 51 119s17 90 17 141c0 26-2 50-4 73-3 23-8 45-14 65s-15 38-25 54-22 29-36 40c26 30 49 54 68 73l48 43c13 10 22 18 29 23s12 9 15 13 5 8 6 13 2 10 2 17z" fill="#ae4a84" fill-rule="nonzero"></path><path d="M1015 931c0-38-4-75-11-111s-19-69-36-99-38-57-66-80c-27-23-61-41-102-54-40-13-74-17-101-11-28 6-50 18-67 38-17 19-29 44-37 75s-11 65-11 101c0 39 3 77 10 113s18 69 35 99c16 30 38 57 66 80 27 23 62 41 103 55 41 13 75 17 103 11s50-19 67-39 29-45 36-76 11-65 11-102z" fill="url(#B)"></path><path d="M61 900l330-283c1 4 2 8 4 12 9 35 14 72 14 109 0 48-5 87-15 118s-25 55-46 71c-20 16-45 24-76 25s-67-6-111-20L61 899z" fill="url(#E)"></path><path d="M61 900l330-283c-9-31-23-59-40-85-20-30-45-56-75-79s-69-42-116-58l-99-33v538z" fill="#ffd900"></path></svg>
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Creator: CorelDRAW X6 -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="120px" height="120px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
|
||||||
viewBox="0 0 1512 1386"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<defs>
|
|
||||||
<style type="text/css">
|
|
||||||
<![CDATA[
|
|
||||||
.fil6 {fill:#FFD900}
|
|
||||||
.fil2 {fill:#AE4A84;fill-rule:nonzero}
|
|
||||||
.fil3 {fill:url(#id0)}
|
|
||||||
.fil4 {fill:url(#id1)}
|
|
||||||
.fil1 {fill:url(#id2)}
|
|
||||||
.fil0 {fill:url(#id3)}
|
|
||||||
.fil5 {fill:url(#id4)}
|
|
||||||
]]>
|
|
||||||
</style>
|
|
||||||
<linearGradient id="id0" gradientUnits="userSpaceOnUse" x1="1476.52" y1="1395.4" x2="1034.86" y2="872.762">
|
|
||||||
<stop offset="0" style="stop-color:#EF7F1A"/>
|
|
||||||
<stop offset="0.34902" style="stop-color:#FFED00"/>
|
|
||||||
<stop offset="0.94902" style="stop-color:#CC6F3C"/>
|
|
||||||
<stop offset="1" style="stop-color:#CC6F3C"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id1" gradientUnits="userSpaceOnUse" x1="1023.98" y1="1127.45" x2="575.667" y2="596.929">
|
|
||||||
<stop offset="0" style="stop-color:#EF7F1A"/>
|
|
||||||
<stop offset="0.0588235" style="stop-color:#CC6F3C"/>
|
|
||||||
<stop offset="0.670588" style="stop-color:#FFED00"/>
|
|
||||||
<stop offset="1" style="stop-color:#CC6F3C"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id2" gradientUnits="userSpaceOnUse" xlink:href="#id0" x1="1398.9" y1="1160.31" x2="613.548" y2="230.976">
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id3" gradientUnits="userSpaceOnUse" xlink:href="#id0" x1="805.595" y1="948.571" x2="21.0238" y2="20.1429">
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id4" gradientUnits="userSpaceOnUse" x1="381.071" y1="958.69" x2="89.0714" y2="613.119">
|
|
||||||
<stop offset="0" style="stop-color:#EF7F1A"/>
|
|
||||||
<stop offset="0.831373" style="stop-color:#FFED00"/>
|
|
||||||
<stop offset="0.94902" style="stop-color:#CC6F3C"/>
|
|
||||||
<stop offset="1" style="stop-color:#CC6F3C"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g id="Layer_x0020_1">
|
|
||||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
|
||||||
<path class="fil0" d="M746 677l-341 293c21,-19 37,-44 49,-75 14,-37 20,-83 20,-137 0,-47 -6,-92 -19,-135 -13,-43 -32,-82 -58,-118 -26,-36 -57,-67 -95,-95 -38,-27 -84,-50 -139,-68l-136 -44c-6,-2 -12,-2 -18,1 -1,0 -2,1 -3,2l345 -296c1,-1 2,-2 4,-2 6,-3 12,-3 18,-1l136 44c55,18 101,41 139,68 38,27 69,59 95,95 25,36 45,75 58,118 13,43 19,88 19,135 0,54 -7,100 -20,137 -12,34 -30,60 -53,79z"/>
|
|
||||||
<path class="fil1" d="M1005 1173c12,-10 22,-22 30,-35 10,-16 18,-34 25,-54 6,-20 11,-41 14,-65 3,-23 4,-48 4,-73 0,-50 -6,-97 -17,-141 -12,-44 -29,-83 -51,-119 -23,-35 -51,-66 -85,-92 -34,-26 -74,-47 -119,-62 -48,-16 -90,-22 -126,-17 -36,4 -66,17 -91,40l345 -295c24,-22 54,-34 90,-39 36,-4 78,2 126,17 45,15 85,35 119,62 34,26 63,57 85,92 23,35 40,75 51,119 11,44 17,90 17,141 0,26 -2,50 -4,73 -3,23 -8,45 -14,65 -6,20 -15,38 -25,54 -9,14 -20,27 -33,37l-341 292z"/>
|
|
||||||
<path class="fil2" d="M409 739c0,-38 -5,-74 -14,-109 -10,-35 -24,-68 -44,-97 -20,-30 -45,-56 -75,-79 -30,-23 -69,-42 -116,-58l-99 -33 0 538 100 33c44,14 81,21 111,20 30,-1 56,-9 76,-25 20,-16 35,-40 46,-71 10,-31 15,-71 15,-118zm65 18c0,54 -7,100 -20,137 -14,37 -34,65 -60,84 -27,19 -59,30 -98,31 -39,1 -86,-7 -140,-24l-127 -42c-6,-2 -12,-6 -18,-13 -6,-7 -10,-16 -10,-28l0 -583c0,-12 3,-19 10,-22 6,-3 12,-3 18,-1l136 44c55,18 101,41 139,68 38,27 69,59 95,95 25,36 45,75 58,118 13,43 19,88 19,135z"/>
|
|
||||||
<path class="fil3" d="M1343 884l-333 285c-3,3 -7,6 -10,9 26,30 49,54 68,73 20,18 36,33 48,43 13,10 22,18 29,23 7,5 12,9 15,13 3,4 5,8 6,13 1,5 2,10 2,17 0,5 0,10 -1,13 -1,3 -2,6 -4,8 -1,1 -1,1 -2,2l344 -294c1,0 1,-1 2,-2 2,-2 3,-5 4,-8 1,-4 1,-8 1,-13 0,-7 -1,-12 -2,-17 -1,-5 -3,-9 -6,-13 -3,-4 -8,-8 -15,-13 -7,-5 -17,-13 -29,-23 -13,-10 -29,-25 -48,-43 -20,-19 -42,-43 -68,-73z"/>
|
|
||||||
<path class="fil2" d="M1015 931c0,-38 -4,-75 -11,-111 -7,-36 -19,-69 -36,-99 -17,-30 -38,-57 -66,-80 -27,-23 -61,-41 -102,-54 -40,-13 -74,-17 -101,-11 -28,6 -50,18 -67,38 -17,19 -29,44 -37,75 -8,31 -11,65 -11,101 0,39 3,77 10,113 7,36 18,69 35,99 16,30 38,57 66,80 27,23 62,41 103,55 41,13 75,17 103,11 28,-6 50,-19 67,-39 17,-20 29,-45 36,-76 7,-31 11,-65 11,-102zm153 430c0,5 0,10 -1,13 -1,3 -2,6 -4,8 -2,2 -3,3 -5,3 -2,1 -3,0 -5,0 -4,-1 -12,-7 -26,-16 -13,-10 -29,-22 -48,-39 -19,-16 -39,-35 -61,-57 -22,-22 -44,-46 -66,-72 -17,6 -39,9 -65,9 -27,0 -58,-6 -94,-18 -48,-16 -89,-37 -124,-63 -35,-27 -63,-57 -85,-93 -22,-35 -38,-75 -49,-119 -10,-44 -16,-91 -16,-142 0,-51 6,-95 18,-132 12,-37 29,-67 53,-88 24,-21 53,-34 89,-38 36,-4 78,2 126,17 45,15 85,35 119,62 34,26 63,57 85,92 23,35 40,75 51,119 11,44 17,90 17,141 0,26 -2,50 -4,73 -3,23 -8,45 -14,65 -6,20 -15,38 -25,54 -10,16 -22,29 -36,40 26,30 49,54 68,73 20,18 36,33 48,43 13,10 22,18 29,23 7,5 12,9 15,13 3,4 5,8 6,13 1,5 2,10 2,17z"/>
|
|
||||||
<path class="fil4" d="M1015 931c0,-38 -4,-75 -11,-111 -7,-36 -19,-69 -36,-99 -17,-30 -38,-57 -66,-80 -27,-23 -61,-41 -102,-54 -40,-13 -74,-17 -101,-11 -28,6 -50,18 -67,38 -17,19 -29,44 -37,75 -8,31 -11,65 -11,101 0,39 3,77 10,113 7,36 18,69 35,99 16,30 38,57 66,80 27,23 62,41 103,55 41,13 75,17 103,11 28,-6 50,-19 67,-39 17,-20 29,-45 36,-76 7,-31 11,-65 11,-102z"/>
|
|
||||||
<path class="fil5" d="M61 900l330 -283c1,4 2,8 4,12 9,35 14,72 14,109 0,48 -5,87 -15,118 -10,31 -25,55 -46,71 -20,16 -45,24 -76,25 -31,1 -67,-6 -111,-20l-100 -33z"/>
|
|
||||||
<path class="fil6" d="M61 900l330 -283c-9,-31 -23,-59 -40,-85 -20,-30 -45,-56 -75,-79 -30,-23 -69,-42 -116,-58l-99 -33 0 538z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 3.9 KiB |
1
public/googleacb673eb0c31969f.html
Executable file
@@ -0,0 +1 @@
|
|||||||
|
google-site-verification: googleea5d443319529114.html
|
||||||
1
public/googleea5d443319529114.html
Executable file
@@ -0,0 +1 @@
|
|||||||
|
google-site-verification: googleacb673eb0c31969f.html
|
||||||
28
public/llms.txt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Dicquo (Dictum & Quotes)
|
||||||
|
|
||||||
|
> Dicquo — это коллекция отобранных вручную цитат, оформленных с уважением к типографике. Место для вдумчивого чтения.
|
||||||
|
|
||||||
|
## О проекте
|
||||||
|
|
||||||
|
Этот сайт представляет собой минималистичную коллекцию афоризмов, цитат и высказываний известных людей мира музыки.
|
||||||
|
Основной акцент сделан на качестве текста (типографика) и визуальной подаче ("медитативный" дизайн).
|
||||||
|
|
||||||
|
## Структура контента
|
||||||
|
|
||||||
|
Каждая страница содержит одну цитату.
|
||||||
|
Основные сущности:
|
||||||
|
- **Цитата (Dictum)**: Текст высказывания.
|
||||||
|
- **Автор (Author)**: Имя автора.
|
||||||
|
- **Теги (Tags)**: Ключевые слова.
|
||||||
|
- **Интро (Intro)**: Контекст или вступление к цитате (опционально).
|
||||||
|
|
||||||
|
## Доступ к данным
|
||||||
|
|
||||||
|
- **Sitemap**: /sitemap.xml — полная карта сайта со всеми цитатами.
|
||||||
|
- **Главная**: / — показывает случайную цитату (или последнюю, в зависимости от логики).
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Контент (цитаты) принадлежит их авторам.
|
||||||
|
Оформление и подборка © Sergei Erjemin.
|
||||||
|
|
||||||
27
public/media/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Папка для медиа-файлов (Media)
|
||||||
|
|
||||||
|
Эта директория предназначена для хранения загружаемого контента (изображений, документов), который генерируется или загружается пользователями в процессе работы Django-приложения.
|
||||||
|
|
||||||
|
## Как это работает с Docker
|
||||||
|
|
||||||
|
В `docker-compose.prod.yml` настроено монтирование этой папки как Docker Volume:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./media:/app/public/media
|
||||||
|
```
|
||||||
|
|
||||||
|
* **На хосте (сервере):** Файлы физически хранятся в папке `media` рядом с `docker-compose.yml`.
|
||||||
|
* **В контейнере:** Django видит их по пути `/app/public/media` (настройка `MEDIA_ROOT`).
|
||||||
|
* **Nginx:** Внешний Nginx настроен на прямую отдачу файлов из этой папки хоста, минуя Django.
|
||||||
|
|
||||||
|
## Содержимое репозитория
|
||||||
|
|
||||||
|
В репозитории эта папка содержит только этот `README.md`.
|
||||||
|
При развертывании контейнера через docker-compose (с volume `./media:/app/public/media`), содержимое этой папки на хосте становится доступным приложению.
|
||||||
|
|
||||||
|
**Автоматически создаваемые файлы:**
|
||||||
|
При старте контейнера в этой папке автоматически создается каталог `errors/`, куда копируются статические HTML-файлы для отображения ошибок (404.html, 500.html), чтобы внешний Nginx мог их отдавать, даже если Django, gunicorn или весь контейнер недоступен.
|
||||||
|
|
||||||
|
> **Важно:** Убедитесь, что у пользователя, под которым работает Nginx (обычно `www-data`), есть права на чтение этой директории и файлов в ней.
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 156 KiB |
@@ -1,7 +1,14 @@
|
|||||||
# DicQuo
|
# DicQuo
|
||||||
User-Agent: *
|
User-Agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
Disallow:
|
Disallow: /*?tag=
|
||||||
|
Disallow: /*?
|
||||||
|
|
||||||
|
# Optimize for Yandex
|
||||||
|
Clean-param: tag /
|
||||||
|
|
||||||
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
|
||||||
23
public/site.webmanifest
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "DicQuo",
|
||||||
|
"short_name": "DicQuo",
|
||||||
|
"description": "Коллекция цитат и афоризмов.",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/favicon.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/web-app-manifest-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -1,17 +1,209 @@
|
|||||||
@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; /* Очень плавное появление */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*****************************************************************
|
/* Header */
|
||||||
* Настройки для анимирования цвета ссылок:
|
header {
|
||||||
* рецепт взят из: https://habr.com/ru/company/ruvds/blog/491702/
|
display: flex;
|
||||||
*****************************************************************/
|
justify-content: space-between;
|
||||||
.tags a {
|
align-items: center;
|
||||||
|
padding: 1vmin 4vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > #logo {
|
||||||
|
margin-top: 1vmin;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > #logo a {
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > #logo a > img {
|
||||||
|
width:50px;
|
||||||
|
height:46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > nav {
|
||||||
|
border: #555555;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > nav > a { /* бургер */
|
||||||
|
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;
|
||||||
|
width: 0.9em;
|
||||||
|
height: 0.9em;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin-right: .2em;
|
||||||
|
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) */
|
||||||
|
.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eye Icon (Views) */
|
||||||
|
.icon-views {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 0.5ex;
|
padding: 0 0.5ex;
|
||||||
@@ -29,106 +221,110 @@
|
|||||||
background-position: 100%;
|
background-position: 100%;
|
||||||
/* плавное позиионирование градиента */
|
/* плавное позиионирование градиента */
|
||||||
transition: background-position 0.65s ease;
|
transition: background-position 0.65s ease;
|
||||||
margin-right: 2vh;
|
margin-right: 2vmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags a:hover {
|
nav > div a:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-position: 0 100%;
|
background-position: 0 100%;
|
||||||
border-bottom: solid 1px white;
|
border-bottom: solid 1px white;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[name="cookies_accept"] {
|
nav > div > div {
|
||||||
font-family:'Roboto', 'Lucida Grande', Verdana, Arial, sans-serif;
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > div > div a {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ПОДВАЛ-КУКИ (ДЗЕН-СТИЛЬ) --- */
|
||||||
|
footer {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0; left: 0;
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 2vh 2vw;
|
padding: 2vmin 4vmin;
|
||||||
color: black;
|
color: silver; /* Мягкий серый цвет текста */
|
||||||
background-color: gray;
|
background-color: rgba(30, 30, 30, 0.8); /* Темный полупрозрачный фон */
|
||||||
|
backdrop-filter: blur(5px); /* Эффект матового стекла (современно и медитативно) */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-top: 1px solid #444; /* Тонкая грань */
|
||||||
|
font-size: 0.9em;
|
||||||
|
z-index: 1000; /* Чтобы точно было поверх всего */
|
||||||
}
|
}
|
||||||
|
|
||||||
div[name="cookies_accept"] button {
|
footer small {
|
||||||
padding:0.5vh 0.5vw;
|
display: inline-block;
|
||||||
background: silver;
|
margin-right: 2vmin;
|
||||||
color: black;
|
letter-spacing: 0.05em; /* Немного воздуха в тексте */
|
||||||
margin-left: 2vw;
|
}
|
||||||
|
|
||||||
|
footer button {
|
||||||
|
padding: 0.5vmin 1.5vmin;
|
||||||
|
background: transparent;
|
||||||
|
color: silver;
|
||||||
|
border: 1px solid silver;
|
||||||
|
border-radius: 2em; /* Округлые, мягкие формы */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9em;
|
||||||
|
transition: all 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
footer button:hover {
|
||||||
margin-top:1vh;
|
background: silver;
|
||||||
filter: alpha(Opacity=75); /* Полупрозрачность для IE */
|
color: #111;
|
||||||
opacity: 0.75;
|
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2); /* Легкое свечение при наведении */
|
||||||
float: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo a {
|
/* Отзывчивость: для мобильных устройств колонки свапаются */
|
||||||
border: none;
|
@media (max-width: 768px) {
|
||||||
text-decoration: none;
|
main > article {
|
||||||
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
table { width: 80%; }
|
main > article > div {
|
||||||
|
flex: 0 0 auto;
|
||||||
#menu {
|
margin: 8vmin 0 2vmin 0;
|
||||||
display: none;
|
|
||||||
color: silver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#mm {
|
main > article > div {
|
||||||
text-decoration: none;
|
width: 80vmin;
|
||||||
color: silver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#image {
|
main > article > div > div {
|
||||||
width: 30vw;
|
width: 36vmax;
|
||||||
text-align: center;
|
height: 36vmax;
|
||||||
vertical-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#image > center > div {
|
main > article > div > div > div > img {
|
||||||
width: 22vw;
|
height: 36vmax;
|
||||||
height: 22vw;
|
|
||||||
padding:0.5vw;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#image > center > div > div {
|
/* Для мобильных путь иконка будет поменьше и поскромнее */
|
||||||
border-radius:50%;
|
header > #logo a > img {
|
||||||
overflow: hidden;
|
width:25px;
|
||||||
display: flex;
|
height:23px;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#image > center > div > div > img {
|
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
|
||||||
width: auto;
|
/* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
|
||||||
height: 22vw;
|
.etp-laquo { margin-left: -0.49em; } /* « */
|
||||||
|
.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ “ */
|
||||||
|
.etp-lsquo { margin-left: -0.22em; } /* ’ */
|
||||||
|
.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 {
|
||||||
|
border: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#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; }
|
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
116
public/static/js/bg-generator.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// bg-generator.js:
|
||||||
|
// - Генерирует уникальный градиентный фон на основе текстового содержимого
|
||||||
|
// - Реализует плавное появление и исчезновение при навигации
|
||||||
|
// - Авто-редирект через 15 секунд для создания "медитативного" слайд-шоу эффекта
|
||||||
|
// - Обрабатывает принятие Cookie
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// 1. Получаем текст для хеширования (из скрытого span в base.html)
|
||||||
|
const rawSpan = document.getElementById('dq-content-raw');
|
||||||
|
let text = rawSpan ? rawSpan.innerText.trim() : "";
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
text = "DictumAndQuotesDefault" + Math.random(); // Случайный вариант, если текста нет
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Хеш-функция (DJB2)
|
||||||
|
let hash = 5381;
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
// Принудительная 32-битная целочисленная арифметика
|
||||||
|
hash = ((hash << 5) + hash) + text.charCodeAt(i);
|
||||||
|
hash = hash & hash; // Преобразование в 32-битное целое
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Детерминированная генерация 6 цветовых компонентов из хеша
|
||||||
|
// Нам нужно 6 чисел от 0 до 255.
|
||||||
|
// Используем генератор псевдослучайных чисел с затравкой из хеша
|
||||||
|
|
||||||
|
function Mulberry32(a) {
|
||||||
|
return function() {
|
||||||
|
var t = a += 0x6D2B79F5;
|
||||||
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rand = Mulberry32(hash); // Генератор случайных чисел с seed
|
||||||
|
|
||||||
|
// Генерация 6 цветовых компонентов в темном диапазоне для "медитативного" ощущения
|
||||||
|
let colors = [];
|
||||||
|
for(let i=0; i<6; i++) {
|
||||||
|
// Генерируем число от 10 до 80 (темные цвета)
|
||||||
|
colors.push(Math.floor(rand() * 70) + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Немного перетасовываем на основе случайности, чтобы позволить вариации при обновлении (опционально)
|
||||||
|
colors.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
const rgb1 = `rgb(${colors[0]}, ${colors[1]}, ${colors[2]})`;
|
||||||
|
const rgb2 = `rgb(${colors[3]}, ${colors[4]}, ${colors[5]})`;
|
||||||
|
|
||||||
|
console.log("DQ BG Generator:", text.substring(0, 20) + "...", hash, rgb1, rgb2);
|
||||||
|
|
||||||
|
// 4. Применяем к body.
|
||||||
|
// Используем линейный градиент вправо со стандартным синтаксисом
|
||||||
|
const bgString = `linear-gradient(90deg, ${rgb1} 0%, ${rgb2} 100%)`;
|
||||||
|
document.body.style.background = bgString;
|
||||||
|
|
||||||
|
// 5. Применяем к контейнеру фона изображения (если он есть на главной странице)
|
||||||
|
const imgBgContainer = document.querySelector('.image-col center > div');
|
||||||
|
if (imgBgContainer) {
|
||||||
|
// Используем первый цвет градиента с прозрачностью 0.7
|
||||||
|
imgBgContainer.style.background = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.7)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Показываем контент (эффект плавного появления - Fade In)
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.style.opacity = 1;
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// 7. Обработка плавного исчезновения (Fade Out) при клике по ссылкам
|
||||||
|
document.body.addEventListener('click', function(e) {
|
||||||
|
// Ищем, была ли нажата ссылка (всплытие)
|
||||||
|
const link = e.target.closest('a');
|
||||||
|
if (link && link.href && link.target !== '_blank') {
|
||||||
|
const hrefAttr = link.getAttribute('href');
|
||||||
|
if (hrefAttr && !hrefAttr.startsWith('#') && !link.href.includes('javascript:')) {
|
||||||
|
// Проверяем, является ли ссылка внутренней (тот же домен)
|
||||||
|
if (new URL(link.href).origin === window.location.origin) {
|
||||||
|
e.preventDefault(); // Останавливаем немедленный переход
|
||||||
|
document.body.style.opacity = 0; // Запускаем Fade Out
|
||||||
|
|
||||||
|
// Ждем завершения перехода (соответствует времени CSS transition 0.9s (было 1.5s))
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = link.href;
|
||||||
|
}, 900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. Авто-редирект ("медитативное" слайд-шоу)
|
||||||
|
// Ищем ссылку "ДАЛЕЕ" и симулируем клик по ней через 15 секунд
|
||||||
|
const nextLink = document.querySelector('#next a');
|
||||||
|
if (nextLink) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Вызываем событие клика по ссылке, чтобы наш обработчик выше (шаг 7) поймал его
|
||||||
|
// и выполнил анимацию плавного исчезновения.
|
||||||
|
nextLink.click();
|
||||||
|
}, 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
42
public/static/js/counters.js
Normal 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');
|
||||||
|
})();
|
||||||
@@ -1,53 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="46" viewBox="0 0 1512 1386" shape-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd"><defs><linearGradient id="A" gradientUnits="userSpaceOnUse" x1="1476.52" y1="1395.4" x2="1034.86" y2="872.762"><stop offset="0" stop-color="#ef7f1a"/><stop offset=".349" stop-color="#ffed00"/><stop offset=".949" stop-color="#cc6f3c"/></linearGradient><linearGradient id="B" gradientUnits="userSpaceOnUse" x1="1023.98" y1="1127.45" x2="575.667" y2="596.929"><stop offset="0" stop-color="#ef7f1a"/><stop offset=".059" stop-color="#cc6f3c"/><stop offset=".671" stop-color="#ffed00"/><stop offset="1" stop-color="#cc6f3c"/></linearGradient><linearGradient id="C" href="#A" x1="1398.9" y1="1160.31" x2="613.548" y2="230.976"/><linearGradient id="D" href="#A" x1="805.595" y1="948.571" x2="21.024" y2="20.143"/><linearGradient id="E" gradientUnits="userSpaceOnUse" x1="381.071" y1="958.69" x2="89.071" y2="613.119"><stop offset="0" stop-color="#ef7f1a"/><stop offset=".831" stop-color="#ffed00"/><stop offset=".949" stop-color="#cc6f3c"/></linearGradient></defs><path d="M746 677L405 970c21-19 37-44 49-75 14-37 20-83 20-137 0-47-6-92-19-135s-32-82-58-118-57-67-95-95c-38-27-84-50-139-68L27 298c-6-2-12-2-18 1-1 0-2 1-3 2L351 5c1-1 2-2 4-2 6-3 12-3 18-1l136 44c55 18 101 41 139 68s69 59 95 95c25 36 45 75 58 118s19 88 19 135c0 54-7 100-20 137-12 34-30 60-53 79z" fill="url(#D)"/><path d="M1005 1173c12-10 22-22 30-35 10-16 18-34 25-54 6-20 11-41 14-65 3-23 4-48 4-73 0-50-6-97-17-141-12-44-29-83-51-119-23-35-51-66-85-92s-74-47-119-62c-48-16-90-22-126-17-36 4-66 17-91 40l345-295c24-22 54-34 90-39 36-4 78 2 126 17 45 15 85 35 119 62 34 26 63 57 85 92 23 35 40 75 51 119s17 90 17 141c0 26-2 50-4 73-3 23-8 45-14 65s-15 38-25 54c-9 14-20 27-33 37l-341 292z" fill="url(#C)"/><path d="M409 739c0-38-5-74-14-109-10-35-24-68-44-97-20-30-45-56-75-79s-69-42-116-58l-99-33v538l100 33c44 14 81 21 111 20s56-9 76-25 35-40 46-71c10-31 15-71 15-118zm65 18c0 54-7 100-20 137-14 37-34 65-60 84-27 19-59 30-98 31s-86-7-140-24L29 943c-6-2-12-6-18-13S1 914 1 902V319c0-12 3-19 10-22 6-3 12-3 18-1l136 44c55 18 101 41 139 68s69 59 95 95c25 36 45 75 58 118s19 88 19 135z" fill="#ae4a84" fill-rule="nonzero"/><path d="M1343 884l-333 285c-3 3-7 6-10 9 26 30 49 54 68 73l48 43c13 10 22 18 29 23s12 9 15 13 5 8 6 13 2 10 2 17c0 5 0 10-1 13s-2 6-4 8l-2 2 344-294c1 0 1-1 2-2 2-2 3-5 4-8 1-4 1-8 1-13 0-7-1-12-2-17s-3-9-6-13-8-8-15-13-17-13-29-23c-13-10-29-25-48-43-20-19-42-43-68-73z" fill="url(#A)"/><path d="M1015 931c0-38-4-75-11-111s-19-69-36-99-38-57-66-80c-27-23-61-41-102-54-40-13-74-17-101-11-28 6-50 18-67 38-17 19-29 44-37 75s-11 65-11 101c0 39 3 77 10 113s18 69 35 99c16 30 38 57 66 80 27 23 62 41 103 55 41 13 75 17 103 11s50-19 67-39 29-45 36-76 11-65 11-102zm153 430c0 5 0 10-1 13s-2 6-4 8-3 3-5 3c-2 1-3 0-5 0-4-1-12-7-26-16-13-10-29-22-48-39-19-16-39-35-61-57s-44-46-66-72c-17 6-39 9-65 9-27 0-58-6-94-18-48-16-89-37-124-63-35-27-63-57-85-93-22-35-38-75-49-119-10-44-16-91-16-142s6-95 18-132 29-67 53-88 53-34 89-38 78 2 126 17c45 15 85 35 119 62 34 26 63 57 85 92 23 35 40 75 51 119s17 90 17 141c0 26-2 50-4 73-3 23-8 45-14 65s-15 38-25 54-22 29-36 40c26 30 49 54 68 73l48 43c13 10 22 18 29 23s12 9 15 13 5 8 6 13 2 10 2 17z" fill="#ae4a84" fill-rule="nonzero"/><path d="M1015 931c0-38-4-75-11-111s-19-69-36-99-38-57-66-80c-27-23-61-41-102-54-40-13-74-17-101-11-28 6-50 18-67 38-17 19-29 44-37 75s-11 65-11 101c0 39 3 77 10 113s18 69 35 99c16 30 38 57 66 80 27 23 62 41 103 55 41 13 75 17 103 11s50-19 67-39 29-45 36-76 11-65 11-102z" fill="url(#B)"/><path d="M61 900l330-283c1 4 2 8 4 12 9 35 14 72 14 109 0 48-5 87-15 118s-25 55-46 71c-20 16-45 24-76 25s-67-6-111-20L61 899z" fill="url(#E)"/><path d="M61 900l330-283c-9-31-23-59-40-85-20-30-45-56-75-79s-69-42-116-58l-99-33v538z" fill="#ffd900"/></svg>
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Creator: CorelDRAW X6 -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="50px" height="46px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
|
||||||
viewBox="0 0 1512 1386"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<defs>
|
|
||||||
<style type="text/css">
|
|
||||||
<![CDATA[
|
|
||||||
.fil6 {fill:#FFD900}
|
|
||||||
.fil2 {fill:#AE4A84;fill-rule:nonzero}
|
|
||||||
.fil3 {fill:url(#id0)}
|
|
||||||
.fil4 {fill:url(#id1)}
|
|
||||||
.fil1 {fill:url(#id2)}
|
|
||||||
.fil0 {fill:url(#id3)}
|
|
||||||
.fil5 {fill:url(#id4)}
|
|
||||||
]]>
|
|
||||||
</style>
|
|
||||||
<linearGradient id="id0" gradientUnits="userSpaceOnUse" x1="1476.52" y1="1395.4" x2="1034.86" y2="872.762">
|
|
||||||
<stop offset="0" style="stop-color:#EF7F1A"/>
|
|
||||||
<stop offset="0.34902" style="stop-color:#FFED00"/>
|
|
||||||
<stop offset="0.94902" style="stop-color:#CC6F3C"/>
|
|
||||||
<stop offset="1" style="stop-color:#CC6F3C"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id1" gradientUnits="userSpaceOnUse" x1="1023.98" y1="1127.45" x2="575.667" y2="596.929">
|
|
||||||
<stop offset="0" style="stop-color:#EF7F1A"/>
|
|
||||||
<stop offset="0.0588235" style="stop-color:#CC6F3C"/>
|
|
||||||
<stop offset="0.670588" style="stop-color:#FFED00"/>
|
|
||||||
<stop offset="1" style="stop-color:#CC6F3C"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id2" gradientUnits="userSpaceOnUse" xlink:href="#id0" x1="1398.9" y1="1160.31" x2="613.548" y2="230.976">
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id3" gradientUnits="userSpaceOnUse" xlink:href="#id0" x1="805.595" y1="948.571" x2="21.0238" y2="20.1429">
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="id4" gradientUnits="userSpaceOnUse" x1="381.071" y1="958.69" x2="89.0714" y2="613.119">
|
|
||||||
<stop offset="0" style="stop-color:#EF7F1A"/>
|
|
||||||
<stop offset="0.831373" style="stop-color:#FFED00"/>
|
|
||||||
<stop offset="0.94902" style="stop-color:#CC6F3C"/>
|
|
||||||
<stop offset="1" style="stop-color:#CC6F3C"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g id="Layer_x0020_1">
|
|
||||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
|
||||||
<path class="fil0" d="M746 677l-341 293c21,-19 37,-44 49,-75 14,-37 20,-83 20,-137 0,-47 -6,-92 -19,-135 -13,-43 -32,-82 -58,-118 -26,-36 -57,-67 -95,-95 -38,-27 -84,-50 -139,-68l-136 -44c-6,-2 -12,-2 -18,1 -1,0 -2,1 -3,2l345 -296c1,-1 2,-2 4,-2 6,-3 12,-3 18,-1l136 44c55,18 101,41 139,68 38,27 69,59 95,95 25,36 45,75 58,118 13,43 19,88 19,135 0,54 -7,100 -20,137 -12,34 -30,60 -53,79z"/>
|
|
||||||
<path class="fil1" d="M1005 1173c12,-10 22,-22 30,-35 10,-16 18,-34 25,-54 6,-20 11,-41 14,-65 3,-23 4,-48 4,-73 0,-50 -6,-97 -17,-141 -12,-44 -29,-83 -51,-119 -23,-35 -51,-66 -85,-92 -34,-26 -74,-47 -119,-62 -48,-16 -90,-22 -126,-17 -36,4 -66,17 -91,40l345 -295c24,-22 54,-34 90,-39 36,-4 78,2 126,17 45,15 85,35 119,62 34,26 63,57 85,92 23,35 40,75 51,119 11,44 17,90 17,141 0,26 -2,50 -4,73 -3,23 -8,45 -14,65 -6,20 -15,38 -25,54 -9,14 -20,27 -33,37l-341 292z"/>
|
|
||||||
<path class="fil2" d="M409 739c0,-38 -5,-74 -14,-109 -10,-35 -24,-68 -44,-97 -20,-30 -45,-56 -75,-79 -30,-23 -69,-42 -116,-58l-99 -33 0 538 100 33c44,14 81,21 111,20 30,-1 56,-9 76,-25 20,-16 35,-40 46,-71 10,-31 15,-71 15,-118zm65 18c0,54 -7,100 -20,137 -14,37 -34,65 -60,84 -27,19 -59,30 -98,31 -39,1 -86,-7 -140,-24l-127 -42c-6,-2 -12,-6 -18,-13 -6,-7 -10,-16 -10,-28l0 -583c0,-12 3,-19 10,-22 6,-3 12,-3 18,-1l136 44c55,18 101,41 139,68 38,27 69,59 95,95 25,36 45,75 58,118 13,43 19,88 19,135z"/>
|
|
||||||
<path class="fil3" d="M1343 884l-333 285c-3,3 -7,6 -10,9 26,30 49,54 68,73 20,18 36,33 48,43 13,10 22,18 29,23 7,5 12,9 15,13 3,4 5,8 6,13 1,5 2,10 2,17 0,5 0,10 -1,13 -1,3 -2,6 -4,8 -1,1 -1,1 -2,2l344 -294c1,0 1,-1 2,-2 2,-2 3,-5 4,-8 1,-4 1,-8 1,-13 0,-7 -1,-12 -2,-17 -1,-5 -3,-9 -6,-13 -3,-4 -8,-8 -15,-13 -7,-5 -17,-13 -29,-23 -13,-10 -29,-25 -48,-43 -20,-19 -42,-43 -68,-73z"/>
|
|
||||||
<path class="fil2" d="M1015 931c0,-38 -4,-75 -11,-111 -7,-36 -19,-69 -36,-99 -17,-30 -38,-57 -66,-80 -27,-23 -61,-41 -102,-54 -40,-13 -74,-17 -101,-11 -28,6 -50,18 -67,38 -17,19 -29,44 -37,75 -8,31 -11,65 -11,101 0,39 3,77 10,113 7,36 18,69 35,99 16,30 38,57 66,80 27,23 62,41 103,55 41,13 75,17 103,11 28,-6 50,-19 67,-39 17,-20 29,-45 36,-76 7,-31 11,-65 11,-102zm153 430c0,5 0,10 -1,13 -1,3 -2,6 -4,8 -2,2 -3,3 -5,3 -2,1 -3,0 -5,0 -4,-1 -12,-7 -26,-16 -13,-10 -29,-22 -48,-39 -19,-16 -39,-35 -61,-57 -22,-22 -44,-46 -66,-72 -17,6 -39,9 -65,9 -27,0 -58,-6 -94,-18 -48,-16 -89,-37 -124,-63 -35,-27 -63,-57 -85,-93 -22,-35 -38,-75 -49,-119 -10,-44 -16,-91 -16,-142 0,-51 6,-95 18,-132 12,-37 29,-67 53,-88 24,-21 53,-34 89,-38 36,-4 78,2 126,17 45,15 85,35 119,62 34,26 63,57 85,92 23,35 40,75 51,119 11,44 17,90 17,141 0,26 -2,50 -4,73 -3,23 -8,45 -14,65 -6,20 -15,38 -25,54 -10,16 -22,29 -36,40 26,30 49,54 68,73 20,18 36,33 48,43 13,10 22,18 29,23 7,5 12,9 15,13 3,4 5,8 6,13 1,5 2,10 2,17z"/>
|
|
||||||
<path class="fil4" d="M1015 931c0,-38 -4,-75 -11,-111 -7,-36 -19,-69 -36,-99 -17,-30 -38,-57 -66,-80 -27,-23 -61,-41 -102,-54 -40,-13 -74,-17 -101,-11 -28,6 -50,18 -67,38 -17,19 -29,44 -37,75 -8,31 -11,65 -11,101 0,39 3,77 10,113 7,36 18,69 35,99 16,30 38,57 66,80 27,23 62,41 103,55 41,13 75,17 103,11 28,-6 50,-19 67,-39 17,-20 29,-45 36,-76 7,-31 11,-65 11,-102z"/>
|
|
||||||
<path class="fil5" d="M61 900l330 -283c1,4 2,8 4,12 9,35 14,72 14,109 0,48 -5,87 -15,118 -10,31 -25,55 -46,71 -20,16 -45,24 -76,25 -31,1 -67,-6 -111,-20l-100 -33z"/>
|
|
||||||
<path class="fil6" d="M61 900l330 -283c-9,-31 -23,-59 -40,-85 -20,-30 -45,-56 -75,-79 -30,-23 -69,-42 -116,-58l-99 -33 0 538z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 3.8 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
6
public/yandex_e5c0008fb741485c.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
</head>
|
||||||
|
<body>Verification: e5c0008fb741485c</body>
|
||||||
|
</html>
|
||||||
25
pyproject.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "dicquo"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Веб-приложение для коллекции цитат и афоризмов."
|
||||||
|
authors = ["Sergei Erjemin <erjemin@gmail.com>"]
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.12"
|
||||||
|
django = "^6.0.2"
|
||||||
|
django-taggit = "^6.1.0"
|
||||||
|
pillow = "^12.1.1"
|
||||||
|
pytils = "^0.4.4"
|
||||||
|
etpgrf = "^0.1.6"
|
||||||
|
django-environ = "^0.13.0"
|
||||||
|
whitenoise = "^6.11.0"
|
||||||
|
gunicorn = "^25.1.0"
|
||||||
|
tqdm = "^4.67.3"
|
||||||
|
django-select2 = "^8.4.8"
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||