Fix admin API gaps for users, exchange checks, and parser logs

This commit is contained in:
2026-03-19 16:48:38 +01:00
parent 25176f31b4
commit 941c268d32
22 changed files with 817 additions and 28 deletions

View File

@@ -5,6 +5,7 @@ Views для приложения парсеров.
Добавление и удаление данных - через парсеры и админку.
"""
import csv
import hashlib
import time
import uuid
@@ -43,7 +44,9 @@ from apps.parsers.serializers import (
from apps.parsers.source_cards import SourceCardService
from apps.parsers.tasks import process_fns_file
from django.conf import settings
from django.db.models import Count
from django.db.models import CharField, Count, Q
from django.db.models.functions import Cast
from django.http import HttpResponse
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
@@ -64,6 +67,46 @@ FNS_TAG = swagger_tag("ФНС - Бухгалтерская отчетность"
SOURCES_TAG = swagger_tag("Источники для фронта", "frontend_sources")
SYSTEM_TAG = swagger_tag("Системные", "system")
PARSER_LOG_ORDERING_FIELDS = {
"id",
"batch_id",
"source",
"status",
"records_count",
"created_at",
"updated_at",
}
def _get_parser_logs_queryset(*, search_query: str = ""):
queryset = ParserLoadLog.objects.all().order_by("-created_at")
search_term = search_query.strip()
if not search_term:
return queryset
return queryset.annotate(
batch_id_text=Cast("batch_id", output_field=CharField())
).filter(
Q(source__icontains=search_term)
| Q(status__icontains=search_term)
| Q(error_message__icontains=search_term)
| Q(batch_id_text__icontains=search_term)
)
def _apply_safe_ordering(queryset, ordering: str, allowed_fields: set[str]):
order_by_fields = []
for raw_field in (item.strip() for item in ordering.split(",") if item.strip()):
field_name = raw_field[1:] if raw_field.startswith("-") else raw_field
if field_name not in allowed_fields:
continue
order_by_fields.append(raw_field)
if not order_by_fields:
return queryset
return queryset.order_by(*order_by_fields)
# =============================================================================
# Минпромторг - Сертификаты промышленного производства
@@ -703,10 +746,15 @@ class ParserLoadLogViewSet(ReadOnlyModelViewSet):
Только для администраторов.
"""
queryset = ParserLoadLog.objects.all().order_by("-created_at")
serializer_class = ParserLoadLogSerializer
permission_classes = [IsAdminUser]
filterset_fields = ["source", "status", "batch_id"]
ordering_fields = list(PARSER_LOG_ORDERING_FIELDS)
def get_queryset(self):
return _get_parser_logs_queryset(
search_query=self.request.query_params.get("search", "")
)
@swagger_auto_schema(
tags=[SYSTEM_TAG],
@@ -714,8 +762,30 @@ class ParserLoadLogViewSet(ReadOnlyModelViewSet):
operation_description=(
"Возвращает историю загрузок данных парсерами.\n"
"Доступно только администраторам.\n"
"Поддерживает фильтрацию по: source, status, batch_id."
"Поддерживает фильтрацию по: source, status, batch_id.\n"
"Поддерживает search по source, status, batch_id и error_message.\n"
"Поддерживает ordering по: id, batch_id, source, status, records_count, "
"created_at, updated_at."
),
manual_parameters=[
openapi.Parameter(
name="search",
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=False,
description="Поиск по source, status, batch_id и error_message",
),
openapi.Parameter(
name="ordering",
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=False,
description=(
"Сортировка по id, batch_id, source, status, records_count, "
"created_at, updated_at. Для обратной сортировки используйте префикс -"
),
),
],
responses={
200: ParserLoadLogSerializer(many=True),
**ErrorResponses.ADMIN,
@@ -737,6 +807,121 @@ class ParserLoadLogViewSet(ReadOnlyModelViewSet):
return super().retrieve(request, *args, **kwargs)
class ParserLoadLogExportView(APIView):
"""Экспорт истории загрузок парсеров в CSV."""
permission_classes = [IsAdminUser]
@swagger_auto_schema(
tags=[SYSTEM_TAG],
operation_summary="Экспорт истории загрузок",
operation_description=(
"Выгружает историю загрузок парсеров в CSV. "
"Поддерживает те же фильтры, search и ordering, что и список логов."
),
manual_parameters=[
openapi.Parameter(
name="source",
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=False,
description="Фильтр по коду источника",
),
openapi.Parameter(
name="status",
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=False,
description="Фильтр по статусу",
),
openapi.Parameter(
name="batch_id",
in_=openapi.IN_QUERY,
type=openapi.TYPE_INTEGER,
required=False,
description="Фильтр по ID пакета",
),
openapi.Parameter(
name="search",
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=False,
description="Поиск по source, status, batch_id и error_message",
),
openapi.Parameter(
name="ordering",
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
required=False,
description="Сортировка по тем же полям, что и в списке логов",
),
],
responses={
200: "CSV файл",
**ErrorResponses.ADMIN,
},
)
def get(self, request):
queryset = _get_parser_logs_queryset(
search_query=request.query_params.get("search", "")
)
source = request.query_params.get("source")
status_value = request.query_params.get("status")
batch_id = request.query_params.get("batch_id")
if source:
queryset = queryset.filter(source=source)
if status_value:
queryset = queryset.filter(status=status_value)
if batch_id:
queryset = queryset.filter(batch_id=batch_id)
queryset = _apply_safe_ordering(
queryset,
request.query_params.get("ordering", ""),
PARSER_LOG_ORDERING_FIELDS,
)
serializer = ParserLoadLogSerializer(queryset, many=True)
response = HttpResponse(content_type="text/csv; charset=utf-8")
response["Content-Disposition"] = 'attachment; filename="parser-load-logs.csv"'
writer = csv.writer(response)
writer.writerow(
[
"id",
"batch_id",
"source",
"source_display",
"records_count",
"organizations_count",
"status",
"error_message",
"created_at",
"updated_at",
]
)
for row in serializer.data:
writer.writerow(
[
row["id"],
row["batch_id"],
row["source"],
row["source_display"],
row["records_count"],
row["organizations_count"],
row["status"],
row["error_message"],
row["created_at"],
row["updated_at"],
]
)
return response
class ProxyViewSet(ReadOnlyModelViewSet):
"""
API для просмотра списка прокси-серверов.