Fix admin API gaps for users, exchange checks, and parser logs
This commit is contained in:
@@ -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 для просмотра списка прокси-серверов.
|
||||
|
||||
Reference in New Issue
Block a user