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

This commit is contained in:
2026-04-28 15:16:12 +02:00
parent d823e11f19
commit 3392502449
6 changed files with 124 additions and 5 deletions

View File

@@ -19,6 +19,9 @@ from drf_yasg.utils import swagger_auto_schema
app_name = "parser_results"
ROUTE_TITLES = {
"eis/contracts": "ЕИС Контракты",
"eis/procurements-44fz": "ЕИС Закупки 44-ФЗ",
"eis/procurements-223fz": "ЕИС Закупки 223-ФЗ",
"zakupki": "ЕИС Закупки",
"proverki": "Проверки Генпрокуратуры",
}

View File

@@ -140,7 +140,7 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
access_method="eis_official_api",
parser_strategy="eis_44fz_search",
source_notes="Официальный поиск ЕИС; XML-выгрузки ЕИС требуют отдельного discovery по реестрам.",
api_route="zakupki",
api_route="eis/procurements-44fz",
),
"procurements_223fz": ParserSourceDescriptor(
key="procurements_223fz",
@@ -155,7 +155,7 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
access_method="eis_official_api",
parser_strategy="eis_223fz_search",
source_notes="Официальный реестр положений о закупках 223-ФЗ в ЕИС.",
api_route="zakupki",
api_route="eis/procurements-223fz",
),
"contracts": ParserSourceDescriptor(
key="contracts",
@@ -169,7 +169,7 @@ PARSER_SOURCES: dict[str, ParserSourceDescriptor] = {
upstream_url="https://zakupki.gov.ru/epz/contract/search/results.html",
access_method="eis_official_api",
parser_strategy="eis_contract_registry",
api_route="zakupki",
api_route="eis/contracts",
),
"unfair_suppliers": ParserSourceDescriptor(
key="unfair_suppliers",

View File

@@ -1089,6 +1089,7 @@
const tokenKey = "mostovik_dashboard_access";
const refreshKey = "mostovik_dashboard_refresh";
let accessToken = localStorage.getItem(tokenKey) || "";
let currentUser = null;
let dashboardData = null;
let activeMainTab = "analytics";
let activeDrawerTab = "jobs";
@@ -1316,6 +1317,20 @@
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) {
activeMainTab = tab;
document.querySelectorAll("[data-main-tab]").forEach((button) => {
@@ -1599,6 +1614,7 @@
localStorage.removeItem(tokenKey);
localStorage.removeItem(refreshKey);
accessToken = "";
currentUser = null;
}
function renderRegistryUploadPanel(registries) {
@@ -1695,7 +1711,18 @@
` : `<div class="empty-state">Расписание выгрузки во внешнюю БД еще не настроено.</div>`;
}
function renderExchangeRestricted() {
setSelectOptions("exchangeCopyTable", []);
setSelectOptions("exchangeScheduleTable", []);
$("exchangeConnections").innerHTML = `<div class="empty-state">Внешняя БД доступна только пользователям с правами администратора.</div>`;
$("exchangeSchedules").innerHTML = "";
}
async function refreshExchange() {
if (!userCanManageExchange()) {
renderExchangeRestricted();
return;
}
try {
const [connections, tables, schedules] = await Promise.all([
apiFetch("/api/v1/exchange/connections/"),
@@ -1864,6 +1891,7 @@
async function refreshDashboard() {
if (!accessToken) return;
try {
await loadCurrentUser();
const data = await apiFetch("/api/v1/parsers/dashboard/");
setAuthenticated(true);
renderDashboard(data);
@@ -1984,6 +2012,10 @@
$("exchangeConnectionForm").addEventListener("submit", async (event) => {
event.preventDefault();
if (!userCanManageExchange()) {
renderExchangeRestricted();
return;
}
await apiFetch("/api/v1/exchange/connections/", {
method: "POST",
body: JSON.stringify(formPayload(event.target)),
@@ -1996,6 +2028,10 @@
$("exchangeCopyForm").addEventListener("submit", async (event) => {
event.preventDefault();
if (!userCanManageExchange()) {
renderExchangeRestricted();
return;
}
const data = formPayload(event.target);
if (data.mode !== "single") delete data.table;
await apiFetch("/api/v1/exchange/copy/", {
@@ -2007,6 +2043,10 @@
$("exchangeScheduleForm").addEventListener("submit", async (event) => {
event.preventDefault();
if (!userCanManageExchange()) {
renderExchangeRestricted();
return;
}
const data = formPayload(event.target);
if (data.mode !== "single") delete data.table;
if (data.schedule_type === "interval") {
@@ -2057,6 +2097,10 @@
}
if (target.dataset.sourceDetail) openSourceDetail(target.dataset.sourceDetail);
if (target.dataset.exchangeAction === "test") {
if (!userCanManageExchange()) {
renderExchangeRestricted();
return;
}
const data = formPayload($("exchangeConnectionForm"));
await apiFetch("/api/v1/exchange/connections/test/", {
method: "POST",
@@ -2105,6 +2149,10 @@
await refreshDashboard();
}
if (target.dataset.exchangeToggle) {
if (!userCanManageExchange()) {
renderExchangeRestricted();
return;
}
await apiFetch(`/api/v1/exchange/periodic-tasks/${target.dataset.exchangeToggle}/`, {
method: "PATCH",
body: JSON.stringify({ enabled: target.dataset.enabled === "true" }),
@@ -2112,6 +2160,10 @@
await refreshExchange();
}
if (target.dataset.exchangeDelete) {
if (!userCanManageExchange()) {
renderExchangeRestricted();
return;
}
await apiFetch(`/api/v1/exchange/periodic-tasks/${target.dataset.exchangeDelete}/`, { method: "DELETE" });
await refreshExchange();
}

View File

@@ -146,6 +146,7 @@ class FrontendUserWithProfileSerializer(serializers.ModelSerializer):
profile = FrontendUserProfileSerializer(read_only=True)
role = serializers.SerializerMethodField()
role_label = serializers.SerializerMethodField()
capabilities = serializers.SerializerMethodField()
class Meta:
model = User
@@ -157,6 +158,7 @@ class FrontendUserWithProfileSerializer(serializers.ModelSerializer):
"is_active",
"role",
"role_label",
"capabilities",
"profile",
)
read_only_fields = fields
@@ -167,6 +169,9 @@ class FrontendUserWithProfileSerializer(serializers.ModelSerializer):
def get_role_label(self, obj) -> str:
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):
"""Короткий сериализатор пользователя без вложенного профиля."""