Compare commits
94 Commits
v0.1.0
..
855017a0b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 855017a0b6 | |||
| be673483ae | |||
| 3928066147 | |||
| 64880da11a | |||
| be542e37ab | |||
| 08bfce2c4c | |||
| 01779a89f0 | |||
| 13ad08e294 | |||
| b316efc425 | |||
| 7b623e975d | |||
| 01710200b2 | |||
| d53330554b | |||
| 8ed61bae13 | |||
| c7844aa3d7 | |||
| 48df2fb32b | |||
| 562b58be47 | |||
| 5b339d1a53 | |||
| 73f0c615f0 | |||
| 8b3fe1fe41 | |||
| e970d59173 | |||
| a30d7f5483 | |||
| afef6b639c | |||
| eceeaaa55b | |||
| 6a178c9c9b | |||
| 3d9f0a6b5d | |||
| 80fef9791c | |||
| 0c3c7bc9ac | |||
| 6d69cd1d41 | |||
| 4a24037ae8 | |||
| 96071cdded | |||
| 57a6e36b33 | |||
| e08694ea82 | |||
| 0cf6363671 | |||
| 23cb31bc40 | |||
| 60700076d1 | |||
| 8b45839c25 | |||
| f2f30bbd0a | |||
| 6d3f5578f1 | |||
| 8f5b2034c7 | |||
| c47a4bc39e | |||
| 7d534ba2a1 | |||
| 5d71ca291d | |||
| de81cc1b34 | |||
| bb915c82e1 | |||
| 7f5ea8b567 | |||
| 989722bf16 | |||
| fed2a1c5a0 | |||
| c7049f7d8d | |||
| 1ea2292833 | |||
| 280383f02d | |||
| 0ad98fec9d | |||
| 71cac55221 | |||
| a5425b212d | |||
| 6d6bb873e9 | |||
| 7fb5b0e44f | |||
| dc94b8a4df | |||
| fa559f2517 | |||
| 7e63fae613 | |||
| 3d301ad326 | |||
| 6a1f923fe4 | |||
| f99bbb5c6b | |||
| 62fe0722e5 | |||
| a6d9f963b3 | |||
| a17906c224 | |||
| d3edbd8315 | |||
| 2e5f8b923e | |||
| 5088cffece | |||
| 7da0e466b3 | |||
| 094983c66a | |||
| f081361c28 | |||
| b0846e13b9 | |||
| e933b4a9b2 | |||
| 48e38fb9cf | |||
| fd70a44c81 | |||
| b2154e815e | |||
| 87ce677945 | |||
| edb7741ffa | |||
| 76e0350688 | |||
| 61be28786b | |||
| dbb6103378 | |||
| eec127912c | |||
| 3181d71227 | |||
| 0741735fa5 | |||
| efd4695960 | |||
| 3f45a7e397 | |||
| be4839ecc2 | |||
| b2b38783cb | |||
| f0502e605f | |||
| 75d359aeb3 | |||
| 5b6c8a8ded | |||
| 5cb05a353f | |||
| 6bf79c460b | |||
| 447d2ab9dd | |||
| 63c52eaeaa |
@@ -0,0 +1,35 @@
|
||||
# Локальная конфигурация для dev-окружения lpon-site.
|
||||
DJANGO_DEBUG=True
|
||||
DJANGO_SECRET_KEY=insecure-dev-key-change-me-in-prod
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,testserver,lpon.ru
|
||||
DJANGO_ADMINS=YouName:you@email.com
|
||||
|
||||
# Доверенные источники для CSRF (важно для Docker/Nginx)
|
||||
# Укажите здесь URL, по которому вы заходите на сайт (с протоколом и портом gunicorn)
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,http://testserver,https://lpon.ru
|
||||
|
||||
# URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти)
|
||||
DJANGO_ADMIN_URL=admin/
|
||||
|
||||
# База данных (SQLite)
|
||||
DJANGO_SQLITE_NAME=lpon-db.sqlite3
|
||||
|
||||
# Почта SMTP
|
||||
DJANGO_EMAIL_HOST=smtp.mail.ru
|
||||
DJANGO_EMAIL_PORT=2525
|
||||
DJANGO_EMAIL_HOST_USER=you@email.com
|
||||
DJANGO_EMAIL_HOST_PASSWORD=CHANGE_ME
|
||||
DJANGO_EMAIL_FROM=you@email.com
|
||||
|
||||
# ==============================================
|
||||
# Системные пути на хосте (ТОЛЬКО ДЛЯ ПРОДАКШЕНА)
|
||||
# Используется скриптом для генерации корректного Nginx конфига и alias к media-файлам.
|
||||
# На локальной машине разработчика (Dev) эта переменная игнорируется.
|
||||
# В ПРОДАКШЕНЕ: Укажите полный путь к папке проекта на сервере
|
||||
HOST_PROJECT_PATH=/home/user/docker-app/your-site
|
||||
|
||||
|
||||
# ==============================================
|
||||
# Настройки достпа к пакетам в репозитории, чтобы watchtower мог проверять их свежесть и скачивать
|
||||
REPO_USER=xxxxx
|
||||
REPO_PASS=xxxxx
|
||||
+3
-2
@@ -363,7 +363,8 @@ db.sqlite3-wal
|
||||
public/media/*
|
||||
!public/media/README.md
|
||||
staticfiles/
|
||||
public/static/static_collected/
|
||||
public/static_collected/
|
||||
.github/
|
||||
|
||||
# Data Backup
|
||||
database/data.json
|
||||
database/
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
# Сплеш-экран сайта LPON.RU
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://git.cube2.ru/erjemin/2018-lpon-site)
|
||||
[](https://typograph.cube2.ru/)
|
||||
|
||||
|
||||
## Архив
|
||||
Легаси проекта, который я задумал в 2018 году, но так и не собрался его реализовать.
|
||||
|
||||
_Не пора ли продолжить?_ :)
|
||||
_Не пора ли продолжить?_ :)
|
||||
|
||||
Ведется разработка на Python 3.12 и Django 6.0.
|
||||
@@ -13,7 +13,7 @@ server {
|
||||
|
||||
# Все домены, которые будет обслуживать этот сервер.
|
||||
# Certbot выпустит сертификаты для всех перечисленных доменов.
|
||||
server_name lpon.ru t.lpon.ru;
|
||||
server_name lpon.ru;
|
||||
|
||||
# --- ЛОГИ ---
|
||||
access_log /var/log/nginx/lpon.access.log;
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
# Пробрасываем свободный порт 8020 на хосте на стандартный порт 80 внутри контейнера
|
||||
- "8020:80"
|
||||
- "127.0.0.1:8020:80"
|
||||
# Настройка ротации логов
|
||||
logging:
|
||||
driver: "json-file"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Каталог для NPM-сборки Codemirror6 (для фронтенда админки).
|
||||
|
||||
# Папка `node_modules/` будет создана в процессе NPM-сборки Codemirror 6 (для фронтенда админки).
|
||||
# Её соджержимое не должно попадать в репозиторий. Готовая сборка будет
|
||||
# перемещна в ../public/static/codemirror/editor.js
|
||||
node_modules/
|
||||
|
||||
src/
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# CodeMirror 6 для Django Admin на LPON.RU
|
||||
|
||||
## Обзор
|
||||
|
||||
Это руководство описывает, как использовать и обновлять сборку CodeMirror 6, которая используется для редактирования текстовых полей в админке Django.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
- `frontend-assembly/`: Исходные файлы и скрипты для сборки.
|
||||
- `package.json`: Список зависимостей CodeMirror 6.
|
||||
- `package-lock.json`: Файл с зафиксированными версиями зависимостей. **Не редактировать вручную!**
|
||||
- `build-codemirror6.sh`: Скрипт, который генерирует и собирает финальный JS-бандл.
|
||||
- `public/static/codemirror/`: Каталог с готовыми файлами для Django.
|
||||
- `editor.js`: Финальный, минифицированный JS-бандл. **Не редактировать вручную!**
|
||||
- `codemirror-styles.css`: Кастомные стили для интеграции с админкой.
|
||||
- `codemirror-patch.js`: JS-патч для управления размерами редактора.
|
||||
|
||||
## Как собрать бандл
|
||||
|
||||
После клонирования репозитория или обновления зависимостей, выполните:
|
||||
|
||||
```bash
|
||||
# Перейдите в каталог сборки
|
||||
cd frontend-assembly
|
||||
|
||||
# Установите зависимости и соберите бандл
|
||||
bash ./build-codemirror6.sh
|
||||
```
|
||||
|
||||
## Как использовать в Django Admin
|
||||
|
||||
### 1. Подключение в ModelAdmin
|
||||
|
||||
В вашем `admin.py` используйте `Media` класс для подключения необходимых файлов.
|
||||
|
||||
```python
|
||||
# frontend/admin.py
|
||||
|
||||
class YourModelAdmin(admin.ModelAdmin):
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('codemirror/codemirror-styles.css',)
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js',
|
||||
'codemirror/codemirror-patch.js',
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Активация для полей
|
||||
|
||||
Чтобы превратить стандартный `Textarea` в редактор CodeMirror, используйте кастомную форму или метод `formfield_for_dbfield` и добавьте к виджету специальные `data-` атрибуты.
|
||||
|
||||
```python
|
||||
# frontend/admin.py
|
||||
|
||||
class YourModelAdmin(admin.ModelAdmin):
|
||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||
|
||||
# Активируем CodeMirror для всех текстовых полей
|
||||
if isinstance(formfield.widget, forms.Textarea):
|
||||
formfield.widget.attrs['data-codemirror-editor'] = 'true'
|
||||
|
||||
# Устанавливаем язык в зависимости от имени поля
|
||||
if 'j_' in db_field.name:
|
||||
# Обычно в моделях LPON поля с префиксом 'j_' содержат JSON
|
||||
formfield.widget.attrs['data-language'] = 'json'
|
||||
elif 'html' in db_field.name:
|
||||
formfield.widget.attrs['data-language'] = 'html'
|
||||
else:
|
||||
formfield.widget.attrs['data-language'] = 'text' # Обычный текст
|
||||
|
||||
# Добавляем утилитарные CSS-классы для стилизации
|
||||
if db_field.name == 'i_img_sort':
|
||||
formfield.widget.attrs['class'] = 'codemirror-width-s codemirror-no-lines'
|
||||
elif db_field.name == 'j_seller_metadata':
|
||||
formfield.widget.attrs['class'] = 'codemirror-width-xl codemirror-min-height-m'
|
||||
|
||||
return formfield
|
||||
```
|
||||
|
||||
### 3. Поддерживаемые языки
|
||||
|
||||
- `javascript`
|
||||
- `css`
|
||||
- `json`
|
||||
- `html` (по умолчанию, в него встроенны режимы подсветки синтенза HTML, XML, CSS и JavaScript)
|
||||
- `text` (обычный текст, без подсветки синтакса, unocode-режим)
|
||||
|
||||
## Кастомизация
|
||||
|
||||
### Управление размерами и стилями
|
||||
|
||||
В файле `public/static/codemirror/codemirror-styles.css` определены утилитарные CSS-классы для управления внешним
|
||||
видом редакторов:
|
||||
|
||||
- **Ширина**: `.codemirror-width-s`, `.codemirror-width-m`, `.codemirror-width-l`, `.codemirror-width-xl`
|
||||
- **Минимальная высота**: `.codemirror-min-height-5` и `.codemirror-min-height-10` (по 5 и 10 строк соответственно)
|
||||
- **Отключение номеров строк**: `.codemirror-no-lines`
|
||||
|
||||
Просто примените нужный класс к атрибуту `class` виджета в `admin.py`. По аналогии можно создавать свои классы
|
||||
для своих настройек.
|
||||
|
||||
## Как добавить новый язык (например, jinja2 или xml)
|
||||
|
||||
1. **Добавить зависимость**: Откройте `frontend-assembly/package.json` и добавьте в `devDependencies` новый языковой пакет. Версию выбирайте близкую к другим пакетам `@codemirror/lang-*`.
|
||||
|
||||
```json
|
||||
// package.json
|
||||
...
|
||||
"@codemirror/lang-json": "6.0.1",
|
||||
"@codemirror/lang-xml": "6.1.0", // <-- Новая строка для добавления xml
|
||||
"@codemirror/language": "6.12.3",
|
||||
...
|
||||
```
|
||||
|
||||
2. **Обновить скрипт сборки**: Откройте `frontend-assembly/build-codemirror6.sh` и внесите два изменения в секцию `cat > "$WORK_DIR/src/editor.js" <<'EOF'`:
|
||||
- Добавьте импорт:
|
||||
```javascript
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
```
|
||||
- Добавьте условие для его использования:
|
||||
```javascript
|
||||
} else if (language === 'xml') {
|
||||
extensions.unshift(xml());
|
||||
}
|
||||
```
|
||||
|
||||
3. **Пересобрать бандл**:
|
||||
```bash
|
||||
cd frontend-assembly
|
||||
npm install # Установит новый пакет и обновит package-lock.json
|
||||
bash build-codemirror6.sh # Пересоберет editor.js с поддержкой XML
|
||||
```
|
||||
|
||||
4. **Использовать в админке**: Теперь вы можете использовать `data-language="xml"` для любого поля.
|
||||
|
||||
## Обновление зависимостей
|
||||
|
||||
1. **Проверить устаревшие пакеты**:
|
||||
```bash
|
||||
cd frontend-assembly
|
||||
npm outdated
|
||||
```
|
||||
2. **Обновить версии**: Аккуратно обновите номера версий в `package.json`.
|
||||
3. **Установить обновления и пересобрать**:
|
||||
```bash
|
||||
npm install
|
||||
bash build-codemirror6.sh
|
||||
```
|
||||
4. **Закоммитить изменения**: Добавьте в коммит обновленные `package.json`, `package-lock.json` и `public/static/codemirror/editor.js`.
|
||||
@@ -0,0 +1,91 @@
|
||||
# frontend-assembly
|
||||
|
||||
Эта папка отвечает за сборку CodeMirror 6 для админки Django.
|
||||
Идея здесь простая: код редактора собирается как готовый минифицированный бандл,
|
||||
а в репозиторий и в `public/static` попадает только готовый JS CodeMirror.
|
||||
|
||||
## Что здесь лежит
|
||||
|
||||
### `package.json`
|
||||
Главный файл npm-проекта.
|
||||
|
||||
В нём описано:
|
||||
- имя сборочного пакета;
|
||||
- версия;
|
||||
- зависимость от `esbuild` и пакетов CodeMirror 6;
|
||||
- npm-скрипт `build`.
|
||||
|
||||
Скрипт `build` собирает `src/editor.js` в файл:
|
||||
|
||||
```bash
|
||||
../public/static/codemirror/editor.js
|
||||
```
|
||||
|
||||
При сборке используется `--minify`, потому что редактор нужен как готовый
|
||||
чёрный ящик для админки, а не как объект для отладки.
|
||||
|
||||
### `package-lock.json`
|
||||
Файл фиксации версий npm-зависимостей.
|
||||
|
||||
Он нужен, чтобы сборка была воспроизводимой:
|
||||
- `npm ci` ставит ровно те версии, которые уже зафиксированы;
|
||||
- одинаковый результат получается и на любом компьютере, и при любом новом клоне репозитория;
|
||||
- при повторной сборке не подтягиваются случайные новые версии пакетов.
|
||||
|
||||
### `build-codemirror6.sh`
|
||||
Основной скрипт сборки.
|
||||
|
||||
Он делает всё сам:
|
||||
1. проверяет наличие `npm`;
|
||||
2. создаёт временную рабочую папку;
|
||||
3. временно создаёт там `src/editor.js`;
|
||||
4. устанавливает зависимости через `npm ci`;
|
||||
5. запускает `npm run build`;
|
||||
6. кладёт готовый бандл в `public/static/codemirror/editor.js`;
|
||||
7. удаляет временные `src/` и `node_modules/` после завершения.
|
||||
|
||||
Это означает, что в рабочем дереве не остаётся мусора от сборки.
|
||||
|
||||
### `src/editor.js`
|
||||
Временный исходник редактора.
|
||||
|
||||
Он **не хранится в репозитории** как постоянный файл: скрипт создаёт его во
|
||||
временной директории только на время сборки.
|
||||
|
||||
Внутри этого файла живёт минимальная инициализация CodeMirror 6:
|
||||
- монтирование на `textarea[data-codemirror-editor]`;
|
||||
- HTML-режим;
|
||||
- JavaScript-режим;
|
||||
- CSS-режим;
|
||||
- тема `oneDark`;
|
||||
- перенос строк;
|
||||
- синхронизация значения редактора обратно в скрытую textarea.
|
||||
|
||||
Если позже захочется менять поведение редактора, логика правится либо в шаблоне,
|
||||
который выдаёт этот исходник, либо прямо в сборочном скрипте.
|
||||
|
||||
### `public/static/codemirror/editor.js`
|
||||
|
||||
Готовый результат сборки -- `public/static/codemirror/editor.js`. Именно этот файл подключается в Django-админку.
|
||||
Он уже минифицирован и готов к использованию как обычная статика.
|
||||
|
||||
## Как запускать сборку
|
||||
|
||||
Из корня проекта:
|
||||
|
||||
```bash
|
||||
bash ./frontend-assembly/build-codemirror6.sh
|
||||
```
|
||||
|
||||
После успешного запуска в проекте должен обновиться только один полезный артефакт:
|
||||
|
||||
```bash
|
||||
public/static/codemirror/editor.js
|
||||
```
|
||||
|
||||
## Коротко о логике работы
|
||||
|
||||
Смысл этой папки такой:
|
||||
- собрать CodeMirror 6 один раз;
|
||||
- не держать в репозитории и контейнере лишний фронтенд-мусор;
|
||||
- оставить Django только готовый JS-бандл CodeMirror для админки.
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
# Скрипт создаёт временную рабочую папку, ставит зависимости через `npm ci`, собирает
|
||||
# минимизированный бандл и затем сам удаляет временные `src/` и `node_modules/`.
|
||||
# В проекте остаётся только готовая статика:
|
||||
# * `public/static/codemirror/editor.js`
|
||||
#
|
||||
# Запуск:
|
||||
# bash ./frontend-assembly/build-codemirror6.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OUTPUT_DIR="$ROOT_DIR/../public/static/codemirror"
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/codemirror6.XXXXXX")"
|
||||
|
||||
log() {
|
||||
printf '[codemirror6] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[codemirror6] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
rm -rf "$ROOT_DIR/src" "$ROOT_DIR/node_modules"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
fail 'Не найден `npm`. Установи Node.js и повтори сборку.'
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ROOT_DIR/package.json" ]]; then
|
||||
fail "Не найден package.json: $ROOT_DIR/package.json"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ROOT_DIR/package-lock.json" ]]; then
|
||||
fail "Не найден package-lock.json: $ROOT_DIR/package-lock.json"
|
||||
fi
|
||||
|
||||
mkdir -p "$WORK_DIR/src" "$OUTPUT_DIR"
|
||||
cp "$ROOT_DIR/package.json" "$ROOT_DIR/package-lock.json" "$WORK_DIR/"
|
||||
|
||||
cat > "$WORK_DIR/src/editor.js" <<'EOF'
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { EditorView, lineNumbers, placeholder } from '@codemirror/view';
|
||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { solarizedDark, solarizedLight } from '@uiw/codemirror-theme-solarized';
|
||||
|
||||
const themeCompartment = new Compartment();
|
||||
const processedForms = new Set();
|
||||
|
||||
function isDarkTheme() {
|
||||
const rootTheme = document.documentElement.dataset.theme;
|
||||
if (rootTheme === 'dark') return true;
|
||||
if (rootTheme === 'light') return false;
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
function reconfigureTheme(view) {
|
||||
view.dispatch({
|
||||
effects: themeCompartment.reconfigure(isDarkTheme() ? solarizedDark : solarizedLight),
|
||||
});
|
||||
}
|
||||
|
||||
function initCodeMirrorEditors() {
|
||||
document.querySelectorAll('textarea[data-codemirror-editor]').forEach((textarea) => {
|
||||
const language = textarea.dataset.language || 'text';
|
||||
const inputMode = textarea.dataset.inputMode || 'default';
|
||||
let initialDoc = textarea.value ?? '';
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'cm6-editor-wrapper';
|
||||
textarea.insertAdjacentElement('beforebegin', wrapper);
|
||||
|
||||
if (language === 'json') {
|
||||
try {
|
||||
const parsed = JSON.parse(initialDoc);
|
||||
initialDoc = JSON.stringify(parsed, null, 2);
|
||||
} catch (e) {
|
||||
console.warn("CodeMirror: Initial content is not valid JSON, displaying as is.", e);
|
||||
}
|
||||
}
|
||||
|
||||
const syncTextarea = EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
textarea.value = update.state.doc.toString();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Собираем расширения в правильном порядке ---
|
||||
const extensions = [
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
syncTextarea,
|
||||
themeCompartment.of(isDarkTheme() ? solarizedDark : solarizedLight),
|
||||
];
|
||||
|
||||
// Язык
|
||||
if (language === 'javascript') extensions.push(javascript());
|
||||
else if (language === 'css') extensions.push(css());
|
||||
else if (language === 'json') extensions.push(json());
|
||||
else if (language === 'html') extensions.push(html());
|
||||
|
||||
// Режим ввода
|
||||
if (inputMode === 'url') {
|
||||
extensions.push(placeholder('https://example.com/path'));
|
||||
// Для URL не добавляем lineWrapping и lineNumbers
|
||||
} else {
|
||||
extensions.push(EditorView.lineWrapping);
|
||||
if (!textarea.classList.contains('codemirror-no-lines')) {
|
||||
extensions.push(lineNumbers());
|
||||
}
|
||||
}
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: initialDoc,
|
||||
extensions,
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state,
|
||||
parent: wrapper,
|
||||
});
|
||||
|
||||
textarea.cmView = view;
|
||||
|
||||
reconfigureTheme(view);
|
||||
|
||||
const observer = new MutationObserver(() => reconfigureTheme(view));
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme', 'class'],
|
||||
});
|
||||
|
||||
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
colorScheme.addEventListener('change', () => reconfigureTheme(view));
|
||||
|
||||
const form = textarea.closest('form');
|
||||
if (form && !processedForms.has(form)) {
|
||||
form.addEventListener('submit', () => {
|
||||
form.querySelectorAll('textarea[data-language="json"]').forEach(jsonTextarea => {
|
||||
if (jsonTextarea.cmView) {
|
||||
const prettyJson = jsonTextarea.cmView.state.doc.toString();
|
||||
try {
|
||||
const parsed = JSON.parse(prettyJson);
|
||||
const minifiedJson = JSON.stringify(parsed);
|
||||
jsonTextarea.value = minifiedJson;
|
||||
} catch (e) {
|
||||
console.warn("CodeMirror: Could not minify invalid JSON before submit.", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
processedForms.add(form);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCodeMirrorEditors, { once: true });
|
||||
} else {
|
||||
initCodeMirrorEditors();
|
||||
}
|
||||
EOF
|
||||
|
||||
log "СОБИРАЮ CodeMirror 6 ДЛЯ ФРОНТЕНДА АДМИНКИ ПРОЕКТА"
|
||||
log "Временная рабочая папка: $WORK_DIR"
|
||||
cd "$WORK_DIR"
|
||||
|
||||
log 'Устанавливаю зависимости через npm ci'
|
||||
npm ci
|
||||
|
||||
log 'Собираю CodeMirror 6'
|
||||
export CM6_OUTPUT_DIR="$OUTPUT_DIR"
|
||||
npm run build
|
||||
|
||||
log 'ГОТОВО'
|
||||
Generated
+753
@@ -0,0 +1,753 @@
|
||||
{
|
||||
"name": "lpon-codemirror6",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lpon-codemirror6",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/lang-css": "6.3.1",
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/lang-javascript": "6.2.5",
|
||||
"@codemirror/lang-json": "6.0.1",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.1",
|
||||
"@uiw/codemirror-theme-solarized": "4.25.10",
|
||||
"esbuild": "0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.11",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
|
||||
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.1.tgz",
|
||||
"integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@uiw/codemirror-theme-solarized": {
|
||||
"version": "4.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-solarized/-/codemirror-theme-solarized-4.25.10.tgz",
|
||||
"integrity": "sha512-mTP09g375qD86T09aRKQLHqkRSWDoot4dFUCtiyIsrvp1VeMBcA5ZSJHnI8A0lc8+hHRrvpL5fe3SnRFE+FoZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uiw/codemirror-themes": "4.25.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-themes": {
|
||||
"version": "4.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.10.tgz",
|
||||
"integrity": "sha512-Fqiz1HIuDlDftcL+/O53V333UOH6MqQ84VbiQB5egn6u+uDwAqACp1FrdAoi4wgpR3b3TGW4Gr0wIYcrJSSz1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "lpon-codemirror6",
|
||||
"version": "0.1.0",
|
||||
"description": "CodeMirror build for LPON.RU Django Admin",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "esbuild src/editor.js --bundle --format=iife --minify --outfile=${CM6_OUTPUT_DIR:-../public/static/codemirror}/editor.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/lang-css": "6.3.1",
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/lang-javascript": "6.2.5",
|
||||
"@codemirror/lang-json": "6.0.1",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.1",
|
||||
"@uiw/codemirror-theme-solarized": "4.25.10",
|
||||
"esbuild": "0.28.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
# Кастомная конфигурация Django Admin для LPON сайта.
|
||||
# Регистрируем модели с удобным интерфейсом.
|
||||
|
||||
from django import forms
|
||||
from django.forms import Textarea
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html, mark_safe
|
||||
from easy_thumbnails.files import get_thumbnailer
|
||||
from .models import (
|
||||
TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller,
|
||||
TbOffer, TbSource, TbOfferHistory, TbMusicStyle
|
||||
)
|
||||
from .utils_validators import validate_entity_for_admin_form, generate_admin_save_message
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ЛОКАЛЬНЫЕ ХЕЛПЕРЫ ДЛЯ АДМИНКИ
|
||||
# ============================================================================
|
||||
|
||||
def get_related_article_description(model_class):
|
||||
"""
|
||||
Генерирует HTML-описание для fieldset'а 'Связанная публикация'.
|
||||
|
||||
Описание объясняет роль связанной статьи и предлагает создавать её вручную.
|
||||
Использует verbose_name из модели для универсальности.
|
||||
|
||||
Args:
|
||||
model_class: Класс модели (TbLabel, TbArtist, TbMusicStyle и т.д.)
|
||||
|
||||
Returns:
|
||||
str: HTML-описание для fieldset'а
|
||||
|
||||
Пример:
|
||||
'description': get_related_article_description(TbLabel)
|
||||
"""
|
||||
# Получаем наименование модели в родительном падеже (для описания)
|
||||
verbose_name = model_class._meta.verbose_name.upper()
|
||||
|
||||
# Получаем наименование во множественном числе (для списков альбомов)
|
||||
verbose_name_plural = model_class._meta.verbose_name_plural.upper()
|
||||
|
||||
return (f'Прикреп­ленная статья (если есть) будет отображаться на странице <u>{verbose_name}</u>'
|
||||
f' на сайте. Также позволяет получать список всех альбомов <u>{verbose_name_plural}</u>,'
|
||||
f' управлять SEO-атрибутами для улучшения видимости поисковых систем, иметь красивый'
|
||||
f' slag для URL-странички, подсчитывать число просмотров и добавлений'
|
||||
f' в избранные. <b style=\'color: green;\'>ОЧЕНЬ РЕКОМЕН­ДУЕТСЯ СОЗДАВАТЬ'
|
||||
f' И ПРИВЯЗЫВАТЬ СТАТЬЮ ВРУЧНУЮ</b>. Если публикация не создана вручную,'
|
||||
f' то она будет создана автоматически (пустая) при сохранении <u>{verbose_name}</u>,'
|
||||
f' со всеми SEO-атрибутами и slag, но автоматика несовершенна.<br /> ')
|
||||
|
||||
|
||||
class RequestInFormMixin(admin.ModelAdmin):
|
||||
"""
|
||||
Миксин для передачи request объекта в форму.
|
||||
|
||||
Используется когда форма нуждается в доступе к request для проверки POST параметров
|
||||
или другой информации о текущем HTTP-запросе.
|
||||
|
||||
Переопределяет get_form() и передает request в __init__ формы через kwargs.
|
||||
"""
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""
|
||||
Переопределяем get_form чтобы передать request в форму.
|
||||
Создаем оборачивающий класс который передаст request в __init__.
|
||||
"""
|
||||
FormClass = super().get_form(request, obj, **kwargs)
|
||||
|
||||
# Сохраняем request в замыкании для доступа в классе
|
||||
request_ref = request
|
||||
|
||||
class FormWithRequest(FormClass):
|
||||
"""Оборачивающий класс который передает request при инстанцировании"""
|
||||
def __init__(form_instance, *args, **init_kwargs):
|
||||
# Добавляем request в kwargs перед вызовом __init__ родителя
|
||||
init_kwargs['request'] = request_ref
|
||||
super().__init__(*args, **init_kwargs)
|
||||
|
||||
return FormWithRequest
|
||||
|
||||
# ============================================================================
|
||||
# АДМИНИСТРИРОВАНИЕ TbImage
|
||||
#
|
||||
# Кастомня форма для админки TbImage
|
||||
class TbImageAdminForm(forms.ModelForm):
|
||||
"""
|
||||
Кастомная форма для TbImage в админке.
|
||||
Добавляет виртуальные поля для редактирования метаданных filer_image
|
||||
(default_alt_text и default_caption), которые не хранятся в TbImage, но есть в filer_image
|
||||
"""
|
||||
# Виртуальные поля для заполнения метаданных filer_image
|
||||
alt_text = forms.CharField(
|
||||
max_length=255,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Введите alt-текст для картинки',
|
||||
}),
|
||||
label='ALT (новый)',
|
||||
help_text='Текст для alt-атрибута картинки <tt><img alt="" .../></tt>.'
|
||||
' Будет сохранён в filer_image.default_alt_text'
|
||||
)
|
||||
title_text = forms.CharField(
|
||||
max_length=255,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Введите title-описание картинки',
|
||||
}),
|
||||
label='TITLE (новый)',
|
||||
help_text='Текст для title-атрибута картинки <tt><img title="" .../></tt>.'
|
||||
' Будет сохранён в filer_image.default_caption'
|
||||
)
|
||||
copyright_text = forms.CharField(
|
||||
max_length=255,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': 'XXXX, Авторские права на изображение',
|
||||
}),
|
||||
label='Copyright',
|
||||
help_text='Авторские права на изображение (например: <tt>2025, Sergei Erjemin</tt>. Будет сохранён в filer_image.author'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TbImage
|
||||
fields = ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort', 'f_img_confidence_score')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
При инициализации формы подгружаем текущие значения alt/caption из filer_image.
|
||||
"""
|
||||
# Атрибуты для активации CodeMirror редактора
|
||||
codemirror_attrs = {
|
||||
'data-codemirror-editor': '1',
|
||||
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||
}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Если редактируем существующую запись, получаем текущие значения из filer
|
||||
if self.instance and self.instance.pk and hasattr(self.instance, 'image') and self.instance.image:
|
||||
try:
|
||||
# Получаем связанные данные из filer_image
|
||||
filer_image = self.instance.image
|
||||
|
||||
# Устанавливаем значения из filer в виртуальные поля
|
||||
self.fields['alt_text'].initial = filer_image.default_alt_text or ''
|
||||
self.fields['title_text'].initial = filer_image.default_caption or ''
|
||||
self.fields['copyright_text'].initial = filer_image.author or ''
|
||||
|
||||
# Активируем CodeMirror и устанавливаем CSS-классы для виртуальных полей
|
||||
self.fields['alt_text'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-m',
|
||||
})
|
||||
self.fields['title_text'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-l',
|
||||
})
|
||||
self.fields['copyright_text'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-m',
|
||||
})
|
||||
except Exception:
|
||||
# Если ошибка при получении filer_image, просто оставляем пустые значения
|
||||
pass
|
||||
|
||||
# Активируем CodeMirror и устанавливаем классы для реальных полей
|
||||
self.fields['s_img_src_url'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'data-language': 'url',
|
||||
'class': 'codemirror-width-xl',
|
||||
})
|
||||
self.fields['i_img_sort'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-s codemirror-no-lines',
|
||||
})
|
||||
self.fields['f_img_confidence_score'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-s codemirror-no-lines',
|
||||
})
|
||||
|
||||
# Админка для TbImage с кастомной формой
|
||||
class ImageAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Админ для изображений TbImage с поддержкой редактирования метаданных filer_image.
|
||||
|
||||
Позволяет пользователю заполнить default_alt_text и default_caption для картинки в filer
|
||||
прямо в админке TbImage, без необходимости отдельного редактирования filer.
|
||||
"""
|
||||
form = TbImageAdminForm # Используем кастомную форму с виртуальными полями
|
||||
|
||||
# Подключаем JS через Media (правильный способ!)
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js', # Основной CodeMirror
|
||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||
)
|
||||
|
||||
list_display = ('id', 'image_thumbnail', 'image', '_display_alt_text', 'i_img_sort', 't_img_created')
|
||||
list_display_links = ('id', 'image_thumbnail', 'image')
|
||||
list_filter = ('l_img_source', 'l_img_reality', 't_img_created')
|
||||
ordering = ('image', 'i_img_sort')
|
||||
readonly_fields = ('t_img_created', 't_img_updated', '_display_alt_text', '_display_title_text')
|
||||
|
||||
fieldsets = (
|
||||
('Изображение', {
|
||||
'fields': ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort',
|
||||
'f_img_confidence_score'),
|
||||
'description': 'Основные данные об изображении и источнике',
|
||||
}),
|
||||
('Метаданные filer (SEO для картинок)', {
|
||||
'fields': ('_display_alt_text', '_display_title_text', 'alt_text', 'title_text',
|
||||
'copyright_text'),
|
||||
'description': 'Редактируемые поля для заполнения ALT-, TITLE- и ©-текста в filer. Если не заполнить,'
|
||||
' текущие значения останутся без изменений (и не будут заполнены при создании).',
|
||||
# 'classes': ('collapse',),
|
||||
}),
|
||||
('Служебная информация', {
|
||||
'fields': ('t_img_created', 't_img_updated'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
def image_thumbnail(self, obj):
|
||||
"""
|
||||
Отображает миниатюру картинки (40x40) в списке и в list_display_links.
|
||||
Использует easy_thumbnails для автоматического создания и кэширования миниатюр.
|
||||
"""
|
||||
if obj and obj.image:
|
||||
try:
|
||||
# Получаем thumbnailer для картинки через easy_thumbnails
|
||||
thumbnailer = get_thumbnailer(obj.image.file)
|
||||
|
||||
# Генерируем или получаем уже созданную миниатюру размером 40x40
|
||||
thumbnail = thumbnailer.get_thumbnail({
|
||||
'size': (40, 40), # Размер миниатюры: 40x40 пикселей
|
||||
'crop': 'smart', # Умное обрезание для сохранения центра картинки
|
||||
'quality': 95, # Качество JPEG/WebP (95% для хорошего вида)
|
||||
})
|
||||
|
||||
# Возвращаем HTML тег img с миниатюрой
|
||||
# format_html автоматически экранирует опасные символы
|
||||
return format_html(
|
||||
'<img src="{}" width="40" height="40" alt="{}" '
|
||||
'style="border-radius: 4px; object-fit: cover; cursor: pointer;"/>',
|
||||
thumbnail.url,
|
||||
obj.image.name or 'картинка' # Alt текст для доступности
|
||||
)
|
||||
except Exception as e:
|
||||
# Если ошибка при генерации миниатюры (нет файла, ошибка формата и т.д.)
|
||||
return mark_safe('<span style="color: #ccc;">(ошибка)</span>')
|
||||
|
||||
# Если картинка не привязана
|
||||
return mark_safe('<span style="color: #ccc;">(нет картинки)</span>')
|
||||
|
||||
# Установляем название столбца в админке
|
||||
image_thumbnail.short_description = 'Миниатюра (40x40)'
|
||||
|
||||
def _display_alt_text(self, obj):
|
||||
"""
|
||||
Display-метод для отображения текущего alt-текста в filer (read-only).
|
||||
Показывает, какой текст сейчас установлен в filer_image.
|
||||
"""
|
||||
if obj and obj.pk and obj.image:
|
||||
try:
|
||||
current = obj.image.default_alt_text or '(пусто)'
|
||||
return f'ALT: {current}'
|
||||
except Exception:
|
||||
return '(ошибка при получении)'
|
||||
return '(новая запись, значение будет установлено после сохранения)'
|
||||
|
||||
_display_alt_text.short_description = 'ALT из filer'
|
||||
|
||||
def _display_title_text(self, obj):
|
||||
"""
|
||||
Display-метод для отображения текущего caption в filer (read-only).
|
||||
Показывает, какой текст сейчас установлен в filer_image.
|
||||
"""
|
||||
if obj and obj.pk and obj.image:
|
||||
try:
|
||||
current = obj.image.default_caption or '(пусто)'
|
||||
return f'TITLE: {current}'
|
||||
except Exception:
|
||||
return '(ошибка при получении)'
|
||||
return '(новая запись, значение будет установлено после сохранения)'
|
||||
|
||||
_display_title_text.short_description = 'TITLE из filer'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Переопределяем save_model для обновления метаданных filer_image.
|
||||
|
||||
Если пользователь заполнил виртуальные поля alt_text или title_text,
|
||||
их значения сохраняются в соответствующие поля filer_image.
|
||||
Если поля не заполнены, текущие значения в filer остаются без изменений.
|
||||
"""
|
||||
# Сначала сохраняем саму запись TbImage
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
# Работаем с meta-данными filer только если картинка привязана
|
||||
if obj.image:
|
||||
try:
|
||||
filer_image = obj.image
|
||||
|
||||
# Обновляем alt_text (ALT), если было заполнено в форме
|
||||
alt_text = form.cleaned_data.get('alt_text', '').strip()
|
||||
if alt_text: # Если пользователь что-то ввел
|
||||
filer_image.default_alt_text = alt_text
|
||||
|
||||
# Обновляем caption (TITLE), если было заполнено в форме
|
||||
caption = form.cleaned_data.get('title_text', '').strip()
|
||||
if caption: # Если пользователь что-то ввел
|
||||
filer_image.default_caption = caption
|
||||
|
||||
# Обновляем author (copyrughight), если было заполнено в форме
|
||||
author = form.cleaned_data.get('copyright_text', '').strip()
|
||||
if author:
|
||||
filer_image.author = author
|
||||
|
||||
# Сохраняем filer_image с новыми метаданными
|
||||
filer_image.save()
|
||||
|
||||
except Exception as e:
|
||||
# Логируем ошибку, но не прерываем процесс сохранения
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f'Ошибка при сохранении метаданных filer_image: {e}')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# АДМИНКА для музыкальных стилей, таблица TbMusicStyle
|
||||
#
|
||||
# Кастомная форма для MusicStyleAdmin
|
||||
class MusicStyleAdminForm(forms.ModelForm):
|
||||
"""
|
||||
Кастомная форма для админки музыкальных стилей.
|
||||
Добавляет виджеты CodeMirror для текстовых полей
|
||||
"""
|
||||
class Meta:
|
||||
model = TbMusicStyle
|
||||
fields = ('s_style_name', 'j_style_synonyms', 'k_style_to_article',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
При инициализации формы подгружаем
|
||||
"""
|
||||
# Атрибуты для активации CodeMirror редактора
|
||||
codemirror_attrs = {
|
||||
'data-codemirror-editor': '1',
|
||||
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||
}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Активируем CodeMirror и устанавливаем классы для реальных полей
|
||||
self.fields['s_style_name'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'data-codemirror-mode': 'text',
|
||||
'class': 'codemirror-width-xl codemirror-no-lines',
|
||||
})
|
||||
self.fields['j_style_synonyms'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'data-language': 'json',
|
||||
'class': 'codemirror-width-l codemirror-min-height-5',
|
||||
})
|
||||
|
||||
# Админка для TbMusicStyle с кастомной формой MusicStyleAdminForm
|
||||
class MusicStyleAdmin(admin.ModelAdmin):
|
||||
"""Админ для музыкальных стилей"""
|
||||
form = MusicStyleAdminForm
|
||||
|
||||
# Подключаем JS через Media (правильный способ!)
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js', # Основной CodeMirror
|
||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||
)
|
||||
|
||||
list_display = ('id', 's_style_name', 'j_style_synonyms', 't_style_created', 't_style_updated',)
|
||||
list_display_links = ('id', 's_style_name',)
|
||||
search_fields = ('s_style_name', 'j_style_synonyms',)
|
||||
readonly_fields = ('t_style_created', 't_style_updated',)
|
||||
|
||||
fieldsets = (
|
||||
('Основные данные о музыкальном стиле', {
|
||||
'fields': ('s_style_name', 'j_style_synonyms',),
|
||||
}),
|
||||
('Связанная публикация', {
|
||||
'fields': ('k_style_to_article',),
|
||||
'description': get_related_article_description(TbMusicStyle),
|
||||
# 'classes': ('collapse',),
|
||||
}),
|
||||
('Служебная информация', {
|
||||
'fields': ('t_style_created', 't_style_updated'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# АДМИНКА для Исполнителей/Групп/Артистов, таблица TbArtist
|
||||
#
|
||||
# Кастомная форма для ArtistAdmin
|
||||
class ArtistAdminForm(forms.ModelForm):
|
||||
"""
|
||||
Кастомная форма для админки продавца (Seller).
|
||||
Добавляет виджеты CodeMirror для текстовых полей
|
||||
"""
|
||||
class Meta:
|
||||
model = TbArtist
|
||||
fields = ('s_artist', 'k_artist_to_article', 'j_artist_metadata', )
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
При инициализации формы подгружаем
|
||||
"""
|
||||
# Атрибуты для активации CodeMirror редактора
|
||||
codemirror_attrs = {
|
||||
'data-codemirror-editor': '1',
|
||||
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||
}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Активируем CodeMirror и устанавливаем классы для реальных полей
|
||||
self.fields['s_artist'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'data-codemirror-mode': 'text',
|
||||
'class': 'codemirror-width-xl codemirror-no-lines',
|
||||
})
|
||||
self.fields['j_artist_metadata'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'data-language': 'json',
|
||||
'class': 'codemirror-width-l codemirror-min-height-5',
|
||||
})
|
||||
|
||||
# Админка для TbArtist с кастомной формой ArtistAdminForm
|
||||
class ArtistAdmin(admin.ModelAdmin):
|
||||
"""Админ для артистов"""
|
||||
form = ArtistAdminForm # Используем кастомную форму с виртуальными полями
|
||||
|
||||
# Подключаем JS через Media (правильный способ!)
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js', # Основной CodeMirror
|
||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||
)
|
||||
|
||||
list_display = ('id', 's_artist', 't_artist_created')
|
||||
list_display_links = ('id', 's_artist',)
|
||||
search_fields = ('s_artist',)
|
||||
readonly_fields = ('t_artist_created', 't_artist_updated')
|
||||
fieldsets = (
|
||||
('Основные данные об исполнителе (артисте, группе)', {
|
||||
'fields': ('s_artist', 'j_artist_metadata',),
|
||||
}),
|
||||
('Связанная публикация', {
|
||||
'fields': ('k_artist_to_article', ),
|
||||
'description': get_related_article_description(TbArtist),
|
||||
# 'classes': ('collapse',),
|
||||
}),
|
||||
('Служебная информация', {
|
||||
'fields': ('t_artist_created', 't_artist_updated'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
# ================
|
||||
# АДМИН-ПАНЕЛЬ ДЛЯ ЛЕЙБЛОВ/ИЗДАТЕЛЕЙ
|
||||
#
|
||||
# Кастомная форма
|
||||
class LabelAdminForm(forms.ModelForm):
|
||||
"""
|
||||
Кастомная форма для админки лейблов (Label).
|
||||
Добавляет виджеты CodeMirror для текстовых полей
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = TbLabel
|
||||
fields = ('s_label', 'k_label_to_article', 'j_label_metadata',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
При инициализации формы подгружаем CodeMirror.
|
||||
Получаем request из kwargs, переданных из get_form_kwargs в AdminClass.
|
||||
"""
|
||||
# Извлекаем request из kwargs если он есть
|
||||
self.request = kwargs.pop('request', None)
|
||||
|
||||
# Атрибуты для активации CodeMirror редактора
|
||||
codemirror_attrs = {
|
||||
'data-codemirror-editor': '1',
|
||||
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||
}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# Активируем CodeMirror и устанавливаем классы для реальных полей
|
||||
self.fields['s_label'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-xl codemirror-no-lines',
|
||||
'data-language': 'text',
|
||||
})
|
||||
self.fields['j_label_metadata'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-l codemirror-min-height-5',
|
||||
'data-language': 'json',
|
||||
})
|
||||
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Валидируем форму: проверяем на совпадения (дубликаты) основного поля s_label.
|
||||
Используем GET параметр ignore_validate для пропуска валидации при переотправке.
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Используем универсальный хелпер для проверки дубликатов
|
||||
# Модель берется автоматически из self.Meta.model
|
||||
# Передаем request для проверки GET параметра ignore_validate
|
||||
validate_entity_for_admin_form(
|
||||
self,
|
||||
cleaned_data,
|
||||
main_field_name='s_label',
|
||||
metadata_field_name='j_label_metadata',
|
||||
request=self.request,
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
# Админ для лейбла (Label) через миксин
|
||||
class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
|
||||
"""Админ для лейблов с поддержкой передачи request в форму"""
|
||||
form = LabelAdminForm # Используем кастомную форму с виджетами CodeMirror
|
||||
|
||||
# Подключаем JS через Media (правильный способ!)
|
||||
class Media:
|
||||
css = {
|
||||
'all': (
|
||||
'codemirror/codemirror-styles.css', # Стили для CodeMirror
|
||||
'css/validation-override.css', # Стили для обхода валидации
|
||||
)
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js', # Основной CodeMirror
|
||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||
'js/form-field-watcher.js', # Вотчер для отслеживания изменений полей формы
|
||||
)
|
||||
|
||||
list_display = ('id', 's_label', 't_label_created')
|
||||
list_display_links = ('id', 's_label',)
|
||||
search_fields = ('s_label',)
|
||||
readonly_fields = ('t_label_created', 't_label_updated')
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Переопределяем save_model для добавления информативных сообщений в админку.
|
||||
|
||||
Максимально поджарый код - все сложности с получением старого значения
|
||||
и определением типа операции делает хелпер generate_admin_save_message().
|
||||
"""
|
||||
# Стандартное сохранение записи через Django
|
||||
# (в т.ч. автоматическое создание связанной статьи в методе save модели TbLabel)
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
# Генерируем и отправляем информативное сообщение об сохранении
|
||||
# Хелпер сам:
|
||||
# - определяет тип операции (create vs update)
|
||||
# - формирует нужное сообщение (success vs warning)
|
||||
generate_admin_save_message(
|
||||
request=request,
|
||||
obj=obj,
|
||||
is_new=not change, # Django: change=False для новых, True для существующих
|
||||
related_article=obj.k_label_to_article,
|
||||
obj_field_name='s_label',
|
||||
article_title_field='s_article_title',
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
('Основные данные о лейбле/издателе', {
|
||||
'fields': ('s_label', 'j_label_metadata',),
|
||||
}),
|
||||
('Связанная публикация', {
|
||||
'fields': ('k_label_to_article', ),
|
||||
'description': get_related_article_description(TbLabel),
|
||||
# 'classes': ('collapse',),
|
||||
}),
|
||||
('Служебная информация', {
|
||||
'fields': ('t_label_created', 't_label_updated'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
|
||||
# ================
|
||||
# АДМИН-ПАНЕЛЬ ДЛЯ ПРОДАВЦА/SELLER
|
||||
#
|
||||
# Кастомная форма
|
||||
class SellerAdminForm(forms.ModelForm):
|
||||
"""
|
||||
Кастомная форма для админки продавца (Seller).
|
||||
Добавлaет виджеты CodeMirror для текстовых полей
|
||||
"""
|
||||
class Meta:
|
||||
model = TbSeller
|
||||
fields = ('id', 's_seller', 'l_seller_currency', 'k_seller_to_article', 'l_seller_type',
|
||||
'j_seller_metadata',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
При инициализации формы подгружаем
|
||||
"""
|
||||
# Атрибуты для активации CodeMirror редактора
|
||||
codemirror_attrs = {
|
||||
'data-codemirror-editor': '1',
|
||||
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||
}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Активируем CodeMirror и устанавливаем классы для реальных полей
|
||||
self.fields['s_seller'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-l codemirror-no-lines',
|
||||
'data-language': 'text',
|
||||
})
|
||||
self.fields['j_seller_metadata'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-xl codemirror-min-height-5',
|
||||
'data-language': 'json',
|
||||
})
|
||||
|
||||
# Админ для продавца (Seller)
|
||||
class SellerAdmin(admin.ModelAdmin):
|
||||
"""Админ для продавцов"""
|
||||
form = SellerAdminForm # Используем кастомную форму с виртуальными полями
|
||||
|
||||
# Подключаем JS через Media (правильный способ!)
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js', # Основной CodeMirror
|
||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||
)
|
||||
list_display = ('id', 's_seller', 'l_seller_type', 'l_seller_currency', 't_seller_created',)
|
||||
list_display_links = ('id', 's_seller',)
|
||||
list_filter = ('l_seller_type', 'l_seller_currency',)
|
||||
search_fields = ('s_seller',)
|
||||
readonly_fields = ('t_seller_created', 't_seller_updated',)
|
||||
fieldsets = (
|
||||
('Основные данные о продавце', {
|
||||
'fields': ('s_seller', 'l_seller_currency', 'l_seller_type',
|
||||
'j_seller_metadata',),
|
||||
}),
|
||||
('Связанная публикация', {
|
||||
'fields': ('k_seller_to_article', ),
|
||||
'description': 'Прикреп­ленная статья (если есть) будет отображаться на странице продавца'
|
||||
' на сайте. Также позволяет получать список всех предложений продавца, управлять'
|
||||
' SEO-атрибутами для улучшения видимости поисковых систем, иметь красивый'
|
||||
' slag для URL-странички, подсчитывать число просмотров и добавлений'
|
||||
' в избранные. <b style=\'color: green;\'>ОЧЕНЬ РЕКОМЕН­ДУЕТСЯ СОЗДАВАТЬ'
|
||||
' И ПРИВЯЗЫВАТЬ СТАТЬЮ ВРУЧНУЮ</b>. Если публикация не создана вручную,'
|
||||
' то она будет создана автоматически (пустая) при сохранении продавца,'
|
||||
' со всеми SEO-атрибутами и slag, но автоматика несовершенна.<br /> ',
|
||||
# 'classes': ('collapse',),
|
||||
}),
|
||||
('Служебная информация', {
|
||||
'fields': ('t_seller_created', 't_seller_updated'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# АДМИНКА ИСТОЧНИКОВ ДАННЫХ
|
||||
#
|
||||
# Кастомная форма
|
||||
class SourceAdminForm(forms.ModelForm):
|
||||
"""
|
||||
Кастомная форма для админки источников данных (TbSource).
|
||||
Добавляет виджеты CodeMirror для текстовых полей
|
||||
"""
|
||||
class Meta:
|
||||
model = TbSource
|
||||
fields = ('id', 'k_source_to_seller', 's_source_name', 'l_source_type', 't_source_data',
|
||||
'source_file', 's_source_url', 'j_source_metadata',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
При инициализации формы подгружаем
|
||||
"""
|
||||
# Атрибуты для активации CodeMirror редактора
|
||||
codemirror_attrs = {
|
||||
'data-codemirror-editor': '1',
|
||||
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||
}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Активируем CodeMirror и устанавливаем классы для реальных полей
|
||||
self.fields['s_source_name'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-l codemirror-no-lines',
|
||||
'data-language': 'text',
|
||||
})
|
||||
self.fields['s_source_url'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-xl codemirror-no-lines',
|
||||
'data-language': 'url',
|
||||
})
|
||||
self.fields['j_source_metadata'].widget = Textarea(attrs={
|
||||
**codemirror_attrs,
|
||||
'class': 'codemirror-width-l codemirror-min-height-5',
|
||||
'data-language': 'json',
|
||||
})
|
||||
|
||||
#
|
||||
class SourceAdmin(admin.ModelAdmin):
|
||||
"""Админ для источников"""
|
||||
form = SourceAdminForm # Используем кастомную форму с виртуальными полями
|
||||
|
||||
# Подключаем JS через Media (правильный способ!)
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js', # Основной CodeMirror
|
||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||
)
|
||||
list_display = ('id', 's_source_name', 'k_source_to_seller', 'l_source_type', 't_source_data')
|
||||
list_display_links = ('id', 's_source_name',)
|
||||
list_filter = ('l_source_type', 't_source_data')
|
||||
search_fields = ('s_source_name',)
|
||||
readonly_fields = ('t_source_created', 't_source_updated')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Остальные ModelAdmin классы
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ItemAdmin(admin.ModelAdmin):
|
||||
"""Админ для товаров"""
|
||||
list_display = ('id', 's_item', 't_item_date', 't_item_created')
|
||||
list_filter = ('t_item_date', 't_item_created')
|
||||
search_fields = ('s_item',)
|
||||
filter_horizontal = ('k_item_to_artist', 'k_item_to_style')
|
||||
readonly_fields = ('t_item_created', 't_item_updated')
|
||||
|
||||
|
||||
class OfferAdmin(admin.ModelAdmin):
|
||||
"""Админ для предложений"""
|
||||
list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views')
|
||||
list_filter = ('l_offer_condition_media', 'l_offer_condition_sleeve', 't_offer_created', 'l_offer_to_format')
|
||||
search_fields = ('s_offer',)
|
||||
filter_horizontal = ('k_offer_to_image',)
|
||||
readonly_fields = ('s_offer_skip32', 't_offer_created', 't_offer_updated', 'i_offer_views', 'i_offer_favorites')
|
||||
|
||||
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
"""Админ для статей"""
|
||||
list_display = ('id', 's_article_title', 'l_article_type', 'b_article_published', 't_article_created')
|
||||
list_filter = ('l_article_type', 'b_article_published', 't_article_created')
|
||||
search_fields = ('s_article_title', 'slug')
|
||||
prepopulated_fields = {'slug': ('s_article_title',)}
|
||||
readonly_fields = ('t_article_created', 't_article_updated')
|
||||
# filter_horizontal = ('k_article_to_styles',)
|
||||
|
||||
|
||||
class OfferHistoryAdmin(admin.ModelAdmin):
|
||||
"""Админ для истории изменений офферов"""
|
||||
list_display = ('id', 'k_history_to_offer', 'f_history_price', 'i_history_quantity', 't_history_created')
|
||||
list_filter = ('t_history_created',)
|
||||
readonly_fields = ('t_history_created',)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Регистрация моделей в дефолтном admin.site
|
||||
# ============================================================================
|
||||
admin.site.register(TbImage, ImageAdmin)
|
||||
admin.site.register(TbArticle, ArticleAdmin)
|
||||
admin.site.register(TbMusicStyle, MusicStyleAdmin)
|
||||
admin.site.register(TbArtist, ArtistAdmin)
|
||||
admin.site.register(TbItem, ItemAdmin)
|
||||
admin.site.register(TbLabel, LabelAdmin)
|
||||
admin.site.register(TbSeller, SellerAdmin)
|
||||
admin.site.register(TbSource, SourceAdmin)
|
||||
admin.site.register(TbOffer, OfferAdmin)
|
||||
admin.site.register(TbOfferHistory, OfferHistoryAdmin)
|
||||
|
||||
# User и Group уже зарегистрированы auth приложением
|
||||
|
||||
# ============================================================================
|
||||
# Кастомизация админ-сайта через ready() в apps.py (переименование через verbose_name)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import os
|
||||
import hashlib
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Tuple
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.core.files.base import ContentFile, File
|
||||
from django.core.exceptions import ValidationError
|
||||
from PIL import Image as PILImage
|
||||
import pillow_heif
|
||||
|
||||
from lpon_site.settings import (
|
||||
THUMBNAIL_WEBP_QUALITY,
|
||||
FILER_ENABLE_PERMISSIONS,
|
||||
FILER_UPLOADER_MAX_FILE_SIZE,
|
||||
FILER_WHITELIST_FOR_PATH_ACCESS,
|
||||
MIME_TYPE_WHITELIST,
|
||||
FILE_VALIDATORS,
|
||||
)
|
||||
|
||||
# Регистрируем плагин HEIF в Pillow для поддержки HEIC/HEIF файлов
|
||||
pillow_heif.register_heif_opener()
|
||||
|
||||
# Настройка параллельного декодирования HEIF/HEIC файлов
|
||||
# DECODE_THREADS: количество потоков для декодирования
|
||||
# - 0 = использовать количество ядер процессора (рекомендуется)
|
||||
# - N > 0 = использовать N потоков (больше потоков = быстрее, но больше памяти)
|
||||
pillow_heif.options.DECODE_THREADS = 0
|
||||
|
||||
# Дополнительные опции Pillow для обработки изображений
|
||||
# LOAD_TRUNCATED_IMAGES: загружать некорректно завершенные изображения
|
||||
# (полезно для некачественных или поврежденных файлов)
|
||||
PILImage.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Конфигурация приложения для фронтенда lpon
|
||||
# ==============================================================================
|
||||
class FrontendConfig(AppConfig):
|
||||
name = 'frontend'
|
||||
verbose_name = 'Сайт lpon.ru'
|
||||
default_auto_field = 'django.db.models.AutoField'
|
||||
|
||||
def ready(self) -> None:
|
||||
"""Инициализация Django Admin с кастомным заголовком и названиями."""
|
||||
from django.contrib import admin
|
||||
admin.site.site_header = 'Управление LPON'
|
||||
admin.site.site_title = 'LPON Administrator'
|
||||
admin.site.index_title = 'Добро пожаловать в LPON'
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Кастомная конфигурация для django-filer
|
||||
# Патчинг MultiStorageFieldFile.save() для автоматической конвертации в WebP
|
||||
# ==============================================================================
|
||||
class CustomFilerConfig(AppConfig):
|
||||
name = 'filer'
|
||||
verbose_name = 'Медиафайлы'
|
||||
|
||||
# Атрибуты конфигурации Django-Filer (импортированы из settings.py)
|
||||
FILER_ENABLE_PERMISSIONS = FILER_ENABLE_PERMISSIONS
|
||||
FILER_UPLOADER_MAX_FILE_SIZE = FILER_UPLOADER_MAX_FILE_SIZE
|
||||
FILER_WHITELIST_FOR_PATH_ACCESS = FILER_WHITELIST_FOR_PATH_ACCESS
|
||||
MIME_TYPE_WHITELIST = MIME_TYPE_WHITELIST
|
||||
FILE_VALIDATORS = FILE_VALIDATORS
|
||||
|
||||
@staticmethod
|
||||
def _convert_to_webp_if_needed(
|
||||
name: str, content: File
|
||||
) -> Tuple[File, str, bool, int | None, int | None]:
|
||||
"""
|
||||
Преобразует загруженное изображение в WebP формат.
|
||||
Поддерживает JPEG, PNG, BMP, TIFF и HEIC/HEIF.
|
||||
|
||||
Args:
|
||||
name: Имя файла с расширением
|
||||
content: Содержимое файла (Django File object)
|
||||
|
||||
Returns:
|
||||
Кортеж: (content, new_name, was_converted, image_width, image_height)
|
||||
- content: WebP файл или исходный файл
|
||||
- new_name: Новое имя файла (.webp)
|
||||
- was_converted: Был ли произведен конвертирование
|
||||
- image_width: Ширина изображения в пикселях (None если не конвертирован)
|
||||
- image_height: Высота изображения в пикселях (None если не конвертирован)
|
||||
|
||||
Raises:
|
||||
ValidationError: Если произошла ошибка при конвертации
|
||||
|
||||
Размеры ОБЯЗАТЕЛЬНО получаются из исходного изображения перед конвертацией.
|
||||
При конвертации в WebP размеры пикселей не меняются, меняется только качество файла.
|
||||
"""
|
||||
_, original_ext = os.path.splitext(name)
|
||||
|
||||
# PNG исключено, т.к. их планирую использовать для иконок (может быть, верну позже)
|
||||
convertible_extensions = [".jpg", ".jpeg", ".bmp", ".tiff", ".heic", ".heif"]
|
||||
|
||||
if original_ext.lower() in convertible_extensions:
|
||||
try:
|
||||
# Открываем исходный файл с помощью PIL
|
||||
content.seek(0)
|
||||
img = PILImage.open(BytesIO(content.read()))
|
||||
|
||||
# Получаем размеры изображения ДО конвертации (они не меняются при сохранении в WebP)
|
||||
image_width, image_height = img.size
|
||||
|
||||
# Конвертируем CMYK в RGB если требуется
|
||||
if img.mode == 'CMYK':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Сохраняем в WebP формат в памяти
|
||||
webp_buffer = BytesIO()
|
||||
img.save(webp_buffer, format="WEBP", quality=THUMBNAIL_WEBP_QUALITY)
|
||||
webp_data = webp_buffer.getvalue()
|
||||
|
||||
# Изменяем расширение на .webp
|
||||
new_name = os.path.splitext(name)[0] + ".webp"
|
||||
|
||||
return ContentFile(webp_data), new_name, True, image_width, image_height
|
||||
|
||||
except Exception as e:
|
||||
# Поднимаем ValidationError чтобы она показывалась в админке
|
||||
error_msg = f"Ошибка при конвертации '{name}' в WebP: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
raise ValidationError(error_msg)
|
||||
|
||||
# Если файл не нуждается в конвертации, возвращаем как есть
|
||||
content.seek(0)
|
||||
return content, name, False, None, None
|
||||
|
||||
def ready(self) -> None:
|
||||
"""
|
||||
Инициализация: патчинг MultiStorageFieldFile.save() для WebP конвертации.
|
||||
Добавление поддержки HEIC/HEIF в filer.
|
||||
"""
|
||||
from filer.fields.multistorage_file import MultiStorageFieldFile
|
||||
from filer import settings as filer_settings
|
||||
|
||||
original_save = MultiStorageFieldFile.save
|
||||
|
||||
# Важно: Добавляем поддержку HEIC/HEIF файлов в filer
|
||||
# IMAGE_EXTENSIONS это глобальные константы в filer, которые нельзя переопределить через settings.py
|
||||
# Единственный способ - модифицировать после импорта в ready() методе AppConfig
|
||||
filer_settings.IMAGE_EXTENSIONS = list(set(filer_settings.IMAGE_EXTENSIONS + ['.heic', '.heif']))
|
||||
filer_settings.IMAGE_MIME_TYPES = list(set(filer_settings.IMAGE_MIME_TYPES + ['heic', 'heif']))
|
||||
|
||||
def patched_save(
|
||||
self_instance, name: str, content: File, save: bool = True
|
||||
) -> str | None:
|
||||
"""
|
||||
Патчированг MultiStorageFieldFile.save().
|
||||
Конвертирует загруженные картинки в WebP и обновляет метаданные.
|
||||
|
||||
Args:
|
||||
self_instance: Экземпляр MultiStorageFieldFile
|
||||
name: Имя файла
|
||||
content: Содержимое файла
|
||||
save: Нужно ли сохранять в БД (по умолчанию True)
|
||||
|
||||
Returns:
|
||||
Имя сохраненного файла или None
|
||||
"""
|
||||
new_content, new_name, converted, img_width, img_height = (
|
||||
CustomFilerConfig._convert_to_webp_if_needed(name, content)
|
||||
)
|
||||
|
||||
if converted:
|
||||
# Переустанавливаем корректный mime_type (всё станет WebP)
|
||||
self_instance.instance.mime_type = "image/webp"
|
||||
|
||||
# Обновляем original_filename если есть
|
||||
if hasattr(self_instance.instance, 'original_filename'):
|
||||
if self_instance.instance.original_filename:
|
||||
original_filename, _ = os.path.splitext(self_instance.instance.original_filename)
|
||||
self_instance.instance.original_filename = f"{original_filename}.webp"
|
||||
|
||||
# КРИТИЧНО: Обновляем размер файла и sha1 на основе WebP данных, а не исходных
|
||||
new_content.seek(0)
|
||||
webp_bytes = new_content.read()
|
||||
new_content.seek(0)
|
||||
self_instance.instance._file_size = len(webp_bytes)
|
||||
self_instance.instance.sha1 = hashlib.sha1(webp_bytes).hexdigest()
|
||||
|
||||
# Вызываем оригинальный save() метод
|
||||
result = original_save(self_instance, new_name, new_content, save)
|
||||
|
||||
# После сохранения гарантируем что mime_type=image/webp в БД
|
||||
if converted and new_name.lower().endswith('.webp'):
|
||||
if self_instance.instance.id:
|
||||
self_instance.instance.__class__.objects.filter(
|
||||
id=self_instance.instance.id
|
||||
).update(mime_type='image/webp')
|
||||
|
||||
return result
|
||||
|
||||
MultiStorageFieldFile.save = patched_save
|
||||
@@ -0,0 +1,265 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-04 12:31
|
||||
|
||||
import datetime
|
||||
import django.db.models.deletion
|
||||
import filer.fields.file
|
||||
import filer.fields.image
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('filer', '0018_alter_file_options'),
|
||||
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TbArticle',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('s_article_title', models.CharField(default='', help_text='Технический заголовок статьи для внутреннего использования, например: "Album: Abbey Road" или "Bio: The Beatles".', max_length=255, unique=True, verbose_name='Технический заголовок')),
|
||||
('l_article_type', models.CharField(blank=True, choices=[('artist', 'Artis: артист, группа или бренд'), ('item', 'Item: Альбом, релиз или товар (кассета, hifi, аксессуар)'), ('offer', 'Offer: конкретное предложение от продавца'), ('seller', 'Seller: продавец или магазин'), ('blog', 'Новость или блог'), ('action', 'Спецпредложение, акция, распродажа и т.д.'), ('to_main', 'Текст/Блок для главной страницы'), ('adv', 'Реклама или баннер'), ('???', 'Другое')], db_index=True, default='???', max_length=7, verbose_name='Тип статьи')),
|
||||
('b_article_published', models.BooleanField(db_index=True, default=True, verbose_name='Опубликовано')),
|
||||
('t_article_started', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата начала публикации')),
|
||||
('t_article_ended', models.DateTimeField(blank=True, db_index=True, default=None, help_text='Если указано, статья будет отображаться только между датой начала и датой окончания публикации. Если не указано, статья будет отображаться всегда (или до тех пор, пока не будет удалена или снята с публикации через `b_article_published`)', null=True, verbose_name='Дата окончания публикации')),
|
||||
('s_article_title_html', models.CharField(blank=True, default='', help_text='Заголовок статьи, например: "Описание релиза Abbey Road" или "Биография группы The Beatles". Может содержать HTML-разметку для типографирования (html-мнемоники и -теги). Если не указано, будет отображаться без заголовка.', max_length=255, verbose_name='Заголовок')),
|
||||
('s_article_teaser_html', models.TextField(blank=True, default='', help_text='Короткий анонс статьи, который будет отображаться в списках. Может содержать HTML-вёрсту (теги, мнемоники, спецсимволы) для типографирования.', null=True, verbose_name='Тизер статьи')),
|
||||
('s_article_content_html', models.TextField(blank=True, default='', help_text='Полный текст статьи. Может содержать HTML-вёрсту (теги, мнемоники, спецсимволы) для типографирования.', null=True, verbose_name='Статья')),
|
||||
('i_article_views', models.IntegerField(db_index=True, default=0, verbose_name='Число просмотров')),
|
||||
('i_article_favorites', models.IntegerField(db_index=True, default=0, verbose_name='Число в избранном')),
|
||||
('slug', models.SlugField(default='', max_length=255, unique=True, verbose_name='Слаг статьи')),
|
||||
('seo_title', models.CharField(blank=True, default='', help_text='SEO Title для статьи. Если не указано, будет использоваться заголовок статьи (s_article_title_html) без HTML-тегов.', max_length=255, verbose_name='SEO Title')),
|
||||
('seo_description', models.CharField(blank=True, default='', help_text='SEO Description для статьи. Если не указано, будет использоваться обрезанный тизер статьи (s_article_teaser_html) без HTML-тегов.', max_length=255, verbose_name='SEO Description')),
|
||||
('seo_keywords', models.CharField(blank=True, default='', help_text='SEO Keywords для статьи, через запятую. Например: "The Beatles, Abbey Road, Vinyl, 1969"', max_length=255, verbose_name='SEO Keywords')),
|
||||
('t_article_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_article_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Статья',
|
||||
'verbose_name_plural': 'Статьи',
|
||||
'ordering': ('-t_article_updated', '-t_article_created', 's_article_title'),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbFormat',
|
||||
fields=[
|
||||
('id', models.SmallAutoField(primary_key=True, serialize=False)),
|
||||
('s_format', models.CharField(db_index=True, help_text='Название формата носителя, например: "LP", "CD", "Blu-ray", "Compact Cassette", "MiniDisc", "Hi-Fi", "Accessory" и т.д.', max_length=16, unique=True, verbose_name='Формат носителя')),
|
||||
('s_format_slug', models.SlugField(max_length=16, unique=True, verbose_name='Слаг')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Формат носителя',
|
||||
'verbose_name_plural': 'Форматы носителей',
|
||||
'ordering': ('s_format',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbMusicStyle',
|
||||
fields=[
|
||||
('id', models.SmallAutoField(primary_key=True, serialize=False)),
|
||||
('s_style_name', models.CharField(db_index=True, help_text='Основное название стиля. Например: "Rock", "Jazz", "Classical"', max_length=100, unique=True, verbose_name='Стиль (канонический)')),
|
||||
('s_style_slug', models.SlugField(editable=False, help_text='Автоматически генерируется из названия. Используется в URL и API.', unique=True, verbose_name='Слаг (уникальный идентификатор)')),
|
||||
('j_style_synonyms', models.JSONField(blank=True, default=list, help_text='Список вариантов названия из Discogs, MusicBrainz и т.д. для матчинга. Пример: ["rock", "Rock Music", "Rock & Roll", "Hard Rock"]', verbose_name='Синонимы из источников')),
|
||||
('t_style_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_style_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Музыкальный стиль',
|
||||
'verbose_name_plural': 'Музыкальные стили',
|
||||
'ordering': ('s_style_name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbArtist',
|
||||
fields=[
|
||||
('id', models.SmallAutoField(primary_key=True, serialize=False)),
|
||||
('s_artist', models.CharField(help_text='Техническое название исполнителя для внутреннего использования, например: "The Beatles" или"David Bowie".', max_length=128, unique=True, verbose_name='Исполнитель')),
|
||||
('j_artist_metadata', models.JSONField(blank=True, default=list, help_text='Включая варианты написания в источниках Список вариантов: ["The Beatles", "Beatles", "Beatles, The"]', null=True, verbose_name='Метаданные JSON')),
|
||||
('t_artist_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_artist_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('k_artist_to_article', models.OneToOneField(blank=True, default=None, help_text='Связанная статья об исполнителе (Типографированные заголовок, тизер и текст статьи. Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)<br /><b>ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ</b> т.к. через статью получаем слаг для URL артиста.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_artist', to='frontend.tbarticle', verbose_name='Связанная статья')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Исполнитель',
|
||||
'verbose_name_plural': 'Исполнители',
|
||||
'ordering': ('s_artist',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbImage',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('l_img_source', models.CharField(choices=[('parser', 'Загружено парсером (из Discogs, Meshok или другого сайта)'), ('manual', 'Ручная загрузка пользователем'), ('vendor', 'От продавца'), ('other', 'Другое')], default='manual', help_text='Как был получен этот снимок: загружен вручную, получен парсером из внешнего источника (например, Discogs), предоставлен продавцом и т.д.', max_length=10, verbose_name='Источник')),
|
||||
('l_img_reality', models.CharField(choices=[('real', 'Реальная фотография товара'), ('abstract', 'Абстрактное (из внешнего источника)')], default='abstract', help_text='Реальная фотография товара или картинка из внешнего источника?', max_length=10, verbose_name='Тип снимка')),
|
||||
('s_img_src_url', models.URLField(blank=True, help_text='Если изображение взято из внешнего источника (например, Discogs)', null=True, verbose_name='URL источника')),
|
||||
('i_img_sort', models.IntegerField(db_index=True, default=0, help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. Можно использовать для указания обложки (0), задника (1) и т.д.', verbose_name='Cортировка')),
|
||||
('f_img_confidence_score', models.FloatField(blank=True, default=None, help_text='0.0 - 1.0, насколько уверены, что это правильное изображение', null=True, verbose_name='Уверенность (для автоматических данных)')),
|
||||
('s_img_copyright', models.CharField(blank=True, default='', help_text='Например: "© 2024 User" или "CC-BY"', max_length=255, verbose_name='Авторские права / Лицензия')),
|
||||
('t_img_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')),
|
||||
('t_img_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('image', filer.fields.image.FilerImageField(blank=True, help_text='Файл изображения, загруженный через django_filer.', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.FILER_IMAGE_MODEL, verbose_name='Файл изображения')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Изображение',
|
||||
'verbose_name_plural': 'Изображения',
|
||||
'ordering': ('-t_img_created', 'i_img_sort'),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tbarticle',
|
||||
name='k_article_to_image',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='image_to_article', to='frontend.tbimage', verbose_name='Изображение для статьи'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('s_item', models.CharField(help_text='Техническое название товара (альбома, релиза, аксессуара) для внутреннего использования,например: "Abbey Road (LP)" или "TDK CDing I (кассета для записи)".', max_length=128, unique=True, verbose_name='Товар')),
|
||||
('s_item_date', models.CharField(blank=True, default='XXXX-XX-XX', help_text='Например: 1969-05-25, или 1969-05-XX (если день неизвестен), или 1969-XX-XX (если известен только год, или XXXX-XX-XX (если дата релиза неизвестна). Менее приоритетное поле для отображения даты релиза, чем t_release_date, так как может содержать неполную дату и/или текстовую информацию. Срабатывает только если t_release_date не указано.', max_length=10, null=True, verbose_name='Дата релиза (str)')),
|
||||
('t_item_date', models.DateField(blank=True, help_text='Полная дата если известна, например: 1969-09-26. Если точно известен.', null=True, verbose_name='Дата релиза')),
|
||||
('i_discogs_master_id', models.IntegerField(blank=True, default=None, help_text='Уникальный идентификатор мастер-релиза на Discogs, если он там есть. Например: <tt>306323</tt>', null=True, verbose_name='ID на мастер-релиз Discogs')),
|
||||
('j_item_metadata', models.JSONField(blank=True, default=dict, help_text='Дополнительные данные и метаданные релиза (страна, жанр, количество треков и т.д.) или товавра в виде JSON-словаря. Сюда же включены варианты написания релиза в источниках', null=True, verbose_name='Дополнительные данные')),
|
||||
('t_item_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_item_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('k_item_to_article', models.OneToOneField(blank=True, default=None, help_text='Связанная статья об альбоме/релизе/товаре (Типографированные заголовок, тизер и текст статьи. Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)<br /><b>ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ</b> т.к. через статью получаем слаг для URL альбома/релиза/товара.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_item', to='frontend.tbarticle', verbose_name='Связанная статья')),
|
||||
('k_item_to_artist', models.ManyToManyField(blank=True, db_index=True, help_text='Один или несколько для коллабораций', related_name='artist_to_item', to='frontend.tbartist', verbose_name='Исполнители')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Товар в каталоге (релиз, носитель, аксессуар)',
|
||||
'verbose_name_plural': 'Товары в каталоге',
|
||||
'ordering': ('s_item',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbLabel',
|
||||
fields=[
|
||||
('id', models.SmallAutoField(primary_key=True, serialize=False)),
|
||||
('s_label', models.CharField(help_text='Техническое название лейбла. Например: "Sony Records" или "Мелодия"', max_length=128, unique=True, verbose_name='Лейбл')),
|
||||
('j_label_metadata', models.JSONField(blank=True, default=dict, help_text='JSON: страна лейбла, официальный сайт и т.д.', null=True, verbose_name='Метаданные')),
|
||||
('t_label_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_label_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('k_label_to_article', models.OneToOneField(blank=True, default=None, help_text='Связанная статья об лейбле (Типографированные заголовок, тизер и текст статьи. Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)<br /><b>ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ</b> т.к. через статью получаем слаг для URL лейбла.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_label', to='frontend.tbarticle', verbose_name='Связанная статья')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Лейбл',
|
||||
'verbose_name_plural': 'Лейблы',
|
||||
'ordering': ('s_label',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tbarticle',
|
||||
name='k_article_to_styles',
|
||||
field=models.ManyToManyField(blank=True, db_index=True, help_text='Стили этой статьи/артиста/релиза (Rock, Jazz, Classical, ...)', related_name='style_to_article', to='frontend.tbmusicstyle', verbose_name='Музыкальные стили'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbOffer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('s_offer', models.CharField(db_index=True, help_text='Техническое название оффера для внутреннего использования, например: "Abbey Road (LP) AnTrop NM/NM (МЗГ)" или "TDK CDing I 60 (б/у) VG/VG (Janan, 198x синяя-градиент)"', max_length=128, verbose_name='Название оффера')),
|
||||
('s_offer_catalog_num', models.TextField(blank=True, default='', help_text='Например: "SD 16023" или "5099923452355"', verbose_name='Каталожный номер / Barcode')),
|
||||
('d_offer_date_release', models.DateField(blank=True, default=None, help_text='Дата релиза, если она известна (например, дата выпуска переиздания)', null=True, verbose_name='Дата релиза')),
|
||||
('i_offer_discogs_id', models.IntegerField(blank=True, default=0, help_text='Уникальный идентификатор релиза на Discogs, если он там есть. Например: <tt>306323</tt>', verbose_name='ID на релиз Discogs')),
|
||||
('l_offer_condition_media', models.CharField(choices=[('s', 'Still Sealed (новое, запечатано)'), ('m', 'Mint (новое, распакованное)'), ('nm', 'Near Mint (почти новое)'), ('vg', 'Very Good (очень хорошее)'), ('g', 'Good (хорошее)'), ('f', 'Fair (удовлетворительное)'), ('p', 'Poor (плохое)'), ('??', 'Other')], default='s', help_text='Состояние носителя (пластинки, CD и т.п.) по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).', max_length=2, verbose_name='Состояние носителя')),
|
||||
('l_offer_condition_sleeve', models.CharField(choices=[('s', 'Still Sealed (новое, запечатано)'), ('m', 'Mint (новое, распакованное)'), ('nm', 'Near Mint (почти новое)'), ('vg', 'Very Good (очень хорошее)'), ('g', 'Good (хорошее)'), ('f', 'Fair (удовлетворительное)'), ('p', 'Poor (плохое)'), ('??', 'Other')], default='s', help_text='Состояние обложки по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).', max_length=2, verbose_name='Состояние обложки')),
|
||||
('f_offer_price', models.DecimalField(db_index=True, decimal_places=2, default=0.0, help_text='Цена в валюте источника. Валюта определяется в TbSource: offer.k_offer_to_source.l_currency', max_digits=10, verbose_name='Цена')),
|
||||
('i_offer_quantity', models.IntegerField(blank=True, default=0, verbose_name='Количество в наличии')),
|
||||
('i_offer_discount_to_daily_sale', models.IntegerField(blank=True, db_index=True, default=0, help_text='Процент возможной скидки, если участвует в "ежедневной распродаже" или акции. Если указано <tt>0</tt> то данное предложение не может участвовать в распродажах, спецпредложениях и акциях', verbose_name='Скидка')),
|
||||
('j_offer_metadata', models.JSONField(default=dict, help_text='Дополнительные данные о предложении в виде JSON-словаря.', null=True, verbose_name='Дополнительные данные')),
|
||||
('s_offer_skip32', models.CharField(help_text='Уникальный код товара для идентификации в корзине и при заказе (чтобы не светить id). Например: "4gfFCJ". Формируется автоматически связкой Skip32 (хаотичное перемешивание) и Base62 (компактная упаковка) из id оффера в методе save().', max_length=12, unique=True, verbose_name='Код товара')),
|
||||
('i_offer_views', models.IntegerField(db_index=True, default=0, verbose_name='Просмотры')),
|
||||
('i_offer_favorites', models.IntegerField(db_index=True, default=0, verbose_name='В избранном')),
|
||||
('t_offer_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_offer_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('k_offer_to_article', models.ForeignKey(blank=True, default=None, help_text='Связанная статья об оффере (HTML-готовые заголовок, тизер и текст статьи). Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)<br /><b>МОЖНО НЕ УКАЗЫВАТЬ</b> т.к. URL оффера (для корзины) формируется через id или хеш.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_offer', to='frontend.tbarticle', verbose_name='Связанная статья')),
|
||||
('k_offer_to_format', models.ManyToManyField(blank=True, db_index=True, help_text='Форматы носителей (пластинка, CD, кассета и т.п.). Можно выбрать несколько.', related_name='format_to_offer', to='frontend.tbformat', verbose_name='Форматы')),
|
||||
('k_offer_to_image', models.ManyToManyField(blank=True, db_index=True, help_text='Картинки этого товара. Порядок определяется полем i_img_sort в ка��тинке.', related_name='image_to_offer', to='frontend.tbimage', verbose_name='Изображения')),
|
||||
('k_offer_to_item', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='item_to_offer', to='frontend.tbitem', verbose_name='Релиз (товар)')),
|
||||
('k_offer_to_label', models.ForeignKey(default=None, help_text='Лейбл, на котором был выпущен релиз, если он известен. Например: <tt>Atlantic</tt> или <tt>Мелодия</tt>', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='label_to_offer', to='frontend.tblabel', verbose_name='Лейбл')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Оффер (предложение)',
|
||||
'verbose_name_plural': 'Офферы (предложения)',
|
||||
'ordering': ('-t_offer_updated', '-t_offer_created', 's_offer'),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbOfferHistory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('f_history_price', models.DecimalField(blank=True, decimal_places=2, default=0.0, max_digits=12, null=True, verbose_name='Старая цена')),
|
||||
('i_history_quantity', models.IntegerField(default=0, verbose_name='Старое количество')),
|
||||
('j_history_metadata', models.JSONField(blank=True, default=dict, help_text='Метаданные, указывающие координаты данных внутри источника (например, внутри Excel-файла: название вкладки, номер строки, номер столбца с ценой и количеством, или URL + CSS-селектор для HTML-страницы и т.п.', verbose_name='Метаданные')),
|
||||
('t_history_created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Дата создания')),
|
||||
('k_history_to_offer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='offer_to_history', to='frontend.tboffer', verbose_name='Оффер')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'История оффера',
|
||||
'verbose_name_plural': 'Истории офферов',
|
||||
'ordering': ('-t_history_created',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbSeller',
|
||||
fields=[
|
||||
('id', models.SmallAutoField(primary_key=True, serialize=False)),
|
||||
('s_seller', models.CharField(help_text='Техническое название продавца или магазина. Например: <tt>Клюква Рекодс</tt>. Может совпадать с названием продавца, если лейбл сам реализует свои издания через сайт.', max_length=128, unique=True, verbose_name='Название продавца')),
|
||||
('l_seller_type', models.CharField(choices=[('seller', 'Продавец'), ('label', 'Лейбл (издатель)'), ('diy', 'Самиздат группы'), ('crowd', 'Краудфандинг'), ('???', 'Другое')], default='seller', max_length=6, verbose_name='Тип продавца')),
|
||||
('j_seller_metadata', models.JSONField(blank=True, default=dict, help_text='Дополнительные данные о продавце в виде JSON-словаря. Телефон, email, адрес, ссылка на сайт и т.д.', null=True, verbose_name='Дополнительные данные')),
|
||||
('t_seller_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_seller_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('k_seller_to_article', models.OneToOneField(blank=True, default=None, help_text='Связанная статья о продавце (HTML-готовые заголовок, тизер и текст статьи). Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)<br /><b>ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ</b> т.к. через статью получаем слаг для URL продавца.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_seller', to='frontend.tbarticle', verbose_name='Связанная статья')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Продавец',
|
||||
'verbose_name_plural': 'Продавцы',
|
||||
'ordering': ('s_seller',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TbSource',
|
||||
fields=[
|
||||
('id', models.SmallAutoField(primary_key=True, serialize=False)),
|
||||
('l_source_currency', models.CharField(choices=[('rub', 'RUB: российский рубль'), ('usd', 'USD: американский доллар'), ('eur', 'EUR: евро'), ('try', 'TRY: турецкая лира'), ('amd', 'AMD: армянский драм'), ('jpy', 'JPY: японская иена'), ('gbp', 'GBP: британский фунт'), ('cny', 'CNY: китайский юань'), ('byn', 'BYN: белорусский рубль'), ('ton', 'TON: криптовалюта TON'), ('??', 'Other')], default='rub', help_text='В какой валюте указаны цены в этом источнике. Все офферы из этого источника будут в этой валюте.', max_length=3, verbose_name='Валюта источника')),
|
||||
('s_source_name', models.CharField(blank=True, default='', help_text='Название источника данных (для удобства), например: <tt>Предзаказ на RSD-2025 от Полуэкта.</tt>', max_length=128, verbose_name='Название источника')),
|
||||
('l_source_type', models.CharField(choices=[('excel', 'Excel-файл от продавца или издателя'), ('csv', 'CSV-файл от продавца или издателя'), ('url', 'URL страницы с данными (например, HTML-страница с каталогом товаров)'), ('??', 'Другое')], default='excel', help_text='Тип источника данных, например: <tt>Excel-файл от продавца или издателя</tt>, <tt>URL страницы с данными</tt> и т.д.', max_length=5, verbose_name='Тип источника')),
|
||||
('t_source_data', models.DateField(blank=True, default=datetime.date.today, help_text='Дата, к которой относятся данные в источнике. Например, если это исторический Excel-файл.', verbose_name='Дата данных')),
|
||||
('s_source_url', models.TextField(blank=True, default='', help_text='URL страницы с данными, например, HTML-страница с каталогом товаров. Если данные в источнике представлены в виде файла, можно не указывать URL, а загрузить файл в поле выше.', max_length=255, verbose_name='URL источника')),
|
||||
('j_source_metadata', models.JSONField(blank=True, default=dict, help_text='Дополнительные данные об источнике (внутреннем устройстве: вкладках и стоkбцах Excel-файла, структуре HTML-страницы и т.п.) в виде JSON-словаря', verbose_name='Дополнительные данные')),
|
||||
('t_source_created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('t_source_updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('k_source_to_seller', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='seller_to_source', to='frontend.tbseller', verbose_name='Продавец')),
|
||||
('source_file', filer.fields.file.FilerFileField(blank=True, help_text='Файл-источник, например, Excel-файл от продавца или издателя. Если данные в источнике представлены на странице в интернете, можно не указывать файл, а указать URL в поле ниже.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='filer.file', verbose_name='Файл-источник')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Источник данных',
|
||||
'verbose_name_plural': 'Источники данных',
|
||||
'ordering': ('-t_source_data', '-t_source_created'),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tboffer',
|
||||
name='k_offer_to_source',
|
||||
field=models.ForeignKey(default=None, help_text='Обязательно - каждый оффер должен иметь источник. Через источник получаем данные продавца: offer.k_offer_to_source.k_source_to_seller', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_to_offer', to='frontend.tbsource', verbose_name='Источник данных'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tbarticle',
|
||||
index=models.Index(fields=['l_article_type', 'b_article_published', '-t_article_created'], name='idx_articles_by_type_published'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tbofferhistory',
|
||||
index=models.Index(fields=['k_history_to_offer', '-t_history_created'], name='idx_history_by_offer_date'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tboffer',
|
||||
index=models.Index(fields=['k_offer_to_item', '-f_offer_price'], name='idx_offer_by_item_price'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tboffer',
|
||||
index=models.Index(fields=['k_offer_to_item', 'i_offer_quantity'], name='idx_offer_by_item_qty'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-12 10:39
|
||||
|
||||
import django.db.models.deletion
|
||||
import filer.fields.image
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('frontend', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='tboffer',
|
||||
name='k_offer_to_format',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tbimage',
|
||||
name='s_img_copyright',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tboffer',
|
||||
name='l_offer_to_format',
|
||||
field=models.CharField(choices=[('lp', 'Vinyl Long-Play (12")'), ('ep', 'Vinyl Extended-Play (12", 10", 7")'), ('45', 'Vinyl 7" (45 rpm)'), ('cd', 'Compact Disc'), ('ld', 'LaserDisc'), ('md', 'MiniDisc Record'), ('ms', 'Used MiniDisc (для записи)'), ('cs', 'Cassette Record'), ('uc', 'Used Cassette (для записи)'), ('tp', 'Tape Reel Record'), ('ur', 'Used Tape Reel (для записи)'), ('??', 'Other')], db_index=True, default='??', help_text='Форматы основного носителей (пластинка, CD, кассета и т.п.). Если несколько носителей (и разных), то это указывать в "Дополнительных данных" в JSON-формате. Например: <tt>{"formats": {"lp": 2, "cd": 1},}</tt>', max_length=2, verbose_name='Формат'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tbimage',
|
||||
name='f_img_confidence_score',
|
||||
field=models.FloatField(blank=True, default=10.0, help_text='Уверенность (для автоматических данных) 0.0 - 10.0, насколько уверены, что это правильное изображение', null=True, verbose_name='Достоверность'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tbimage',
|
||||
name='i_img_sort',
|
||||
field=models.IntegerField(db_index=True, default=0, help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. Можно использовать для указания обложки (0), задника (1) и т.д.', verbose_name='Сортировка'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tbimage',
|
||||
name='image',
|
||||
field=filer.fields.image.FilerImageField(default=1, help_text='Файл изображения, загруженный через django_filer.', on_delete=django.db.models.deletion.DO_NOTHING, to=settings.FILER_IMAGE_MODEL, verbose_name='Файл изображения'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tbimage',
|
||||
name='s_img_src_url',
|
||||
field=models.URLField(blank=True, help_text='URL источника, если изображение взято (в том числе и парсером) из внешнего источника (например, Discogs)', null=True, verbose_name='URL'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='TbFormat',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-12 12:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('frontend', '0002_remove_tboffer_k_offer_to_format_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='tbsource',
|
||||
name='l_source_currency',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tbseller',
|
||||
name='l_seller_currency',
|
||||
field=models.CharField(choices=[('rub', 'RUB: российский рубль'), ('usd', 'USD: американский доллар'), ('eur', 'EUR: евро'), ('amd', 'AMD: армянских драм'), ('try', 'TRY: турецкая лира'), ('jpy', 'JPY: японская иена'), ('gbp', 'GBP: британский фунт'), ('cny', 'CNY: китайский юань'), ('byn', 'BYN: белорусский рубль'), ('ton', 'TON: криптовалюта TON'), ('??', 'Other')], default='rub', help_text='В какой валюте указаны цены в этом источнике. Все офферы из этого источника будут в этой валюте.', max_length=3, verbose_name='Валюта источника'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 6.0.5 on 2026-06-13 18:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('frontend', '0003_remove_tbsource_l_source_currency_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='tbarticle',
|
||||
name='k_article_to_styles',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tbmusicstyle',
|
||||
name='s_style_slug',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tbitem',
|
||||
name='k_item_to_style',
|
||||
field=models.ManyToManyField(blank=True, db_index=True, help_text='Один или несколько стилей, характеризующих альбом/товар. Например: Rock, Progressive Rock.', related_name='style_to_item', to='frontend.tbmusicstyle', verbose_name='Музыкальные стили'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tbmusicstyle',
|
||||
name='k_style_to_article',
|
||||
field=models.OneToOneField(blank=True, default=None, help_text='Связанная статья о музыкальном стиле (Типографированные заголовок, тизер и текст статьи. Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)<br /><b>ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ</b> т.к. через статью получаем слаг для URL музыкального стиля.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_style', to='frontend.tbarticle', verbose_name='Связанная статья'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tbarticle',
|
||||
name='l_article_type',
|
||||
field=models.CharField(blank=True, choices=[('artist', 'Artis: артист, группа или бренд'), ('style', 'Slyle: музыкальный стиль'), ('item', 'Item: Альбом, релиз или товар (кассета, hifi, аксессуар)'), ('offer', 'Offer: конкретное предложение от продавца'), ('seller', 'Seller: продавец или магазин'), ('blog', 'Новость или блог'), ('action', 'Спецпредложение, акция, распродажа и т.д.'), ('to_main', 'Текст/Блок для главной страницы'), ('adv', 'Реклама или баннер'), ('???', 'Другое')], db_index=True, default='???', max_length=7, verbose_name='Тип статьи'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tbsource',
|
||||
name='l_source_type',
|
||||
field=models.CharField(choices=[('excel', 'Excel-файл от продавца или издателя'), ('csv', 'CSV-файл от продавца или издателя'), ('url', 'URL страницы с данными (например, HTML-страница с каталогом товаров)'), ('??', 'Другое (включая ручной ввод)')], default='excel', help_text='Тип источника данных, например: <tt>Excel-файл от продавца или издателя</tt>, <tt>URL страницы с данными</tt> и т.д.', max_length=5, verbose_name='Тип источника'),
|
||||
),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,407 @@
|
||||
# frontend/utils.py
|
||||
# Служебные функции и хелперы проекта
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import pytils
|
||||
import random
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from bs4 import BeautifulSoup
|
||||
from html import unescape
|
||||
from etpgrf.config import HANGING_PUNCTUATION_SPACE_CHARS as SPACE_CHARS
|
||||
from django.utils.html import mark_safe
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.db.models import Model
|
||||
from lpon_site.settings import (
|
||||
SLUG_MAX_LENGTH, KEY_SYNONYM,
|
||||
)
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def normalize_string(s: str) -> str:
|
||||
"""
|
||||
Нормализует строку: удаляет невидимые символы, начальные, конечные и дублирующие пробелы.
|
||||
Также приводит к нижнему регистру для case-insensitive сравнения при поиске дублей.
|
||||
|
||||
Работает со ВСЕМИ типами пробельных символов (не-breaking space, thin space и т.д.).
|
||||
|
||||
Args:
|
||||
s: Строка для нормализации
|
||||
|
||||
Returns:
|
||||
str: Нормализованная строка в нижнем регистре (или пустая если была пуста)
|
||||
|
||||
Пример:
|
||||
>> normalize_string(" Sony Music ")
|
||||
'sony music'
|
||||
>> normalize_string("SONY MUSIC")
|
||||
'sony music'
|
||||
>> normalize_string("Sony\u00a0\u202FMusic") # с неразрывными пробелами
|
||||
'sony music'
|
||||
"""
|
||||
if not s:
|
||||
return ""
|
||||
|
||||
result = str(s)
|
||||
|
||||
# Преобразуем в нижний регистр для case-insensitive сравнения
|
||||
# (регистр не должен быть значимым при поиске дублей артистов/лейблов/стилей)
|
||||
result = result.lower()
|
||||
|
||||
# Удаляем невидимые символы (не заменять, а полностью удалять)
|
||||
result = result.translate({
|
||||
ord("\xad"): None, # символ мягкого переноса
|
||||
ord("\u200b"): None, # символ нулевой ширины (zero-width space)
|
||||
ord("\u200c"): None, # символ нулевой ширины (zero-width non-joiner)
|
||||
ord("\u200d"): None, # символ Zero Width Joiner (ZWJ)
|
||||
ord("\u2060"): None, # символ Word Joiner (WJ)
|
||||
ord("\ufeff"): None, # символ Zero Width No-Break Space (BOM)
|
||||
})
|
||||
|
||||
# Все типы пробельных символов для замены на обычный пробел
|
||||
all_spaces = SPACE_CHARS | frozenset([
|
||||
"\u00a0", # non-breaking space ( )
|
||||
"\u202F", # narrow no-break space (тонкий неразрывный пробел)
|
||||
])
|
||||
|
||||
# Заменяем ВСЕ типы пробелов на обычный пробел
|
||||
for space_char in all_spaces:
|
||||
result = result.replace(space_char, " ")
|
||||
|
||||
# Удаляем начальные/конечные пробелы и нормализуем множественные пробелы
|
||||
# Финальная подстраховка: regex для ВСЕХ unicode whitespace символов (даже неизвестных)
|
||||
result = re.sub(r'\s+', ' ', result).strip()
|
||||
return result
|
||||
|
||||
|
||||
def super_normalize_string(s: str) -> str:
|
||||
"""
|
||||
Супер-нормализация строки для проверки частичного совпадения слов (PARTIAL_MATCH).
|
||||
|
||||
Выполняет глубокую нормализацию для сравнения словарных совпадений:
|
||||
- Очищает HTML через safe_html_special_symbols() (которая вызывает normalize_string())
|
||||
- Удаляет все символы кроме букв, цифр и пробелов
|
||||
- Сохраняет буквы и цифры ВСЕХ языков (Unicode-safe)
|
||||
- Приводит к нижнему регистру (через safe_html_special_symbols → normalize_string)
|
||||
- Нормализует пробелы (множественные → одиночные)
|
||||
|
||||
Результат используется для поиска словарных совпадений:
|
||||
"The Beatles" и "Beatles, The" оба станут: "the beatles"
|
||||
Затем разбиваются на слова: ["the", "beatles"]
|
||||
|
||||
Args:
|
||||
s: Строка для нормализации
|
||||
|
||||
Returns:
|
||||
str: Строка со словами, разделенными пробелами (в нижнем регистре)
|
||||
|
||||
Пример:
|
||||
>> super_normalize_string("The Beatles (Rock)")
|
||||
'the beatles rock'
|
||||
>> super_normalize_string("Beatles, The - Rock Band!")
|
||||
'beatles the rock band'
|
||||
"""
|
||||
if not s:
|
||||
return ""
|
||||
|
||||
# Сначала очищаем HTML и спецсимволы через существующую функцию
|
||||
# (safe_html_special_symbols вызывает normalize_string(), который уже приводит к нижнему регистру)
|
||||
cleaned = safe_html_special_symbols(s)
|
||||
|
||||
# Удаляем все символы кроме букв, цифр и пробелов (Unicode-safe)
|
||||
# \w в Python regex с флагом UNICODE включает: буквы всех языков, цифры и подчеркивание
|
||||
# Используем [^\w\s] для удаления всего кроме слов и пробелов, затем исключаем подчеркивание
|
||||
cleaned = re.sub(r'[^\w\s]|_', '', cleaned, flags=re.UNICODE)
|
||||
|
||||
# Нормализуем пробелы (множественные → одиночные)
|
||||
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def safe_html_special_symbols(s: str) -> str:
|
||||
"""Преобразует HTML-фрагмент в чистый текст.
|
||||
|
||||
Удаляет все HTML-теги и декодирует HTML-мнемоники в Unicode.
|
||||
Затем нормализует пробелы через normalize_string().
|
||||
|
||||
Args:
|
||||
s: Строка, которую надо очистить (с возможной HTML-разметкой).
|
||||
Returns:
|
||||
str: Чистый текст без HTML-разметки, спецсимволов и нормализованный.
|
||||
|
||||
Example:
|
||||
>> safe_html_special_symbols('<p>Привет <b>мир</b>!</p>')
|
||||
'Привет мир!'
|
||||
>> safe_html_special_symbols('Текст с\\u00a0неразрывным и \\u202Fтонким пробелом')
|
||||
'Текст с неразрывным и тонким пробелом'
|
||||
"""
|
||||
if not s:
|
||||
return ""
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(s, "html.parser")
|
||||
except Exception as e:
|
||||
logger.warning(f"BeautifulSoup parse error, using raw string: {e}")
|
||||
return str(s)
|
||||
|
||||
# Скрипты и стили в чистый текст не нужны — выкидываем их целиком.
|
||||
for tag in soup(["script", "style", "noscript", "code", "kbd", "pre"]):
|
||||
tag.decompose()
|
||||
|
||||
result = soup.get_text()
|
||||
result = unescape(result)
|
||||
|
||||
# Нормализуем: удаляем невидимые символы, все типы пробелов, дубли и края
|
||||
return normalize_string(result)
|
||||
|
||||
|
||||
def make_slug(
|
||||
slug_it: str,
|
||||
max_length: int = SLUG_MAX_LENGTH,
|
||||
slug_default: str = "content") -> str:
|
||||
"""Готовит чистый slug из HTML/Unicode текста (включая русский текст).
|
||||
|
||||
Преобразует текст в URL-friendly slug:
|
||||
- Удаляет HTML-теги
|
||||
- Транслитерирует русский текст
|
||||
- Удаляет спецсимволы
|
||||
- Обрезает до max_length
|
||||
- Гарантирует уникальность (fallback на случайное значение)
|
||||
|
||||
Args:
|
||||
slug_it: Строка для преобразования в slug.
|
||||
max_length: Максимальная длина slug (по умолчанию из settings).
|
||||
slug_default: Значение по умолчанию, если slug пустой после обработки.
|
||||
|
||||
Returns:
|
||||
str: Чистый slug, готовый для использования в URL.
|
||||
|
||||
Example:
|
||||
>> make_slug('<b>The Beatles</b>')
|
||||
'the-beatles'
|
||||
>> make_slug('Какой-то текст')
|
||||
'kakoj-to-tekst'
|
||||
"""
|
||||
if not slug_it:
|
||||
return f"{slug_default}-{random.randint(1, 4095):03x}"
|
||||
|
||||
# Вычисляем минимальную длину fallback'а: "slug_default-xyz"
|
||||
min_fallback_length = len(slug_default) + 1 + 3 # "-" и 3 hex-символа
|
||||
|
||||
# Очищаем текст от HTML и спецсимволов
|
||||
clean_text = safe_html_special_symbols(slug_it).lower()
|
||||
|
||||
# Транслитерируем и создаем slug (pytils подходит для русского)
|
||||
slug = pytils.translit.slugify(clean_text)
|
||||
|
||||
# Нормализуем множественные дефисы, удаляем дефисы в начале/конце
|
||||
slug = re.sub(pattern=r"-+", repl="-", string=slug).strip("-")
|
||||
|
||||
# Обрезаем излишнее (но только если это не нарушит fallback)
|
||||
# Если max_length недостаточен для slug_default, не обрезаем
|
||||
if max_length >= min_fallback_length:
|
||||
slug = slug[:max_length]
|
||||
|
||||
# Если все еще пусто — генерируем fallback (БЕЗ обрезания!)
|
||||
return slug or f"{slug_default}-{random.randint(1, 4095):03x}"
|
||||
|
||||
|
||||
def update_synonyms_in_metadata(
|
||||
instance: Model,
|
||||
main_field_name: str,
|
||||
metadata_field_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Обновляет список синонимов в метаданных экземпляра модели.
|
||||
|
||||
Универсальный хелпер для управления синонимами во всех моделях (TbLabel, TbArtist, TbMusicStyle и т.д.)
|
||||
|
||||
Логика:
|
||||
- При создании новой записи: добавляет текущее значение поля в SYNONYM
|
||||
- При редактировании: если значение поля изменилось, добавляет ОБА (старое и новое) в SYNONYM
|
||||
- Очищает дубликаты в списке синонимов, сохраняя порядок
|
||||
- Использует KEY_SYNONYM из settings как ключ в metadata словаре
|
||||
|
||||
Args:
|
||||
instance: Экземпляр модели (self из save методе). Обязателен!
|
||||
main_field_name: Имя основного поля ('s_label', 's_artist', 's_style_name'). Обязателен!
|
||||
metadata_field_name: Имя поля метаданных ('j_label_metadata', 'j_artist_metadata'). Обязателен!
|
||||
|
||||
Пример использования в TbLabel.save():
|
||||
def save(self, *args, **kwargs):
|
||||
validate_and_raise_for_duplicates(self, 's_label', 'j_label_metadata')
|
||||
update_synonyms_in_metadata(self, 's_label', 'j_label_metadata')
|
||||
# ... остальная логика save()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
Пример использования в TbArtist.save():
|
||||
def save(self, *args, **kwargs):
|
||||
validate_and_raise_for_duplicates(self, 's_artist', 'j_artist_metadata')
|
||||
update_synonyms_in_metadata(self, 's_artist', 'j_artist_metadata')
|
||||
# ... остальная логика save()
|
||||
super().save(*args, **kwargs)
|
||||
"""
|
||||
model_class = instance.__class__
|
||||
|
||||
# Проверяем, что указанные поля существуют в модели
|
||||
for field_name in [main_field_name, metadata_field_name]:
|
||||
if not hasattr(instance, field_name):
|
||||
raise AttributeError(
|
||||
f"{model_class.__name__} instance has no attribute '{field_name}'. "
|
||||
f"Check that main_field_name and metadata_field_name are correct."
|
||||
)
|
||||
|
||||
# ===== ОПРЕДЕЛЯЕМ, ЭТО СОЗДАНИЕ ИЛИ РЕДАКТИРОВАНИЕ =====
|
||||
# Получаем текущее значение основного поля
|
||||
current_field_value = getattr(instance, main_field_name)
|
||||
|
||||
# Определяем новая ли это запись или обновление
|
||||
is_new = instance.pk is None
|
||||
|
||||
# Получаем старое значение поля (для редактирования)
|
||||
old_field_value = None
|
||||
if not is_new:
|
||||
try:
|
||||
old_instance = model_class.objects.get(pk=instance.pk)
|
||||
old_field_value = getattr(old_instance, main_field_name)
|
||||
except model_class.DoesNotExist:
|
||||
# На случай если что-то пошло не так, считаем это новым
|
||||
is_new = True
|
||||
|
||||
# ===== ИНИЦИАЛИЗИРУЕМ МЕТАДАННЫЕ =====
|
||||
# Инициализируем metadata если оно пусто
|
||||
metadata_dict = getattr(instance, metadata_field_name)
|
||||
if not metadata_dict:
|
||||
metadata_dict = {}
|
||||
setattr(instance, metadata_field_name, metadata_dict)
|
||||
|
||||
# Убеждаемся, что ключ 'SYNONYM' существует и это список
|
||||
if KEY_SYNONYM not in metadata_dict or not isinstance(metadata_dict[KEY_SYNONYM], list):
|
||||
metadata_dict[KEY_SYNONYM] = []
|
||||
|
||||
# ===== ДОБАВЛЯЕМ СИНОНИМЫ =====
|
||||
# Добавляем синонимы при создании ИЛИ если значение поля изменилось
|
||||
if is_new or old_field_value != current_field_value:
|
||||
# Если поле было обновлено и значение изменилось - добавляем старое значение
|
||||
if old_field_value and old_field_value not in metadata_dict[KEY_SYNONYM]:
|
||||
metadata_dict[KEY_SYNONYM].append(old_field_value)
|
||||
|
||||
# Добавляем текущее значение если его еще нет в синонимах
|
||||
if current_field_value not in metadata_dict[KEY_SYNONYM]:
|
||||
metadata_dict[KEY_SYNONYM].append(current_field_value)
|
||||
|
||||
# ===== ОЧИЩАЕМ ДУБЛИКАТЫ =====
|
||||
# Удаляем дубликаты в списке синонимов, сохраняя порядок
|
||||
# (может случиться если пользователь вручную редактировал метаданные)
|
||||
if KEY_SYNONYM in metadata_dict and isinstance(metadata_dict[KEY_SYNONYM], list):
|
||||
metadata_dict[KEY_SYNONYM] = list(dict.fromkeys(metadata_dict[KEY_SYNONYM]))
|
||||
|
||||
# ===== СООБЩАЕМ DJANGO ЧТО ПОЛЕ ИЗМЕНИЛОСЬ =====
|
||||
# Для JSONField нужно явно сообщить что мы изменили содержимое
|
||||
# иначе Django может не сохранить изменения
|
||||
setattr(instance, metadata_field_name, metadata_dict)
|
||||
|
||||
def create_or_get_related_article(
|
||||
instance: Model,
|
||||
article_type: str,
|
||||
main_field_name: str,
|
||||
metadata_field_name: str,
|
||||
related_fk_field_name: str,
|
||||
) -> Model:
|
||||
"""
|
||||
Создает или получает связанную статью для сущности модели.
|
||||
|
||||
Универсальный хелпер для создания статей при сохранении лейблов, артистов и т.д.
|
||||
|
||||
Алгоритм:
|
||||
1. Получает значение FK поля напрямую через getattr() (никаких запросов в БД)
|
||||
2. Если статья найдена → возвращает её
|
||||
3. Если FK поле пусто → создает новую статью с автоматическими SEO параметрами
|
||||
|
||||
Параметры:
|
||||
-----------
|
||||
instance : django.db.models.Model
|
||||
Экземпляр модели (TbLabel, TbArtist, TbMusicStyle и т.д.)
|
||||
Должен иметь поле k_{type}_to_article для привязки статьи
|
||||
|
||||
article_type : TbArticle.ArticleType
|
||||
Тип статьи (например, TbArticle.ArticleType.LABEL)
|
||||
|
||||
main_field_name : str
|
||||
Имя основного поля сущности (например, 's_label' для TbLabel)
|
||||
|
||||
metadata_field_name : str
|
||||
Имя поля метаданных со словарем синонимов (например, 'j_label_metadata')
|
||||
|
||||
related_fk_field_name : str
|
||||
Имя FK поля в модели instance (например, 'k_label_to_article')
|
||||
Передается явно из save() модели
|
||||
|
||||
Returns:
|
||||
TbArticle: Созданная или найденная статья
|
||||
|
||||
Пример использования в TbLabel.save():
|
||||
article = create_or_get_related_article(
|
||||
self,
|
||||
TbArticle.ArticleType.LABEL,
|
||||
's_label',
|
||||
'j_label_metadata',
|
||||
'k_label_to_article'
|
||||
)
|
||||
self.k_label_to_article = article
|
||||
"""
|
||||
from .models import TbArticle
|
||||
|
||||
# Получаем verbose_name модели для универсальности
|
||||
verbose_name = instance._meta.verbose_name # "Лейбл", "Артист" и т.д.
|
||||
|
||||
# Генерируем slug используя verbose_name (более гибко чем хардкод)
|
||||
# "Лейбл" → "label", "Артист" → "artist"
|
||||
article_slug_default = make_slug(slug_it=verbose_name)
|
||||
|
||||
# Получаем значение основного поля (например, "Sony Records")
|
||||
main_field_value = getattr(instance, main_field_name, '')
|
||||
|
||||
# ===== ПОПЫТКА НАЙТИ СУЩЕСТВУЮЩУЮ СТАТЬЮ =====
|
||||
# Получаем статью через FK поле напрямую (никаких запросов в БД)
|
||||
try:
|
||||
article = getattr(instance, related_fk_field_name)
|
||||
if article:
|
||||
# FK поле содержит статью - возвращаем её
|
||||
return article
|
||||
except:
|
||||
# FK поле не существует или пусто, создаем новую
|
||||
pass
|
||||
|
||||
# ===== СТАТЬЯ НЕ ПРИВЯЗАНА. СОЗДАËМ НОВУЮ СТАТЬЮ =====
|
||||
|
||||
# Собираем синонимы из метаданных для SEO ключевых слов и исключаем текущее значение поля
|
||||
# из списка (оно будет добавлено первым для приоритета)
|
||||
other_synonyms = [s for s in instance.__dict__.get(metadata_field_name, {}).get(KEY_SYNONYM, []) if str(s) != main_field_value]
|
||||
|
||||
# Собираем все синонимы с текущим значением первым
|
||||
all_synonyms = [main_field_value] + other_synonyms if other_synonyms else [main_field_value]
|
||||
synonyms_str = ", ".join(str(s) for s in all_synonyms)
|
||||
|
||||
# Создаем объект статьи
|
||||
article = TbArticle(
|
||||
s_article_title=f"[{verbose_name}] {main_field_value} (auto-make)", # Техническое название для админки
|
||||
s_article_title_html=main_field_value,
|
||||
seo_title=main_field_value,
|
||||
seo_keywords=f"{synonyms_str}, {verbose_name}, {article_slug_default}",
|
||||
seo_description=f"{main_field_value} ({verbose_name}): информация.",
|
||||
l_article_type=article_type,
|
||||
b_article_published=True,
|
||||
slug=make_slug(slug_it=main_field_value, slug_default=article_slug_default),
|
||||
)
|
||||
|
||||
# Сохраняем статью
|
||||
article.save()
|
||||
|
||||
return article
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for lpon_site project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lpon_site.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Django settings for lpon_site project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 6.0.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import environ
|
||||
import os
|
||||
from enum import IntEnum
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Читаем переменные окружения
|
||||
env = environ.Env()
|
||||
environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env'))
|
||||
|
||||
def _normalize_admin_url(value: str) -> str:
|
||||
"""Приводит URL админки к виду `segment/` без ведущего слэша."""
|
||||
normalized = value.strip().lstrip('/')
|
||||
if not normalized:
|
||||
return 'admin/'
|
||||
if not normalized.endswith('/'):
|
||||
normalized += '/'
|
||||
return normalized
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env('DJANGO_SECRET_KEY', default='3xym$l+!)erah-k23lf0=t=c_4$e0nr*zls&l%pbz@k6v6qn89')
|
||||
ADMIN_URL = _normalize_admin_url(env('DJANGO_ADMIN_URL', default='admin/'))
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env.bool('DJANGO_DEBUG', default=False)
|
||||
|
||||
ALLOWED_HOSTS = env.list(
|
||||
'DJANGO_ALLOWED_HOSTS',
|
||||
default=['127.0.0.1', 'localhost', 'testserver', 'lpon.ru'],
|
||||
)
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=['127.0.0.1', 'localhost', 'testserver'])
|
||||
|
||||
#########################################
|
||||
# Настройки сообщений об ошибках когда все упало и т.п.
|
||||
ADMINS = tuple(
|
||||
tuple(item.split(':', 1))
|
||||
for item in env.list('DJANGO_ADMINS', default=['S.Erjemin:erjemin@gmail.com'])
|
||||
)
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# Django core
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# Django-filer и его зависимости
|
||||
# Порядок важен! polymorphic должен быть ДО filer, easy_thumbnails тоже ДО filer
|
||||
# 'polymorphic',
|
||||
'easy_thumbnails',
|
||||
# 'filer',
|
||||
# Кастомная надстройка над filer для переопределения verbose_name (и других настроек)
|
||||
'frontend.apps.CustomFilerConfig',
|
||||
# Наше приложение
|
||||
'frontend.apps.FrontendConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'lpon_site.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'lpon_site.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR.parent.joinpath('database', env('DJANGO_SQLITE_NAME', default='lpon-db.sqlite3')),
|
||||
'CONN_MAX_AGE': 600, # время жизни соединения с базой
|
||||
'OPTIONS': {
|
||||
'timeout': 25,
|
||||
'init_command': "PRAGMA journal_mode=WAL; PRAGMA auto_vacuum=2;",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
LANGUAGE_CODE = 'ru-RU' #
|
||||
# TIME_ZONE = 'Etc/GMT+3' #
|
||||
TIME_ZONE = 'Europe/Moscow' #
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
FIRST_DAY_OF_WEEK = 1 # неделя начинается с понедельника
|
||||
DEFAULT_CHARSET = 'utf-8'
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||
STATIC_URL = '/static/'
|
||||
MEDIA_URL = '/media/'
|
||||
# Локальные каталоги проекта: медиа и статика лежат рядом в `public`.
|
||||
PUBLIC_DIR = BASE_DIR.parent.joinpath('public') # Папка `public` находится в корне пректа
|
||||
MEDIA_ROOT = PUBLIC_DIR.joinpath('media')
|
||||
STATICFILES_DIRS = [PUBLIC_DIR.joinpath('static')]
|
||||
STATIC_ROOT = PUBLIC_DIR.joinpath('staticfiles')
|
||||
|
||||
# ============================================================================
|
||||
# Django-Filer & Easy-Thumbnails Configuration
|
||||
# ============================================================================
|
||||
# Примечание о потоках обработки:
|
||||
# - Django: количество worker-процессов настраивается через gunicorn/uWSGI (--workers, --threads)
|
||||
# - Pillow-HEIF: параллелизм настраивается через pillow_heif.options.DECODE_THREADS в apps.py
|
||||
# - Easy-Thumbnails: использует Pillow напрямую, параллелизм через Pillow
|
||||
# ============================================================================
|
||||
FILER_STORAGES = {
|
||||
'public': {
|
||||
'main': {
|
||||
'ENGINE': 'filer.storage.PublicFileSystemStorage',
|
||||
'OPTIONS': {
|
||||
'location': MEDIA_ROOT / 'flr',
|
||||
'base_url': MEDIA_URL + 'flr/',
|
||||
},
|
||||
'UPLOAD_TO': 'filer.utils.generate_filename.randomized',
|
||||
'UPLOAD_TO_PREFIX': '',
|
||||
},
|
||||
'thumbnails': {
|
||||
'ENGINE': 'filer.storage.PublicFileSystemStorage',
|
||||
'OPTIONS': {
|
||||
'location': MEDIA_ROOT / 'flrm',
|
||||
'base_url': MEDIA_URL + 'flrm/',
|
||||
},
|
||||
# Переопределяем THUMBNAIL_OPTIONS чтобы убрать prefix 'filer_public_thumbnails'
|
||||
# По умолчанию filer добавляет 'base_dir': 'filer_public_thumbnails', мы это отключаем
|
||||
# Почему-то этого нет в документации. :(
|
||||
'THUMBNAIL_OPTIONS': {
|
||||
'base_dir': '',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Настройки easy_thumbnails для генерации миниатюр
|
||||
# Преимущественно используется для django-filer
|
||||
THUMBNAIL_PRESERVE_FORMAT = False
|
||||
THUMBNAIL_FORMAT = 'WEBP'
|
||||
THUMBNAIL_DEBUG = DEBUG
|
||||
THUMBNAIL_QUALITY= 80
|
||||
THUMBNAIL_WEBP_QUALITY = THUMBNAIL_QUALITY # Наша, кастомная настройка для WebP-сжатия исходных картинок
|
||||
THUMBNAIL_ENGINE = 'easy_thumbnails.engines.pil_engine.PilEngine'
|
||||
|
||||
# Список расширений, которые нужно сохранять при генерации миниатюр
|
||||
# Это важно для WebP файлов - они не должны быть сконвертированы в другой формат
|
||||
THUMBNAIL_PRESERVE_EXTENSIONS = ['png', 'gif', 'webp']
|
||||
|
||||
# Нейминг миниатюр - стандартный нейминг easy_thumbnails
|
||||
# Формат: {source_name}{dimensions}{options}{quality}{extra}
|
||||
# Например: image.webp__40x40_q85_crop_subsampling-2.jpg
|
||||
THUMBNAIL_NAMER = 'easy_thumbnails.namers.default'
|
||||
|
||||
# Показывать ошибки при генерации миниатюр (полезно для отладки)
|
||||
THUMBNAIL_VERBOSE = DEBUG
|
||||
|
||||
THUMBNAIL_ALIASES = {
|
||||
'': {
|
||||
# Примечание: filer автоматически генерирует свои миниатюры для админки (40x40, 210x210 и 420x420)
|
||||
# через DEFAULT_THUMBNAILS (см. BaseImage класс в filer).
|
||||
'small': {'size': (256, 256), 'crop': True},
|
||||
'medium': {'size': (512, 512), 'crop': True},
|
||||
'large': {'size': (1024, 1024), 'crop': 'smart'},
|
||||
},
|
||||
}
|
||||
FILER_UPLOADER_MAX_FILES = 3
|
||||
FILER_UPLOADER_MAX_FILE_SIZE = 100 * 1024 * 1024
|
||||
FILER_MAX_IMAGE_PIXELS = 4096 * 4096
|
||||
|
||||
FILER_ENABLE_PERMISSIONS = DEBUG
|
||||
FILER_WHITELIST_FOR_PATH_ACCESS = (
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.heic', '.heif',
|
||||
'.doc', '.docx', '.pdf', '.xls', '.xlsx', '.txt', '.csv',
|
||||
)
|
||||
MIME_TYPE_WHITELIST = (
|
||||
'image/jpeg', # .jpg / .jpeg
|
||||
'image/png', 'image/x-png', # .png
|
||||
'image/gif',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
'image/heic', 'image/heif', # форматы Apple HEIC/HEIF (без анимации 'image/heic-sequence и 'image/heif-sequence')
|
||||
'application/pdf',
|
||||
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', # .doc / .docx
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # .xls / .xlsx
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
)
|
||||
FILE_VALIDATORS = {}
|
||||
|
||||
# Настройки для "умной" обрезки изображений
|
||||
THUMBNAIL_PROCESSORS = (
|
||||
'easy_thumbnails.processors.colorspace',
|
||||
'easy_thumbnails.processors.autocrop',
|
||||
# 'easy_thumbnails.processors.scale_and_crop',
|
||||
'filer.thumbnail_processors.scale_and_crop_with_subject_location',
|
||||
'easy_thumbnails.processors.filters',
|
||||
)
|
||||
|
||||
# Конфигурация логирования для отладки загрузок и обработки файлов
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '[{levelname}] {asctime} {name}:{funcName}() line {lineno} - {message}',
|
||||
'style': '{',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
'file': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': os.path.join(BASE_DIR.parent, 'logs', 'uploads.log'),
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'frontend.apps': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console', 'file'],
|
||||
'propagate': False,
|
||||
},
|
||||
'frontend.models': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console', 'file'],
|
||||
'propagate': False,
|
||||
},
|
||||
'easy_thumbnails': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console', 'file'],
|
||||
'propagate': False,
|
||||
},
|
||||
'filer': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console', 'file'],
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# ===================================================
|
||||
# СПЕЦИФИЧНЫЕ ДЛЯ ПРОЕКТА КОНСТАНТЫ И НАСТРОЙКИ
|
||||
|
||||
# Максимальная длина slug: Весь URL оптимально иметь до 70 символов (методичка Google, так как такой URL виден целиком
|
||||
# на страничке поисковой выдачи, вызывает больше доверия и кликов). С учетом того, что
|
||||
# в URL входит имя домена и slug-и разделов, оптимально устанавливать slug даже короче
|
||||
# (20-30 символов максимум).
|
||||
# SLUG_MAX_LENGTH задает ограничение для slug-ов созданных автоматически. Вручную в admin можно задать до 255 символов.
|
||||
SLUG_MAX_LENGTH = 60
|
||||
|
||||
# Ключи для типовых параметров в мета-полях (для TbLabel, TbSeller, TbArtist, TbMusicStyle и т.д.)
|
||||
KEY_SYNONYM = 'SYNONYM'
|
||||
|
||||
# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
|
||||
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
|
||||
VALIDATE_KEY__MODEL = 'MODEL'
|
||||
VALIDATE_KEY__VALUE = 'MATCH_VALUE'
|
||||
|
||||
# Типы совпадений как Enum (более информативно чем просто числа, работает в match-case)
|
||||
class ValidateMatchType(IntEnum):
|
||||
"""Типы совпадений при поиске дубликатов."""
|
||||
IS_DUPLICATE = 1 # Точное совпадение основного поля (s_label, s_artist и т.д.)
|
||||
FIND_IN_SYNONYM = 2 # Точное совпадение в синонимах
|
||||
EXACT_SYNONYM_MATCH = 3 # Точное совпадение синонимов проверяемой записи с синонимами остальных записей
|
||||
PARTIAL_MATCH = 4 # Схожесть: частичное совпадение (например, часть названия артиста или лейбла в других записях).
|
||||
# Основного поля и синонимами проверяемой записи и всех остальных записей.
|
||||
PARTIAL_MATCH__RISK_SHORT_WORDS = 5 # Схожесть: Частичное совпадение, но с короткими словами (например,
|
||||
# Японская группа "B'Z" или лейбл "XL"), что может вызвать ложные срабатывания
|
||||
# схожести. Нужны быть аккуратнее, проверять вручную (особенно если короткие
|
||||
# слова среди синонимов внутри метаданных других записей). Довольно частое
|
||||
# явление для распростарннеых "суффиксов" MC, DJ, артиклей, инициалов
|
||||
# исполнителей и т.п.
|
||||
|
||||
|
||||
# Минимальная длина слова для поиска совпадений схожести в синонимах (например, в "The Beatles": "The" не учитывается,
|
||||
# а "Beatles" учитывается)
|
||||
MIN_SYNONYM_WORD_LENGTH = 4
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
URL configuration for lpon_site project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/6.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
# Админ-сайт с переименованными приложениями (переопределен в frontend/apps.py)
|
||||
path(settings.ADMIN_URL, admin.site.urls),
|
||||
]
|
||||
|
||||
# Сервировать медиа-файлы в разработке (в production использовать nginx/Apache)
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for lpon_site project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lpon_site.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lpon_site.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Generated
+906
@@ -0,0 +1,906 @@
|
||||
# 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.15.0"
|
||||
description = "Screen-scraping library"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9"},
|
||||
{file = "beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7"},
|
||||
]
|
||||
|
||||
[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 = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"},
|
||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"},
|
||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"},
|
||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"},
|
||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"},
|
||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"},
|
||||
{file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"},
|
||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"},
|
||||
{file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"},
|
||||
{file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssselect2"
|
||||
version = "0.9.0"
|
||||
description = "CSS selectors for Python ElementTree"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563"},
|
||||
{file = "cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tinycss2 = "*"
|
||||
webencodings = "*"
|
||||
|
||||
[package.extras]
|
||||
doc = ["furo", "sphinx"]
|
||||
test = ["pytest", "ruff"]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "6.0.5"
|
||||
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.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0"},
|
||||
{file = "django-6.0.5.tar.gz", hash = "sha256:bc6d6872e98a2864c836e42edd644b362db311147dd5aa8d5b82ba7a032f5269"},
|
||||
]
|
||||
|
||||
[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-environ"
|
||||
version = "0.13.0"
|
||||
description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
|
||||
optional = false
|
||||
python-versions = "<4,>=3.9"
|
||||
files = [
|
||||
{file = "django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e"},
|
||||
{file = "django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"]
|
||||
docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"]
|
||||
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "django-extensions"
|
||||
version = "3.2.3"
|
||||
description = "Extensions for Django"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
|
||||
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=3.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-filer"
|
||||
version = "3.4.4"
|
||||
description = "A file management application for django that makes handling of files and images a breeze."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "django_filer-3.4.4-py3-none-any.whl", hash = "sha256:5df64bb144c861ad2c2697bd6a3709282bda6e14f8623ccd934797474d415c4c"},
|
||||
{file = "django_filer-3.4.4.tar.gz", hash = "sha256:465eec9fe63310a5b718267beebe7c348ee44afd16c6b5056a7bab49ee483412"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=3.2"
|
||||
django-polymorphic = "<5.0"
|
||||
easy-thumbnails = {version = "*", extras = ["svg"]}
|
||||
svglib = ">=1.5.1,<1.6.0"
|
||||
|
||||
[package.extras]
|
||||
heif = ["pillow-heif"]
|
||||
|
||||
[[package]]
|
||||
name = "django-polymorphic"
|
||||
version = "4.11.5"
|
||||
description = "Seamless polymorphic inheritance for Django models."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.10"
|
||||
files = [
|
||||
{file = "django_polymorphic-4.11.5-py3-none-any.whl", hash = "sha256:8c65b5c109c057c51e059197b01765dd5e096b3fd393ee3038bffc294681b3be"},
|
||||
{file = "django_polymorphic-4.11.5.tar.gz", hash = "sha256:9a6fe66fab12cfe06aac81942d14670c6b0204aebd30ce9fb58fb53213c613eb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=4.2"
|
||||
typing-extensions = ">=4.12.0"
|
||||
|
||||
[[package]]
|
||||
name = "easy-thumbnails"
|
||||
version = "2.10.1"
|
||||
description = "Easy thumbnails for Django"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "easy_thumbnails-2.10.1-py3-none-any.whl", hash = "sha256:24462d63dd31543ef1585538b2bfefe0db96d3409bb431c70b81548fb2cfc5be"},
|
||||
{file = "easy_thumbnails-2.10.1.tar.gz", hash = "sha256:a50aa5f99387546c35ab5ba1ea9b3cbbc5658e65601cd34949f62137c32c222e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=4.2"
|
||||
pillow = "*"
|
||||
reportlab = {version = "*", optional = true, markers = "extra == \"svg\""}
|
||||
svglib = {version = "*", optional = true, markers = "extra == \"svg\""}
|
||||
|
||||
[package.extras]
|
||||
svg = ["reportlab", "svglib"]
|
||||
|
||||
[[package]]
|
||||
name = "etpgrf"
|
||||
version = "0.1.6.post1"
|
||||
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "etpgrf-0.1.6.post1-py3-none-any.whl", hash = "sha256:0863b14385bdacdd405f137ca2ce6bdb6f683f0189e8c927196a1eee754366be"},
|
||||
{file = "etpgrf-0.1.6.post1.tar.gz", hash = "sha256:984d201cff232a58c05b6f4455a50f822162520df829ad4d543bfe0b7fd962a9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
beautifulsoup4 = ">=4.10.0"
|
||||
lxml = ">=4.9.0"
|
||||
regex = ">=2022.1.18"
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.1.1"
|
||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5"},
|
||||
{file = "lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28"},
|
||||
{file = "lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77"},
|
||||
{file = "lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660"},
|
||||
{file = "lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137"},
|
||||
{file = "lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e"},
|
||||
{file = "lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c"},
|
||||
{file = "lxml-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6"},
|
||||
{file = "lxml-6.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88"},
|
||||
{file = "lxml-6.1.1-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3"},
|
||||
{file = "lxml-6.1.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4"},
|
||||
{file = "lxml-6.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e"},
|
||||
{file = "lxml-6.1.1-cp38-cp38-win32.whl", hash = "sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3"},
|
||||
{file = "lxml-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-win32.whl", hash = "sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730"},
|
||||
{file = "lxml-6.1.1-cp39-cp39-win_arm64.whl", hash = "sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9"},
|
||||
{file = "lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e"},
|
||||
{file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004"},
|
||||
{file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e"},
|
||||
{file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2"},
|
||||
{file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf"},
|
||||
{file = "lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84"},
|
||||
{file = "lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cssselect = ["cssselect (>=0.7)"]
|
||||
html-clean = ["lxml_html_clean"]
|
||||
html5 = ["html5lib"]
|
||||
htmlsoup = ["BeautifulSoup4"]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.2.0"
|
||||
description = "Python Imaging Library (fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5"},
|
||||
{file = "pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60"},
|
||||
{file = "pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5"},
|
||||
{file = "pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b"},
|
||||
{file = "pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98"},
|
||||
{file = "pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"},
|
||||
{file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"},
|
||||
{file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1"},
|
||||
{file = "pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e"},
|
||||
{file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"},
|
||||
]
|
||||
|
||||
[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 = "pillow-heif"
|
||||
version = "1.3.0"
|
||||
description = "Python interface for libheif library"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "pillow_heif-1.3.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0addc7d25133a2abd0149d1f1b8063808268c9ed75dc228c2196c90d56639a25"},
|
||||
{file = "pillow_heif-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e557b7082e785ff4505b2be258409fc3983162a3802f5438aa3177201d0e5a41"},
|
||||
{file = "pillow_heif-1.3.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e031f0036200da349cdac1328a3617a9d13b7f9145355c0a36306ce3b9f1d622"},
|
||||
{file = "pillow_heif-1.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d00218ce66aa74cddb5cb64e59a8867ed6722cc430b240d578ef3bc1307998a3"},
|
||||
{file = "pillow_heif-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4c4eef69c7caec0f41af13308bba5883bde751119f27a51c9a299b83852f3430"},
|
||||
{file = "pillow_heif-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:625b67f7000b3aae645e4aea4170a6f0bb9015577c69cf249c360b10e071e8a7"},
|
||||
{file = "pillow_heif-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a924fbb505674546973518b8906f499a56bb3332752a144bc272becd59c141"},
|
||||
{file = "pillow_heif-1.3.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:68bea3b9396fdd6c711e66fc645df92bbe53d48892909192ce9a30e4c619878a"},
|
||||
{file = "pillow_heif-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a59db0091556d11ab26c2b34532b7992965520027ba0a64084771bcc9a31156"},
|
||||
{file = "pillow_heif-1.3.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b69cc05b4f22ac57f1fd8f5f7ae96ae7c752036659ea975ec5f5565efadd87f"},
|
||||
{file = "pillow_heif-1.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a9f26d9ece400f55ac006e2fed079392e44a550023c99122e281fcd72b7c06"},
|
||||
{file = "pillow_heif-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a25a58368222c388a96b111f4621423eac6fa07a0bbcb2ac5eaf624153cde04e"},
|
||||
{file = "pillow_heif-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6d3cdc335df30652ae9b07438e9898ce5cb5dbc47012fa14f93be1df9d446dc5"},
|
||||
{file = "pillow_heif-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:b5a1458bc11ca83cd72ec4a93f739ad3ae4315ae66766022ec16d12993a863a4"},
|
||||
{file = "pillow_heif-1.3.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:079abbcaeb42ef0849a33f35c1a96ccd431feb56b242a0d4f8435a1c8ca02c7d"},
|
||||
{file = "pillow_heif-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76c33f80ec111492642b98309db98516a7fba9677dcda9ec5fe9111b7e38d720"},
|
||||
{file = "pillow_heif-1.3.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33b838d06e2fd730f806af5a76bfc4cd3de9d146d88d37572e40f7a4c4ff8221"},
|
||||
{file = "pillow_heif-1.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f92b387af891cf5d98f52e79eeaf51ee7955a54fe2deeec12bfb7519e41464b5"},
|
||||
{file = "pillow_heif-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f9f73246836f93f99343cbc3052b61d212d27e59ddf40262d494a1e3e54af31a"},
|
||||
{file = "pillow_heif-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84c3816742c2e49176e651895e73c555b9c3b0f3561d60230242f3be0c9d272b"},
|
||||
{file = "pillow_heif-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:5f0db0bf49162fb1d73d13340a9576b3a2805bde026a9a40038bcc1a0878d710"},
|
||||
{file = "pillow_heif-1.3.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:641c50a064aa9ad6626a6b2b914b65855202f937d573d53838e344feb2e8c6d1"},
|
||||
{file = "pillow_heif-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9390dd7987887aa09779fbd88bbab715c732c9ad3a71d6707284035e3ca93379"},
|
||||
{file = "pillow_heif-1.3.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e8444ccb330015e1db930207d269886e4b6c666121cd9e5fdad88735950b09f"},
|
||||
{file = "pillow_heif-1.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d30054ccc97ecbe5ee3fa486a505ccc33bfbb27f005ad624ddb4c17b80ddd57"},
|
||||
{file = "pillow_heif-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dc1b9c9efdf8345d703118449ff69696d0827bdf28e3b52f82015f5714f7c23e"},
|
||||
{file = "pillow_heif-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee26b2155721e7f5f7b10fa93ca2ad3be59547c5c5e5d9d50e6ea17531b81d60"},
|
||||
{file = "pillow_heif-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:17ecbbadfe10ea12a65c1c12354dc1ed8ae1e5d1b7092ea753641b029f7d6f9e"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8267a73d3b2d07a47a96428bd8cd4c406e1637a94f29d4c16ce08b31b8e50a07"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:36bbea7679467caa3a154db11c04f1ca2fa8591e886f06f40f7831c14b58d771"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea3a4b2de4b6c63407af72afdac901616807c6e6a030fe77851d227bca3727a"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05149bd26b08dae5af7a389af6db13cef4f12c7871db73d84e40a1f3c83b0142"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f8b7a50058fc3152f42b68aa2b30601249f61aa5c6c27876af076785c7051fd9"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:edb3ef437e8841475db14721f0529e600bb55c41b549ad1794a0831e28f33bac"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:bdd6695d5be0d98ae0e9a5f88fe26f1a6eca0a5b6d43d0a92a97f89fea5842f7"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:65c5d05cb7f5e1eadbe9c605ae3a4dd3ef953adb33e7d809d5fb56f8a6753588"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dc177fbdf598770cad4afa99c082a30b9d090e60c39656904338717803ae59b2"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71f88d180547bb5112b56310c8c5e338d8358320a402c80afabc6b2f39eadddb"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9acee893186bdde6140d30a7dc6d7c928e4ad3007989764f6e54a7a517faa332"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7cf893689132bec18f0c55a505da9ebf3a8feb33dd354fe2ac050f20f4f862e0"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:54404c9b6f0323114527579f54cc966b47206f99d943e47d73e1091ab0b9d2ba"},
|
||||
{file = "pillow_heif-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:18c7c35a9d98ed9eaaf2db601ee43425ebccc698801df9c008aa04e00756a22e"},
|
||||
{file = "pillow_heif-1.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2723c4b85c5ad0420cb0b3e512ac0aa015e3c8b13013b4738816833aa431f919"},
|
||||
{file = "pillow_heif-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6bdb197b43a629e2118fd33a9ebcf39abdabe5540b80d8862c53a7611edb42ab"},
|
||||
{file = "pillow_heif-1.3.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d433048bd23436afa2a33e08fc622712ec97fea1c230d5b5a0fafd5512628c8"},
|
||||
{file = "pillow_heif-1.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:142b307de087eba33d3174aa29a9669b65a7d006192ef8c98579920be3b56f64"},
|
||||
{file = "pillow_heif-1.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:633f5d7fbf60a489ba301fbba06e75539a5cd22ff0b036162db2167803416470"},
|
||||
{file = "pillow_heif-1.3.0.tar.gz", hash = "sha256:af8d2bda85e395677d5bb50d7bda3b5655c946cc95b913b5e7222fabacbb467f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pillow = ">=11.1.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage", "defusedxml", "numpy", "opencv-python (==4.13.0.92)", "packaging", "pre-commit", "pylint", "pympler", "pytest", "setuptools"]
|
||||
docs = ["sphinx (>=4.4)", "sphinx-issues (>=3.0.1)", "sphinx-rtd-theme (>=1.0)"]
|
||||
tests = ["defusedxml", "numpy", "packaging", "pympler", "pytest"]
|
||||
tests-min = ["defusedxml", "packaging", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
|
||||
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[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.5.9"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03"},
|
||||
{file = "regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b"},
|
||||
{file = "regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e"},
|
||||
{file = "regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca"},
|
||||
{file = "regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4"},
|
||||
{file = "regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763"},
|
||||
{file = "regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77"},
|
||||
{file = "regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa"},
|
||||
{file = "regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "4.5.1"
|
||||
description = "The Reportlab Toolkit"
|
||||
optional = false
|
||||
python-versions = "<4,>=3.9"
|
||||
files = [
|
||||
{file = "reportlab-4.5.1-py3-none-any.whl", hash = "sha256:06fce8cb56c83307cfa4909cdf4e6a2ddbb44e5d6ef4d2edca896d7e9769f091"},
|
||||
{file = "reportlab-4.5.1.tar.gz", hash = "sha256:9fdf68f4de9171ec66acb4a5feed8f8ca2af43479e707a6fbb0daa75d88e5494"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
charset-normalizer = "*"
|
||||
pillow = ">=9.0.0"
|
||||
|
||||
[package.extras]
|
||||
accel = ["rl_accel (>=0.9.0,<1.1)"]
|
||||
bidi = ["rlbidi"]
|
||||
pycairo = ["freetype-py (>=2.3.0,<2.4)", "rlPyCairo (>=0.2.0,<1)"]
|
||||
renderpm = ["rl_renderPM (>=4.0.3,<4.1)"]
|
||||
shaping = ["uharfbuzz"]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8.4"
|
||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65"},
|
||||
{file = "soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e"},
|
||||
]
|
||||
|
||||
[[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 = "svglib"
|
||||
version = "1.5.1"
|
||||
description = "A pure-Python library for reading and converting SVG"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "svglib-1.5.1.tar.gz", hash = "sha256:3ae765d3a9409ee60c0fb4d24c2deb6a80617aa927054f5bcd7fc98f0695e587"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cssselect2 = ">=0.2.0"
|
||||
lxml = "*"
|
||||
reportlab = "*"
|
||||
tinycss2 = ">=0.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "tinycss2"
|
||||
version = "1.5.1"
|
||||
description = "A tiny CSS parser"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661"},
|
||||
{file = "tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
webencodings = ">=0.4"
|
||||
|
||||
[package.extras]
|
||||
doc = ["furo", "sphinx"]
|
||||
test = ["pytest", "ruff"]
|
||||
|
||||
[[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 = "2026.2"
|
||||
description = "Provider of IANA time zone data"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
files = [
|
||||
{file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"},
|
||||
{file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webencodings"
|
||||
version = "0.5.1"
|
||||
description = "Character encoding aliases for legacy web content"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
|
||||
{file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "69bad093bc7511b8da08bb4673d5883f7cbeb9b45123b6a17350dd885e2a3a33"
|
||||
@@ -0,0 +1,15 @@
|
||||
# CodeMirror 6 static bundle
|
||||
|
||||
Эта папка содержит готовый результат сборки CodeMirror 6 для админки Django.
|
||||
|
||||
Главный файл здесь — `editor.js`. Его не нужно править вручную: это собранный и
|
||||
минифицированный бандл.
|
||||
|
||||
Если нужна более свежая версия CodeMirror, меняй сборку в
|
||||
`frontend-assembly/build-codemirror6.sh` и затем заново запускай:
|
||||
|
||||
```bash
|
||||
bash ./frontend-assembly/build-codemirror6.sh
|
||||
```
|
||||
|
||||
После пересборки в этой папке должен обновиться только `editor.js`.
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* CodeMirror 6 - Height Control Patch
|
||||
*
|
||||
* Этот патч перехватывает создание CodeMirror редакторов и копирует
|
||||
* атрибут data-height с textarea на создаваемый wrapper div.
|
||||
*
|
||||
* Использование в admin.py:
|
||||
* textarea widget: attrs={'data-height': '50px', 'data-codemirror-editor': '1'}
|
||||
*
|
||||
* Загружается ПОСЛЕ editor.js через Media в Admin класс.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Применяем высоту к уже созданным wrapper'ам
|
||||
* (CodeMirror уже загрузился и создал обёртки)
|
||||
*/
|
||||
function applyHeightsToExistingWrappers() {
|
||||
// Находим все textarea'ы с data-codemirror-editor
|
||||
document.querySelectorAll('textarea[data-codemirror-editor]').forEach((textarea) => {
|
||||
// Соседний div (wrapper, созданный CodeMirror)
|
||||
const wrapper = textarea.previousElementSibling;
|
||||
|
||||
// Проверяем что это действительно wrapper CodeMirror
|
||||
if (wrapper && wrapper.classList && wrapper.classList.contains('cm6-editor-wrapper')) {
|
||||
// Копируем data-height на style если указано
|
||||
if (textarea.dataset.height) {
|
||||
wrapper.style.height = textarea.dataset.height;
|
||||
}
|
||||
|
||||
// Копируем data-width на style если указано (для ширины)
|
||||
if (textarea.dataset.width) {
|
||||
wrapper.style.width = textarea.dataset.width;
|
||||
wrapper.style.minWidth = textarea.dataset.width;
|
||||
|
||||
// Также устанавливаем ширину на сам .cm-editor внутри wrapper
|
||||
// Используем !important чтобы переопределить flex: 1 1 auto из CSS
|
||||
const cmEditor = wrapper.querySelector('.cm-editor');
|
||||
// if (cmEditor) {
|
||||
// cmEditor.style.cssText = `width: ${textarea.dataset.width} !important; flex: 0 0 auto;`;
|
||||
// }
|
||||
}
|
||||
|
||||
// Копируем классы с textarea если они есть
|
||||
// (например cm6-editor-wrapper-xs, cm6-editor-wrapper-sm и т.д.)
|
||||
if (textarea.className) {
|
||||
// Добавляем классы из textarea (сохраняя cm6-editor-wrapper)
|
||||
textarea.className.split(' ').forEach((cls) => {
|
||||
if (cls && cls !== 'cm6-editor-wrapper') {
|
||||
wrapper.classList.add(cls);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Применяем высоты когда DOM готов
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', applyHeightsToExistingWrappers);
|
||||
} else {
|
||||
// DOM уже загружен
|
||||
applyHeightsToExistingWrappers();
|
||||
}
|
||||
|
||||
// Также применяем для динамически добавленных элементов (если загружают CodeMirror после инициализации)
|
||||
const observer = new MutationObserver(() => {
|
||||
applyHeightsToExistingWrappers();
|
||||
});
|
||||
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/* CodeMirror 6 - Стили для правильного отображения */
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--brdr: light-dark(#333, #ccc);
|
||||
--accent-mode-color: light-dark(#222, #eee);
|
||||
}
|
||||
|
||||
.cm6-editor-wrapper {
|
||||
/* Основной wrapper, созданный CodeMirror */
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
/* Сам редактор внутри wrapper - растягиваем на всю доступную высоту */
|
||||
flex: 1 1 auto;
|
||||
font-family: monospace;
|
||||
line-height: 1;
|
||||
border: 1px solid var(--brdr);;
|
||||
}
|
||||
|
||||
/*
|
||||
Утилитарные классы для управления шириной.
|
||||
Мы ищем редактор .cm-editor ВНУТРИ элемента с нашим классом.
|
||||
*/
|
||||
|
||||
.codemirror-width-s > .cm-editor {
|
||||
/* Маленький (для чисел, коротких ID) */
|
||||
max-width: 8em !important;
|
||||
}
|
||||
|
||||
.codemirror-width-m > .cm-editor {
|
||||
/* Средний (для URL, коротких строк) */
|
||||
max-width: calc(50% - 13em) !important; /* Вычитание 13em подобрано. Это ширина бокового меню слева */
|
||||
}
|
||||
|
||||
.codemirror-width-l > .cm-editor {
|
||||
/* Большой (для текста, JSON, HTML) */
|
||||
max-width: calc(75% - 13em) !important;
|
||||
}
|
||||
|
||||
.codemirror-width-xl > .cm-editor {
|
||||
/* Во всю ширину контейнера */
|
||||
max-width: calc(100% - 13em) !important;
|
||||
}
|
||||
|
||||
/* Ширина редактора зависит от видимости бокового меню Django Admin.
|
||||
На экранах < 767px меню прячется, поэтому вычитание 13em неактуально.
|
||||
Подобрано значение 3ex. */
|
||||
@media (max-width: 767px) {
|
||||
.codemirror-width-m > .cm-editor,
|
||||
.codemirror-width-l > .cm-editor,
|
||||
.codemirror-width-xl > .cm-editor {
|
||||
max-width: calc(100% - 3ex) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Новый класс для минимальной высоты --- */
|
||||
.codemirror-min-height-5 .cm-editor {
|
||||
min-height: 8em; /* 5 строк */
|
||||
}
|
||||
.codemirror-min-height-10 .cm-editor {
|
||||
min-height: 16em; /* 10 строк */
|
||||
}
|
||||
|
||||
.codemirror-no-lines .cm-gutters {
|
||||
/* Скрываем нумерацию строк */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Скрыть оригинальный textarea */
|
||||
textarea[data-codemirror-editor] {
|
||||
display: none !important;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Стили для кнопок при обходе валидации
|
||||
* Отображает пользователю что валидация была обойдена
|
||||
*/
|
||||
|
||||
/* Кнопки в режиме обхода валидации (при появлении ошибки и предупреждений синонимов и схожести) */
|
||||
input[type=submit].force-ignore-validation {
|
||||
background-color: #f39c12 !important; /* Оранжевый/жёлтый цвет */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
input[type=submit].force-ignore-validation:hover {
|
||||
background-color: #e67e22 !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
input[type=submit].force-ignore-validation:not(:hover) {
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
|
||||
|
||||
/* Блок красной кнопки подтверждения игнорирования валидации */
|
||||
.confirmation-button-container {
|
||||
display: block;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.confirmation-button-container button {
|
||||
padding: 10px 15px;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.confirmation-button-container button:hover {
|
||||
background: #c0392b;
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
.confirmation-button-container button:not(:hover) {
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
|
||||
.confirmation-button-container em {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Вотчер для отслеживания изменений полей формы и управления режимом обхода валидации.
|
||||
*
|
||||
* Используется для валидации с поддержкой игнорирования:
|
||||
* 1. Добавляет функцию addGetParam для динамического добавления GET параметров к form action
|
||||
* 2. Отслеживает изменения всех типов полей в форме:
|
||||
* - Стандартные: input (все типы кроме submit), textarea, select
|
||||
* - CodeMirror редакторы (div.codemirror)
|
||||
* - Редактируемое содержимое (contenteditable элементы)
|
||||
* 3. При изменении данных:
|
||||
* - Удаляет класс force-ignore-validation ко кнопок (возвращает нормальный цвет)
|
||||
* - Удаляет GET параметр из action кнопок
|
||||
* - Скрывает сообщения об ошибках валидации (.errornote, .errorlist)
|
||||
* 4. Визуально показывает пользователю статус обхода валидации цветом кнопок
|
||||
*
|
||||
* Универсальное решение: работает для любых форм в админке, не только для лейблов.
|
||||
*/
|
||||
|
||||
// Функция для добавления GET параметра к form action кнопки
|
||||
function addGetParam(button, key, value) {
|
||||
const form = button.form; // Находим форму, в которой лежит кнопка
|
||||
const baseAction = form.getAttribute('action'); // Получаем текущий action
|
||||
|
||||
// Удаляем старый параметр, если он есть
|
||||
let cleanAction = baseAction.split('?')[0];
|
||||
|
||||
// Проверяем, есть ли уже в action другие GET-параметры
|
||||
const separator = cleanAction === baseAction ? '?' : '&';
|
||||
|
||||
// Динамически прописываем измененный URL в formAction кнопки
|
||||
button.formAction = baseAction + separator + key + '=' + value;
|
||||
}
|
||||
|
||||
// Функция для добавления класса force-ignore-validation ко всем submit-кнопкам формы
|
||||
// Используется при клике на кнопку "Я проверил и уверен!"
|
||||
function markSubmitButtonsToIgnoreValidation() {
|
||||
// Находим все submit-кнопки на странице и добавляем им класс
|
||||
// form-field-watcher.js потом отследит добавление класса через MutationObserver
|
||||
// и добавит соответствующие onclick обработчики
|
||||
document.querySelectorAll('input[type=submit]').forEach(function (btn) {
|
||||
btn.classList.add('force-ignore-validation');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Находим все submit-кнопки администратора
|
||||
let submitButtons = document.querySelectorAll('input[type=submit]');
|
||||
|
||||
// Если нет submit-кнопок, выходим (не админская форма)
|
||||
if (submitButtons.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Находим форму
|
||||
let form = document.querySelector('form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем оригинальный action формы
|
||||
let originalAction = form.getAttribute('action');
|
||||
|
||||
// Функция для добавления onclick обработчика
|
||||
function addOnclickHandler(btn) {
|
||||
// Проверяем есть ли уже onclick
|
||||
if (!btn.getAttribute('onclick')) {
|
||||
btn.setAttribute('onclick', 'if (this.classList.contains("force-ignore-validation")) { ' +
|
||||
'const form = this.form; ' +
|
||||
'const baseAction = form.getAttribute("action") || ""; ' +
|
||||
'let cleanAction = baseAction.split("?")[0]; ' +
|
||||
'const separator = cleanAction === baseAction ? "?" : "&"; ' +
|
||||
'form.setAttribute("action", baseAction + separator + "ignore_validate=1"); ' +
|
||||
'}');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для удаления onclick обработчика
|
||||
function removeOnclickHandler(btn) {
|
||||
btn.removeAttribute('onclick');
|
||||
}
|
||||
|
||||
// Отслеживаем изменения всех типов полей в форме
|
||||
let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]');
|
||||
|
||||
// Функция которая срабатывает при любом изменении
|
||||
function handleChange() {
|
||||
// При изменении любого поля:
|
||||
// 1. Удаляем класс force-ignore-validation (кнопки вернут нормальный цвет)
|
||||
// 2. Удаляем onclick обработчик
|
||||
// 3. Восстанавливаем оригинальный action формы
|
||||
submitButtons.forEach(function (btn) {
|
||||
if (btn.classList.contains('force-ignore-validation')) {
|
||||
btn.classList.remove('force-ignore-validation');
|
||||
removeOnclickHandler(btn);
|
||||
}
|
||||
});
|
||||
form.setAttribute('action', originalAction);
|
||||
|
||||
// Скрываем сообщения об ошибках валидации
|
||||
let errorNotes = document.querySelectorAll('.errornote, .errorlist');
|
||||
errorNotes.forEach(function (errorElement) {
|
||||
errorElement.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
formInputs.forEach(function (input) {
|
||||
// Слушаем оба события: 'change' для обычных input/select
|
||||
// и 'input' для CodeMirror и других редакторов
|
||||
input.addEventListener('change', handleChange);
|
||||
input.addEventListener('input', handleChange);
|
||||
});
|
||||
|
||||
// Мониторим изменение класса force-ignore-validation на кнопках
|
||||
// Используем MutationObserver для отслеживания добавления/удаления класса
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
let btn = mutation.target;
|
||||
if (btn.tagName === 'INPUT' && btn.type === 'submit') {
|
||||
// Если класс добавлен - навешиваем onclick
|
||||
if (btn.classList.contains('force-ignore-validation')) {
|
||||
addOnclickHandler(btn);
|
||||
}
|
||||
// Если класс удален - удаляем onclick
|
||||
else if (btn.getAttribute('onclick')) {
|
||||
removeOnclickHandler(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Настраиваем observer для отслеживания изменения класса
|
||||
submitButtons.forEach(function (btn) {
|
||||
observer.observe(btn, {attributes: true, attributeFilter: ['class']});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
[tool.poetry]
|
||||
name = "lpon-site"
|
||||
version = "0.1.0"
|
||||
description = "LPON - platform for selling vinyl records (and related products)"
|
||||
authors = ["Sergei Erjemin <erjemin@gmail.com>"]
|
||||
readme = "README.md"
|
||||
packages = [
|
||||
{ include = "lpon_site" }
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
django = "^6.0"
|
||||
python-dotenv = "^1.0.0"
|
||||
django-environ = "^0.13.0"
|
||||
django-filer = "^3.4.4"
|
||||
pillow-heif = "^1.3.0"
|
||||
pytils = "^0.4.4"
|
||||
etpgrf = "^0.1.6.post1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
django-extensions = "^3.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
Reference in New Issue
Block a user