feat(vues): resolveur pur du parametre ?vue= (#112)
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""Résolution publique d'une vue depuis le paramètre d'URL ?vue=<slug>_<token>.
|
||||
|
||||
Fonction pure (aucune dépendance à Dash), testable directement : elle prend le
|
||||
paramètre brut et le schéma, renvoie un dict décrivant la vue à appliquer ou une
|
||||
erreur. Le slug est ignoré ; seul le jeton fait foi.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from src.saved_views import db, ui
|
||||
from src.utils import logger
|
||||
from src.utils.query_ast import ast_from_dict, ast_to_filtermodel
|
||||
|
||||
NOT_FOUND_MESSAGE = "Cette vue est introuvable ou a été supprimée."
|
||||
|
||||
|
||||
def _error() -> dict:
|
||||
return {
|
||||
"found": False,
|
||||
"filter_model": None,
|
||||
"column_state": None,
|
||||
"hidden_columns": None,
|
||||
"token": None,
|
||||
"url": None,
|
||||
"error": NOT_FOUND_MESSAGE,
|
||||
}
|
||||
|
||||
|
||||
def resolve_vue_param(vue_param: str | None, schema) -> dict:
|
||||
token = ui.token_from_vue_param(vue_param)
|
||||
if not token:
|
||||
return _error()
|
||||
row = db.get_by_token(token)
|
||||
if row is None:
|
||||
return _error()
|
||||
try:
|
||||
view = json.loads(row["query"])
|
||||
# AST canonique stocké par save_view (cf. spec vues sauvegardées).
|
||||
ast = ast_from_dict(view.get("ast"))
|
||||
filter_model = ast_to_filtermodel(ast, schema)
|
||||
column_state = view.get("columnState") or []
|
||||
except (json.JSONDecodeError, TypeError, AttributeError):
|
||||
# Vue pré-migration (query string, pas du JSON) : repli propre, même
|
||||
# message que pour un jeton inconnu (anti-énumération).
|
||||
logger.warning(
|
||||
"Vue partagée au format pré-migration, non applicable : "
|
||||
f"token={token!r} name={row['name']!r}"
|
||||
)
|
||||
return _error()
|
||||
hidden_columns = [c["colId"] for c in column_state if c.get("hide")]
|
||||
return {
|
||||
"found": True,
|
||||
"filter_model": filter_model,
|
||||
"column_state": column_state,
|
||||
"hidden_columns": hidden_columns,
|
||||
"token": row["token"],
|
||||
"url": ui.build_view_url(row["name"], row["token"]),
|
||||
"error": None,
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import json
|
||||
|
||||
from src.auth import db as auth_db
|
||||
from src.db import schema
|
||||
from src.saved_views import db as saved_views_db
|
||||
from src.saved_views import resolve
|
||||
from src.utils.query_ast import And, Condition, ast_to_dict
|
||||
|
||||
|
||||
def _make_user(email="u@ex.fr"):
|
||||
auth_db.init_schema()
|
||||
return auth_db.create_user(email, "hash")
|
||||
|
||||
|
||||
def _seed_view(uid, name="Ma vue"):
|
||||
ast = And([Condition("objet", "contains", "route")])
|
||||
column_state = [
|
||||
{"colId": "montant", "sort": "desc"},
|
||||
{"colId": "acheteur_nom", "hide": True},
|
||||
]
|
||||
query = json.dumps({"ast": ast_to_dict(ast), "columnState": column_state})
|
||||
return saved_views_db.upsert(uid, "tableau", name, query)
|
||||
|
||||
|
||||
def test_resolve_found_applies_view(monkeypatch, users_db_path):
|
||||
monkeypatch.setattr(resolve.ui, "DOMAIN_NAME", "test.colibre.fr")
|
||||
saved_views_db.init_schema()
|
||||
uid = _make_user()
|
||||
token = _seed_view(uid, "Mes Marchés")
|
||||
|
||||
out = resolve.resolve_vue_param(f"mes-marches_{token}", schema)
|
||||
|
||||
assert out["found"] is True
|
||||
assert out["filter_model"] == {
|
||||
"objet": {"filterType": "text", "type": "contains", "filter": "route"}
|
||||
}
|
||||
assert out["hidden_columns"] == ["acheteur_nom"]
|
||||
assert out["token"] == token
|
||||
assert out["url"] == f"https://test.colibre.fr/tableau?vue=mes-marches_{token}"
|
||||
assert out["error"] is None
|
||||
|
||||
|
||||
def test_resolve_slug_is_ignored(monkeypatch, users_db_path):
|
||||
monkeypatch.setattr(resolve.ui, "DOMAIN_NAME", "test.colibre.fr")
|
||||
saved_views_db.init_schema()
|
||||
uid = _make_user()
|
||||
token = _seed_view(uid)
|
||||
# Slug bidon → même résolution.
|
||||
out = resolve.resolve_vue_param(f"nimportequoi_{token}", schema)
|
||||
assert out["found"] is True
|
||||
assert out["token"] == token
|
||||
|
||||
|
||||
def test_resolve_unknown_token_returns_error(users_db_path):
|
||||
saved_views_db.init_schema()
|
||||
_make_user()
|
||||
out = resolve.resolve_vue_param("slug_zzzzzz", schema)
|
||||
assert out["found"] is False
|
||||
assert out["error"] == resolve.NOT_FOUND_MESSAGE
|
||||
assert out["filter_model"] is None
|
||||
|
||||
|
||||
def test_resolve_empty_param_returns_error(users_db_path):
|
||||
saved_views_db.init_schema()
|
||||
out = resolve.resolve_vue_param("", schema)
|
||||
assert out["found"] is False
|
||||
assert out["error"] == resolve.NOT_FOUND_MESSAGE
|
||||
|
||||
|
||||
def test_resolve_corrupt_query_returns_error(users_db_path):
|
||||
saved_views_db.init_schema()
|
||||
uid = _make_user()
|
||||
# query pré-migration (pas du JSON) → même message de repli.
|
||||
token = saved_views_db.upsert(uid, "tableau", "Vieille", "filtres=a&tris=b")
|
||||
out = resolve.resolve_vue_param(f"vieille_{token}", schema)
|
||||
assert out["found"] is False
|
||||
assert out["error"] == resolve.NOT_FOUND_MESSAGE
|
||||
Reference in New Issue
Block a user