6 Commits

Author SHA1 Message Date
c5035f8a71 Merge remote-tracking branch 'origin/main' into dev
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 33s
CI/CD Pipeline / Build and Push Images (push) Successful in 18s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 24s
# Conflicts:
#	tests/apps/exchange/test_state_corp_services.py
2026-07-22 17:27:04 +02:00
avm
c0f6e72177 Merge pull request 'Align source API and exchange demo data' (#37) from feature/frontend-exchange-corrections-20260722 into main
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 24s
CI/CD Pipeline / Build and Push Images (push) Successful in 1s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 1s
Deploy Customer Main / Build, Push, Deploy (push) Successful in 1m7s
2026-07-22 18:21:32 +03:00
08c3d84652 test: align demo company exchange expectations
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 36s
CI/CD Pipeline / Build and Push Images (push) Successful in 19s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 24s
2026-07-22 14:33:08 +02:00
991b08263f fix: exclude demo companies from state corp exchange
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 38s
CI/CD Pipeline / Build and Push Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
CI/CD Pipeline / Internal Notify (push) Successful in 0s
2026-07-22 14:23:04 +02:00
b4c67a6188 style(parsers): format date result filters
All checks were successful
CI/CD Pipeline / Quality Gate (push) Successful in 28s
CI/CD Pipeline / Build and Push Images (push) Successful in 16s
CI/CD Pipeline / Internal Notify (push) Successful in 0s
CI/CD Pipeline / Deploy Dev via Compose (push) Successful in 23s
2026-07-21 18:23:07 +02:00
2667646d02 feat(parsers): filter source results by date
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 28s
CI/CD Pipeline / Build and Push Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped
CI/CD Pipeline / Internal Notify (push) Successful in 0s
2026-07-21 18:18:56 +02:00
4 changed files with 176 additions and 12 deletions

View File

@@ -701,6 +701,8 @@ class ParserResultQuerySerializer(serializers.Serializer):
batch_id = serializers.IntegerField(required=False, min_value=1) batch_id = serializers.IntegerField(required=False, min_value=1)
status = serializers.CharField(required=False, allow_blank=True) status = serializers.CharField(required=False, allow_blank=True)
record_date = serializers.CharField(required=False, allow_blank=True) record_date = serializers.CharField(required=False, allow_blank=True)
date_from = serializers.DateField(required=False)
date_to = serializers.DateField(required=False)
search = serializers.CharField(required=False, allow_blank=True) search = serializers.CharField(required=False, allow_blank=True)
ordering = serializers.CharField(required=False, allow_blank=True) ordering = serializers.CharField(required=False, allow_blank=True)
include_payload = serializers.BooleanField(required=False, default=True) include_payload = serializers.BooleanField(required=False, default=True)
@@ -712,6 +714,14 @@ class ParserResultQuerySerializer(serializers.Serializer):
attrs["page_size"] = attrs["limit"] attrs["page_size"] = attrs["limit"]
if attrs.get("batch_id") and not attrs.get("load_batch"): if attrs.get("batch_id") and not attrs.get("load_batch"):
attrs["load_batch"] = attrs["batch_id"] attrs["load_batch"] = attrs["batch_id"]
if (
attrs.get("date_from")
and attrs.get("date_to")
and attrs["date_from"] > attrs["date_to"]
):
raise serializers.ValidationError(
{"date_to": "date_to must be greater than or equal to date_from"}
)
return attrs return attrs

View File

@@ -10,6 +10,7 @@ import json
import uuid import uuid
from collections import defaultdict from collections import defaultdict
from apps.core.filters import BaseFilterSet
from apps.core.openapi import CommonResponses, ErrorResponses, swagger_tag from apps.core.openapi import CommonResponses, ErrorResponses, swagger_tag
from apps.core.response import api_error_response, api_response from apps.core.response import api_error_response, api_response
from apps.core.serializers import BackgroundJobListSerializer from apps.core.serializers import BackgroundJobListSerializer
@@ -73,6 +74,7 @@ from django.db.models.functions import Cast, Lower
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.text import get_valid_filename from django.utils.text import get_valid_filename
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, PeriodicTask from django_celery_beat.models import CrontabSchedule, IntervalSchedule, PeriodicTask
from django_filters import rest_framework as filters
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.utils import no_body, swagger_auto_schema from drf_yasg.utils import no_body, swagger_auto_schema
@@ -331,6 +333,20 @@ ORDERING_PARAM = openapi.Parameter(
description="Сортировка", description="Сортировка",
type=openapi.TYPE_STRING, type=openapi.TYPE_STRING,
) )
DATE_FROM_PARAM = openapi.Parameter(
"date_from",
openapi.IN_QUERY,
description="Дата записи с которой включительно отбирать результаты (YYYY-MM-DD)",
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATE,
)
DATE_TO_PARAM = openapi.Parameter(
"date_to",
openapi.IN_QUERY,
description="Дата записи по которую включительно отбирать результаты (YYYY-MM-DD)",
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATE,
)
INCLUDE_PAYLOAD_PARAM = openapi.Parameter( INCLUDE_PAYLOAD_PARAM = openapi.Parameter(
"include_payload", "include_payload",
openapi.IN_QUERY, openapi.IN_QUERY,
@@ -355,6 +371,8 @@ RESULT_LIST_PARAMS = [
OGRN_PARAM, OGRN_PARAM,
LOAD_BATCH_PARAM, LOAD_BATCH_PARAM,
STATUS_PARAM, STATUS_PARAM,
DATE_FROM_PARAM,
DATE_TO_PARAM,
SEARCH_PARAM, SEARCH_PARAM,
ORDERING_PARAM, ORDERING_PARAM,
INCLUDE_PAYLOAD_PARAM, INCLUDE_PAYLOAD_PARAM,
@@ -790,6 +808,32 @@ class IndustrialProductViewSet(ReadOnlyModelViewSet):
# ============================================================================= # =============================================================================
class InspectionFilter(BaseFilterSet):
"""Filters inspections by their normalized start date."""
date_from = filters.DateFilter(
field_name="start_date_normalized",
lookup_expr="gte",
)
date_to = filters.DateFilter(
field_name="start_date_normalized",
lookup_expr="lte",
)
class Meta:
model = InspectionRecord
fields = [
"inn",
"ogrn",
"registration_number",
"is_federal_law_248",
"data_year",
"data_month",
"load_batch",
"registry_organization",
]
class InspectionViewSet(ReadOnlyModelViewSet): class InspectionViewSet(ReadOnlyModelViewSet):
""" """
API для просмотра проверок из Единого реестра проверок. API для просмотра проверок из Единого реестра проверок.
@@ -804,16 +848,15 @@ class InspectionViewSet(ReadOnlyModelViewSet):
).order_by("-created_at") ).order_by("-created_at")
serializer_class = InspectionSerializer serializer_class = InspectionSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filterset_fields = [ filterset_class = InspectionFilter
"inn", ordering_fields = [
"ogrn", "id",
"registration_number", "start_date_normalized",
"is_federal_law_248", "end_date_normalized",
"data_year", "created_at",
"data_month", "updated_at",
"load_batch",
"registry_organization",
] ]
ordering = ["-start_date_normalized", "-created_at"]
search_fields = [ search_fields = [
"organisation_name", "organisation_name",
"registration_number", "registration_number",
@@ -829,7 +872,10 @@ class InspectionViewSet(ReadOnlyModelViewSet):
operation_description=( operation_description=(
"Возвращает список проверок из Единого реестра.\n" "Возвращает список проверок из Единого реестра.\n"
"Поддерживает фильтрацию по: inn, ogrn, registration_number, " "Поддерживает фильтрацию по: inn, ogrn, registration_number, "
"is_federal_law_248, data_year, data_month, load_batch.\n" "is_federal_law_248, data_year, data_month, load_batch, "
"date_from, date_to.\n"
"Поддерживает сортировку по: start_date_normalized, "
"end_date_normalized, created_at, updated_at.\n"
"Поддерживает поиск по: organisation_name, registration_number, " "Поддерживает поиск по: organisation_name, registration_number, "
"inn, ogrn, control_authority.\n" "inn, ogrn, control_authority.\n"
f"{REGISTRY_ORGANIZATION_SEARCH_DESCRIPTION}" f"{REGISTRY_ORGANIZATION_SEARCH_DESCRIPTION}"
@@ -2202,7 +2248,7 @@ def _native_field_map(source: str) -> dict[str, str]:
"external_id": "registration_number", "external_id": "registration_number",
"organisation_name": "organisation_name", "organisation_name": "organisation_name",
"title": "control_authority", "title": "control_authority",
"record_date": "start_date", "record_date": "start_date_normalized",
"status": "status", "status": "status",
} }
@@ -2273,6 +2319,19 @@ def _apply_native_search(queryset, source: str, search: str):
) )
def _apply_record_date_range(queryset, record_date_field: str, params: dict):
"""Filter normalized or ISO-compatible record dates inclusively."""
date_from = params.get("date_from")
if date_from:
queryset = queryset.filter(
**{f"{record_date_field}__gte": date_from.isoformat()}
)
date_to = params.get("date_to")
if date_to:
queryset = queryset.filter(**{f"{record_date_field}__lte": date_to.isoformat()})
return queryset
def _route_model_sources(descriptor) -> set[str]: def _route_model_sources(descriptor) -> set[str]:
if descriptor.source == ParserLoadLog.Source.TRUDVSEM: if descriptor.source == ParserLoadLog.Source.TRUDVSEM:
return set(VACANCY_RECORD_SOURCES) return set(VACANCY_RECORD_SOURCES)
@@ -2317,6 +2376,8 @@ def _filter_native_result_queryset(source: str, params: dict, sources: set[str])
queryset = queryset.filter(**{model_field: value}) queryset = queryset.filter(**{model_field: value})
if params.get("record_date") and field_map.get("record_date"): if params.get("record_date") and field_map.get("record_date"):
queryset = queryset.filter(**{field_map["record_date"]: params["record_date"]}) queryset = queryset.filter(**{field_map["record_date"]: params["record_date"]})
if field_map.get("record_date"):
queryset = _apply_record_date_range(queryset, field_map["record_date"], params)
if params.get("search"): if params.get("search"):
queryset = _apply_native_search(queryset, source, params["search"]) queryset = _apply_native_search(queryset, source, params["search"])
ordering = _safe_ordering( ordering = _safe_ordering(
@@ -2356,6 +2417,7 @@ def _filter_generic_result_queryset(
queryset = queryset.filter(**{field: value}) queryset = queryset.filter(**{field: value})
if params.get("record_date"): if params.get("record_date"):
queryset = queryset.filter(record_date=params["record_date"]) queryset = queryset.filter(record_date=params["record_date"])
queryset = _apply_record_date_range(queryset, "record_date", params)
if params.get("search"): if params.get("search"):
search = params["search"] search = params["search"]
queryset = queryset.filter( queryset = queryset.filter(

View File

@@ -133,7 +133,7 @@ class TestCompaniesCommandsTest(TestCase):
self.assertEqual(OrganizationSourceRecord.objects.count(), 20 * 15) self.assertEqual(OrganizationSourceRecord.objects.count(), 20 * 15)
@override_settings(STATE_CORP_EXCHANGE_TOKEN=TEST_EXCHANGE_TOKEN) @override_settings(STATE_CORP_EXCHANGE_TOKEN=TEST_EXCHANGE_TOKEN)
def test_created_source_records_are_exported_to_state_corp_package(self): def test_created_source_records_are_included_in_state_corp_package(self):
call_command("create_test_companies", stdout=StringIO()) call_command("create_test_companies", stdout=StringIO())
company_inns = list( company_inns = list(

View File

@@ -8,6 +8,7 @@ import io
import os import os
import tempfile import tempfile
import zipfile import zipfile
from datetime import date
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from apps.parsers.models import ( from apps.parsers.models import (
@@ -480,6 +481,97 @@ class ParsersViewSetTest(APITestCase):
self.assertEqual(response.data["data"][0]["id"], hh_record.id) self.assertEqual(response.data["data"][0]["id"], hh_record.id)
self.assertEqual(response.data["data"][0]["source"], "hh") self.assertEqual(response.data["data"][0]["source"], "hh")
def test_source_results_filter_and_sort_by_record_date(self):
arbitration_older = GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.ARBITRATION,
external_id="arbitration-older",
record_date="2024-01-15",
)
arbitration_newer = GenericParserRecord.objects.create(
load_batch=1,
source=ParserLoadLog.Source.ARBITRATION,
external_id="arbitration-newer",
record_date="2024-03-15",
)
vacancy_older = GenericParserRecord.objects.create(
load_batch=1,
source="hh",
external_id="hh-older",
record_date="2024-02-01",
payload={"vacancy_source": "hh"},
)
vacancy_newer = GenericParserRecord.objects.create(
load_batch=1,
source="hh",
external_id="hh-newer",
record_date="2024-04-01",
payload={"vacancy_source": "hh"},
)
self.client.force_authenticate(self.user)
arbitration_response = self.client.get(
"/api/v1/arbitration/cases/",
{"date_from": "2024-02-01", "ordering": "-record_date"},
)
vacancy_response = self.client.get(
"/api/v1/trudvsem/vacancies/",
{"date_to": "2024-02-15", "ordering": "record_date"},
)
self.assertEqual(arbitration_response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["id"] for item in arbitration_response.data["data"]],
[arbitration_newer.id],
)
self.assertNotIn(
arbitration_older.id,
[item["id"] for item in arbitration_response.data["data"]],
)
self.assertEqual(vacancy_response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["id"] for item in vacancy_response.data["data"]],
[vacancy_older.id],
)
self.assertNotIn(
vacancy_newer.id,
[item["id"] for item in vacancy_response.data["data"]],
)
def test_inspection_source_results_filter_and_sort_by_record_date(self):
older = InspectionRecordFactory(
start_date="2024-01-10",
start_date_normalized=date(2024, 1, 10),
)
newer = InspectionRecordFactory(
start_date="2024-03-10",
start_date_normalized=date(2024, 3, 10),
)
self.client.force_authenticate(self.user)
response = self.client.get(
"/api/v1/proverki/",
{"date_from": "2024-02-01", "ordering": "-start_date_normalized"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["id"] for item in response.data["data"]],
[newer.id],
)
self.assertNotIn(older.id, [item["id"] for item in response.data["data"]])
source_response = self.client.get(
"/api/v1/parsers/results/inspections/",
{"date_from": "2024-02-01", "ordering": "-record_date"},
)
self.assertEqual(source_response.status_code, status.HTTP_200_OK)
self.assertEqual(
[item["id"] for item in source_response.data["data"]],
[newer.id],
)
def test_parser_results_v1_enrich_missing_organization_fields_without_contract_change( def test_parser_results_v1_enrich_missing_organization_fields_without_contract_change(
self, self,
): ):