add: сбор статистики и вывод агрегированных данных на главную (в футер)
All checks were successful
Build ETPGRF-site / build (push) Successful in 1m47s

This commit is contained in:
2026-01-21 01:22:59 +03:00
parent ea221dcfd2
commit 66f22285ef
7 changed files with 170 additions and 28 deletions

View File

@@ -1,3 +1,33 @@
from django.contrib import admin
from .models import DailyStat
# Register your models here.
@admin.register(DailyStat)
class DailyStatAdmin(admin.ModelAdmin):
list_display = (
'date',
'index_views',
'process_requests',
'copy_count',
'chars_in',
'chars_out',
'chars_copied',
'avg_processing_time_ms_formatted',
)
list_filter = ('date',)
search_fields = ('date',)
ordering = ('-date',)
# Делаем поля только для чтения
readonly_fields = [field.name for field in DailyStat._meta.fields]
def has_add_permission(self, request):
# Запрещаем добавлять записи вручную
return False
def has_delete_permission(self, request, obj=None):
# Запрещаем удалять записи
return False
@admin.display(description='Среднее время (мс)', ordering='total_processing_time_ms')
def avg_processing_time_ms_formatted(self, obj):
return f"{obj.avg_processing_time_ms:.2f}"

View File

@@ -62,10 +62,18 @@
</div>
{# Футер #}
<footer class="footer mt-auto py-3">
<div class="container">
<span class="text-muted small">&copy; Sergei Erjemin, 2025&ndash;{% now 'Y' %}.</span>
<span class="text-muted small float-end">v0.1.2</span>
<footer class="footer mt-auto py-2 mt-4">
<div class="container d-flex justify-content-between align-items-center">
<span class="text-muted small nowrap">&copy; Sergei Erjemin, 2025&ndash;{% now 'Y' %}.</span>
<span class="text-muted small nowrap"><i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i>v0.1.3 / v0.1.3</span>
{# Сводная статистика (HTMX) #}
<span class="text-muted small" hx-get="{% url 'stats_summary' %}" hx-trigger="load">
...
</span>
</div>
</footer>

View File

@@ -0,0 +1,12 @@
<span class="me-3" title="Просмотров">
<i class="bi bi-eye me-1"></i>{{ views }}
</span>
<span class="me-3 nowrap" title="На вход обработано текстов/символов">
<i class="bi bi-box-arrow-in-right me-1"></i>{{ processed }}/{{ chars_in }}
</span>
<span class="me-3" title="На выход получено символов">
<i class="bi bi-box-arrow-right me-1"></i>{{ chars_out }}
</span>
<span class="nowrap" title="Скопировано в буфер текстов/символов">
<i class="bi bi-clipboard-check me-1"></i>{{ copied }}/{{ chars_copied }}
</span>

View File

@@ -2,6 +2,8 @@ from django.urls import path
from . import views
urlpatterns = [
path(route='', view=views.index, name='index'),
path(route='process/', view=views.process_text, name='process_text'),
path('', views.index, name='index'),
path('process/', views.process_text, name='process_text'),
path('stats/summary/', views.get_stats_summary, name='stats_summary'),
path('stats/track-copy/', views.track_copy, name='track_copy'),
]

View File

@@ -1,19 +1,86 @@
from django.shortcuts import render
from django.http import HttpResponse
from django.http import HttpResponse, JsonResponse
from django.db.models import F, Sum
from django.utils import timezone
from django.views.decorators.http import require_POST
from etpgrf.typograph import Typographer
from etpgrf.layout import LayoutProcessor
from etpgrf.hyphenation import Hyphenator
from .models import DailyStat
def index(request):
# Увеличиваем счетчик просмотров главной
try:
today = timezone.now().date()
stat, created = DailyStat.objects.get_or_create(date=today)
DailyStat.objects.filter(pk=stat.pk).update(index_views=F('index_views') + 1)
except Exception as e:
print(f"Stat error: {e}")
return render(request, template_name='typograph/index.html')
def get_stats_summary(request):
"""Возвращает сводную статистику."""
# Убираем try...except для отладки
stats = DailyStat.objects.aggregate(
views=Sum('index_views'),
processed=Sum('process_requests'),
copied=Sum('copy_count'),
chars_in=Sum('chars_in'),
chars_out=Sum('chars_out'),
chars_copied=Sum('chars_copied')
)
# print("Aggregated stats:", stats) # DEBUG
# Функция для форматирования чисел с сокращениями (M, k)
def format_large_number(num):
if num > 1_000_000:
return f"{num / 1_000_000:.3f}M".replace(".", ",")
elif num > 1_000:
return f"{num / 1_000:.2f}k".replace(".", ",")
return str(num)
context = {
'views': f"{(stats['views'] or 0):,}".replace(",", "&thinsp;"),
'processed': f"{(stats['processed'] or 0):,}".replace(",", "&thinsp;"),
'copied': f"{(stats['copied'] or 0):,}".replace(",", "&thinsp;"),
'chars_in': format_large_number(stats['chars_in'] or 0),
'chars_out': format_large_number(stats['chars_out'] or 0),
'chars_copied': format_large_number(stats['chars_copied'] or 0),
}
return render(request, 'typograph/stats_summary.html', context)
@require_POST
def track_copy(request):
"""Увеличивает счетчик копирований и количество скопированных символов."""
try:
char_count = int(request.POST.get('char_count', 0))
today = timezone.now().date()
stat, created = DailyStat.objects.get_or_create(date=today)
DailyStat.objects.filter(pk=stat.pk).update(
copy_count=F('copy_count') + 1,
chars_copied=F('chars_copied') + char_count
)
return HttpResponse("OK")
except (ValueError, TypeError):
return HttpResponse("Invalid char_count", status=400)
except Exception as e:
print(f"Stat error: {e}")
return HttpResponse("Error", status=500)
def process_text(request):
if request.method == 'POST':
text = request.POST.get(key='text', default='')
# 1. Читаем базовые настройки
langs = request.POST.get(key='langs', default='ru')
# 2. Собираем LayoutProcessor
layout_enabled = request.POST.get(key='layout') == 'on'
layout_option = False
@@ -23,13 +90,13 @@ def process_text(request):
custom_units = request.POST.get(key='layout_units_custom', default='').strip()
if custom_units:
process_units = custom_units.split()
layout_option = LayoutProcessor(
langs=langs,
process_initials_and_acronyms=request.POST.get(key='layout_initials') == 'on',
process_units=process_units
)
# 3. Собираем Hyphenator
hyphenation_enabled = request.POST.get(key='hyphenation') == 'on'
hyphenation_option = False
@@ -39,7 +106,7 @@ def process_text(request):
max_len = int(max_len)
except (ValueError, TypeError):
max_len = 12
hyphenation_option = Hyphenator(
langs=langs,
max_unhyphenated_len=max_len
@@ -70,23 +137,40 @@ def process_text(request):
'mode': request.POST.get(key='mode', default='mixed'),
'sanitizer': sanitizer_option,
}
# --- ДИАГНОСТИКА ---
# print("Typographer options:", options)
# -------------------
# Создаем экземпляр типографа
typo = Typographer(**options)
# Обрабатываем текст
processed = typo.process(text)
# print("Processed text length:", len(processed))
# print("Processed text:", processed)
# --- СБОР СТАТИСТИКИ ---
try:
today = timezone.now().date()
stat, created = DailyStat.objects.get_or_create(date=today)
# Обновляем атомарные поля
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 мы пока не считаем, чтобы не усложнять
)
# JSON с настройками пока не пишем, чтобы не усложнять (как договаривались)
except Exception as e:
print(f"Stat error: {e}")
# -----------------------
return render(
request,
template_name='typograph/result_fragment.html',
request,
template_name='typograph/result_fragment.html',
context={'processed_text': processed}
)
return HttpResponse(status=405)