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."""
|
"""Admin для приложения backups."""
|
||||||
|
|
||||||
from apps.backups.models import BackupExportJob
|
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 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)
|
@admin.register(BackupExportJob)
|
||||||
@@ -36,3 +43,92 @@ class BackupExportJobAdmin(admin.ModelAdmin):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
ordering = ["-actual_date", "-created_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 по выбранному реестру",
|
description="Синхронный импорт Excel по выбранному реестру",
|
||||||
url_name="admin:registers_registerupload_upload_excel",
|
url_name="admin:registers_registerupload_upload_excel",
|
||||||
),
|
),
|
||||||
|
_build_quick_action(
|
||||||
|
label="Добавить загрузку реестра",
|
||||||
|
description="Создать запись загрузки реестра вручную",
|
||||||
|
url_name="admin:registers_registerupload_add",
|
||||||
|
),
|
||||||
_build_quick_action(
|
_build_quick_action(
|
||||||
label="ФНС Excel",
|
label="ФНС Excel",
|
||||||
description="Загрузить один или несколько файлов бухгалтерской отчётности",
|
description="Загрузить один или несколько файлов бухгалтерской отчётности",
|
||||||
url_name="admin:parsers_financialreport_upload_excel",
|
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(
|
_build_quick_action(
|
||||||
label="История обновлений",
|
label="История обновлений",
|
||||||
description="Проверить последние загрузки и ошибки по источникам",
|
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" %}
|
{% extends "admin/change_list.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
{% extends "admin/change_list.html" %}
|
{% extends "admin/change_list.html" %}
|
||||||
{% load admin_urls %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% 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 }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from django.contrib.admin.sites import AdminSite
|
|||||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import RequestFactory, TestCase, override_settings
|
from django.test import RequestFactory, TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
|
|
||||||
from tests.apps.parsers.factories import (
|
from tests.apps.parsers.factories import (
|
||||||
@@ -100,6 +101,7 @@ class ParsersAdminTest(TestCase):
|
|||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.user = UserFactory.create_superuser()
|
self.user = UserFactory.create_superuser()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def _request(self, path="/"):
|
def _request(self, path="/"):
|
||||||
request = self.factory.get(path)
|
request = self.factory.get(path)
|
||||||
@@ -150,17 +152,17 @@ class ParsersAdminTest(TestCase):
|
|||||||
self.assertIn("mx-object-tool-form", content)
|
self.assertIn("mx-object-tool-form", content)
|
||||||
|
|
||||||
def test_financial_report_changelist_renders_toolbar_buttons(self):
|
def test_financial_report_changelist_renders_toolbar_buttons(self):
|
||||||
admin = FinancialReportAdmin(FinancialReport, self.site)
|
response = self.client.get(reverse("admin:index"))
|
||||||
response = admin.changelist_view(
|
|
||||||
self._request("/admin/parsers/financialreport/")
|
|
||||||
)
|
|
||||||
response.render()
|
|
||||||
content = response.content.decode("utf-8")
|
content = response.content.decode("utf-8")
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("Загрузить Excel бухгалтерской отчетности", content)
|
self.assertIn("ФНС Excel", content)
|
||||||
self.assertIn("Загрузить ZIP бухгалтерской отчетности", content)
|
self.assertIn("ФНС ZIP", content)
|
||||||
self.assertIn("mx-admin-action-bar", 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")
|
@patch("apps.parsers.admin.ProxyToolsSyncService.sync_ru_proxies")
|
||||||
def test_proxy_admin_sync_view_calls_service(self, sync_mock):
|
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.contrib.messages.storage.fallback import FallbackStorage
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
|
|
||||||
from tests.apps.registers.factories import RegisterFactory
|
from tests.apps.registers.factories import RegisterFactory
|
||||||
@@ -45,6 +46,7 @@ class RegistersAdminTest(TestCase):
|
|||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.user = UserFactory.create_superuser()
|
self.user = UserFactory.create_superuser()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def _request(self, path="/admin/registers/registerupload/upload-excel/"):
|
def _request(self, path="/admin/registers/registerupload/upload-excel/"):
|
||||||
request = self.factory.get(path)
|
request = self.factory.get(path)
|
||||||
@@ -81,17 +83,19 @@ class RegistersAdminTest(TestCase):
|
|||||||
self.assertIn("multiple", content)
|
self.assertIn("multiple", content)
|
||||||
|
|
||||||
def test_register_upload_changelist_renders_toolbar_buttons(self):
|
def test_register_upload_changelist_renders_toolbar_buttons(self):
|
||||||
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
response = self.client.get(reverse("admin:index"))
|
||||||
response = admin.changelist_view(
|
|
||||||
self._request("/admin/registers/registerupload/")
|
|
||||||
)
|
|
||||||
response.render()
|
|
||||||
content = response.content.decode("utf-8")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("Загрузить справочники из Excel", content)
|
self.assertIn("Загрузить реестр", response.content.decode("utf-8"))
|
||||||
self.assertIn("Добавить загрузку реестра", content)
|
self.assertIn("Добавить загрузку реестра", response.content.decode("utf-8"))
|
||||||
self.assertIn("mx-admin-action-bar", content)
|
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):
|
def test_register_upload_admin_upload_excel_success(self):
|
||||||
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
||||||
|
|||||||
Reference in New Issue
Block a user