Move export and upload actions to admin dashboard
Some checks failed
Some checks failed
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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="Проверить последние загрузки и ошибки по источникам",
|
||||
|
||||
21
src/templates/admin/backups/backupexportjob/change_list.html
Normal file
21
src/templates/admin/backups/backupexportjob/change_list.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user