f1c28acc96
Marque is_home=True sur les marqueurs du home_type (acheteur ou titulaire selon la page) dans get_org_location_map. Côté client, pointToLayer donne à ce point un rayon légèrement plus grand (8 vs 5) et le ramène toujours au premier plan (bringToFront), indépendamment de l'ordre des couches.
323 lines
10 KiB
Python
323 lines
10 KiB
Python
import polars as pl
|
|
|
|
|
|
def _make_lff(rows):
|
|
return pl.LazyFrame(rows)
|
|
|
|
|
|
def test_compute_considerations_stats_basic():
|
|
from src.figures import compute_considerations_stats
|
|
|
|
lff = _make_lff(
|
|
[
|
|
# u1 : social oui (Clause), env non (Sans objet)
|
|
{
|
|
"uid": "u1",
|
|
"considerationsSociales": "Clause sociale",
|
|
"considerationsEnvironnementales": "Sans objet",
|
|
},
|
|
# u2 : social non (Sans objet), env oui (Critère)
|
|
{
|
|
"uid": "u2",
|
|
"considerationsSociales": "Sans objet",
|
|
"considerationsEnvironnementales": "Critère environnemental",
|
|
},
|
|
# u3 : social oui (Marché réservé), env null
|
|
{
|
|
"uid": "u3",
|
|
"considerationsSociales": "Marché réservé",
|
|
"considerationsEnvironnementales": None,
|
|
},
|
|
# u4 : social autre valeur (pas "Sans objet"), env null
|
|
{
|
|
"uid": "u4",
|
|
"considerationsSociales": "Pas de considération sociale",
|
|
"considerationsEnvironnementales": "Sans objet",
|
|
},
|
|
]
|
|
)
|
|
|
|
stats = compute_considerations_stats(lff)
|
|
|
|
# champs_renseignes : basé sur sociales. 4 non-null / 4 total -> (4, 100%).
|
|
assert stats["champs_renseignes"] == (4, 100)
|
|
# Sociales renseignées : dén=4 non-null, num=3 != "Sans objet" (u1/u3/u4) -> (4, 3, 75%).
|
|
assert stats["sociales_renseignees"] == (4, 3, 75)
|
|
# Env renseignées : dén=3 non-null (u1/u2/u4), num=1 != "Sans objet" (u2) -> (3, 1, 33%).
|
|
assert stats["environnementales_renseignees"] == (3, 1, 33)
|
|
|
|
|
|
def test_compute_considerations_stats_dedup_per_uid():
|
|
from src.figures import compute_considerations_stats
|
|
|
|
lff = _make_lff(
|
|
[
|
|
# u1 présent 2 fois (2 titulaires) -> compté une seule fois
|
|
{
|
|
"uid": "u1",
|
|
"considerationsSociales": "Clause sociale",
|
|
"considerationsEnvironnementales": "Sans objet",
|
|
},
|
|
{
|
|
"uid": "u1",
|
|
"considerationsSociales": "Clause sociale",
|
|
"considerationsEnvironnementales": "Sans objet",
|
|
},
|
|
{
|
|
"uid": "u2",
|
|
"considerationsSociales": "Sans objet",
|
|
"considerationsEnvironnementales": "Sans objet",
|
|
},
|
|
]
|
|
)
|
|
|
|
stats = compute_considerations_stats(lff)
|
|
|
|
# 2 uid distincts. Social 2 non-null, 1 != "Sans objet" (u1) -> (2, 1, 50%).
|
|
assert stats["champs_renseignes"] == (2, 100)
|
|
assert stats["sociales_renseignees"] == (2, 1, 50)
|
|
# Env 2 non-null, 0 != "Sans objet" -> (2, 0, 0%).
|
|
assert stats["environnementales_renseignees"] == (2, 0, 0)
|
|
|
|
|
|
def test_compute_considerations_stats_missing_column():
|
|
from src.figures import compute_considerations_stats
|
|
|
|
lff = _make_lff(
|
|
[
|
|
{"uid": "u1", "considerationsSociales": "Clause sociale"},
|
|
{"uid": "u2", "considerationsSociales": "Sans objet"},
|
|
]
|
|
)
|
|
|
|
stats = compute_considerations_stats(lff)
|
|
|
|
# Colonne env absente -> (0, 0). Social : 2 non-null, 1 != "Sans objet" -> (2, 1, 50%).
|
|
assert stats["champs_renseignes"] == (2, 100)
|
|
assert stats["sociales_renseignees"] == (2, 1, 50)
|
|
assert stats["environnementales_renseignees"] == (0, 0)
|
|
|
|
|
|
def test_compute_considerations_stats_empty():
|
|
from src.figures import compute_considerations_stats
|
|
|
|
lff = pl.LazyFrame(
|
|
{
|
|
"uid": pl.Series([], dtype=pl.String),
|
|
"considerationsSociales": pl.Series([], dtype=pl.String),
|
|
"considerationsEnvironnementales": pl.Series([], dtype=pl.String),
|
|
}
|
|
)
|
|
|
|
stats = compute_considerations_stats(lff)
|
|
|
|
assert stats["champs_renseignes"] == (0, 0)
|
|
assert stats["sociales_renseignees"] == (0, 0)
|
|
assert stats["environnementales_renseignees"] == (0, 0)
|
|
|
|
|
|
def test_get_considerations_card_content_returns_three_progress_bars():
|
|
import dash_bootstrap_components as dbc
|
|
from dash import html
|
|
|
|
from src.figures import get_considerations_card_content
|
|
|
|
lff = pl.LazyFrame(
|
|
[
|
|
{
|
|
"uid": "u1",
|
|
"considerationsSociales": "Clause sociale",
|
|
"considerationsEnvironnementales": "Sans objet",
|
|
},
|
|
{
|
|
"uid": "u2",
|
|
"considerationsSociales": "Sans objet",
|
|
"considerationsEnvironnementales": "Critère environnemental",
|
|
},
|
|
]
|
|
)
|
|
|
|
div = get_considerations_card_content(lff)
|
|
|
|
assert isinstance(div, html.Div)
|
|
|
|
def find_progress(component, found):
|
|
if isinstance(component, dbc.Progress):
|
|
found.append(component)
|
|
children = getattr(component, "children", None)
|
|
if isinstance(children, (list, tuple)):
|
|
for c in children:
|
|
find_progress(c, found)
|
|
elif children is not None and not isinstance(children, str):
|
|
find_progress(children, found)
|
|
return found
|
|
|
|
inner_bars = [b for b in find_progress(div, []) if getattr(b, "bar", False)]
|
|
assert len(inner_bars) == 3
|
|
|
|
bar_ren, bar_social, bar_env = inner_bars
|
|
# Bar 1 : champs renseignés (2/2 = 100%, gris)
|
|
assert bar_ren.value == 100
|
|
assert bar_ren.color == "#6c757d"
|
|
# Bar 2 : sociales parmi renseignés (u1 != "Sans objet" -> 1/2 = 50%, rose)
|
|
assert bar_social.value == 50
|
|
assert bar_social.color == "#CC6677"
|
|
# Bar 3 : env parmi renseignés (u2 != "Sans objet" -> 1/2 = 50%, vert)
|
|
assert bar_env.value == 50
|
|
assert bar_env.color == "#117733"
|
|
|
|
|
|
def test_build_org_markers_groups_and_counts():
|
|
from src.figures import build_org_markers
|
|
|
|
lff = pl.LazyFrame(
|
|
[
|
|
{
|
|
"uid": "u1",
|
|
"acheteur_longitude": 2.35,
|
|
"acheteur_latitude": 48.85,
|
|
"acheteur_nom": "ACHETEUR A",
|
|
},
|
|
{
|
|
"uid": "u2",
|
|
"acheteur_longitude": 2.35,
|
|
"acheteur_latitude": 48.85,
|
|
"acheteur_nom": "ACHETEUR A",
|
|
},
|
|
{
|
|
"uid": "u3",
|
|
"acheteur_longitude": -1.68,
|
|
"acheteur_latitude": 48.11,
|
|
"acheteur_nom": "ACHETEUR B",
|
|
},
|
|
]
|
|
)
|
|
|
|
markers = build_org_markers(lff, "acheteur")
|
|
|
|
assert len(markers) == 2
|
|
marker_a = next(m for m in markers if m["tooltip"].startswith("ACHETEUR A"))
|
|
assert marker_a["tooltip"] == "ACHETEUR A (2 marchés)"
|
|
assert marker_a["lat"] == 48.85
|
|
assert marker_a["lon"] == 2.35
|
|
assert marker_a["marker_color"] == "#E69F00"
|
|
|
|
|
|
def test_build_org_markers_filters_null_coordinates():
|
|
from src.figures import build_org_markers
|
|
|
|
lff = pl.LazyFrame(
|
|
[
|
|
{
|
|
"uid": "u1",
|
|
"acheteur_longitude": None,
|
|
"acheteur_latitude": None,
|
|
"acheteur_nom": "ACHETEUR A",
|
|
},
|
|
{
|
|
"uid": "u2",
|
|
"acheteur_longitude": 2.35,
|
|
"acheteur_latitude": 48.85,
|
|
"acheteur_nom": "ACHETEUR B",
|
|
},
|
|
]
|
|
)
|
|
|
|
markers = build_org_markers(lff, "acheteur")
|
|
|
|
assert len(markers) == 1
|
|
assert markers[0]["tooltip"] == "ACHETEUR B (1 marchés)"
|
|
|
|
|
|
def test_build_org_markers_missing_columns_returns_empty():
|
|
from src.figures import build_org_markers
|
|
|
|
lff = pl.LazyFrame([{"uid": "u1", "acheteur_nom": "ACHETEUR A"}])
|
|
|
|
assert build_org_markers(lff, "acheteur") == []
|
|
assert build_org_markers(lff, "titulaire") == []
|
|
|
|
|
|
def _org_map_dff():
|
|
return pl.DataFrame(
|
|
[
|
|
{
|
|
"uid": "u1",
|
|
"acheteur_longitude": 2.35,
|
|
"acheteur_latitude": 48.85,
|
|
"acheteur_nom": "ACHETEUR A",
|
|
"titulaire_longitude": -1.68,
|
|
"titulaire_latitude": 48.11,
|
|
"titulaire_nom": "TITULAIRE A",
|
|
}
|
|
]
|
|
)
|
|
|
|
|
|
def test_get_org_location_map_bounds_cover_home_and_counterpart():
|
|
import dash_leaflet as dl
|
|
|
|
from src.figures import get_org_location_map
|
|
|
|
leaflet_map = get_org_location_map(_org_map_dff(), "acheteur", "test-map")
|
|
|
|
assert isinstance(leaflet_map, dl.Map)
|
|
assert leaflet_map.bounds == [[48.11, -1.68], [48.85, 2.35]]
|
|
|
|
geojson_layers = [c for c in leaflet_map.children if isinstance(c, dl.GeoJSON)]
|
|
assert {layer.id for layer in geojson_layers} == {
|
|
"test-map-acheteur",
|
|
"test-map-titulaire",
|
|
}
|
|
|
|
|
|
def test_get_org_location_map_marks_home_org_marker_acheteur():
|
|
import dash_leaflet as dl
|
|
|
|
from src.figures import get_org_location_map
|
|
|
|
leaflet_map = get_org_location_map(_org_map_dff(), "acheteur", "test-map")
|
|
|
|
geojson_layers = {
|
|
layer.id: layer
|
|
for layer in leaflet_map.children
|
|
if isinstance(layer, dl.GeoJSON)
|
|
}
|
|
acheteur_features = geojson_layers["test-map-acheteur"].data["features"]
|
|
titulaire_features = geojson_layers["test-map-titulaire"].data["features"]
|
|
|
|
assert all(f["properties"]["is_home"] for f in acheteur_features)
|
|
assert all(not f["properties"].get("is_home") for f in titulaire_features)
|
|
|
|
|
|
def test_get_org_location_map_marks_home_org_marker_titulaire():
|
|
import dash_leaflet as dl
|
|
|
|
from src.figures import get_org_location_map
|
|
|
|
leaflet_map = get_org_location_map(_org_map_dff(), "titulaire", "test-map")
|
|
|
|
geojson_layers = {
|
|
layer.id: layer
|
|
for layer in leaflet_map.children
|
|
if isinstance(layer, dl.GeoJSON)
|
|
}
|
|
acheteur_features = geojson_layers["test-map-acheteur"].data["features"]
|
|
titulaire_features = geojson_layers["test-map-titulaire"].data["features"]
|
|
|
|
assert all(f["properties"]["is_home"] for f in titulaire_features)
|
|
assert all(not f["properties"].get("is_home") for f in acheteur_features)
|
|
|
|
|
|
def test_get_org_location_map_defaults_to_france_view_without_coordinates():
|
|
from src.figures import get_org_location_map
|
|
|
|
dff = pl.DataFrame(
|
|
[{"uid": "u1", "acheteur_nom": "ACHETEUR A", "titulaire_nom": "TITULAIRE A"}]
|
|
)
|
|
|
|
leaflet_map = get_org_location_map(dff, "acheteur", "test-map")
|
|
|
|
assert leaflet_map.center == [46.6, 2.2]
|
|
assert leaflet_map.zoom == 5
|