Templates (v7.71.x) : - registre unifié \ emplates\ (migrations 48-49) + TemplateService.instantiate unique (UI, API v2, agent, scheduler) - sélecteur (pilule page vide, menu •••, commande /template), gestionnaire /templates, menu New ▾, From template, base inline dans un document - 141 presets système (59 pages, 42 bases, 15 blocs, 25 lignes), titre auto depuis le template, variables title réservée - récurrences RRULE + scheduler dédupliqué, agent apply_template/list_templates, API /api/templates + /api/v2/fd-templates - correctifs : bouton Templates, centrage fenêtre, filtres CSP, flux de création, variable title - tests : tests/test_fd_templates.py (19) et e2e/templates_picker.spec.js (8) Inclut le travail déjà présent dans le working tree (vues Notion : view_query/view_aggregate/form_projection/geocoding, property_types, database_table, docs agents-skills) et ignore .playwright-mcp/.
138 lines
5.9 KiB
Python
138 lines
5.9 KiB
Python
"""Vues Notion (doc architecture-vues-notion-flowdeck.md §2.4, §21).
|
|
|
|
Tests d'acceptation CU-Chart / CU-Timeline / CU-Map / CU-Feed /
|
|
CU-Dashboard / CU-Form + type place + migrations 44-47.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
|
|
def _make_base(c):
|
|
r = c.post('/db/api', json={'name': 'TodoNotion'})
|
|
assert r.status_code == 200, r.text
|
|
cid = r.json()['id']
|
|
c.post(f'/db/{cid}/properties/api', json={'name': 'Status', 'prop_type': 'select'})
|
|
c.post(f'/db/{cid}/properties/api', json={'name': 'Due', 'prop_type': 'date'})
|
|
c.post(f'/db/{cid}/properties/api', json={'name': 'Place', 'prop_type': 'place'})
|
|
# 4 Not started, 1 In progress, 1 Done (cf. CU-Chart §2.4)
|
|
for i, s in enumerate(['Not started'] * 4 + ['In progress', 'Done']):
|
|
c.post(f'/db/{cid}/pages/api', json={'title': f'tache {i}', 'properties': {'Status': s}})
|
|
# Une ligne datee 22 -> 27 octobre 2026 (CU-Timeline)
|
|
c.post(f'/db/{cid}/pages/api', json={'title': 'bruno', 'properties': {
|
|
'Status': 'In progress', 'Due': '2026-10-22...2026-10-27',
|
|
'Place': {'name': 'Beloeil', 'address': 'Beloeil, QC', 'lat': 45.56, 'lng': -73.20}}})
|
|
return cid
|
|
|
|
|
|
def test_migrations_44_47_applied(client):
|
|
from app.db import get_conn
|
|
with get_conn() as conn:
|
|
v = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()[0]
|
|
assert v >= 47
|
|
for tbl in ('geocode_cache', 'dashboard_widgets', 'form_questions'):
|
|
assert conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name=?", (tbl,)).fetchone()
|
|
|
|
|
|
def test_place_type_validation(client):
|
|
c = client
|
|
cid = _make_base(c)
|
|
r = c.post(f'/db/{cid}/pages/api', json={'title': 'Bad', 'properties': {'Place': {'lat': 1, 'lng': 2}}})
|
|
assert r.status_code == 400
|
|
r = c.post(f'/db/{cid}/pages/api', json={'title': 'Bad2', 'properties': {
|
|
'Place': {'name': 'X', 'lat': 200, 'lng': 0}}})
|
|
assert r.status_code == 400
|
|
r = c.post(f'/db/{cid}/pages/api', json={'title': 'Addr', 'properties': {'Place': 'Montreal, QC, Canada'}})
|
|
assert r.status_code in (200, 201)
|
|
|
|
|
|
def test_cu_chart_donut_and_drilldown(client):
|
|
c = client
|
|
cid = _make_base(c)
|
|
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Chart', 'view_type': 'chart', 'config': {}})
|
|
vid = r.json()['id']
|
|
c.put(f"/db/views/{vid}/config", json={'chart_type': 'doughnut', 'group_by': 'Status',
|
|
'measure': {'kind': 'count'}, 'omit_zero': False})
|
|
r = c.post(f"/db/{cid}/views/{vid}/aggregate", json={})
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data['total'] == 7
|
|
by_key = {g['key']: g for g in data['groups']}
|
|
assert by_key['Not started']['count'] == 4
|
|
assert abs(by_key['Not started']['percent'] - 4 / 7) < 0.01
|
|
r = c.get(f"/db/{cid}/views/{vid}/drilldown", params={'group': 'Not started'})
|
|
assert r.json()['count'] == 4
|
|
|
|
|
|
def test_cu_timeline_nodate_bucket(client):
|
|
c = client
|
|
cid = _make_base(c)
|
|
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'TL', 'view_type': 'timeline', 'config': {}})
|
|
vid = r.json()['id']
|
|
c.put(f"/db/views/{vid}/config", json={'date_property': 'Due', 'scale': 'quarter'})
|
|
r = c.get(f"/db/{cid}/views/{vid}/rows", params={'limit': 50})
|
|
assert r.status_code == 200
|
|
assert r.json()['undated'] == 6 # 6 lignes sans date
|
|
|
|
|
|
def test_cu_map_ceiling_and_geocode_off(client):
|
|
c = client
|
|
cid = _make_base(c)
|
|
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Map', 'view_type': 'map', 'config': {'place_property': 'Place'}})
|
|
vid = r.json()['id']
|
|
r = c.get(f"/db/{cid}/views/{vid}/rows", params={'limit': 50})
|
|
assert r.status_code == 200
|
|
# Provider none par defaut : pas d'appel externe, recherche vide.
|
|
r = c.get('/db/geocode/search', params={'q': 'Beloeil, QC'})
|
|
assert r.status_code == 200
|
|
assert r.json()['suggestions'] == []
|
|
|
|
|
|
def test_cu_dashboard_global_filter(client):
|
|
c = client
|
|
cid = _make_base(c)
|
|
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Chart', 'view_type': 'chart',
|
|
'config': {'group_by': 'Status', 'measure': {'kind': 'count'}}})
|
|
cv = r.json()['id']
|
|
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Dash', 'view_type': 'dashboard', 'config': {}})
|
|
dv = r.json()['id']
|
|
r = c.put(f"/db/{cid}/views/{dv}/widgets", json={'widgets': [
|
|
{'row_index': 0, 'position': 0, 'source_collection_id': cid,
|
|
'source_view_id': cv, 'inline_config': {}}]})
|
|
assert r.status_code == 200
|
|
widgets = c.get(f"/db/{cid}/views/{dv}/widgets").json()['widgets']
|
|
assert len(widgets) == 1
|
|
r = c.get(f"/db/{cid}/views/{dv}/widgets/{widgets[0]['id']}/data")
|
|
assert r.status_code == 200
|
|
# 13 widgets refuses (plafond 12).
|
|
r = c.put(f"/db/{cid}/views/{dv}/widgets",
|
|
json={'widgets': [{'row_index': 0, 'position': i} for i in range(13)]})
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_cu_form_builder_projection(client):
|
|
c = client
|
|
cid = _make_base(c)
|
|
r = c.post(f'/db/{cid}/views/save-as', json={'name': 'Form', 'view_type': 'form', 'config': {}})
|
|
fv = r.json()['id']
|
|
r = c.put(f"/db/{cid}/views/{fv}/form", json={
|
|
'questions': [{'property_name': 'Status', 'position': 0, 'required': True}],
|
|
'config': {'title': 'Demande', 'sharing': {'audience': 'workspace', 'anonymous': False}}})
|
|
assert r.status_code == 200
|
|
assert r.json()['projected_fields'] == 1
|
|
r = c.get(f"/db/{cid}/views/{fv}/form")
|
|
assert len(r.json()['questions']) == 1
|
|
r = c.post(f"/db/{cid}/views/{fv}/form/preview-token")
|
|
assert r.json()['submittable'] is False
|
|
|
|
|
|
def test_ssr_compat_untouched(client):
|
|
"""Les rendus SSR restent servis (compat §7.6) : chart/timeline/feed/map."""
|
|
c = client
|
|
cid = _make_base(c)
|
|
for vt in ('chart', 'timeline', 'feed', 'map', 'form', 'gantt'):
|
|
r = c.get(f"/db/{cid}/view/{vt}")
|
|
assert r.status_code == 200, vt
|
|
|
|
|
|
|