Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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]
|
||||||
65
.gitea/workflows/docker-publish.yaml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
12
.gitignore
vendored
@@ -255,3 +255,15 @@ 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
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Data Backup
|
||||||
|
database/data.json
|
||||||
|
|||||||
63
Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# ==========================================
|
||||||
|
# Dockerfile для Django + Gunicorn + WhiteNoise
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# 1. Базовый образ: Python 3.12 (Slim версия для меньшего размера)
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# 2. Переменные окружения для Python
|
||||||
|
# PYTHONDONTWRITEBYTECODE: Запрещает Python писать .pyc файлы
|
||||||
|
# PYTHONUNBUFFERED: Гарантирует, что вывод консоли (logs) виден сразу (не буферизуется)
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
# Poetry настройки: не создавать виртуальное окружение внутри контейнера (ставим системно).
|
||||||
|
# Дублирует `poetry config virtualenvs.create false` в пп.7 (на всякий случай).
|
||||||
|
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||||
|
# Путь настройки Django (по умолчанию для production) на случай если контейнер будет запущен не через docker-compose.
|
||||||
|
ENV DJANGO_SETTINGS_MODULE=dicquo.settings
|
||||||
|
|
||||||
|
# 3. Рабочая директория внутри контейнера
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 4. Установка системных зависимостей
|
||||||
|
# - libjpeg-dev zlib1g-dev: библиотеки для работы с изображениями (Pillow)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libjpeg-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 5. Установка Poetry через pip (быстро и надежно)
|
||||||
|
RUN pip install --no-cache-dir poetry
|
||||||
|
|
||||||
|
# 6. Копируем файлы зависимостей (pyproject.toml и poetry.lock)
|
||||||
|
# Делаем это ДО копирования всего кода, чтобы использовать кэш Docker layers.
|
||||||
|
COPY pyproject.toml poetry.lock /app/
|
||||||
|
|
||||||
|
# 7. Установка зависимостей проекта
|
||||||
|
# --no-interaction: не будет спрашивать подтверждения
|
||||||
|
# --no-ansi: уберваем цветные символы из логов сборки (они иногда мусорят)
|
||||||
|
# --no-root: не устанавливать сам проект как пакет (мы просто копируем код)
|
||||||
|
# --only main: не ставить dev-зависимости (тесты, линтеры и т.п.) для продакшена
|
||||||
|
# RUN poetry install --no-root --only main
|
||||||
|
# Настройка Poetry: не создавать venv и установка зависимостей (без dev-зависимостей для продакшена)
|
||||||
|
RUN poetry config virtualenvs.create false \
|
||||||
|
&& poetry install --no-interaction --no-ansi --no-root --only main
|
||||||
|
|
||||||
|
# 8. Копируем весь исходный код проекта в контейнер
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# 9. Сборка статики (CSS, JS)
|
||||||
|
# Важно: Запускаем collectstatic с фейковым SECRET_KEY, так как на этапе сборки env файла может не быть.
|
||||||
|
RUN SECRET_KEY=dummy_build_key python dicquo/manage.py collectstatic --noinput --clear
|
||||||
|
|
||||||
|
# 10. Открываем порт 8000
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# 11. Команда запуска
|
||||||
|
# Переходим в подпапку dicquo, где лежит код Django проекта
|
||||||
|
WORKDIR /app/dicquo
|
||||||
|
|
||||||
|
# Запускаем Gunicorn (по умолчанию, если не переопределено в docker-compose) на три воркера, привязывая его к
|
||||||
|
# порту 8000 и указывая на точку входа приложения (wsgi.py).
|
||||||
|
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 ----------------
|
|
||||||
124
configs/nginx/dq-app--external-nginx.conf
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# ЭТАЛОННЫЙ КОНФИГУРАЦИОННЫЙ ФАЙЛ 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;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Тайм-ауты (важно для долгих операций, если они есть)
|
||||||
|
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,18 @@ 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 (в секундах)
|
||||||
|
# При сложных операциях (например, каскадное удаление тегов) нужно больше времени
|
||||||
|
'timeout': 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
@@ -113,7 +121,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 +134,23 @@ 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
|
||||||
|
|||||||
@@ -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")),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
if settings.DEBUG:
|
||||||
|
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,34 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>{% load static %}
|
||||||
{% load static %}<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8"/>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<meta http-equiv="content-language" content="ru" />
|
{# SEO & Meta #}<title>{% block Title %}{% endblock %}</title>
|
||||||
<meta http-equiv="Date" content="{% block Date4Meta %}{% now 'c' %}{% endblock %}" />
|
<meta name="description" content="{% block Description %}{% endblock %}"/>
|
||||||
<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% now 'c' %}{% endblock %}" />
|
<meta name="keywords" content="{% block Keywords %}{% endblock %}"/>
|
||||||
<meta http-equiv="Expires" content="{% block Expires4Meta %}{% now 'c' %}{% endblock %}" />
|
<meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}."/>
|
||||||
<meta http-equiv="Cache-Control" content="no-cache">
|
<meta name="robots" content="index,follow"/>
|
||||||
<meta name="GENERATOR" content="Microsoft FrontPage 1.0" />
|
{# Open Graph / Social Media #}<meta property="og:type" content="article"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:85 }}{% endblock %}"/>
|
||||||
<meta name="description" content="{% block Description %}{% endblock %}" />
|
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}"/>
|
||||||
<meta name="keywords" content="{% block Keywords %}{% endblock %}" />
|
<meta property="og:url" content="{{ request.build_absolute_uri }}"/>
|
||||||
<meta name="copyright" lang="ru" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}." />
|
<meta property="og:site_name" content="DicQuo"/>
|
||||||
<meta name="robots" content="index,follow" />
|
{% if IMAGE %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}"/>{% endif %}
|
||||||
<meta name="document-state" content="{{ META_DOCUMENT_STATE|default:'Dynamic' }}" />
|
{# Шутка #}<meta name="generator" content="Microsoft FrontPage 1.0"/>
|
||||||
<meta name="generator" content="FAVICON -- 0.01β by Python/Django" />
|
{# Canonical #}<link rel="canonical" href="{{ request.build_absolute_uri }}"/>
|
||||||
<title>{% block Title %}{% endblock %}</title>
|
{# Favicons #}<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}"/>
|
||||||
<!-- Favicons -->
|
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}" />
|
{# Technical Meta #}<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}"/>
|
||||||
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}" />
|
{# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.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>
|
||||||
<noscript>
|
</div>
|
||||||
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}">
|
</nav>
|
||||||
</noscript>
|
<noscript>
|
||||||
|
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
|
||||||
|
</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,176 @@
|
|||||||
# -*- 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
|
||||||
|
|
||||||
|
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)
|
||||||
|
# choices: список всех существующих тегов по имени.
|
||||||
|
# Важно: на этапах вроде collectstatic таблицы taggit ещё может не быть,
|
||||||
|
# поэтому оборачиваем в try/except и молча игнорируем отсутствие БД.
|
||||||
|
try:
|
||||||
|
self.choices = [(t.name, t.name) for t in Tag.objects.all()]
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
self.choices = []
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
"all": ("css/select2_taggit_admin.css",),
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||||
|
"""
|
||||||
|
Настраиваем Select2 так, чтобы пробел НЕ разделял тег
|
||||||
|
на несколько частей (нужны теги с пробелами: «Сергей Курёхин»).
|
||||||
|
Оставляем в разделителях только запятую.
|
||||||
|
"""
|
||||||
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||||
|
# По умолчанию django-select2 ставит: [",", " "]
|
||||||
|
# Нам нужен только разделитель-запятая.
|
||||||
|
# Строка '[","]' — корректный JSON-массив из одного элемента.
|
||||||
|
# Важно: сюда нужно класть СТРОКУ с JSON-массивом, а не python-список.
|
||||||
|
# Иначе в HTML окажется "['[\", \"]', ...]" и Select2 будет вести себя непредсказуемо.
|
||||||
|
attrs["data-token-separators"] = '[","]'
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
"""
|
||||||
|
Преобразуем значение из TaggableManager/TagField
|
||||||
|
в список ИМЁН тегов, который ожидает Select2TagWidget.
|
||||||
|
"""
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# QuerySet или список Tag-объектов
|
||||||
|
if isinstance(value, QuerySet):
|
||||||
|
return [t.name for t in value]
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
names = []
|
||||||
|
for v in value:
|
||||||
|
if isinstance(v, Tag):
|
||||||
|
names.append(v.name)
|
||||||
|
else:
|
||||||
|
names.append(str(v))
|
||||||
|
return names
|
||||||
|
|
||||||
|
# Строка вида "tag1, tag2" — разбираем в список имён
|
||||||
|
if isinstance(value, str):
|
||||||
|
return parse_tags(value)
|
||||||
|
|
||||||
|
return super().format_value(value)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""
|
||||||
|
Django-Select2 возвращает список значений (['Сергей Курёхин', 'Другой тег']).
|
||||||
|
Taggit (TagField) ждёт ОДНУ строку, которую потом парсит в список тегов.
|
||||||
|
Если отдать список, он превратится в строку `"['Сергей', 'Курёхин']"`,
|
||||||
|
и распарсится в кривые теги — этого мы избегаем.
|
||||||
|
"""
|
||||||
|
values = super().value_from_datadict(data, files, name)
|
||||||
|
if not values:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Для нашего виджета value — это уже список имён тегов
|
||||||
|
tag_names = [str(v).strip() for v in values if str(v).strip()]
|
||||||
|
if not tag_names:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ОДИН многословный тег: "Сергей Курёхин" -> "Сергей Курёхин,"
|
||||||
|
# Тогда parse_tags переключится в режим "деление по запятым"
|
||||||
|
if len(tag_names) == 1:
|
||||||
|
single = tag_names[0]
|
||||||
|
if " " in single and "," not in single and '"' not in single:
|
||||||
|
return single + ","
|
||||||
|
return single
|
||||||
|
|
||||||
|
# Несколько тегов — явная запятая между ними.
|
||||||
|
return ", ".join(tag_names)
|
||||||
|
|
||||||
|
|
||||||
|
class DictumAdminForm(forms.ModelForm):
|
||||||
|
# Виртуальные поля для настройки типографа
|
||||||
|
etp_language = forms.ChoiceField(
|
||||||
|
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', 'Справа'), ('both', 'Обе стороны')],
|
||||||
|
initial='left',
|
||||||
|
required=False,
|
||||||
|
help_text="Выносить кавычки за границу текстового блока"
|
||||||
|
)
|
||||||
|
etp_hyphenation = forms.BooleanField(
|
||||||
|
label="Переносы",
|
||||||
|
initial=True,
|
||||||
|
required=False,
|
||||||
|
help_text="Расставлять мягкие переносы (&shy;)"
|
||||||
|
)
|
||||||
|
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 +179,113 @@ 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': (
|
||||||
|
('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') # 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 +307,11 @@ class AdmImages(admin.ModelAdmin):
|
|||||||
list_display_links = ('id', 'szCaption')
|
list_display_links = ('id', 'szCaption')
|
||||||
empty_value_display = u"<b style='color:red;'>-empty-</b>"
|
empty_value_display = u"<b style='color:red;'>-empty-</b>"
|
||||||
|
|
||||||
|
# Добавляем виджет для тегов
|
||||||
|
formfield_overrides = {
|
||||||
|
TaggableManager: {'widget': TagSelect2Widget},
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super().get_queryset(request).prefetch_related('tags')
|
return super().get_queryset(request).prefetch_related('tags')
|
||||||
|
|
||||||
@@ -53,6 +325,11 @@ class AdmAuthor(admin.ModelAdmin):
|
|||||||
list_display_links = ('id', 'szAuthor')
|
list_display_links = ('id', 'szAuthor')
|
||||||
empty_value_display = u"<b style='color:red;'>-empty-</b>"
|
empty_value_display = u"<b style='color:red;'>-empty-</b>"
|
||||||
|
|
||||||
|
# Добавляем виджет для тегов
|
||||||
|
formfield_overrides = {
|
||||||
|
TaggableManager: {'widget': TagSelect2Widget},
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return super().get_queryset(request).prefetch_related('tags')
|
return super().get_queryset(request).prefetch_related('tags')
|
||||||
|
|
||||||
|
|||||||
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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
|
||||||
from typus import en_typus, ru_typus
|
try:
|
||||||
|
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:
|
||||||
@@ -284,24 +349,38 @@ class TbDictumAndQuotes(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=u"Вступление HTML",
|
verbose_name=u"Вступление HTML",
|
||||||
help_text=u"Автор и, если необходимо, краткая справка<br />"
|
help_text=u"Автор и, если необходимо, краткая справка<br />"
|
||||||
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,227 @@
|
|||||||
# -*- 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})
|
||||||
|
|
||||||
|
# --- 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)
|
||||||
|
|||||||
115
docker-compose.prod.yml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# Docker Compose для PRODUCTION
|
||||||
|
# Этот файл запускается на боевом сервере.
|
||||||
|
# Вариант 1 (если переименовали в docker-compose.yml): docker compose up -d
|
||||||
|
# Вариант 2 (если оставили имя): docker compose -f docker-compose.prod.yml up -d
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
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)
|
||||||
|
# Выполняем цепочку команд внутри контейнера при запуске:
|
||||||
|
# 1. Миграции
|
||||||
|
# 2. Collectstatic
|
||||||
|
# 3. Создаем папку nginx в примонтированном томе конфигов (если нет)
|
||||||
|
# 4. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем через переменную окружения HOST_PROJECT_PATH)
|
||||||
|
# 5. Инициализация боевого конфига (если нет)
|
||||||
|
# 6. Создаем папку для ошибок и копируем туда статические страницы 404/500
|
||||||
|
# 7. Запуск 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\" /app/configs/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 /app/dicquo/templates/static_404.html /app/public/media/errors/404.html &&
|
||||||
|
cp /app/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 с хоста в папку с базой внутри контейнера.
|
||||||
|
# Путь в контейнере: /app/database (так как Django ищет базу в BASE_DIR.parent/database)
|
||||||
|
- ./database:/app/database
|
||||||
|
|
||||||
|
# Медиа (папка media должна быть рядом с docker-compose.yml)
|
||||||
|
- ./media:/app/public/media
|
||||||
|
|
||||||
|
# Конфиги (Монтируем папку ./config с хоста в /nginx_configs_host внутри контейнера)
|
||||||
|
# Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг.
|
||||||
|
- ./config:/nginx_configs_host
|
||||||
|
|
||||||
|
# 6. Переменные окружения
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DJANGO_SETTINGS_MODULE=dicquo.settings
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать
|
||||||
|
- HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site}
|
||||||
|
|
||||||
|
# 7. Логирование (Ротация)
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# 8. Ресурсы
|
||||||
|
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
|
||||||
|
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()
|
|
||||||
678
poetry.lock
generated
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
# 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.2"
|
||||||
|
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.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6"},
|
||||||
|
{file = "django-6.0.2.tar.gz", hash = "sha256:3046a53b0e40d4b676c3b774c73411d7184ae2745fe8ce5e45c0f33d3ddb71a7"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[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.12.1"
|
||||||
|
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.12.1-py2.py3-none-any.whl", hash = "sha256:064ba2d5082f833e6d7fe4def4928bde1eedc0248a417575da7db147aeec1c20"},
|
||||||
|
{file = "django_environ-0.12.1.tar.gz", hash = "sha256:22859c6e905ab7637fa3348d1787543bb4492f38d761104a3ce0519b7b752845"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)", "sphinx (>=5.0)", "sphinx-notfound-page"]
|
||||||
|
docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-notfound-page"]
|
||||||
|
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"]
|
||||||
|
|
||||||
|
[[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.4"
|
||||||
|
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.4-py3-none-any.whl", hash = "sha256:62d4371e1b5fab06b99f79bd351767aed8baf7d041cae7e5d4eb63f7c9545114"},
|
||||||
|
{file = "etpgrf-0.1.4.tar.gz", hash = "sha256:c699382c292e3110915331dd5539e7dde0c961e4f4ca65cf8db0e01e84dab72f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[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.1.15"
|
||||||
|
description = "Alternative regular expression module, to replace re."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"},
|
||||||
|
{file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"},
|
||||||
|
{file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"},
|
||||||
|
{file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"},
|
||||||
|
{file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"},
|
||||||
|
{file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"},
|
||||||
|
{file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"},
|
||||||
|
{file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.11.0"
|
||||||
|
description = "Radically simplified static file serving for WSGI applications"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "whitenoise-6.11.0-py3-none-any.whl", hash = "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258"},
|
||||||
|
{file = "whitenoise-6.11.0.tar.gz", hash = "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.12"
|
||||||
|
content-hash = "b5fca935982220439294d6b37caaf1d893492df96d65abd6389dfd3c9464b992"
|
||||||
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>
|
||||||
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,26 @@
|
|||||||
# DicQuo
|
# DicQuo
|
||||||
User-Agent: *
|
User-Agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
Disallow:
|
Disallow: /admin/
|
||||||
|
Disallow: /*?tag=
|
||||||
|
Disallow: /*?
|
||||||
|
|
||||||
|
# Optimize for Yandex
|
||||||
|
Clean-param: tag /
|
||||||
|
|
||||||
|
# AI and LLM bots settings
|
||||||
|
# OpenAI GPT
|
||||||
|
# User-agent: GPTBot
|
||||||
|
# Disallow:
|
||||||
|
|
||||||
|
# Common Crawl (used by many AI models)
|
||||||
|
# User-agent: CCBot
|
||||||
|
# Disallow:
|
||||||
|
|
||||||
|
# Google Bard/Gemini
|
||||||
|
# User-agent: Google-Extended
|
||||||
|
# Disallow:
|
||||||
|
|
||||||
Host: dq.cube2.ru
|
Host: dq.cube2.ru
|
||||||
Sitemap: https://dq.cube2.ru/sitemap.xml
|
Sitemap: https://dq.cube2.ru/sitemap.xml
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -19,7 +211,7 @@
|
|||||||
color: white;
|
color: white;
|
||||||
border-bottom: dotted 1px silver;
|
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: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4));
|
||||||
/* обрезка градиента */
|
/* обрезка градиента */
|
||||||
background-clip: initial;
|
background-clip: initial;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
@@ -29,106 +221,99 @@
|
|||||||
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;
|
||||||
position:fixed;
|
}
|
||||||
bottom: 0; left: 0;
|
|
||||||
|
nav > div > div a {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ПОДВАЛ-КУКИ (ДЗЕН-СТИЛЬ) --- */
|
||||||
|
footer {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
position: fixed;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > div {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 8vmin 0 2vmin 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > div {
|
||||||
|
width: 80vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > div > div {
|
||||||
|
width: 36vmax;
|
||||||
|
height: 36vmax;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > article > div > div > div > img {
|
||||||
|
height: 36vmax;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table { width: 80%; }
|
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
|
||||||
|
/* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
|
||||||
|
.etp-laquo {margin-left: -0.44em;} /* « */
|
||||||
|
.etp-ldquo, .etp-bdquo { margin-left: -0.4em;} /* “ „ */
|
||||||
|
.etp-lsquo {margin-left: -0.22em;} /* ‘ */
|
||||||
|
.etp-lpar, .etp-lsqb, .etp-lcub {margin-left: -0.25em;}/* ( [ { */
|
||||||
|
|
||||||
#menu {
|
/* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) --- */
|
||||||
display: none;
|
.counter-pixel {
|
||||||
color: silver;
|
border: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mm {
|
|
||||||
text-decoration: none;
|
|
||||||
color: silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
#image {
|
|
||||||
width: 30vw;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#image > center > div {
|
|
||||||
width: 22vw;
|
|
||||||
height: 22vw;
|
|
||||||
padding:0.5vw;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#image > center > div > div {
|
|
||||||
border-radius:50%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#image > center > div > div > img {
|
|
||||||
width: auto;
|
|
||||||
height: 22vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
#author {
|
|
||||||
color: silver;
|
|
||||||
font-size: 3.5vh;
|
|
||||||
line-height: 4vh;
|
|
||||||
text-align: right;
|
|
||||||
padding-top: 4vh;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
#info {
|
|
||||||
color: silver;
|
|
||||||
font-size: 2.5vh;
|
|
||||||
line-height: 3vh;
|
|
||||||
padding-bottom: 2vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bb {
|
|
||||||
color: whitesmoke;
|
|
||||||
font-size: 4.5vh;
|
|
||||||
line-height: 5vh
|
|
||||||
}
|
|
||||||
|
|
||||||
#next { float: right; }
|
|
||||||
|
|
||||||
#next a { border-bottom: none; }
|
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
|
})();
|
||||||
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.4"
|
||||||
|
django-environ = "^0.12.1"
|
||||||
|
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"
|
||||||