diff --git a/src/utils/table.py b/src/utils/table.py index 871f89b..612e0a8 100644 --- a/src/utils/table.py +++ b/src/utils/table.py @@ -5,6 +5,7 @@ import polars as pl from dash import no_update from polars import selectors as cs +from src.cache import cache from src.db import query_marches, schema from src.utils import logger from src.utils.data import DATA_SCHEMA @@ -373,6 +374,37 @@ def get_default_hidden_columns(page): return hidden_columns +@cache.memoize() +def _load_filter_sort_postprocess(filter_query, sort_by_key): + logger.debug( + f"Cache miss — recomputing for filter={filter_query!r} sort={sort_by_key!r}" + ) + + lff: pl.LazyFrame = query_marches().lazy() + + if filter_query: + lff = filter_table_data(lff, filter_query, "tableau") + + if sort_by_key: + sort_by = [ + {"column_id": col, "direction": direction} for col, direction in sort_by_key + ] + lff = sort_table_data(lff, sort_by) + + lff = lff.cast(pl.String) + lff = lff.fill_null("") + + dff: pl.DataFrame = lff.collect() + + dff = add_links(dff) + if "sourceFile" in dff.columns: + dff = add_resource_link(dff) + if dff.height > 0: + dff = format_values(dff) + + return dff + + def prepare_table_data( data, data_timestamp, filter_query, page_current, page_size, sort_by, source_table ): diff --git a/tests/test_table.py b/tests/test_table.py index a633332..e67676f 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -78,3 +78,80 @@ def test_normalize_sort_by_preserves_order(): [{"column_id": "b", "direction": "asc"}, {"column_id": "a", "direction": "asc"}] ) assert a_then_b != b_then_a + + +@pytest.fixture(scope="module") +def flask_app(): + """Minimal Flask app with SimpleCache so @cache.memoize() works in tests.""" + from flask import Flask + + from src.cache import cache + + app = Flask(__name__) + cache.init_app(app, config={"CACHE_TYPE": "SimpleCache"}) + return app + + +@pytest.fixture(autouse=True) +def reset_cache(flask_app): + """Ensure the flask-caching backend is empty between tests so that + cache-hit assertions are meaningful. Falls back to no-op when no + Flask app context is active (NullCache).""" + from src.cache import cache + + with flask_app.app_context(): + try: + cache.clear() + except (RuntimeError, AttributeError): + # No app context — cache is NullCache, nothing to clear + pass + yield + + +def test_load_filter_sort_postprocess_returns_dataframe( + flask_app, monkeypatch, sample_lff +): + from src.utils import table + + monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect()) + + with flask_app.app_context(): + df = table._load_filter_sort_postprocess(filter_query=None, sort_by_key=()) + + assert isinstance(df, pl.DataFrame) + assert df.height == 1 + # All values must be strings after post-processing + for col in df.columns: + assert df.schema[col] == pl.String + + +def test_load_filter_sort_postprocess_applies_filter( + flask_app, monkeypatch, sample_lff +): + from src.utils import table + + monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect()) + + with flask_app.app_context(): + df = table._load_filter_sort_postprocess( + filter_query="{objet} icontains travaux", sort_by_key=() + ) + assert df.height == 1 + + df_empty = table._load_filter_sort_postprocess( + filter_query="{objet} icontains nonexistent", sort_by_key=() + ) + assert df_empty.height == 0 + + +def test_load_filter_sort_postprocess_adds_links(flask_app, monkeypatch, sample_lff): + from src.utils import table + + monkeypatch.setattr(table, "query_marches", lambda: sample_lff.collect()) + + with flask_app.app_context(): + df = table._load_filter_sort_postprocess(filter_query=None, sort_by_key=()) + # add_links injects an wrapper around uid, acheteur_nom, titulaire_nom + assert "