From 2d09aef79d1184cd1e488b95a0f8c5633c410595 Mon Sep 17 00:00:00 2001 From: erjemin Date: Thu, 22 Jan 2026 00:26:58 +0300 Subject: [PATCH] =?UTF-8?q?add:=20=D1=81=D0=BE=D0=B1=D0=B8=D1=80=D0=B0?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B0=D0=B3=D1=80=D0=B5=D0=B3=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D1=83=D1=8E=20=D0=B8=D0=BD=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=BE=20=D0=BD?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=D0=BC=20=D1=82?= =?UTF-8?q?=D0=B8=D0=BF=D0=BE=D0=B3=D1=80=D0=B0=D1=84=D0=B0,=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=BE=D1=80=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D1=83=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D0=B8=D0=BC=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D0=BE=D1=80=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D0=BD=D0=B0=20=D1=84?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D0=B5=D1=82.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../typograph/templates/typograph/index.html | 9 ++- etpgrf_site/typograph/views.py | 55 +++++++++++++++---- public/static/js/index.js | 15 +++++ 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/etpgrf_site/typograph/templates/typograph/index.html b/etpgrf_site/typograph/templates/typograph/index.html index 9403978..9eeb1ea 100644 --- a/etpgrf_site/typograph/templates/typograph/index.html +++ b/etpgrf_site/typograph/templates/typograph/index.html @@ -274,9 +274,12 @@ - +
+ + +
diff --git a/etpgrf_site/typograph/views.py b/etpgrf_site/typograph/views.py index 9441273..fd2cc28 100644 --- a/etpgrf_site/typograph/views.py +++ b/etpgrf_site/typograph/views.py @@ -7,6 +7,7 @@ from etpgrf.typograph import Typographer from etpgrf.layout import LayoutProcessor from etpgrf.hyphenation import Hyphenator from .models import DailyStat +import time def index(request): @@ -131,39 +132,71 @@ def process_text(request): 'sanitizer': sanitizer_option, } - # --- ДИАГНОСТИКА --- - # print("Typographer options:", options) - # ------------------- - - # Создаем экземпляр типографа + # Обрабатываем текст с замером времени + start_time = time.perf_counter() + # Создаем экземпляр типографа и передаем настройки в него typo = Typographer(**options) - - # Обрабатываем текст + # Обрабатываем текст в Типографе processed = typo.process(text) + end_time = time.perf_counter() + + duration_ms = (end_time - start_time) * 1000 # --- СБОР СТАТИСТИКИ --- try: today = timezone.now().date() stat, created = DailyStat.objects.get_or_create(date=today) - # Обновляем атомарные поля + # 1. Атомарное обновление счетчиков DailyStat.objects.filter(pk=stat.pk).update( process_requests=F('process_requests') + 1, chars_in=F('chars_in') + len(text), chars_out=F('chars_out') + len(processed), - # total_processing_time_ms мы пока не считаем, чтобы не усложнять + total_processing_time_ms=F('total_processing_time_ms') + duration_ms ) - # JSON с настройками пока не пишем, чтобы не усложнять (как договаривались) + # 2. Обновление JSON с настройками (не атомарно, а значит при высокой нагрузке возможны потери данных) + # Перечитываем объект, чтобы получить актуальный JSON + stat.refresh_from_db() + current_stats = stat.settings_stats + + def inc_stat(key, value): + k = f"{key}:{value}" + current_stats[k] = current_stats.get(k, 0) + 1 + + # Собираем статистику по опциям + # langs может быть строкой или списком + lang_val = options['langs'] + if isinstance(lang_val, list): + lang_val = lang_val[0] if lang_val else 'ru' + inc_stat('lang', lang_val) + + inc_stat('mode', options['mode']) + + if options['quotes']: inc_stat('feature', 'quotes') + if layout_enabled: inc_stat('feature', 'layout') + if options['unbreakables']: inc_stat('feature', 'unbreakables') + if hyphenation_enabled: inc_stat('feature', 'hyphenation') + if options['symbols']: inc_stat('feature', 'symbols') + if hanging_enabled: inc_stat('feature', 'hanging') + if sanitizer_enabled: inc_stat('feature', 'sanitizer') + + stat.settings_stats = current_stats + stat.save(update_fields=['settings_stats']) except Exception as e: print(f"Stat error: {e}") # ----------------------- - return render( + response = render( request, template_name='typograph/result_fragment.html', context={'processed_text': processed} ) + + # Добавляем заголовок с временем обработки (с запятой вместо точки) + response['X-Processing-Time'] = f"{duration_ms:.4f}".replace('.', ',') + + return response return HttpResponse(status=405) diff --git a/public/static/js/index.js b/public/static/js/index.js index 61d279e..2f3a068 100644 --- a/public/static/js/index.js +++ b/public/static/js/index.js @@ -21,6 +21,7 @@ import { const resultWrapper = document.getElementById('cm-result-wrapper'); const btnCopy = document.getElementById('btn-copy'); const sourceTextarea = document.querySelector('textarea[name="text"]'); +const processingTimeSpan = document.getElementById('processing-time'); const themeCompartment = new Compartment(); function getTheme() { @@ -100,6 +101,17 @@ document.body.addEventListener('htmx:afterSwap', function (evt) { if (btnCopy) { btnCopy.classList.remove('d-none'); } + + // Показываем время обработки из заголовка + if (processingTimeSpan) { + const time = evt.detail.xhr.getResponseHeader('X-Processing-Time'); + if (time) { + processingTimeSpan.innerHTML = `${time} ms`; + processingTimeSpan.style.display = 'inline'; + } else { + processingTimeSpan.style.display = 'none'; + } + } } }); @@ -109,6 +121,9 @@ if (sourceTextarea) { if (btnCopy) { btnCopy.classList.add('d-none'); } + if (processingTimeSpan) { + processingTimeSpan.innerText = ''; + } // Сбрасываем редактор на плейсхолдер if (resultView.state.doc.toString() !== PLACEHOLDER_TEXT) { resultView.dispatch({