feat(admin): improve uploads and dashboard UX
This commit is contained in:
@@ -1,15 +1,31 @@
|
||||
"""Tests for core admin configurations."""
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import date, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from apps.core.admin import BackgroundJobAdmin
|
||||
from apps.core.models import BackgroundJob
|
||||
from apps.parsers.models import FinancialReport, FinancialReportLine, ParserLoadLog
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from tests.apps.parsers.factories import (
|
||||
IndustrialCertificateRecordFactory,
|
||||
InspectionRecordFactory,
|
||||
ManufacturerRecordFactory,
|
||||
ParserLoadLogFactory,
|
||||
ProcurementRecordFactory,
|
||||
ProxyFactory,
|
||||
)
|
||||
from tests.apps.registers.factories import (
|
||||
OrganizationFactory,
|
||||
RegisterFactory,
|
||||
RegisterUploadFactory,
|
||||
RegistryMembershipPeriodFactory,
|
||||
)
|
||||
from tests.apps.user.factories import UserFactory
|
||||
from tests.utils.fixtures import fake
|
||||
|
||||
@@ -94,3 +110,97 @@ class CoreAdminTest(TestCase):
|
||||
revoke_mock.assert_called_once_with(job.task_id, terminate=True)
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.status, "revoked")
|
||||
|
||||
|
||||
class AdminDashboardTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create_superuser()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_admin_index_renders_custom_dashboard_with_live_metrics(self):
|
||||
primary_registry = RegisterFactory(name="Реестр Росатом ОПК тестовый")
|
||||
secondary_registry = RegisterFactory(name="Реестр Роскосмос ГОЗ тестовый")
|
||||
primary_upload = RegisterUploadFactory(
|
||||
registry=primary_registry,
|
||||
actual_date=date(2026, 3, 20),
|
||||
rows_count=24,
|
||||
)
|
||||
secondary_upload = RegisterUploadFactory(
|
||||
registry=secondary_registry,
|
||||
actual_date=date(2026, 3, 21),
|
||||
rows_count=11,
|
||||
)
|
||||
first_org = OrganizationFactory(pn_name='АО "Росатом Тест"')
|
||||
second_org = OrganizationFactory(pn_name='АО "Роскосмос Тест"')
|
||||
RegistryMembershipPeriodFactory(
|
||||
registry=primary_registry,
|
||||
organization=first_org,
|
||||
started_by_upload=primary_upload,
|
||||
)
|
||||
RegistryMembershipPeriodFactory(
|
||||
registry=secondary_registry,
|
||||
organization=second_org,
|
||||
started_by_upload=secondary_upload,
|
||||
)
|
||||
|
||||
report = FinancialReport.objects.create(
|
||||
external_id="fns-dashboard-1",
|
||||
ogrn="1027700000001",
|
||||
file_name="fin_dashboard_1.xlsx",
|
||||
file_hash="fns-dashboard-file-hash-1",
|
||||
load_batch=101,
|
||||
status=FinancialReport.Status.SUCCESS,
|
||||
source=FinancialReport.SourceType.API,
|
||||
)
|
||||
FinancialReportLine.objects.create(
|
||||
report=report,
|
||||
form_code="1",
|
||||
line_code="1100",
|
||||
line_name="Итого по разделу",
|
||||
year=2025,
|
||||
period_start=10,
|
||||
period_end=15,
|
||||
)
|
||||
|
||||
IndustrialCertificateRecordFactory(inn="7700000001")
|
||||
ManufacturerRecordFactory(inn="7700000002")
|
||||
InspectionRecordFactory(inn="7800000001")
|
||||
ProcurementRecordFactory(region_code="77", customer_inn="7700000001")
|
||||
ProcurementRecordFactory(region_code="77", customer_inn="7700000002")
|
||||
ProcurementRecordFactory(region_code="78", customer_inn="7800000001")
|
||||
ProxyFactory(is_active=True)
|
||||
|
||||
ParserLoadLogFactory(
|
||||
source=ParserLoadLog.Source.PROCUREMENTS,
|
||||
status="success",
|
||||
records_count=3,
|
||||
)
|
||||
ParserLoadLogFactory(
|
||||
source=ParserLoadLog.Source.INDUSTRIAL,
|
||||
status="failed",
|
||||
records_count=0,
|
||||
error_message="Источник временно недоступен",
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("admin:index"))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Панель данных и загрузок")
|
||||
self.assertContains(response, "Обзор")
|
||||
self.assertContains(response, "Аналитика")
|
||||
self.assertContains(response, "Админка")
|
||||
self.assertContains(response, "Покрытие по внешним данным")
|
||||
self.assertContains(response, "Топ регионов по закупкам")
|
||||
self.assertContains(response, "Реестр Росатом ОПК тестовый")
|
||||
self.assertContains(response, "Реестр Роскосмос ГОЗ тестовый")
|
||||
self.assertContains(response, "Москва")
|
||||
self.assertContains(response, "Санкт-Петербург")
|
||||
self.assertContains(response, "Источник временно недоступен")
|
||||
self.assertContains(
|
||||
response,
|
||||
reverse("admin:registers_registerupload_upload_excel"),
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
reverse("admin:parsers_financialreport_upload_excel"),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import io
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from apps.parsers.admin import (
|
||||
FinancialReportAdmin,
|
||||
@@ -84,14 +85,24 @@ def _build_fns_zip_upload() -> SimpleUploadedFile:
|
||||
)
|
||||
|
||||
|
||||
def _build_fns_excel_upload() -> SimpleUploadedFile:
|
||||
return SimpleUploadedFile(
|
||||
f"fin_{_digits(5)}_{_digits(13)}.xlsx",
|
||||
_build_fns_excel_bytes(),
|
||||
content_type=(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ParsersAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.user = UserFactory.create_superuser()
|
||||
|
||||
def _request(self):
|
||||
request = self.factory.get("/")
|
||||
def _request(self, path="/"):
|
||||
request = self.factory.get(path)
|
||||
request.user = self.user
|
||||
request.session = {}
|
||||
request._messages = FallbackStorage(request)
|
||||
@@ -122,6 +133,42 @@ class ParsersAdminTest(TestCase):
|
||||
proxy.refresh_from_db()
|
||||
self.assertEqual(proxy.fail_count, 0)
|
||||
|
||||
def test_proxy_admin_has_sync_route(self):
|
||||
admin = ProxyAdmin(Proxy, self.site)
|
||||
route_names = [route.name for route in admin.get_urls()]
|
||||
|
||||
self.assertIn("parsers_proxy_sync_proxy_tools", route_names)
|
||||
|
||||
def test_proxy_admin_changelist_renders_sync_button(self):
|
||||
admin = ProxyAdmin(Proxy, self.site)
|
||||
response = admin.changelist_view(self._request("/admin/parsers/proxy/"))
|
||||
response.render()
|
||||
content = response.content.decode("utf-8")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Обновить список прокси", content)
|
||||
self.assertIn("mx-object-tool-form", content)
|
||||
|
||||
@patch("apps.parsers.admin.ProxyToolsSyncService.sync_ru_proxies")
|
||||
def test_proxy_admin_sync_view_calls_service(self, sync_mock):
|
||||
sync_mock.return_value = {
|
||||
"status": "success",
|
||||
"fetched": 3,
|
||||
"created": 2,
|
||||
"updated": 1,
|
||||
"deactivated": 0,
|
||||
}
|
||||
admin = ProxyAdmin(Proxy, self.site)
|
||||
request = self._post_request(
|
||||
"/admin/parsers/proxy/sync-proxy-tools/",
|
||||
{},
|
||||
)
|
||||
|
||||
response = admin.sync_proxy_tools_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
sync_mock.assert_called_once_with()
|
||||
|
||||
def test_proxy_active_badge(self):
|
||||
admin = ProxyAdmin(Proxy, self.site)
|
||||
active = ProxyFactory(is_active=True)
|
||||
@@ -299,6 +346,17 @@ class ParsersAdminTest(TestCase):
|
||||
self.assertIn("parsers_financialreport_upload_excel", route_names)
|
||||
self.assertIn("parsers_financialreport_upload_zip", route_names)
|
||||
|
||||
def test_financial_report_admin_upload_excel_get_renders_custom_file_picker(self):
|
||||
admin = FinancialReportAdmin(FinancialReport, self.site)
|
||||
response = admin.upload_excel_view(self._request())
|
||||
response.render()
|
||||
content = response.content.decode("utf-8")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('type="file"', content)
|
||||
self.assertIn("mx-upload-file", content)
|
||||
self.assertIn("multiple", content)
|
||||
|
||||
def test_financial_report_admin_upload_zip_view(self):
|
||||
admin = FinancialReportAdmin(FinancialReport, self.site)
|
||||
archive_upload = _build_fns_zip_upload()
|
||||
@@ -311,9 +369,57 @@ class ParsersAdminTest(TestCase):
|
||||
FNS_WATCH_DIRECTORY=os.path.join(tmpdir, "watch"),
|
||||
FNS_PROCESSED_DIRECTORY=os.path.join(tmpdir, "processed"),
|
||||
FNS_FAILED_DIRECTORY=os.path.join(tmpdir, "failed"),
|
||||
):
|
||||
), patch("apps.parsers.fns_upload.process_fns_file") as task_mock:
|
||||
task_mock.apply_async.side_effect = AssertionError(
|
||||
"admin upload should not enqueue celery task"
|
||||
)
|
||||
response = admin.upload_zip_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(FinancialReport.objects.count(), 1)
|
||||
self.assertEqual(FinancialReportLine.objects.count(), 1)
|
||||
|
||||
def test_financial_report_admin_upload_excel_view_processes_multiple_files(self):
|
||||
admin = FinancialReportAdmin(FinancialReport, self.site)
|
||||
first_upload = _build_fns_excel_upload()
|
||||
second_upload = _build_fns_excel_upload()
|
||||
request = self._post_request(
|
||||
"/admin/parsers/financialreport/upload-excel/",
|
||||
{"files": [first_upload, second_upload]},
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir, override_settings(
|
||||
FNS_WATCH_DIRECTORY=os.path.join(tmpdir, "watch"),
|
||||
FNS_PROCESSED_DIRECTORY=os.path.join(tmpdir, "processed"),
|
||||
FNS_FAILED_DIRECTORY=os.path.join(tmpdir, "failed"),
|
||||
), patch("apps.parsers.fns_upload.process_fns_file") as task_mock:
|
||||
task_mock.apply_async.side_effect = AssertionError(
|
||||
"admin upload should not enqueue celery task"
|
||||
)
|
||||
response = admin.upload_excel_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(FinancialReport.objects.count(), 2)
|
||||
self.assertEqual(FinancialReportLine.objects.count(), 2)
|
||||
|
||||
def test_financial_report_admin_upload_excel_view_processes_sync(self):
|
||||
admin = FinancialReportAdmin(FinancialReport, self.site)
|
||||
excel_upload = _build_fns_excel_upload()
|
||||
request = self._post_request(
|
||||
"/admin/parsers/financialreport/upload-excel/",
|
||||
{"files": [excel_upload]},
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir, override_settings(
|
||||
FNS_WATCH_DIRECTORY=os.path.join(tmpdir, "watch"),
|
||||
FNS_PROCESSED_DIRECTORY=os.path.join(tmpdir, "processed"),
|
||||
FNS_FAILED_DIRECTORY=os.path.join(tmpdir, "failed"),
|
||||
), patch("apps.parsers.fns_upload.process_fns_file") as task_mock:
|
||||
task_mock.apply_async.side_effect = AssertionError(
|
||||
"admin upload should not enqueue celery task"
|
||||
)
|
||||
response = admin.upload_excel_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(FinancialReport.objects.count(), 1)
|
||||
self.assertEqual(FinancialReportLine.objects.count(), 1)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for registers admin configuration."""
|
||||
|
||||
import io
|
||||
from unittest.mock import patch
|
||||
|
||||
from apps.registers.admin import RegisterUploadAdmin
|
||||
from apps.registers.models import Organization, RegisterUpload, RegistryMembershipPeriod
|
||||
@@ -45,6 +46,13 @@ class RegistersAdminTest(TestCase):
|
||||
self.factory = RequestFactory()
|
||||
self.user = UserFactory.create_superuser()
|
||||
|
||||
def _request(self):
|
||||
request = self.factory.get("/admin/registers/registerupload/upload-excel/")
|
||||
request.user = self.user
|
||||
request.session = {}
|
||||
request._messages = FallbackStorage(request)
|
||||
return request
|
||||
|
||||
def _post_request(self, data):
|
||||
request = self.factory.post(
|
||||
"/admin/registers/registerupload/upload-excel/",
|
||||
@@ -61,6 +69,16 @@ class RegistersAdminTest(TestCase):
|
||||
|
||||
self.assertIn("registers_registerupload_upload_excel", route_names)
|
||||
|
||||
def test_register_upload_admin_get_renders_custom_file_picker(self):
|
||||
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
||||
response = admin.upload_excel_view(self._request())
|
||||
response.render()
|
||||
content = response.content.decode("utf-8")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('type="file"', content)
|
||||
self.assertIn("mx-upload-file", content)
|
||||
|
||||
def test_register_upload_admin_upload_excel_success(self):
|
||||
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
||||
registry = RegisterFactory()
|
||||
@@ -100,3 +118,29 @@ class RegistersAdminTest(TestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(RegisterUpload.objects.count(), 0)
|
||||
|
||||
def test_register_upload_admin_uses_sync_import_service(self):
|
||||
admin = RegisterUploadAdmin(RegisterUpload, self.site)
|
||||
registry = RegisterFactory()
|
||||
uploaded_file = _build_register_excel_upload()
|
||||
request = self._post_request(
|
||||
{
|
||||
"registry": str(registry.id),
|
||||
"actual_date": "2026-03-20",
|
||||
"file": uploaded_file,
|
||||
}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"apps.registers.admin.RegisterImportService.sync_registry_memberships",
|
||||
return_value={
|
||||
"registry_name": registry.name,
|
||||
"rows_in_file": 1,
|
||||
"organizations_created": 1,
|
||||
"organizations_updated": 0,
|
||||
},
|
||||
) as sync_mock:
|
||||
response = admin.upload_excel_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
sync_mock.assert_called_once()
|
||||
|
||||
@@ -113,7 +113,11 @@ class RegistersViewsTest(APITestCase):
|
||||
names = {item["name"] for item in _extract_results(response.data)}
|
||||
self.assertIn("Реестр предприятий ОПК", names)
|
||||
self.assertIn("Реестр госкорпорации Роскосмос", names)
|
||||
self.assertIn("Реестр госкорпорации Роскосмос ГОЗ", names)
|
||||
self.assertIn("Реестр госкорпорации Роскосмос ОПК", names)
|
||||
self.assertIn("Реестр госкорпорации Росатом", names)
|
||||
self.assertIn("Реестр госкорпорации Росатом ГОЗ", names)
|
||||
self.assertIn("Реестр госкорпорации Росатом ОПК", names)
|
||||
|
||||
def test_organizations_list_and_retrieve(self):
|
||||
organization = OrganizationFactory()
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from apps.user.admin import ProfileAdmin, UserAdmin
|
||||
from apps.user.models import Profile, User
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.admin.utils import flatten_fieldsets
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import RequestFactory, TestCase
|
||||
@@ -76,6 +77,24 @@ class UserAdminTest(TestCase):
|
||||
self.admin.activate_users(request, qs)
|
||||
self.assertTrue(User.objects.filter(is_active=True).count() >= 3)
|
||||
|
||||
def test_add_form_includes_staff_and_superuser_flags(self):
|
||||
add_fields = flatten_fieldsets(self.admin.add_fieldsets)
|
||||
|
||||
self.assertIn("is_staff", add_fields)
|
||||
self.assertIn("is_superuser", add_fields)
|
||||
|
||||
def test_permissions_fieldset_is_visible_with_staff_and_superuser_flags(self):
|
||||
permissions_section = next(
|
||||
section
|
||||
for section in self.admin.fieldsets
|
||||
if "is_staff" in section[1]["fields"]
|
||||
and "is_superuser" in section[1]["fields"]
|
||||
)
|
||||
|
||||
self.assertIn("is_staff", permissions_section[1]["fields"])
|
||||
self.assertIn("is_superuser", permissions_section[1]["fields"])
|
||||
self.assertNotIn("classes", permissions_section[1])
|
||||
|
||||
|
||||
class ProfileAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user