reports: список отчётов со статусом публикации

report_list_rows() строит строки хаба /reports/ (отчёт, отдел, период,
статус: опубликован/черновик/нет публикаций) — обновлён шаблон и тесты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 13:26:58 +03:00
parent 3812098970
commit f7f78f20fd
4 changed files with 85 additions and 14 deletions
+38
View File
@@ -119,6 +119,44 @@ def publish_it_monthly(user, year: int, month: int) -> ReportPublication:
return pub
def report_list_rows(user) -> list[dict]:
"""Строки списка на /reports/: отчёт, отдел, период, статус публикации."""
from apps.reports.registry import visible_reports
dept_labels = {'it': 'IT-отдел'}
can_manage = user_can_manage_it_monthly_report(user)
can_all = user_can_view_all_department_reports(user)
rows = []
for spec in visible_reports(user):
months = available_months_for_user(user, spec.code)
dept_id = None if can_all else getattr(user, 'department_id', None)
published = published_month_ids(spec.code, department_id=dept_id)
latest_published = published[0] if published else ''
latest_available = months[0] if months else ''
if latest_published:
status = 'published'
status_label = 'Опубликован'
elif can_manage:
status = 'draft'
status_label = 'Черновик'
else:
status = 'empty'
status_label = 'Нет публикаций'
rows.append({
'code': spec.code,
'title': spec.title,
'description': spec.description,
'path': spec.path,
'department': spec.department,
'department_label': dept_labels.get(spec.department, spec.department),
'latest_month': latest_published or latest_available,
'status': status,
'status_label': status_label,
'published_count': len(published),
})
return rows
def unpublish_it_monthly(user, year: int, month: int) -> None:
dept_id = getattr(user, 'department_id', None)
if not dept_id:
+2
View File
@@ -226,6 +226,8 @@ class MonthlyReportTests(TestCase):
self.assertEqual(hub.status_code, 200)
self.assertContains(hub, 'Месячный IT-отчёт')
self.assertContains(hub, '/reports/it/monthly/')
self.assertContains(hub, 'IT-отдел')
self.assertContains(hub, 'Открыть')
page = self.client.get('/reports/it/monthly/?month=2026-05')
self.assertEqual(page.status_code, 200)
+2
View File
@@ -24,6 +24,7 @@ from apps.reports.publication import (
available_months_for_user,
publication_for_user_month,
publish_it_monthly,
report_list_rows,
unpublish_it_monthly,
user_can_access_it_monthly_month,
)
@@ -82,6 +83,7 @@ def reports_hub_view(request):
specs = visible_reports(request.user)
return render_page(request, 'reports.html', {
'report_specs': specs,
'report_rows': report_list_rows(request.user),
'can_manage_templates': user_can_manage_templates(request.user),
})
+43 -14
View File
@@ -12,20 +12,49 @@
{% endif %}
</div>
{% if report_specs %}
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(16rem, 1fr)); gap:1rem;">
{% for spec in report_specs %}
<a href="{{ spec.path }}"
hx-get="{{ spec.path }}" hx-target="#main-content" hx-push-url="{{ spec.path }}" hx-indicator="#loader"
style="display:block; text-decoration:none; background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); padding:1.25rem; box-shadow:0 1px 2px rgb(0 0 0/0.05); color:inherit;"
class="tr-hover">
<div style="font-size:0.6875rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--color-muted-foreground); margin-bottom:0.5rem;">
{% if spec.department == 'it' %}IT{% else %}{{ spec.department }}{% endif %}
</div>
<h3 style="font-weight:600; color:var(--color-card-foreground); font-size:0.9375rem; margin:0 0 0.375rem;">{{ spec.title }}</h3>
<p style="font-size:0.8125rem; color:var(--color-muted-foreground); margin:0; line-height:1.4;">{{ spec.description }}</p>
</a>
{% endfor %}
{% if report_rows %}
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05); overflow:hidden;">
<table style="width:100%; border-collapse:collapse; font-size:0.8125rem;">
<thead>
<tr style="background:var(--color-muted);">
<th style="padding:0.625rem 1.25rem; text-align:left; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--color-muted-foreground);">{% trans "Отчёт" %}</th>
<th style="padding:0.625rem 1rem; text-align:left; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--color-muted-foreground);">{% trans "Отдел" %}</th>
<th style="padding:0.625rem 1rem; text-align:left; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--color-muted-foreground);">{% trans "Период" %}</th>
<th style="padding:0.625rem 1rem; text-align:left; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--color-muted-foreground);">{% trans "Статус" %}</th>
<th style="padding:0.625rem 1rem;"></th>
</tr>
</thead>
<tbody>
{% for row in report_rows %}
<tr class="tr-hover" style="border-top:1px solid var(--color-border);">
<td style="padding:0.875rem 1.25rem; vertical-align:top;">
<a href="{{ row.path }}"
hx-get="{{ row.path }}" hx-target="#main-content" hx-push-url="{{ row.path }}" hx-indicator="#loader"
style="font-weight:600; color:var(--color-foreground); text-decoration:none;">{{ row.title }}</a>
<div style="font-size:0.75rem; color:var(--color-muted-foreground); margin-top:0.25rem; line-height:1.4; max-width:36rem;">{{ row.description }}</div>
</td>
<td style="padding:0.875rem 1rem; color:var(--color-foreground); white-space:nowrap;">{{ row.department_label }}</td>
<td style="padding:0.875rem 1rem; color:var(--color-foreground); white-space:nowrap;">{{ row.latest_month|default:"—" }}</td>
<td style="padding:0.875rem 1rem; white-space:nowrap;">
{% if row.status == 'published' %}
<span class="badge badge-success">{{ row.status_label }}</span>
{% elif row.status == 'draft' %}
<span class="badge badge-muted">{{ row.status_label }}</span>
{% else %}
<span class="badge badge-muted">{{ row.status_label }}</span>
{% endif %}
</td>
<td style="padding:0.875rem 1.25rem; text-align:right; white-space:nowrap;">
<a href="{{ row.path }}"
hx-get="{{ row.path }}" hx-target="#main-content" hx-push-url="{{ row.path }}" hx-indicator="#loader"
class="btn btn-secondary" style="font-size:0.75rem; text-decoration:none; border:1px solid var(--color-border);">
{% trans "Открыть" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:2rem; text-align:center; color:var(--color-muted-foreground); font-size:0.875rem;">