Compare commits
92 Commits
v0.1.0
...
3928066147
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,811 @@
|
||||
# Кастомная конфигурация 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 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',
|
||||
)
|
||||
|
||||
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.
|
||||
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