Compare commits
No commits in common. "5b14d8829015bcc12c1e080d5099f93f277c4f2b" and "493142b990bcfb4ec074cda88d31924f5d470dd7" have entirely different histories.
5b14d88290
...
493142b990
3 changed files with 1 additions and 247 deletions
|
|
@ -3454,32 +3454,6 @@ async def get_parcel_best_layouts_pdf(
|
||||||
raise HTTPException(status_code=500, detail="Internal server error") from exc
|
raise HTTPException(status_code=500, detail="Internal server error") from exc
|
||||||
|
|
||||||
|
|
||||||
def _filter_competitors_by_dev(
|
|
||||||
rows: list[dict[str, Any]], exclude_dev: str | None
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Отфильтровать строки-конкуренты по имени застройщика (dev_name).
|
|
||||||
|
|
||||||
`exclude_dev` — CSV нормализованных (casefold) имён застройщиков, которых
|
|
||||||
нужно скрыть из блока «Конкуренты» PDF-снимка. Сравнение регистронезависимо.
|
|
||||||
Пустой/None `exclude_dev` → список возвращается без изменений. Строки с
|
|
||||||
пустым/None dev_name НЕ исключаются (исключаем только явные совпадения).
|
|
||||||
"""
|
|
||||||
if not exclude_dev:
|
|
||||||
return rows
|
|
||||||
excluded = {s.strip().casefold() for s in exclude_dev.split(",") if s.strip()}
|
|
||||||
if not excluded:
|
|
||||||
return rows
|
|
||||||
kept = [r for r in rows if (r.get("dev_name") or "").strip().casefold() not in excluded]
|
|
||||||
dropped = len(rows) - len(kept)
|
|
||||||
if dropped:
|
|
||||||
logger.info(
|
|
||||||
"snapshot_pdf: excluded %d competitors by dev_name filter %s",
|
|
||||||
dropped,
|
|
||||||
sorted(excluded),
|
|
||||||
)
|
|
||||||
return kept
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{cad_num}/snapshot.pdf",
|
"/{cad_num}/snapshot.pdf",
|
||||||
summary="1-page PDF snapshot участка (НСПД + POI + конкуренты)",
|
summary="1-page PDF snapshot участка (НСПД + POI + конкуренты)",
|
||||||
|
|
@ -3487,15 +3461,6 @@ def _filter_competitors_by_dev(
|
||||||
def parcel_snapshot_pdf(
|
def parcel_snapshot_pdf(
|
||||||
cad_num: str,
|
cad_num: str,
|
||||||
db: Annotated[Session, Depends(get_db)],
|
db: Annotated[Session, Depends(get_db)],
|
||||||
exclude_dev: Annotated[
|
|
||||||
str | None,
|
|
||||||
Query(
|
|
||||||
description=(
|
|
||||||
"Скрыть конкурентов этих застройщиков (dev_name) из блока «Конкуренты». "
|
|
||||||
"Несколько — через запятую. Регистронезависимо. Пример: exclude_dev=Практика"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
] = None,
|
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""Генерирует одностраничный PDF-снимок участка (A4).
|
"""Генерирует одностраничный PDF-снимок участка (A4).
|
||||||
|
|
||||||
|
|
@ -3631,11 +3596,6 @@ def parcel_snapshot_pdf(
|
||||||
.all()
|
.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
# 4b) Опционально скрыть конкурентов конкретных застройщиков (client-кейс:
|
|
||||||
# убрать оффер девелопера X из отчёта для девелопера Y). Без exclude_dev —
|
|
||||||
# список идентичен прежнему (zero behavior change).
|
|
||||||
competitor_rows = _filter_competitors_by_dev(competitor_rows, exclude_dev)
|
|
||||||
|
|
||||||
# 5) Получить district (через пересечение с ekb_districts)
|
# 5) Получить district (через пересечение с ekb_districts)
|
||||||
district: str | None = None
|
district: str | None = None
|
||||||
district_row = (
|
district_row = (
|
||||||
|
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
"""Тесты для query-параметра exclude_dev в GET /parcels/{cad}/snapshot.pdf.
|
|
||||||
|
|
||||||
Скрывает конкурентов конкретных застройщиков (dev_name) из блока «Конкуренты»
|
|
||||||
PDF-снимка. Client-кейс: убрать оффер девелопера X из отчёта для девелопера Y.
|
|
||||||
|
|
||||||
Юнит-тесты — над чистой функцией `_filter_competitors_by_dev`.
|
|
||||||
Плюс 1 endpoint smoke (mock DB + patched WeasyPrint-рендер).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
from app.api.v1.parcels import _filter_competitors_by_dev
|
|
||||||
from app.main import app
|
|
||||||
|
|
||||||
|
|
||||||
def _rows() -> list[dict[str, Any]]:
|
|
||||||
return [
|
|
||||||
{"dev_name": "Практика", "comm_name": "ЖК-А", "flat_count": 100, "distance_m": 200.0},
|
|
||||||
{"dev_name": "Брусника", "comm_name": "ЖК-Б", "flat_count": 200, "distance_m": 400.0},
|
|
||||||
{"dev_name": "Атлас", "comm_name": "ЖК-В", "flat_count": 50, "distance_m": 800.0},
|
|
||||||
{"dev_name": None, "comm_name": "ЖК-Г", "flat_count": 30, "distance_m": 900.0},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# ── Unit: _filter_competitors_by_dev ──────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_none_is_noop() -> None:
|
|
||||||
"""exclude_dev=None → список идентичен исходному (zero behavior change)."""
|
|
||||||
rows = _rows()
|
|
||||||
assert _filter_competitors_by_dev(rows, None) == rows
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_empty_string_is_noop() -> None:
|
|
||||||
"""Пустая строка / только запятые → ничего не исключаем."""
|
|
||||||
rows = _rows()
|
|
||||||
assert _filter_competitors_by_dev(rows, "") == rows
|
|
||||||
assert _filter_competitors_by_dev(rows, " , ,") == rows
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_single_dev() -> None:
|
|
||||||
"""exclude_dev=Практика → строки этого застройщика исключены, остальные есть."""
|
|
||||||
kept = _filter_competitors_by_dev(_rows(), "Практика")
|
|
||||||
names = [r["dev_name"] for r in kept]
|
|
||||||
assert "Практика" not in names
|
|
||||||
assert "Брусника" in names
|
|
||||||
assert "Атлас" in names
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_case_insensitive() -> None:
|
|
||||||
"""Регистронезависимо: 'практика' / 'ПРАКТИКА' исключают 'Практика'."""
|
|
||||||
for variant in ("практика", "ПРАКТИКА", "ПрАкТиКа"):
|
|
||||||
kept = _filter_competitors_by_dev(_rows(), variant)
|
|
||||||
assert "Практика" not in [r["dev_name"] for r in kept]
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_multi_dev() -> None:
|
|
||||||
"""exclude_dev=Практика,Брусника → обе исключены, прочие остаются."""
|
|
||||||
kept = _filter_competitors_by_dev(_rows(), "Практика,Брусника")
|
|
||||||
names = [r["dev_name"] for r in kept]
|
|
||||||
assert "Практика" not in names
|
|
||||||
assert "Брусника" not in names
|
|
||||||
assert "Атлас" in names
|
|
||||||
assert None in names # dev_name=None не исключается
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_none_dev_name_not_excluded() -> None:
|
|
||||||
"""Строка с dev_name=None не падает и не исключается даже при фильтре."""
|
|
||||||
kept = _filter_competitors_by_dev(_rows(), "Практика")
|
|
||||||
assert any(r["dev_name"] is None for r in kept)
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_whitespace_in_csv() -> None:
|
|
||||||
"""Пробелы вокруг имён в CSV тримятся."""
|
|
||||||
kept = _filter_competitors_by_dev(_rows(), " Практика , Брусника ")
|
|
||||||
names = [r["dev_name"] for r in kept]
|
|
||||||
assert "Практика" not in names
|
|
||||||
assert "Брусника" not in names
|
|
||||||
|
|
||||||
|
|
||||||
# ── Endpoint smoke: mock DB + patched render ──────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def _parcel_row() -> MagicMock:
|
|
||||||
r = MagicMock()
|
|
||||||
r.__getitem__ = lambda self, k: {
|
|
||||||
"address": "ул. Тестовая, 1",
|
|
||||||
"area_m2": 5000.0,
|
|
||||||
"land_category": "земли населённых пунктов",
|
|
||||||
"vri": "многоэтажная застройка",
|
|
||||||
"cadastral_cost": 10_000_000.0,
|
|
||||||
"last_update": None,
|
|
||||||
}[k]
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def _geom_row() -> MagicMock:
|
|
||||||
r = MagicMock()
|
|
||||||
r.__getitem__ = lambda self, k: {"wkt": "POINT(60.605 56.838)"}[k]
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
def _comp_mapping(dev_name: str | None, obj_id: int) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"obj_id": obj_id,
|
|
||||||
"comm_name": f"ЖК-{obj_id}",
|
|
||||||
"dev_name": dev_name,
|
|
||||||
"obj_class": "comfort",
|
|
||||||
"flat_count": 100,
|
|
||||||
"distance_m": 100.0 * obj_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _make_snapshot_db() -> MagicMock:
|
|
||||||
"""Порядок execute в parcel_snapshot_pdf:
|
|
||||||
1. parcel_row → .mappings().first()
|
|
||||||
2. geom → .mappings().first()
|
|
||||||
3. poi → .mappings().all()
|
|
||||||
4. competitors→ .mappings().all()
|
|
||||||
5. district → .mappings().first()
|
|
||||||
"""
|
|
||||||
db = MagicMock()
|
|
||||||
|
|
||||||
res_parcel = MagicMock()
|
|
||||||
res_parcel.mappings.return_value.first.return_value = _parcel_row()
|
|
||||||
|
|
||||||
res_geom = MagicMock()
|
|
||||||
res_geom.mappings.return_value.first.return_value = _geom_row()
|
|
||||||
|
|
||||||
res_poi = MagicMock()
|
|
||||||
res_poi.mappings.return_value.all.return_value = []
|
|
||||||
|
|
||||||
res_comp = MagicMock()
|
|
||||||
res_comp.mappings.return_value.all.return_value = [
|
|
||||||
_comp_mapping("Практика", 1),
|
|
||||||
_comp_mapping("Брусника", 2),
|
|
||||||
]
|
|
||||||
|
|
||||||
res_district = MagicMock()
|
|
||||||
res_district.mappings.return_value.first.return_value = None
|
|
||||||
|
|
||||||
db.execute.side_effect = [res_parcel, res_geom, res_poi, res_comp, res_district]
|
|
||||||
return db
|
|
||||||
|
|
||||||
|
|
||||||
def _override_db(db: MagicMock):
|
|
||||||
def _get_db_override():
|
|
||||||
yield db
|
|
||||||
|
|
||||||
return _get_db_override
|
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_endpoint_exclude_dev_filters_render_input(monkeypatch) -> None:
|
|
||||||
"""С exclude_dev=Практика конкурент с dev_name='Практика' не доходит до рендера."""
|
|
||||||
captured: dict[str, Any] = {}
|
|
||||||
|
|
||||||
def _fake_render(**kwargs: Any) -> bytes:
|
|
||||||
captured["competitor_rows"] = kwargs.get("competitor_rows")
|
|
||||||
return b"%PDF-1.4 fake"
|
|
||||||
|
|
||||||
monkeypatch.setattr("app.api.v1.parcels.generate_snapshot_pdf", _fake_render)
|
|
||||||
|
|
||||||
from app.core.db import get_db
|
|
||||||
|
|
||||||
app.dependency_overrides[get_db] = _override_db(_make_snapshot_db())
|
|
||||||
try:
|
|
||||||
client = TestClient(app)
|
|
||||||
resp = client.get("/api/v1/parcels/66:41:0303161:5/snapshot.pdf?exclude_dev=Практика")
|
|
||||||
assert resp.status_code == 200, resp.text
|
|
||||||
devs = [r["dev_name"] for r in captured["competitor_rows"]]
|
|
||||||
assert "Практика" not in devs
|
|
||||||
assert "Брусника" in devs
|
|
||||||
finally:
|
|
||||||
app.dependency_overrides.clear()
|
|
||||||
|
|
||||||
|
|
||||||
def test_snapshot_endpoint_no_param_keeps_all(monkeypatch) -> None:
|
|
||||||
"""Без exclude_dev все конкуренты доходят до рендера (baseline)."""
|
|
||||||
captured: dict[str, Any] = {}
|
|
||||||
|
|
||||||
def _fake_render(**kwargs: Any) -> bytes:
|
|
||||||
captured["competitor_rows"] = kwargs.get("competitor_rows")
|
|
||||||
return b"%PDF-1.4 fake"
|
|
||||||
|
|
||||||
monkeypatch.setattr("app.api.v1.parcels.generate_snapshot_pdf", _fake_render)
|
|
||||||
|
|
||||||
from app.core.db import get_db
|
|
||||||
|
|
||||||
app.dependency_overrides[get_db] = _override_db(_make_snapshot_db())
|
|
||||||
try:
|
|
||||||
client = TestClient(app)
|
|
||||||
resp = client.get("/api/v1/parcels/66:41:0303161:5/snapshot.pdf")
|
|
||||||
assert resp.status_code == 200, resp.text
|
|
||||||
devs = [r["dev_name"] for r in captured["competitor_rows"]]
|
|
||||||
assert "Практика" in devs
|
|
||||||
assert "Брусника" in devs
|
|
||||||
finally:
|
|
||||||
app.dependency_overrides.clear()
|
|
||||||
|
|
@ -5240,10 +5240,7 @@ export interface operations {
|
||||||
};
|
};
|
||||||
parcel_snapshot_pdf_api_v1_parcels__cad_num__snapshot_pdf_get: {
|
parcel_snapshot_pdf_api_v1_parcels__cad_num__snapshot_pdf_get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: never;
|
||||||
/** @description Скрыть конкурентов этих застройщиков (dev_name) из блока «Конкуренты». Несколько — через запятую. Регистронезависимо. Пример: exclude_dev=Практика */
|
|
||||||
exclude_dev?: string | null;
|
|
||||||
};
|
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path: {
|
||||||
cad_num: string;
|
cad_num: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue