Move export and upload actions to admin dashboard
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 2m8s
CI/CD Pipeline / Run Tests (push) Successful in 2m52s
CI/CD Pipeline / Run API Inventory E2E Tests (push) Successful in 44s
CI/CD Pipeline / Telegram Notify Success (push) Has been skipped

This commit is contained in:
2026-04-22 10:12:08 +02:00
parent 898e492538
commit 92d5ff4252
7 changed files with 155 additions and 37 deletions

View File

@@ -1,7 +1,14 @@
"""Admin для приложения backups."""
from apps.backups.models import BackupExportJob
from apps.backups.serializers import BackupExportRequestSerializer
from apps.backups.services import BackupExportError, BackupExportJobService
from django.contrib import admin
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import path, reverse
from django.utils import timezone
@admin.register(BackupExportJob)
@@ -36,3 +43,92 @@ class BackupExportJobAdmin(admin.ModelAdmin):
"updated_at",
]
ordering = ["-actual_date", "-created_at"]
change_list_template = "admin/backups/backupexportjob/change_list.html"
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
"export/",
self.admin_site.admin_view(self.export_view),
name="backups_backupexportjob_export",
),
]
return custom_urls + urls
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
extra_context["backup_export_url"] = reverse(
"admin:backups_backupexportjob_export"
)
extra_context["backup_export_default_date"] = timezone.localdate().isoformat()
return super().changelist_view(request, extra_context=extra_context)
def export_view(self, request):
changelist_url = reverse("admin:backups_backupexportjob_changelist")
if request.method != "POST":
self.message_user(
request,
"Выгрузка backup доступна только через POST.",
level=messages.WARNING,
)
return redirect(changelist_url)
serializer = BackupExportRequestSerializer(
data={"actual_date": request.POST.get("actual_date")}
)
if not serializer.is_valid():
self.message_user(
request,
f"Некорректная дата: {serializer.errors}",
level=messages.ERROR,
)
return redirect(changelist_url)
actual_date = serializer.validated_data.get("actual_date") or timezone.localdate()
try:
result = BackupExportJobService.check_or_start_job(
actual_date=actual_date,
requested_by_id=request.user.id if request.user.is_authenticated else None,
)
except BackupExportError as exc:
self.message_user(request, f"Ошибка запуска резервного экспорта: {exc}", level=messages.ERROR)
return redirect(changelist_url)
if result.action in {"started", "wait"}:
self.message_user(
request,
result.message,
level=messages.INFO
if result.action == "started"
else messages.WARNING,
)
return redirect(changelist_url)
if result.action == "download":
try:
artifact = BackupExportJobService.consume_ready_archive(
actual_date=result.actual_date
)
except BackupExportError as exc:
self.message_user(request, f"Ошибка загрузки backup: {exc}", level=messages.ERROR)
return redirect(changelist_url)
response = HttpResponse(artifact.archive_bytes, content_type="application/zip")
response.status_code = 200
response[
"Content-Disposition"
] = f'attachment; filename="{artifact.archive_filename}"'
response["X-Backup-SHA256"] = artifact.checksum_sha256
response["X-Backup-Checksum-File"] = artifact.checksum_filename
response["X-Backup-Organizations"] = str(artifact.organizations_count)
response["X-Backup-Actual-Date"] = artifact.actual_date.isoformat()
return response
self.message_user(
request,
f"Неожиданный статус задачи: {result.action}",
level=messages.ERROR,
)
return redirect(changelist_url)

View File

