feat(dashboard): improve parser API checks
This commit is contained in:
@@ -31,12 +31,12 @@ class ParserSourceDescriptor:
|
|||||||
@property
|
@property
|
||||||
def result_list_url(self) -> str:
|
def result_list_url(self) -> str:
|
||||||
"""Frontend/API URL для списка результата источника."""
|
"""Frontend/API URL для списка результата источника."""
|
||||||
return f"/api/v1/{self.api_route}/" if self.api_route else ""
|
return f"/api/v1/parsers/results/{self.key}/" if self.api_route else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def result_detail_url(self) -> str:
|
def result_detail_url(self) -> str:
|
||||||
"""Frontend/API URL для карточки результата источника."""
|
"""Frontend/API URL для карточки результата источника."""
|
||||||
return f"/api/v1/{self.api_route}/{{id}}/" if self.api_route else ""
|
return f"/api/v1/parsers/results/{self.key}/{{id}}/" if self.api_route else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def upload_url(self) -> str:
|
def upload_url(self) -> str:
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ from apps.parsers.views import (
|
|||||||
SourceCardDetailView,
|
SourceCardDetailView,
|
||||||
SourceCardListView,
|
SourceCardListView,
|
||||||
SourceCardRefreshView,
|
SourceCardRefreshView,
|
||||||
|
SourceResultDetailView,
|
||||||
|
SourceResultListView,
|
||||||
SourceTaskStatusListView,
|
SourceTaskStatusListView,
|
||||||
)
|
)
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
@@ -134,6 +136,16 @@ urlpatterns = [
|
|||||||
ParserUploadView.as_view(),
|
ParserUploadView.as_view(),
|
||||||
name="upload-parser-data",
|
name="upload-parser-data",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"results/<str:source_key>/",
|
||||||
|
SourceResultListView.as_view(),
|
||||||
|
name="source-result-list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"results/<str:source_key>/<int:pk>/",
|
||||||
|
SourceResultDetailView.as_view(),
|
||||||
|
name="source-result-detail",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"schedules/",
|
"schedules/",
|
||||||
ParserScheduleListCreateView.as_view(),
|
ParserScheduleListCreateView.as_view(),
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ NATIVE_RECORD_MODELS = {
|
|||||||
ParserLoadLog.Source.MANUFACTURES: ManufacturerRecord,
|
ParserLoadLog.Source.MANUFACTURES: ManufacturerRecord,
|
||||||
ParserLoadLog.Source.INSPECTIONS: InspectionRecord,
|
ParserLoadLog.Source.INSPECTIONS: InspectionRecord,
|
||||||
ParserLoadLog.Source.PROCUREMENTS: ProcurementRecord,
|
ParserLoadLog.Source.PROCUREMENTS: ProcurementRecord,
|
||||||
|
ParserLoadLog.Source.FNS_REPORTS: FinancialReport,
|
||||||
}
|
}
|
||||||
EXISTING_TASK_PARAMS = {
|
EXISTING_TASK_PARAMS = {
|
||||||
"industrial": {"proxies", "requested_by_id"},
|
"industrial": {"proxies", "requested_by_id"},
|
||||||
@@ -1457,6 +1458,15 @@ def _native_record_to_result(
|
|||||||
url = record.href
|
url = record.href
|
||||||
inn = record.customer_inn
|
inn = record.customer_inn
|
||||||
ogrn = record.customer_ogrn
|
ogrn = record.customer_ogrn
|
||||||
|
elif source == ParserLoadLog.Source.FNS_REPORTS:
|
||||||
|
external_id = record.external_id
|
||||||
|
organisation_name = ""
|
||||||
|
title = record.file_name
|
||||||
|
record_date = ""
|
||||||
|
status_value = record.status
|
||||||
|
url = ""
|
||||||
|
inn = ""
|
||||||
|
ogrn = record.ogrn
|
||||||
else:
|
else:
|
||||||
external_id = record.registration_number
|
external_id = record.registration_number
|
||||||
organisation_name = record.organisation_name
|
organisation_name = record.organisation_name
|
||||||
@@ -1641,6 +1651,14 @@ def _native_field_map(source: str) -> dict[str, str]:
|
|||||||
"record_date": "publish_date",
|
"record_date": "publish_date",
|
||||||
"status": "status",
|
"status": "status",
|
||||||
}
|
}
|
||||||
|
if source == ParserLoadLog.Source.FNS_REPORTS:
|
||||||
|
return {
|
||||||
|
**common,
|
||||||
|
"external_id": "external_id",
|
||||||
|
"ogrn": "ogrn",
|
||||||
|
"title": "file_name",
|
||||||
|
"status": "status",
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
**common,
|
**common,
|
||||||
"external_id": "registration_number",
|
"external_id": "registration_number",
|
||||||
@@ -1681,6 +1699,13 @@ def _native_search_q(source: str, search: str) -> Q:
|
|||||||
| Q(customer_name__icontains=search)
|
| Q(customer_name__icontains=search)
|
||||||
| Q(customer_inn__icontains=search)
|
| Q(customer_inn__icontains=search)
|
||||||
)
|
)
|
||||||
|
if source == ParserLoadLog.Source.FNS_REPORTS:
|
||||||
|
return (
|
||||||
|
Q(file_name__icontains=search)
|
||||||
|
| Q(external_id__icontains=search)
|
||||||
|
| Q(ogrn__icontains=search)
|
||||||
|
| Q(status__icontains=search)
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
Q(organisation_name__icontains=search)
|
Q(organisation_name__icontains=search)
|
||||||
| Q(registration_number__icontains=search)
|
| Q(registration_number__icontains=search)
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ STATE_CORP_EXCHANGE_TOKEN = os.getenv(
|
|||||||
# JWT
|
# JWT
|
||||||
SIMPLE_JWT["SIGNING_KEY"] = SECRET_KEY
|
SIMPLE_JWT["SIGNING_KEY"] = SECRET_KEY
|
||||||
|
|
||||||
|
# Dev frontend can run from any workstation/port in the local network.
|
||||||
|
# Production keeps the restrictive CORS/CSRF policy from settings.production.
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
CORS_ALLOW_PRIVATE_NETWORK = True
|
||||||
|
CSRF_COOKIE_SECURE = False
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
MIDDLEWARE = [
|
||||||
|
middleware
|
||||||
|
for middleware in MIDDLEWARE
|
||||||
|
if middleware != "django.middleware.csrf.CsrfViewMiddleware"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _normalize_local_host(host: str) -> str:
|
def _normalize_local_host(host: str) -> str:
|
||||||
"""Нормализует host для локального запуска backend на хосте."""
|
"""Нормализует host для локального запуска backend на хосте."""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -29,3 +29,50 @@ class ParserDashboardPageTest(TestCase):
|
|||||||
content = response.content.decode()
|
content = response.content.decode()
|
||||||
self.assertIn("function sourceGroups()", content)
|
self.assertIn("function sourceGroups()", content)
|
||||||
self.assertIn("dashboardData?.file_sources", content)
|
self.assertIn("dashboardData?.file_sources", content)
|
||||||
|
|
||||||
|
def test_dashboard_uses_vue_component_table_for_source_results(self):
|
||||||
|
response = self.client.get("/dashboard")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = response.content.decode()
|
||||||
|
self.assertIn("https://cdn.jsdelivr.net/npm/vue@3/", content)
|
||||||
|
self.assertIn("https://cdn.jsdelivr.net/npm/element-plus@2/", content)
|
||||||
|
self.assertIn('id="sourceRecordsApp"', content)
|
||||||
|
self.assertIn("<el-table", content)
|
||||||
|
self.assertIn("Обновляю данные без сброса таблицы", content)
|
||||||
|
|
||||||
|
def test_dashboard_does_not_send_limit_to_paginated_source_result_api(self):
|
||||||
|
response = self.client.get("/dashboard")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = response.content.decode()
|
||||||
|
self.assertIn("if (!source.result_list_url) {", content)
|
||||||
|
self.assertIn("query.limit = this.pageSize", content)
|
||||||
|
|
||||||
|
def test_dashboard_humanizes_schedules_and_registry_completeness(self):
|
||||||
|
response = self.client.get("/dashboard")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = response.content.decode()
|
||||||
|
self.assertIn("function humanizeSchedule", content)
|
||||||
|
self.assertIn("registryCompletenessChart", content)
|
||||||
|
self.assertIn("chart-grid-full", content)
|
||||||
|
self.assertIn("function registryDisplayName", content)
|
||||||
|
self.assertIn('data-main-tab="endpoints"', content)
|
||||||
|
self.assertIn("function buildEndpointCatalog", content)
|
||||||
|
self.assertIn("checkAllEndpointsButton", content)
|
||||||
|
self.assertIn("endpointCheckModal", content)
|
||||||
|
self.assertIn("data-toggle", content)
|
||||||
|
self.assertIn("iconButton", content)
|
||||||
|
|
||||||
|
def test_dashboard_endpoint_checker_uses_row_details_modal(self):
|
||||||
|
response = self.client.get("/dashboard")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = response.content.decode()
|
||||||
|
self.assertIn("latestEndpointResultMap", content)
|
||||||
|
self.assertIn("endpointCheckSummary", content)
|
||||||
|
self.assertIn("data-endpoint-run", content)
|
||||||
|
self.assertIn("data-endpoint-detail", content)
|
||||||
|
self.assertIn("showEndpointDetail", content)
|
||||||
|
self.assertIn("без данных", content)
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class ParsersViewSetTest(APITestCase):
|
|||||||
|
|
||||||
def test_manufacturers_list_and_retrieve(self):
|
def test_manufacturers_list_and_retrieve(self):
|
||||||
record = ManufacturerRecordFactory()
|
record = ManufacturerRecordFactory()
|
||||||
|
second_record = ManufacturerRecordFactory()
|
||||||
self.client.force_authenticate(self.user)
|
self.client.force_authenticate(self.user)
|
||||||
url = reverse("api_v1:minpromtorg:manufacturers-list")
|
url = reverse("api_v1:minpromtorg:manufacturers-list")
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
@@ -102,6 +103,29 @@ class ParsersViewSetTest(APITestCase):
|
|||||||
reverse("api_v1:minpromtorg:manufacturers-detail", args=[record.id])
|
reverse("api_v1:minpromtorg:manufacturers-detail", args=[record.id])
|
||||||
)
|
)
|
||||||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||||||
|
first_page_response = self.client.get(
|
||||||
|
"/api/v1/parsers/results/manufactures/",
|
||||||
|
{"page": 1, "page_size": 1},
|
||||||
|
)
|
||||||
|
second_page_response = self.client.get(
|
||||||
|
"/api/v1/parsers/results/manufactures/",
|
||||||
|
{"page": 2, "page_size": 1},
|
||||||
|
)
|
||||||
|
self.assertEqual(first_page_response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(second_page_response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(first_page_response.data["meta"]["pagination"]["count"], 2)
|
||||||
|
self.assertEqual(second_page_response.data["meta"]["pagination"]["count"], 2)
|
||||||
|
self.assertNotEqual(
|
||||||
|
first_page_response.data["data"][0]["id"],
|
||||||
|
second_page_response.data["data"][0]["id"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
first_page_response.data["data"][0]["id"],
|
||||||
|
second_page_response.data["data"][0]["id"],
|
||||||
|
},
|
||||||
|
{record.id, second_record.id},
|
||||||
|
)
|
||||||
|
|
||||||
def test_products_list_and_retrieve(self):
|
def test_products_list_and_retrieve(self):
|
||||||
record = IndustrialProductRecordFactory()
|
record = IndustrialProductRecordFactory()
|
||||||
@@ -211,15 +235,15 @@ class ParsersViewSetTest(APITestCase):
|
|||||||
sources = {item["key"]: item for item in payload["sources"]}
|
sources = {item["key"]: item for item in payload["sources"]}
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sources["procurements_44fz"]["result_list_url"],
|
sources["procurements_44fz"]["result_list_url"],
|
||||||
"/api/v1/eis/procurements-44fz/",
|
"/api/v1/parsers/results/procurements_44fz/",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sources["procurements_223fz"]["result_list_url"],
|
sources["procurements_223fz"]["result_list_url"],
|
||||||
"/api/v1/eis/procurements-223fz/",
|
"/api/v1/parsers/results/procurements_223fz/",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sources["contracts"]["result_list_url"],
|
sources["contracts"]["result_list_url"],
|
||||||
"/api/v1/eis/contracts/",
|
"/api/v1/parsers/results/contracts/",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
payload["source_counts"][ParserLoadLog.Source.PROCUREMENTS_44FZ],
|
payload["source_counts"][ParserLoadLog.Source.PROCUREMENTS_44FZ],
|
||||||
@@ -269,6 +293,11 @@ class ParsersViewSetTest(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||||||
self.assertIn("lines", detail.data)
|
self.assertIn("lines", detail.data)
|
||||||
|
unified_response = self.client.get("/api/v1/parsers/results/fns_financial/")
|
||||||
|
self.assertEqual(unified_response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(unified_response.data["meta"]["pagination"]["count"], 1)
|
||||||
|
self.assertEqual(unified_response.data["data"][0]["id"], report.id)
|
||||||
|
self.assertEqual(unified_response.data["data"][0]["title"], report.file_name)
|
||||||
|
|
||||||
def test_system_logs_admin_only(self):
|
def test_system_logs_admin_only(self):
|
||||||
log = ParserLoadLogFactory()
|
log = ParserLoadLogFactory()
|
||||||
|
|||||||
Reference in New Issue
Block a user