perf(api): свод 2 — страницы и варианты фильтров в репозитории и сервисе
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f5bccc5ba2
commit
e3a9a08e84
@ -361,6 +361,23 @@ class SheetResponse(BaseModel):
|
||||
sort_order: int | None = None
|
||||
|
||||
|
||||
class Svod2PageResponse(BaseListResponse[SheetResponse]):
|
||||
total: int
|
||||
|
||||
|
||||
class Svod2FacetValue(BaseModel):
|
||||
value: str
|
||||
count: int
|
||||
|
||||
|
||||
class Svod2Facets(BaseModel):
|
||||
total: int
|
||||
search_total: int
|
||||
smeta_type: list[Svod2FacetValue]
|
||||
smeta_direction: list[Svod2FacetValue]
|
||||
org_name: list[Svod2FacetValue]
|
||||
|
||||
|
||||
class CellPatch(BaseModel):
|
||||
line_id: int
|
||||
column: str
|
||||
|
||||
@ -76,6 +76,122 @@ class SummaryRepository:
|
||||
)
|
||||
).all()
|
||||
|
||||
async def get_svod2_page(
|
||||
self,
|
||||
year: int,
|
||||
sheet: str,
|
||||
org_ids: list[int] | None,
|
||||
offset: int | None,
|
||||
limit: int | None,
|
||||
sort_by: str,
|
||||
sort_direction: str,
|
||||
search: str | None,
|
||||
smeta_type: str | None,
|
||||
smeta_direction: str | None,
|
||||
org_name: str | None,
|
||||
compact: bool,
|
||||
) -> tuple[list[tuple], int]:
|
||||
function_name = {
|
||||
"GO": "v3.v_svod2_go",
|
||||
"RF": "v3.v_svod2_rf",
|
||||
}[sheet]
|
||||
query = text(
|
||||
f"""
|
||||
SELECT
|
||||
'INPUT'::VARCHAR AS row_type,
|
||||
3 AS depth,
|
||||
sort_order,
|
||||
data,
|
||||
total_count
|
||||
FROM {function_name}(
|
||||
CAST(:year AS INT),
|
||||
CAST(:org_ids AS INT[]),
|
||||
CAST(:offset AS INT),
|
||||
CAST(:limit AS INT),
|
||||
CAST(:sort_by AS VARCHAR),
|
||||
CAST(:sort_direction AS VARCHAR),
|
||||
CAST(:search AS TEXT),
|
||||
CAST(:smeta_type AS TEXT),
|
||||
CAST(:smeta_direction AS TEXT),
|
||||
CAST(:org_name AS TEXT),
|
||||
CAST(:compact AS BOOLEAN)
|
||||
)
|
||||
"""
|
||||
)
|
||||
result = (
|
||||
await self.db.execute(
|
||||
query,
|
||||
{
|
||||
"year": year,
|
||||
"org_ids": org_ids,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"sort_by": sort_by,
|
||||
"sort_direction": sort_direction,
|
||||
"search": search,
|
||||
"smeta_type": smeta_type,
|
||||
"smeta_direction": smeta_direction,
|
||||
"org_name": org_name,
|
||||
"compact": compact,
|
||||
},
|
||||
)
|
||||
).all()
|
||||
total = int(result[0][4]) if result else 0
|
||||
return [tuple(row[:4]) for row in result], total
|
||||
|
||||
async def get_svod2_facets(
|
||||
self,
|
||||
year: int,
|
||||
sheet: str,
|
||||
org_ids: list[int] | None,
|
||||
search: str | None,
|
||||
smeta_type: str | None,
|
||||
smeta_direction: str | None,
|
||||
org_name: str | None,
|
||||
) -> dict:
|
||||
query = text(
|
||||
"""
|
||||
SELECT kind, value, n
|
||||
FROM v3.v_svod2_facets(
|
||||
CAST(:sheet AS VARCHAR),
|
||||
CAST(:year AS INT),
|
||||
CAST(:org_ids AS INT[]),
|
||||
CAST(:search AS TEXT),
|
||||
CAST(:smeta_type AS TEXT),
|
||||
CAST(:smeta_direction AS TEXT),
|
||||
CAST(:org_name AS TEXT)
|
||||
)
|
||||
ORDER BY kind, value
|
||||
"""
|
||||
)
|
||||
rows = (
|
||||
await self.db.execute(
|
||||
query,
|
||||
{
|
||||
"sheet": sheet,
|
||||
"year": year,
|
||||
"org_ids": org_ids,
|
||||
"search": search,
|
||||
"smeta_type": smeta_type,
|
||||
"smeta_direction": smeta_direction,
|
||||
"org_name": org_name,
|
||||
},
|
||||
)
|
||||
).all()
|
||||
facets: dict = {
|
||||
"total": 0,
|
||||
"search_total": 0,
|
||||
"smeta_type": [],
|
||||
"smeta_direction": [],
|
||||
"org_name": [],
|
||||
}
|
||||
for kind, value, count in rows:
|
||||
if kind in ("total", "search_total"):
|
||||
facets[kind] = int(count)
|
||||
elif kind in facets:
|
||||
facets[kind].append({"value": value, "count": int(count)})
|
||||
return facets
|
||||
|
||||
async def get_svod2_go_export_rows(
|
||||
self,
|
||||
year: int,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import copy
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.db.models.app_user import AppUser
|
||||
@ -6,6 +8,15 @@ from src.repository.summary_repository import SummaryRepository
|
||||
from src.repository.user_repository import UserRepository
|
||||
|
||||
|
||||
EMPTY_SVOD2_FACETS = {
|
||||
"total": 0,
|
||||
"search_total": 0,
|
||||
"smeta_type": [],
|
||||
"smeta_direction": [],
|
||||
"org_name": [],
|
||||
}
|
||||
|
||||
|
||||
class SummaryService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
@ -52,6 +63,72 @@ class SummaryService:
|
||||
sort_direction=sort_direction,
|
||||
)
|
||||
|
||||
async def get_svod2_page(
|
||||
self,
|
||||
year: int,
|
||||
sheet: str,
|
||||
user: AppUser,
|
||||
org_ids: list[int] | None = None,
|
||||
offset: int | None = None,
|
||||
limit: int | None = None,
|
||||
sort_by: str = "org_name",
|
||||
sort_direction: str = "asc",
|
||||
search: str | None = None,
|
||||
smeta_type: str | None = None,
|
||||
smeta_direction: str | None = None,
|
||||
org_name: str | None = None,
|
||||
view: str = "full",
|
||||
) -> tuple[list[tuple], int]:
|
||||
org_ids = await self._allowed_org_ids(user, org_ids)
|
||||
if org_ids == []:
|
||||
return [], 0
|
||||
|
||||
async def fetch(page_offset: int | None, page_limit: int | None):
|
||||
return await self.summary_repo.get_svod2_page(
|
||||
year=year,
|
||||
sheet=sheet,
|
||||
org_ids=org_ids,
|
||||
offset=page_offset,
|
||||
limit=page_limit,
|
||||
sort_by=sort_by,
|
||||
sort_direction=sort_direction,
|
||||
search=search,
|
||||
smeta_type=smeta_type,
|
||||
smeta_direction=smeta_direction,
|
||||
org_name=org_name,
|
||||
compact=view == "compact",
|
||||
)
|
||||
|
||||
rows, total = await fetch(offset, limit)
|
||||
if not rows and offset:
|
||||
# Страница за концом выборки: total берётся из первой строки, которой нет.
|
||||
_, total = await fetch(0, 1)
|
||||
return rows, total
|
||||
|
||||
async def get_svod2_facets(
|
||||
self,
|
||||
year: int,
|
||||
sheet: str,
|
||||
user: AppUser,
|
||||
org_ids: list[int] | None = None,
|
||||
search: str | None = None,
|
||||
smeta_type: str | None = None,
|
||||
smeta_direction: str | None = None,
|
||||
org_name: str | None = None,
|
||||
) -> dict:
|
||||
org_ids = await self._allowed_org_ids(user, org_ids)
|
||||
if org_ids == []:
|
||||
return copy.deepcopy(EMPTY_SVOD2_FACETS)
|
||||
return await self.summary_repo.get_svod2_facets(
|
||||
year=year,
|
||||
sheet=sheet,
|
||||
org_ids=org_ids,
|
||||
search=search,
|
||||
smeta_type=smeta_type,
|
||||
smeta_direction=smeta_direction,
|
||||
org_name=org_name,
|
||||
)
|
||||
|
||||
async def get_svod2_go_export_rows(
|
||||
self,
|
||||
year: int,
|
||||
|
||||
79
api/tests/unit/test_summary2_service.py
Normal file
79
api/tests/unit/test_summary2_service.py
Normal file
@ -0,0 +1,79 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.summary_service import EMPTY_SVOD2_FACETS, SummaryService
|
||||
|
||||
ROW = ("INPUT", 3, 1, {"line_id": 1})
|
||||
|
||||
|
||||
def make_service(monkeypatch, allowed):
|
||||
service = SummaryService.__new__(SummaryService)
|
||||
service.summary_repo = AsyncMock()
|
||||
service.user_repo = AsyncMock()
|
||||
monkeypatch.setattr(service, "_allowed_org_ids", AsyncMock(return_value=allowed))
|
||||
return service
|
||||
|
||||
|
||||
USER = SimpleNamespace(id=1)
|
||||
|
||||
|
||||
async def test_page_passes_params_and_compact_flag(monkeypatch):
|
||||
service = make_service(monkeypatch, [1, 2])
|
||||
service.summary_repo.get_svod2_page.return_value = ([ROW], 7)
|
||||
result = await service.get_svod2_page(
|
||||
2026, "GO", USER, org_ids=[1, 2], offset=0, limit=50,
|
||||
sort_by="name", sort_direction="desc", search="x",
|
||||
smeta_type="КВ", smeta_direction="Support", org_name="Орг", view="compact",
|
||||
)
|
||||
assert result == ([ROW], 7)
|
||||
service.summary_repo.get_svod2_page.assert_awaited_once_with(
|
||||
year=2026, sheet="GO", org_ids=[1, 2], offset=0, limit=50,
|
||||
sort_by="name", sort_direction="desc", search="x",
|
||||
smeta_type="КВ", smeta_direction="Support", org_name="Орг", compact=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_page_empty_intersection_skips_sql(monkeypatch):
|
||||
service = make_service(monkeypatch, [])
|
||||
assert await service.get_svod2_page(2026, "GO", USER, offset=0, limit=10) == ([], 0)
|
||||
service.summary_repo.get_svod2_page.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_page_past_end_second_call_for_total(monkeypatch):
|
||||
service = make_service(monkeypatch, None)
|
||||
service.summary_repo.get_svod2_page.side_effect = [([], 0), ([ROW], 42)]
|
||||
result = await service.get_svod2_page(2026, "GO", USER, offset=600, limit=50)
|
||||
assert result == ([], 42)
|
||||
second = service.summary_repo.get_svod2_page.await_args_list[1].kwargs
|
||||
assert second["offset"] == 0 and second["limit"] == 1
|
||||
|
||||
|
||||
async def test_page_empty_first_page_no_second_call(monkeypatch):
|
||||
service = make_service(monkeypatch, None)
|
||||
service.summary_repo.get_svod2_page.return_value = ([], 0)
|
||||
assert await service.get_svod2_page(2026, "GO", USER, offset=0, limit=50) == ([], 0)
|
||||
assert service.summary_repo.get_svod2_page.await_count == 1
|
||||
|
||||
|
||||
async def test_facets_empty_intersection_zeros_without_sql(monkeypatch):
|
||||
service = make_service(monkeypatch, [])
|
||||
result = await service.get_svod2_facets(2026, "GO", USER, org_ids=[5])
|
||||
assert result == EMPTY_SVOD2_FACETS
|
||||
assert result is not EMPTY_SVOD2_FACETS
|
||||
service.summary_repo.get_svod2_facets.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_facets_passes_allowed_org_ids_and_filters(monkeypatch):
|
||||
service = make_service(monkeypatch, [1])
|
||||
service.summary_repo.get_svod2_facets.return_value = {"total": 3}
|
||||
result = await service.get_svod2_facets(
|
||||
2026, "RF", USER, org_ids=[1, 9], search="x", smeta_type="КВ",
|
||||
smeta_direction="Support", org_name="Орг",
|
||||
)
|
||||
assert result == {"total": 3}
|
||||
service.summary_repo.get_svod2_facets.assert_awaited_once_with(
|
||||
year=2026, sheet="RF", org_ids=[1], search="x", smeta_type="КВ",
|
||||
smeta_direction="Support", org_name="Орг",
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user