Compare commits
3
Commits
73f68e9a2f
...
v0.2.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73b9ae4dd6 | ||
|
|
06a4ea6870 | ||
|
|
1cc070b7c0 |
@@ -5,6 +5,17 @@
|
|||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
|
||||||
и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/).
|
и этот проект придерживается [Semantic Versioning](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [0.2.8] - 2026-10-04
|
||||||
|
|
||||||
|
### Добавлено
|
||||||
|
- Локальная статика для внешних фронтенд-библиотек (Bootstrap 5, Bootstrap Icons, HTMX, Alpine.js, CodeMirror 6) для работы интерфейса при недоступности CDN.
|
||||||
|
- Стратегия fallback-подключения в шаблонах: автоматическое переключение на локальную статику при сбое или блокировке CDN.
|
||||||
|
- Скрипт копирования production-ассетов `frontend-assembly/copy-vendor.js` и обновление сборки через `frontend-assembly/assembly_via_npm.sh`.
|
||||||
|
- Автоматические тесты проверки наличия статических вендорных ассетов и корректности рендеринга fallback-подключений в `typograph/tests.py`.
|
||||||
|
|
||||||
|
### Изменено
|
||||||
|
- Обновлен и структурирован `.gitignore` с подробными комментариями.
|
||||||
|
|
||||||
## [0.2.7] - 2025-03-22
|
## [0.2.7] - 2025-03-22
|
||||||
### Изменено
|
### Изменено
|
||||||
- Обновление всех пактов, включая `etpgrf` (v0.1.5 → v0.1.6.post1).
|
- Обновление всех пактов, включая `etpgrf` (v0.1.5 → v0.1.6.post1).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Основные возможности:
|
Основные возможности:
|
||||||
- Веб-интерфейс для ввода текста и настройки параметров типографики.
|
- Веб-интерфейс для ввода текста и настройки параметров типографики.
|
||||||
"""
|
"""
|
||||||
__version__ = "0.2.7"
|
__version__ = "0.2.8"
|
||||||
__author__ = "Sergei Erjemin"
|
__author__ = "Sergei Erjemin"
|
||||||
__email__ = "erjemin@gmail.com"
|
__email__ = "erjemin@gmail.com"
|
||||||
__license__ = "MIT"
|
__license__ = "MIT"
|
||||||
|
|||||||
@@ -32,145 +32,21 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="post-content mt-4">
|
<div class="post-content mt-4">
|
||||||
|
|
||||||
<h2>Что это такое?</h2>
|
<h4>[0.1.6.post1] - 2026–03–19</h4>
|
||||||
<p><strong>Management Command</strong> — это обычный Python-скрипт, который «встраивается» в экосистему Django. Он имеет доступ к моделям, настройкам (settings.py) и базе данных, но запускается из консоли через <code>python manage.py ...</code>.</p>
|
<h5>Изменено</h5>
|
||||||
<h2>Зачем это нужно?</h2>
|
|
||||||
<p>Представьте ситуации:</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Нужно пересчитать рейтинг для десятки тысяч товаров на сайте или сделать типографику всех публикаций после подключения etpgrf-типографа (или любого другого).</li>
|
<li>Новый алгоритм «висячей пунктуации» (<em>HangingPunctuationProcessor</em>). Добавлены компенсирующие пробелы для висячих символов, чтобы избежать наложения на соседние слова. Теперь «висячие символы» (кавычки, скобки и т. п.) оборачиваются в <code><span></code> вместе с ближайшим словом и пробелом, что обеспечивает отсутствие визуальных смещений внутри строки. Режим <code>both</code> (для одновременного вывешивания в обе стороны) отключен из-за потенциальных конфликтов компенсирующих пробелов и проблем с выравниванием при использовании CSS <code>text-justify</code>.</span></li>
|
||||||
<li>Необходимо обновить цены для всех товарных карточек в соответствии с таблицей excel или по API (или спарсить данные с внешнего сайта).</li>
|
</ul>
|
||||||
<li>Нужно отправить e-mail рассылку пользо­вателям.</li>
|
<h4>Исправлено</h4>
|
||||||
<li>Нужно пометить комментарии или публикации как архивные в соответствии с правилом или почистить базу от старых логов.</li>
|
<ul>
|
||||||
|
<li>Автоматическая замена символов <code>x</code>, <code>X</code>, <code>х</code>, <code>Х</code>, стоящих между числами, на знак умножения <code>×</code>, чтобы выражения вида <code>100x100</code> или <code>100 х 100` корректно обрабатывались и выглядели типографски правильными (<code>100×100</code> или <code>100&nbsp;×&nbsp;100</code>).</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Делать это через <tt>views.py</tt> (views) — плохая идея (страница может отвалиться по тайм-ауту). Писать отделный скрипт рядом <tt>manage.py</tt> — неудобно (нужно вручную настраивать `DJANGO_SETTINGS_MODULE`). Встроенные команды решают эти проблемы элегантно.</p>
|
|
||||||
<h2>Как это работает?</h2>
|
|
||||||
<p>Django использует систему «автообна­ружения» (auto-discovery). Чтобы ваша команда появилась в списке `manage.py`, нужно соблюсти строгую иерархию папок внутри вашего приложения (например, <tt>web</tt>):</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">web/
|
|
||||||
├── __init__.py
|
|
||||||
├── models.py
|
|
||||||
└── management/ <-- 1. Создаем папку management
|
|
||||||
├── __init__.py
|
|
||||||
└── nginx/
|
|
||||||
├── commands/ <-- 2. Внутри неё папку commands
|
|
||||||
├── __init__.py
|
|
||||||
└── my_cool_script.py <-- 3. Наш файл с командой</pre>
|
|
||||||
<p>Файл <tt>my_cool_script.py</tt> автома­тически превратится в команду: <code>python manage.py my_cool_script</code>. Django будет искать его в папках <tt>management/commands/</tt> внутри каждого устано­вленного приложения. Это позволяет легко организовать код и держать все «команды обслуживания» в одном месте.</p>
|
|
||||||
<p><strong>Для справки:</strong> Механизм Custom Management Commands появился еще в Django 0.96 (в глубокой древности) и с тех пор является стандартом де-факто для написания скриптов обслуживания.</p>
|
|
||||||
<h2>Анатомия команды</h2>
|
|
||||||
<p>Вот пример простейшей команды. Мы наследуемся от класса <code>BaseCommand</code> и переопре­деляем метод <code>handle</code>:</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
|
||||||
# web/management/commands/hello.py
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
## [0.1.6] - 2024-03-19
|
||||||
help = 'Выводит приветствие' # Описание для --help
|
### Изменено
|
||||||
|
- Н
|
||||||
def add_arguments(self, parser):
|
###
|
||||||
# Можно добавлять аргументы, как в argparse
|
-
|
||||||
parser.add_argument('name', type=str, help='Имя пользователя')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
# Главная логика
|
|
||||||
name = options['name']
|
|
||||||
self.stdout.write(f"Привет, {name}!")</pre>
|
|
||||||
<p>Запуск:</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
|
||||||
python manage.py hello Иван</pre>
|
|
||||||
<p>Увидим в консоли:</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
|
||||||
Привет, Иван!</pre>
|
|
||||||
<h2>Реальный пример: Массовая типографика через etpgrf</h2>
|
|
||||||
<p>В моём проекте, <a href="https://dq.cube2.ru/" target="_blank">DQ – коллекция цитат: место для вдумчивого чтения</a>, контент был частично обработан с помощью Типографа Муравьёа, частично вручную. И вот я установил и настроил etpgrf (тем более что Типограф Муравьёва «почил в бозе», да и до того не обновлялся с 2018 года). Все новые цитаты типогра­фируются через etpgrf, но что делать со старыми? Их сотни, и открывать и «пересо­хранаять» каждую вручную — это адский труд. Вот тут на помощь и приходит Custom Management Command.</p>
|
|
||||||
<p>Вот как я это реализовал. В <tt>web/management/commands/reprocess_typography.py</tt>:</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from web.models import TbDictumAndQuotes
|
|
||||||
# Импорты библиотеки etpgrf
|
|
||||||
from etpgrf.typograph import Typographer
|
|
||||||
from etpgrf.sanitizer import SanitizerProcessor
|
|
||||||
# ... другие процессоры, если нужно ...
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Переобрабатывает все цитаты'
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
# Добавляем флаг "сухой прогон"
|
|
||||||
parser.add_argument(
|
|
||||||
'--dry-run',
|
|
||||||
action='store_true',
|
|
||||||
help='Запустить без сохранения изменений',
|
|
||||||
)
|
|
||||||
# Ограничение количества
|
|
||||||
parser.add_argument('--limit', type=int, help='Сколько записей обработать')
|
|
||||||
# Смещение (для пагинации)
|
|
||||||
parser.add_argument('--offset', type=int, default=0, help='Сколько пропустить')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
# 1. Настраиваем типограф
|
|
||||||
# ... инициализация процессоров ...
|
|
||||||
settings = {
|
|
||||||
# ...
|
|
||||||
'sanitizer': SanitizerProcessor(mode="html"), # Режим HTML-очистки
|
|
||||||
'hanging_punctuation': 'left', # Режим висячей пунктуации
|
|
||||||
# ...
|
|
||||||
}
|
|
||||||
typographer = Typographer(**settings)
|
|
||||||
|
|
||||||
# 2. Получаем записи из базы с учетом limit/offset
|
|
||||||
qs = TbDictumAndQuotes.objects.all().order_by('id')
|
|
||||||
|
|
||||||
start = options['offset']
|
|
||||||
if options['limit']:
|
|
||||||
qs = qs[start : start + options['limit']]
|
|
||||||
else:
|
|
||||||
qs = qs[start:]
|
|
||||||
|
|
||||||
self.stdout.write(f"Найдено {qs.count()} цитат...")
|
|
||||||
|
|
||||||
# 3. Бежим по циклу с прогресс-баром (tqdm если есть)
|
|
||||||
try:
|
|
||||||
from tqdm import tqdm
|
|
||||||
iterator = tqdm(qs)
|
|
||||||
except ImportError:
|
|
||||||
iterator = qs
|
|
||||||
|
|
||||||
for dq in iterator:
|
|
||||||
try:
|
|
||||||
# Обрабатываем текст
|
|
||||||
new_html = typographer.process(dq.szContent)
|
|
||||||
|
|
||||||
if options['dry_run']:
|
|
||||||
self.stdout.write(f"[{dq.id}] Предпросмотр: {new_html[:50]}...")
|
|
||||||
else:
|
|
||||||
dq.szContentHTML = new_html
|
|
||||||
dq.save(update_fields=['szContentHTML'])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.stdout.write(self.style.ERROR(f"Ошибка id={dq.id}: {e}"))
|
|
||||||
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"\nГотово!"))</pre>
|
|
||||||
<p>Возможно вы заметили, что в моем проекте для контента есть два поля: <code>szContent</code> и <code>szContentHTML</code>. В первом хранится «сырой» текст, во втором — результат типогра­фирования. В вашем проекте, скорее всего, только одно поле для контента, но по сути это ничего не меняет.</p>
|
|
||||||
<p>Еще интересные фишки, которые исполь­зовались при типогра­фировании:</p>
|
|
||||||
<ol>
|
|
||||||
<li><code>self.stdout.write</code> вместо <code>print</code> – позволяет Django перехва­тывать вывод (например, для тестов) и корректно работать с кодировками.</li>
|
|
||||||
<li><code>self.style.SUCCESS</code> и <code>self.style.ERROR</code> – раскрашивает текст в консоли (зеленый/красный) и это очень удобно для визуального восприятия логов.</li>
|
|
||||||
<li>Аргумент <tt>--dry-run</tt> — позволяют безопасно тестировать скрипт на продакшене перед тем, как реально менять данные.</li>
|
|
||||||
<li>Аргументы <tt>--limit</tt> и <tt>--offset</tt> — позволяют обрабатывать базу «порциями», что полезно для больших объемов данных (можно запустить несколько процессов параллельно с разными <tt>offset</tt>).</li>
|
|
||||||
<li>Используется <code>update_fields</code> — позволяет переза­писывать не всю модель целиком (что могло бы затереть изменения, сделанные кем-то другим в ту же секунду), а обновляем только конкретные поля.</li>
|
|
||||||
</ol>
|
|
||||||
<p>Теперь, чтобы типогра­фировать весь контент, нам достаточно одной строку в терминале, и Django сделает всю грязную работу за нас (не забудьте инициировать виртуальное окружение вашего проекта).</p>
|
|
||||||
<p>Тестовый прогон (безопасно, ничего не сохраняет):</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
|
||||||
python manage.py reprocess_typography --dry-run</pre>
|
|
||||||
<p>Боевой запуск, с изменениями данные в базе:</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
|
||||||
python manage.py reprocess_typography</pre>
|
|
||||||
<p>Если у вас очень много записей, можно запускать по частям:</p>
|
|
||||||
<pre class="border p-3 my-3 rounded bg-secondary bg-opacity-25">
|
|
||||||
python manage.py reprocess_typography --limit 1000 --offset 0
|
|
||||||
python manage.py reprocess_typography --limit 1000 --offset 1000
|
|
||||||
python manage.py reprocess_typography --limit 1000 --offset 2000</pre>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
{# Favicons #}<link rel="apple-touch-icon" sizes="180x180" href="{% static 'apple-touch-icon.png' %}" />
|
{# Favicons #}<link rel="apple-touch-icon" sizes="180x180" href="{% static 'apple-touch-icon.png' %}" />
|
||||||
{# Favicons #}<link rel="manifest" href="{% static 'site.webmanifest' %}" />
|
{# Favicons #}<link rel="manifest" href="{% static 'site.webmanifest' %}" />
|
||||||
{# Для ИИ #}<link rel="help" type="text/markdown" href="/llms.txt"/>
|
{# Для ИИ #}<link rel="help" type="text/markdown" href="/llms.txt"/>
|
||||||
{# Bootstrap 5 CSS #}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"/>
|
{# Bootstrap 5 CSS #}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" onerror="this.onerror=null;this.href='{% static 'vendor/bootstrap/bootstrap.min.css' %}';"/>
|
||||||
{# Bootstrap Icons #}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"/>
|
{# Bootstrap Icons #}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" onerror="this.onerror=null;this.href='{% static 'vendor/bootstrap-icons/bootstrap-icons.min.css' %}';"/>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg-image-text: url("{% static 'svg/logo-etpgrf-site-light-txt.svg' %}");
|
--bg-image-text: url("{% static 'svg/logo-etpgrf-site-light-txt.svg' %}");
|
||||||
@@ -46,8 +46,18 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{# Custom CSS #}<link href="{% static 'css/etpgrf.css' %}" rel="stylesheet"/>
|
{# Custom CSS #}<link href="{% static 'css/etpgrf.css' %}" rel="stylesheet"/>
|
||||||
{# HTMX #}<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
{# HTMX #}<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||||
{# Alpine.js #}<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script>window.htmx || document.write('<script src="{% static "vendor/htmx/htmx.min.js" %}"><\/script>')</script>
|
||||||
|
{# Alpine.js #}<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js" onerror="this.onerror=null;var s=document.createElement('script');s.defer=true;s.src='{% static 'vendor/alpinejs/cdn.min.js' %}';document.head.appendChild(s);"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (!window.Alpine) {
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.src = "{% static 'vendor/alpinejs/cdn.min.js' %}";
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -84,7 +94,7 @@
|
|||||||
<div class="container d-flex justify-content-between align-items-center">
|
<div class="container d-flex justify-content-between align-items-center">
|
||||||
<span class="text-muted small nowrap me-2">© Sergei Erjemin, 2025–{% now 'Y' %}.</span>
|
<span class="text-muted small nowrap me-2">© Sergei Erjemin, 2025–{% now 'Y' %}.</span>
|
||||||
<nobr class="text-muted small mx-2">
|
<nobr class="text-muted small mx-2">
|
||||||
<i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i><a href="/changelog">v0.1.6.post1 / v0.2.7</a>
|
<i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i><a href="/changelog">v0.1.6.post1 / v0.2.8</a>
|
||||||
</nobr>
|
</nobr>
|
||||||
{# Сводная статистика (HTMX) #}<span class="text-muted small ms-2" hx-get="{% url 'stats_summary' %}" hx-trigger="load">
|
{# Сводная статистика (HTMX) #}<span class="text-muted small ms-2" hx-get="{% url 'stats_summary' %}" hx-trigger="load">
|
||||||
...
|
...
|
||||||
@@ -106,7 +116,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Bootstrap JS #}<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
{# Bootstrap JS #}<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>window.bootstrap || document.write('<script src="{% static "vendor/bootstrap/bootstrap.bundle.min.js" %}"><\/script>')</script>
|
||||||
{# Custom JS #}<script src="{% static 'js/base.js' %}" defer></script>
|
{# Custom JS #}<script src="{% static 'js/base.js' %}" defer></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block title %}ETPGRF — единая типографика для веба{% endblock %}
|
{% block title %}ETPGRF — единая типографика для веба{% endblock %}
|
||||||
|
|
||||||
{% block description %}Бесплатный онлайн-типограф для подготовки текстов к публикации в вебе. Расставка неразрывных пробелов, правильных кавычек («ёлочки»), тире, спецсимволы, отбивка, компоновка, висячая пунктуация. Идеально для верстки сайтов, статей и постов.{% endblock %}
|
{% block description %}Бесплатный онлайн-типограф для подготовки публикаций в вебе: кавычки «ёлочки», тире, неразрывные пробелы, спецсимволы, отбивка/компоновка, висячая пунктуация… Идеально для верстки сайтов, статей и постов.{% endblock %}
|
||||||
|
|
||||||
{% block keywords %}типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев{% endblock %}
|
{% block keywords %}типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from django.test import TestCase, Client, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(SECURE_SSL_REDIRECT=False)
|
||||||
|
class VendorAssetsAndFallbackTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Тестирование наличия вендорных ассетов и корректности подключения fallback в шаблоне base.html.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def test_vendor_static_files_exist(self):
|
||||||
|
"""Проверяем, что все скомпилированные/скопированные локальные файлы присутствуют в статике."""
|
||||||
|
static_dir = settings.BASE_DIR.parent / 'public' / 'static'
|
||||||
|
|
||||||
|
required_files = [
|
||||||
|
'vendor/bootstrap/bootstrap.min.css',
|
||||||
|
'vendor/bootstrap/bootstrap.bundle.min.js',
|
||||||
|
'vendor/bootstrap-icons/bootstrap-icons.min.css',
|
||||||
|
'vendor/bootstrap-icons/fonts/bootstrap-icons.woff2',
|
||||||
|
'vendor/bootstrap-icons/fonts/bootstrap-icons.woff',
|
||||||
|
'vendor/htmx/htmx.min.js',
|
||||||
|
'vendor/alpinejs/cdn.min.js',
|
||||||
|
'codemirror/editor.js',
|
||||||
|
]
|
||||||
|
|
||||||
|
for rel_path in required_files:
|
||||||
|
file_path = static_dir / rel_path
|
||||||
|
self.assertTrue(
|
||||||
|
file_path.exists(),
|
||||||
|
f"Файл вендора не найден в статике: {rel_path} (ожидался по пути {file_path})"
|
||||||
|
)
|
||||||
|
self.assertGreater(
|
||||||
|
file_path.stat().st_size,
|
||||||
|
0,
|
||||||
|
f"Файл вендора пуст: {rel_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_base_template_fallback_tags(self):
|
||||||
|
"""Проверяем, что главная страница отдает теги с fallback-обработкой ошибок загрузки с CDN."""
|
||||||
|
response = self.client.get(reverse('index'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
content = response.content.decode('utf-8')
|
||||||
|
|
||||||
|
# Проверка fallback для Bootstrap CSS
|
||||||
|
self.assertIn('bootstrap.min.css', content)
|
||||||
|
self.assertIn("vendor/bootstrap/bootstrap.min.css", content)
|
||||||
|
|
||||||
|
# Проверка fallback для Bootstrap Icons
|
||||||
|
self.assertIn('bootstrap-icons.min.css', content)
|
||||||
|
self.assertIn("vendor/bootstrap-icons/bootstrap-icons.min.css", content)
|
||||||
|
|
||||||
|
# Проверка fallback для HTMX
|
||||||
|
self.assertIn('htmx.org', content)
|
||||||
|
self.assertIn('window.htmx || document.write', content)
|
||||||
|
self.assertIn('vendor/htmx/htmx.min.js', content)
|
||||||
|
|
||||||
|
# Проверка fallback для Alpine.js
|
||||||
|
self.assertIn('alpinejs', content)
|
||||||
|
self.assertIn('vendor/alpinejs/cdn.min.js', content)
|
||||||
|
|
||||||
|
# Проверка fallback для Bootstrap JS
|
||||||
|
self.assertIn('bootstrap.bundle.min.js', content)
|
||||||
|
self.assertIn('window.bootstrap || document.write', content)
|
||||||
|
self.assertIn('vendor/bootstrap/bootstrap.bundle.min.js', content)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(SECURE_SSL_REDIRECT=False)
|
||||||
|
class TypographViewsTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Базовые тесты функциональности представлений типографа.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def test_process_text_endpoint(self):
|
||||||
|
"""Проверка эндпоинта типографирования текста."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('process_text'),
|
||||||
|
data={
|
||||||
|
'text': 'Тест "кавычек" и предлога в тексте.',
|
||||||
|
'langs': 'ru',
|
||||||
|
'quotes': 'on',
|
||||||
|
'unbreakables': 'on',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('X-Processing-Time', response.headers)
|
||||||
|
content = response.content.decode('utf-8')
|
||||||
|
# Проверяем типографирование кавычек «ёлочки»
|
||||||
|
self.assertIn('«кавычек»', content)
|
||||||
|
|
||||||
|
def test_stats_summary_endpoint(self):
|
||||||
|
"""Проверка эндпоинта сводной статистики."""
|
||||||
|
response = self.client.get(reverse('stats_summary'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
Regular → Executable
+16
-16
@@ -1,24 +1,24 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
# Необходимо предварительно установить NPM и esbuild:
|
# Переходим в директорию скрипта
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Необходимо предварительно установить Node.js и npm:
|
||||||
# Для Ubuntu/Debian:
|
# Для Ubuntu/Debian:
|
||||||
# sudo apt install npm
|
# sudo apt install nodejs npm
|
||||||
# npm install -g esbuild
|
|
||||||
# Для MacOS (через Homebrew):
|
# Для MacOS (через Homebrew):
|
||||||
# brew install npm
|
# brew install node
|
||||||
# npm install -g esbuild
|
|
||||||
# Для Windows:
|
# Для Windows:
|
||||||
# Скачайте и установите Node.js с официального сайта: https://nodejs.org/
|
# Скачайте и установите Node.js с официального сайта: https://nodejs.org/
|
||||||
# Затем установите esbuild глобально:
|
|
||||||
# npm install -g esbuild
|
|
||||||
|
|
||||||
|
|
||||||
# Устанавливаем CodeMirror и необходимые пакеты
|
# Устанавливаем зависимости
|
||||||
npm install \ [±main ●●]
|
npm install
|
||||||
@codemirror/lang-html \
|
|
||||||
@codemirror/theme-one-dark \
|
|
||||||
@codemirror/commands \
|
|
||||||
@codemirror/language
|
|
||||||
|
|
||||||
# Собираем js в каталог ./public/static/codemirror/editor.js с помощью esbuild
|
# Собираем всё (CodeMirror и копирование вендорных библиотек в public/static/vendor/)
|
||||||
npx esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js
|
npm run build
|
||||||
|
|
||||||
|
# Подчищаем за собой node_modules
|
||||||
|
echo "Очистка node_modules..."
|
||||||
|
rm -rf node_modules
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Целевая папка для статики вендоров
|
||||||
|
const targetDir = path.resolve(__dirname, '../public/static/vendor');
|
||||||
|
|
||||||
|
// Список файлов и директорий для копирования
|
||||||
|
const itemsToCopy = [
|
||||||
|
// Bootstrap 5 (CSS & JS bundle)
|
||||||
|
{
|
||||||
|
src: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
|
||||||
|
dest: 'bootstrap/bootstrap.min.css'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'node_modules/bootstrap/dist/css/bootstrap.min.css.map',
|
||||||
|
dest: 'bootstrap/bootstrap.min.css.map'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js',
|
||||||
|
dest: 'bootstrap/bootstrap.bundle.min.js'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map',
|
||||||
|
dest: 'bootstrap/bootstrap.bundle.min.js.map'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bootstrap Icons (CSS & WOFF/WOFF2 шрифты)
|
||||||
|
{
|
||||||
|
src: 'node_modules/bootstrap-icons/font/bootstrap-icons.min.css',
|
||||||
|
dest: 'bootstrap-icons/bootstrap-icons.min.css'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2',
|
||||||
|
dest: 'bootstrap-icons/fonts/bootstrap-icons.woff2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff',
|
||||||
|
dest: 'bootstrap-icons/fonts/bootstrap-icons.woff'
|
||||||
|
},
|
||||||
|
|
||||||
|
// HTMX
|
||||||
|
{
|
||||||
|
src: 'node_modules/htmx.org/dist/htmx.min.js',
|
||||||
|
dest: 'htmx/htmx.min.js'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Alpine.js
|
||||||
|
{
|
||||||
|
src: 'node_modules/alpinejs/dist/cdn.min.js',
|
||||||
|
dest: 'alpinejs/cdn.min.js'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('Сборка и копирование вендорных библиотек в static/vendor/...');
|
||||||
|
|
||||||
|
let copiedCount = 0;
|
||||||
|
|
||||||
|
for (const item of itemsToCopy) {
|
||||||
|
const srcPath = path.resolve(__dirname, item.src);
|
||||||
|
const destPath = path.join(targetDir, item.dest);
|
||||||
|
|
||||||
|
if (!fs.existsSync(srcPath)) {
|
||||||
|
console.warn(`Предупреждение: исходный файл не найден: ${item.src}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем папку назначения, если не существует
|
||||||
|
const destDirPath = path.dirname(destPath);
|
||||||
|
if (!fs.existsSync(destDirPath)) {
|
||||||
|
fs.mkdirSync(destDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
console.log(` Скопировано: ${item.src} -> ${path.relative(path.resolve(__dirname, '..'), destPath)}`);
|
||||||
|
copiedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Успешно скопировано файлов: ${copiedCount}`);
|
||||||
Generated
+83
-3
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "tmp-frontend",
|
"name": "frontend-assembly",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tmp-frontend",
|
"name": "frontend-assembly",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -13,7 +13,11 @@
|
|||||||
"@codemirror/lang-html": "^6.4.11",
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
"@codemirror/language": "^6.12.1",
|
"@codemirror/language": "^6.12.1",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"codemirror": "^6.0.2"
|
"alpinejs": "^3.14.8",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"htmx.org": "^1.9.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.27.2"
|
"esbuild": "^0.27.2"
|
||||||
@@ -662,6 +666,76 @@
|
|||||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/alpinejs": {
|
||||||
|
"version": "3.14.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.8.tgz",
|
||||||
|
"integrity": "sha512-wT2fuP2DXpGk/jKaglwy7S/IJpm1FD+b7U6zUrhwErjoq5h27S4dxkJEXVvhbdwyPv9U+3OkUuNLkZT4h2Kfrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "~3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bootstrap-icons": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/codemirror": {
|
"node_modules/codemirror": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||||
@@ -725,6 +799,12 @@
|
|||||||
"@esbuild/win32-x64": "0.27.2"
|
"@esbuild/win32-x64": "0.27.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/htmx.org": {
|
||||||
|
"version": "1.9.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz",
|
||||||
|
"integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/style-mod": {
|
"node_modules/style-mod": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
"description": "CodeMirror build for ETPGRF",
|
"description": "CodeMirror build for ETPGRF",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "esbuild src/editor.js --bundle --format=esm --outfile=dist/editor.js --minify",
|
"build:codemirror": "esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js --minify",
|
||||||
"dev": "esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js --minify",
|
"build:vendor": "node copy-vendor.js",
|
||||||
|
"build": "npm run build:codemirror && npm run build:vendor",
|
||||||
|
"dev": "npm run build",
|
||||||
"watch": "esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js --watch"
|
"watch": "esbuild src/editor.js --bundle --format=esm --outfile=../public/static/codemirror/editor.js --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -17,7 +19,11 @@
|
|||||||
"@codemirror/lang-html": "^6.4.11",
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
"@codemirror/language": "^6.12.1",
|
"@codemirror/language": "^6.12.1",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"codemirror": "^6.0.2"
|
"alpinejs": "^3.14.8",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"htmx.org": "^1.9.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.27.2"
|
"esbuild": "^0.27.2"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "2026-etpgrf-site"
|
name = "2026-etpgrf-site"
|
||||||
version = "0.2.7"
|
version = "0.2.8"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["erjemin <erjemin@gmail.com>"]
|
authors = ["erjemin <erjemin@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
Reference in New Issue
Block a user