fix dashboard source result routes
All checks were successful
CI/CD Pipeline / Manual Action Help (push) Has been skipped
CI/CD Pipeline / Start Dev Containers in Dokploy (push) Has been skipped
CI/CD Pipeline / Drop and Recreate Dev Database (push) Has been skipped
CI/CD Pipeline / Quality Gate (push) Successful in 53s
CI/CD Pipeline / Build and Push Images (push) Successful in 3m28s
CI/CD Pipeline / Internal Notify (push) Successful in 1s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s
All checks were successful
CI/CD Pipeline / Manual Action Help (push) Has been skipped
CI/CD Pipeline / Start Dev Containers in Dokploy (push) Has been skipped
CI/CD Pipeline / Drop and Recreate Dev Database (push) Has been skipped
CI/CD Pipeline / Quality Gate (push) Successful in 53s
CI/CD Pipeline / Build and Push Images (push) Successful in 3m28s
CI/CD Pipeline / Internal Notify (push) Successful in 1s
CI/CD Pipeline / Deploy Dev in Dokploy (push) Successful in 1s
This commit is contained in:
@@ -19,6 +19,9 @@ from drf_yasg.utils import swagger_auto_schema
|
|||||||
|
|
||||||
app_name = "parser_results"
|
app_name = "parser_results"
|
||||||
ROUTE_TITLES = {
|
ROUTE_TITLES = {
|
||||||
|
"eis/contracts": "ЕИС Контракты",
|
||||||
|
"eis/procurements-44fz": "ЕИС Закупки 44-ФЗ",
|
||||||
|
"eis/procurements-223fz": "ЕИС Закупки 223-ФЗ",
|
||||||
"zakupki": "ЕИС Закупки",
|
"zakupki": "ЕИС Закупки",
|
||||||
"proverki": "Проверки Генпрокуратуры",
|
"proverki": "Проверки Генпрокуратуры",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
|
|||||||
access_method="eis_official_api",
|
access_method="eis_official_api",
|
||||||
parser_strategy="eis_44fz_search",
|
parser_strategy="eis_44fz_search",
|
||||||
source_notes="Официальный поиск ЕИС; XML-выгрузки ЕИС требуют отдельного discovery по реестрам.",
|
source_notes="Официальный поиск ЕИС; XML-выгрузки ЕИС требуют отдельного discovery по реестрам.",
|
||||||
api_route="zakupki",
|
api_route="eis/procurements-44fz",
|
||||||
),
|
),
|
||||||
"procurements_223fz": ParserSourceDescriptor(
|
"procurements_223fz": ParserSourceDescriptor(
|
||||||
key="procurements_223fz",
|
key="procurements_223fz",
|
||||||
@@ -155,7 +155,7 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
|
|||||||
access_method="eis_official_api",
|
access_method="eis_official_api",
|
||||||
parser_strategy="eis_223fz_search",
|
parser_strategy="eis_223fz_search",
|
||||||
source_notes="Официальный реестр положений о закупках 223-ФЗ в ЕИС.",
|
source_notes="Официальный реестр положений о закупках 223-ФЗ в ЕИС.",
|
||||||
api_route="zakupki",
|
api_route="eis/procurements-223fz",
|
||||||
),
|
),
|
||||||
"contracts": ParserSourceDescriptor(
|
"contracts": ParserSourceDescriptor(
|
||||||
key="contracts",
|
key="contracts",
|
||||||
@@ -169,7 +169,7 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
|
|||||||
upstream_url="https://zakupki.gov.ru/epz/contract/search/results.html",
|
upstream_url="https://zakupki.gov.ru/epz/contract/search/results.html",
|
||||||
access_method="eis_official_api",
|
access_method="eis_official_api",
|
||||||
parser_strategy="eis_contract_registry",
|
parser_strategy="eis_contract_registry",
|
||||||
api_route="zakupki",
|
api_route="eis/contracts",
|
||||||
),
|
),
|
||||||
"unfair_suppliers": ParserSourceDescriptor(
|
"unfair_suppliers": ParserSourceDescriptor(
|
||||||
key="unfair_suppliers",
|
key="unfair_suppliers",
|
||||||
|
|||||||
@@ -1089,6 +1089,7 @@
|
|||||||
const tokenKey = "mostovik_dashboard_access";
|
const tokenKey = "mostovik_dashboard_access";
|
||||||
const refreshKey = "mostovik_dashboard_refresh";
|
const refreshKey = "mostovik_dashboard_refresh";
|
||||||
let accessToken = localStorage.getItem(tokenKey) || "";
|
let accessToken = localStorage.getItem(tokenKey) || "";
|
||||||
|
let currentUser = null;
|
||||||
let dashboardData = null;
|
let dashboardData = null;
|
||||||
let activeMainTab = "analytics";
|
let activeMainTab = "analytics";
|
||||||
let activeDrawerTab = "jobs";
|
let activeDrawerTab = "jobs";
|
||||||
@@ -1316,6 +1317,20 @@
|
|||||||
showMainTab(activeMainTab);
|
showMainTab(activeMainTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userCanManageExchange() {
|
||||||
|
const capabilities = currentUser?.capabilities || {};
|
||||||
|
return Boolean(capabilities.can_manage_exchange || capabilities.can_manage_database_connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCurrentUser() {
|
||||||
|
if (!accessToken) {
|
||||||
|
currentUser = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
currentUser = await apiFetch("/api/v1/users/me/");
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
function showMainTab(tab) {
|
function showMainTab(tab) {
|
||||||
activeMainTab = tab;
|
activeMainTab = tab;
|
||||||
document.querySelectorAll("[data-main-tab]").forEach((button) => {
|
document.querySelectorAll("[data-main-tab]").forEach((button) => {
|
||||||
@@ -1599,6 +1614,7 @@
|
|||||||
localStorage.removeItem(tokenKey);
|
localStorage.removeItem(tokenKey);
|
||||||
localStorage.removeItem(refreshKey);
|
localStorage.removeItem(refreshKey);
|
||||||
accessToken = "";
|
accessToken = "";
|
||||||
|
currentUser = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRegistryUploadPanel(registries) {
|
function renderRegistryUploadPanel(registries) {
|
||||||
@@ -1695,7 +1711,18 @@
|
|||||||
` : `<div class="empty-state">Расписание выгрузки во внешнюю БД еще не настроено.</div>`;
|
` : `<div class="empty-state">Расписание выгрузки во внешнюю БД еще не настроено.</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderExchangeRestricted() {
|
||||||
|
setSelectOptions("exchangeCopyTable", []);
|
||||||
|
setSelectOptions("exchangeScheduleTable", []);
|
||||||
|
$("exchangeConnections").innerHTML = `<div class="empty-state">Внешняя БД доступна только пользователям с правами администратора.</div>`;
|
||||||
|
$("exchangeSchedules").innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshExchange() {
|
async function refreshExchange() {
|
||||||
|
if (!userCanManageExchange()) {
|
||||||
|
renderExchangeRestricted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const [connections, tables, schedules] = await Promise.all([
|
const [connections, tables, schedules] = await Promise.all([
|
||||||
apiFetch("/api/v1/exchange/connections/"),
|
apiFetch("/api/v1/exchange/connections/"),
|
||||||
@@ -1864,6 +1891,7 @@
|
|||||||
async function refreshDashboard() {
|
async function refreshDashboard() {
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
try {
|
try {
|
||||||
|
await loadCurrentUser();
|
||||||
const data = await apiFetch("/api/v1/parsers/dashboard/");
|
const data = await apiFetch("/api/v1/parsers/dashboard/");
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
renderDashboard(data);
|
renderDashboard(data);
|
||||||
@@ -1984,6 +2012,10 @@
|
|||||||
|
|
||||||
$("exchangeConnectionForm").addEventListener("submit", async (event) => {
|
$("exchangeConnectionForm").addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!userCanManageExchange()) {
|
||||||
|
renderExchangeRestricted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await apiFetch("/api/v1/exchange/connections/", {
|
await apiFetch("/api/v1/exchange/connections/", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(formPayload(event.target)),
|
body: JSON.stringify(formPayload(event.target)),
|
||||||
@@ -1996,6 +2028,10 @@
|
|||||||
|
|
||||||
$("exchangeCopyForm").addEventListener("submit", async (event) => {
|
$("exchangeCopyForm").addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!userCanManageExchange()) {
|
||||||
|
renderExchangeRestricted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = formPayload(event.target);
|
const data = formPayload(event.target);
|
||||||
if (data.mode !== "single") delete data.table;
|
if (data.mode !== "single") delete data.table;
|
||||||
await apiFetch("/api/v1/exchange/copy/", {
|
await apiFetch("/api/v1/exchange/copy/", {
|
||||||
@@ -2007,6 +2043,10 @@
|
|||||||
|
|
||||||
$("exchangeScheduleForm").addEventListener("submit", async (event) => {
|
$("exchangeScheduleForm").addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!userCanManageExchange()) {
|
||||||
|
renderExchangeRestricted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = formPayload(event.target);
|
const data = formPayload(event.target);
|
||||||
if (data.mode !== "single") delete data.table;
|
if (data.mode !== "single") delete data.table;
|
||||||
if (data.schedule_type === "interval") {
|
if (data.schedule_type === "interval") {
|
||||||
@@ -2057,6 +2097,10 @@
|
|||||||
}
|
}
|
||||||
if (target.dataset.sourceDetail) openSourceDetail(target.dataset.sourceDetail);
|
if (target.dataset.sourceDetail) openSourceDetail(target.dataset.sourceDetail);
|
||||||
if (target.dataset.exchangeAction === "test") {
|
if (target.dataset.exchangeAction === "test") {
|
||||||
|
if (!userCanManageExchange()) {
|
||||||
|
renderExchangeRestricted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = formPayload($("exchangeConnectionForm"));
|
const data = formPayload($("exchangeConnectionForm"));
|
||||||
await apiFetch("/api/v1/exchange/connections/test/", {
|
await apiFetch("/api/v1/exchange/connections/test/", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -2105,6 +2149,10 @@
|
|||||||
await refreshDashboard();
|
await refreshDashboard();
|
||||||
}
|
}
|
||||||
if (target.dataset.exchangeToggle) {
|
if (target.dataset.exchangeToggle) {
|
||||||
|
if (!userCanManageExchange()) {
|
||||||
|
renderExchangeRestricted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await apiFetch(`/api/v1/exchange/periodic-tasks/${target.dataset.exchangeToggle}/`, {
|
await apiFetch(`/api/v1/exchange/periodic-tasks/${target.dataset.exchangeToggle}/`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ enabled: target.dataset.enabled === "true" }),
|
body: JSON.stringify({ enabled: target.dataset.enabled === "true" }),
|
||||||
@@ -2112,6 +2160,10 @@
|
|||||||
await refreshExchange();
|
await refreshExchange();
|
||||||
}
|
}
|
||||||
if (target.dataset.exchangeDelete) {
|
if (target.dataset.exchangeDelete) {
|
||||||
|
if (!userCanManageExchange()) {
|
||||||
|
renderExchangeRestricted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await apiFetch(`/api/v1/exchange/periodic-tasks/${target.dataset.exchangeDelete}/`, { method: "DELETE" });
|
await apiFetch(`/api/v1/exchange/periodic-tasks/${target.dataset.exchangeDelete}/`, { method: "DELETE" });
|
||||||
await refreshExchange();
|
await refreshExchange();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ class FrontendUserWithProfileSerializer(serializers.ModelSerializer):
|
|||||||
profile = FrontendUserProfileSerializer(read_only=True)
|
profile = FrontendUserProfileSerializer(read_only=True)
|
||||||
role = serializers.SerializerMethodField()
|
role = serializers.SerializerMethodField()
|
||||||
role_label = serializers.SerializerMethodField()
|
role_label = serializers.SerializerMethodField()
|
||||||
|
capabilities = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@@ -157,6 +158,7 @@ class FrontendUserWithProfileSerializer(serializers.ModelSerializer):
|
|||||||
"is_active",
|
"is_active",
|
||||||
"role",
|
"role",
|
||||||
"role_label",
|
"role_label",
|
||||||
|
"capabilities",
|
||||||
"profile",
|
"profile",
|
||||||
)
|
)
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
@@ -167,6 +169,9 @@ class FrontendUserWithProfileSerializer(serializers.ModelSerializer):
|
|||||||
def get_role_label(self, obj) -> str:
|
def get_role_label(self, obj) -> str:
|
||||||
return UserService.get_role_label(self.get_role(obj))
|
return UserService.get_role_label(self.get_role(obj))
|
||||||
|
|
||||||
|
def get_capabilities(self, obj) -> dict:
|
||||||
|
return UserService.get_user_capabilities(obj)
|
||||||
|
|
||||||
|
|
||||||
class FrontendManagedUserSerializer(serializers.ModelSerializer):
|
class FrontendManagedUserSerializer(serializers.ModelSerializer):
|
||||||
"""Короткий сериализатор пользователя без вложенного профиля."""
|
"""Короткий сериализатор пользователя без вложенного профиля."""
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from apps.parsers.models import FinancialReport, FinancialReportLine, ProcurementRecord
|
from apps.parsers.models import (
|
||||||
|
FinancialReport,
|
||||||
|
FinancialReportLine,
|
||||||
|
GenericParserRecord,
|
||||||
|
ParserLoadLog,
|
||||||
|
ProcurementRecord,
|
||||||
|
)
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
@@ -121,6 +127,36 @@ class ParsersViewSetTest(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
self.assertEqual(detail.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_eis_result_endpoint_uses_generic_records_without_breaking_old_zakupki_api(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
old_record = _create_procurement_record()
|
||||||
|
generic_record = GenericParserRecord.objects.create(
|
||||||
|
load_batch=1,
|
||||||
|
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
|
||||||
|
external_id="eis-44fz-1",
|
||||||
|
inn=_digits(10),
|
||||||
|
organisation_name="EIS Customer",
|
||||||
|
title="EIS 44-FZ notice",
|
||||||
|
status="published",
|
||||||
|
payload={"registry": "44fz"},
|
||||||
|
)
|
||||||
|
self.client.force_authenticate(self.user)
|
||||||
|
|
||||||
|
old_response = self.client.get(reverse("api_v1:zakupki:procurements-list"))
|
||||||
|
new_response = self.client.get("/api/v1/eis/procurements-44fz/")
|
||||||
|
detail_response = self.client.get(
|
||||||
|
f"/api/v1/eis/procurements-44fz/{generic_record.id}/"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(old_response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(old_response.data["data"][0]["id"], old_record.id)
|
||||||
|
self.assertEqual(new_response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(new_response.data["meta"]["pagination"]["count"], 1)
|
||||||
|
self.assertEqual(new_response.data["data"][0]["id"], generic_record.id)
|
||||||
|
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(detail_response.data["data"]["payload"], {"registry": "44fz"})
|
||||||
|
|
||||||
def test_dashboard_data_exposes_source_groups_for_page(self):
|
def test_dashboard_data_exposes_source_groups_for_page(self):
|
||||||
self.client.force_authenticate(self.user)
|
self.client.force_authenticate(self.user)
|
||||||
|
|
||||||
@@ -133,6 +169,19 @@ class ParsersViewSetTest(APITestCase):
|
|||||||
self.assertIn("uploads", payload["groups"])
|
self.assertIn("uploads", payload["groups"])
|
||||||
self.assertEqual(payload["api_sources"], payload["groups"]["api"])
|
self.assertEqual(payload["api_sources"], payload["groups"]["api"])
|
||||||
self.assertEqual(payload["file_sources"], payload["groups"]["uploads"])
|
self.assertEqual(payload["file_sources"], payload["groups"]["uploads"])
|
||||||
|
sources = {item["key"]: item for item in payload["sources"]}
|
||||||
|
self.assertEqual(
|
||||||
|
sources["procurements_44fz"]["result_list_url"],
|
||||||
|
"/api/v1/eis/procurements-44fz/",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
sources["procurements_223fz"]["result_list_url"],
|
||||||
|
"/api/v1/eis/procurements-223fz/",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
sources["contracts"]["result_list_url"],
|
||||||
|
"/api/v1/eis/contracts/",
|
||||||
|
)
|
||||||
|
|
||||||
def test_financial_reports_list_and_retrieve(self):
|
def test_financial_reports_list_and_retrieve(self):
|
||||||
report = FinancialReport.objects.create(
|
report = FinancialReport.objects.create(
|
||||||
|
|||||||
@@ -179,12 +179,14 @@ class CurrentUserViewTest(APITestCase):
|
|||||||
"is_active",
|
"is_active",
|
||||||
"role",
|
"role",
|
||||||
"role_label",
|
"role_label",
|
||||||
|
"capabilities",
|
||||||
"profile",
|
"profile",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.data["id"], self.user.id)
|
self.assertEqual(response.data["id"], self.user.id)
|
||||||
self.assertEqual(response.data["email"], self.user.email)
|
self.assertEqual(response.data["email"], self.user.email)
|
||||||
self.assertEqual(response.data["role"], "user")
|
self.assertEqual(response.data["role"], "user")
|
||||||
|
self.assertFalse(response.data["capabilities"]["can_manage_exchange"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
set(response.data["profile"].keys()),
|
set(response.data["profile"].keys()),
|
||||||
{"first_name", "middle_name", "last_name", "full_name"},
|
{"first_name", "middle_name", "last_name", "full_name"},
|
||||||
@@ -267,6 +269,7 @@ class AdminUserManagementViewTest(APITestCase):
|
|||||||
"is_active",
|
"is_active",
|
||||||
"role",
|
"role",
|
||||||
"role_label",
|
"role_label",
|
||||||
|
"capabilities",
|
||||||
"profile",
|
"profile",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -274,9 +277,16 @@ class AdminUserManagementViewTest(APITestCase):
|
|||||||
set(response.data["results"][0]["profile"].keys()),
|
set(response.data["results"][0]["profile"].keys()),
|
||||||
{"first_name", "middle_name", "last_name", "full_name"},
|
{"first_name", "middle_name", "last_name", "full_name"},
|
||||||
)
|
)
|
||||||
usernames = {item["username"] for item in response.data["results"]}
|
users_by_name = {item["username"]: item for item in response.data["results"]}
|
||||||
|
usernames = set(users_by_name)
|
||||||
self.assertIn(self.admin.username, usernames)
|
self.assertIn(self.admin.username, usernames)
|
||||||
self.assertIn(self.user.username, usernames)
|
self.assertIn(self.user.username, usernames)
|
||||||
|
self.assertTrue(
|
||||||
|
users_by_name[self.admin.username]["capabilities"]["can_manage_exchange"]
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
users_by_name[self.user.username]["capabilities"]["can_manage_exchange"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_admin_can_search_users(self):
|
def test_admin_can_search_users(self):
|
||||||
ProfileFactory.create_profile(
|
ProfileFactory.create_profile(
|
||||||
|
|||||||
Reference in New Issue
Block a user