reports: список отчётов со статусом публикации
report_list_rows() строит строки хаба /reports/ (отчёт, отдел, период, статус: опубликован/черновик/нет публикаций) — обновлён шаблон и тесты. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
|
||||
@@ -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;">
|
||||
|
||||
Reference in New Issue
Block a user