@@ -212,11 +212,26 @@ def build_admin_dashboard() -> dict[str, Any]:
description="Синхронный импорт Excel по выбранному реестру",
url_name="admin:registers_registerupload_upload_excel",
),
_build_quick_action(
label="Добавить загрузку реестра",
description="Создать запись загрузки реестра вручную",
url_name="admin:registers_registerupload_add",
),
_build_quick_action(
label="ФНС Excel",
description="Загрузить один или несколько файлов бухгалтерской отчётности",
url_name="admin:parsers_financialreport_upload_excel",
),
_build_quick_action(
label="ФНС ZIP",
description="Загрузить архив файлов бухгалтерской отчетности",
url_name="admin:parsers_financialreport_upload_zip",
),
_build_quick_action(
label="Выгрузить защищённый backup",
description="Сформировать архив с актуальными реестрами и связанными данными",
url_name="admin:backups_backupexportjob_changelist",
),
_build_quick_action(
label="История обновлений",
description="Проверить последние загрузки и ошибки по источникам",

View File

@@ -0,0 +1,21 @@
{% extends "admin/change_list.html" %}
{% block object-tools-items %}
<li>
<form method="post" action="{{ backup_export_url }}" class="mx-object-tool-form">
{% csrf_token %}
<label for="id_actual_date">Дата актуальности:</label>
<input
type="date"
id="id_actual_date"
name="actual_date"
value="{{ backup_export_default_date }}"
required
>
<button type="submit" class="button default mx-object-tool-button">
Выгрузить защищённый backup
</button>
</form>
</li>
{{ block.super }}
{% endblock %}

View File

@@ -1,14 +1,6 @@
{% extends "admin/change_list.html" %}
{% block content %}
<div class="mx-admin-action-bar">
<a href="{{ upload_excel_url }}" class="mx-admin-action-bar__link mx-admin-action-bar__link--primary">
Загрузить Excel бухгалтерской отчетности
</a>
<a href="{{ upload_zip_url }}" class="mx-admin-action-bar__link mx-admin-action-bar__link--secondary">
Загрузить ZIP бухгалтерской отчетности
</a>
</div>
{{ block.super }}
{% endblock %}

View File

@@ -1,18 +1,6 @@
{% extends "admin/change_list.html" %}
{% load admin_urls %}
{% block content %}
<div class="mx-admin-action-bar">
<a href="{{ upload_excel_url }}" class="mx-admin-action-bar__link mx-admin-action-bar__link--primary">
Загрузить справочники из Excel
</a>
{% if has_add_permission %}
{% url cl.opts|admin_urlname:'add' as add_url %}
<a href="{{ add_url }}" class="mx-admin-action-bar__link mx-admin-action-bar__link--ghost">
Добавить загрузку реестра
</a>
{% endif %}
</div>
{{ block.super }}
{% endblock %}

View File

@@ -30,6 +30,7 @@ from django.contrib.admin.sites import AdminSite
from django.contrib.messages.storage.fallback import FallbackStorage
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import RequestFactory, TestCase, override_settings
from django.urls import reverse
from openpyxl import Workbook
from tests.apps.parsers.factories import (
@@ -100,6 +101,7 @@ class ParsersAdminTest(TestCase):
self.site = AdminSite()
self.factory = RequestFactory()
self.user = UserFactory.create_superuser()
self.client.force_login(self.user)
def _request(self, path="/"):
request = self.factory.get(path)
@@ -150,17 +152,17 @@ class ParsersAdminTest(TestCase):
self.assertIn("mx-object-tool-form", content)
def test_financial_report_changelist_renders_toolbar_buttons(self):
admin = FinancialReportAdmin(FinancialReport, self.site)
response = admin.changelist_view(
self._request("/admin/parsers/financialreport/")
)
response.render()
response = self.client.get(reverse("admin:index"))
content = response.content.decode("utf-8")
self.assertEqual(response.status_code, 200)
self.assertIn("Загрузить Excel бухгалтерской отчетности", content)
self.assertIn("Загрузить ZIP бухгалтерской отчетности", content)
self.assertIn("mx-admin-action-bar", content)
self.assertIn("ФНС Excel", content)
self.assertIn("ФНС ZIP", content)
self.assertIn(
reverse("admin:parsers_financialreport_upload_excel"),
content,
)
self.assertIn(reverse("admin:parsers_financialreport_upload_zip"), content)
@patch("apps.parsers.admin.ProxyToolsSyncService.sync_ru_proxies")
def test_proxy_admin_sync_view_calls_service(self, sync_mock):

View File

@@ -9,6 +9,7 @@ from django.contrib.admin.sites import AdminSite
from django.contrib.messages.storage.fallback import FallbackStorage
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import RequestFactory, TestCase
from django.urls import reverse
from openpyxl import Workbook
from tests.apps.registers.factories import RegisterFactory
@@ -45,6 +46,7 @@ class RegistersAdminTest(TestCase):
self.site = AdminSite()
self.factory = RequestFactory()
self.user = UserFactory.create_superuser()
self.client.force_login(self.user)
def _request(self, path="/admin/registers/registerupload/upload-excel/"):
request = self.factory.get(path)
@@ -81,17 +83,19 @@ class RegistersAdminTest(TestCase):
self.assertIn("multiple", content)
def test_register_upload_changelist_renders_toolbar_buttons(self):
admin = RegisterUploadAdmin(RegisterUpload, self.site)
response = admin.changelist_view(
self._request("/admin/registers/registerupload/")
)
response.render()
content = response.content.decode("utf-8")
response = self.client.get(reverse("admin:index"))
self.assertEqual(response.status_code, 200)
self.assertIn("Загрузить справочники из Excel", content)
self.assertIn("Добавить загрузку реестра", content)
self.assertIn("mx-admin-action-bar", content)
self.assertIn("Загрузить реестр", response.content.decode("utf-8"))
self.assertIn("Добавить загрузку реестра", response.content.decode("utf-8"))
self.assertIn(
reverse("admin:registers_registerupload_upload_excel"),
response.content.decode("utf-8"),
)
self.assertIn(
reverse("admin:registers_registerupload_add"),
response.content.decode("utf-8"),
)
def test_register_upload_admin_upload_excel_success(self):
admin = RegisterUploadAdmin(RegisterUpload, self.site)