CI / test (push) Successful in 2m17s
- Table board_columns par espace (clé immuable, nom/couleur/
ordre libres, étiquette mappée + alias, drapeau Focus) ;
définitions semées paresseusement depuis les valeurs
historiques — sans personnalisation, comportement identique.
- Service app/board_columns.py : résolution, mapping
étiquette→colonne (alias compris, ex. kanban_a_faire de
Flowdeck), colonnes Focus/Wallboard.
- Board, API, adoption, miroirs d'étiquettes, déplacements,
capture, santé, focus, wall, dashboard branchés sur les
colonnes de l'espace ; validations dynamiques dans models
(move_issue, set_column_order) et rules.
- Administration → Colonnes : ajouter (avant Terminé),
renommer, réordonner, mapper, supprimer avec migration des
cartes vers une colonne choisie, réinitialiser ; bouton
🧱 Colonnes sur le board ; journal d'audit.
- Backlog et Terminé : colonnes système non supprimables.
- 254 tests verts (7 nouveaux, palier 20) ; smoke réel contre
le vrai Gitea : création, affichage au board, suppression.
278 lines
9.1 KiB
Python
278 lines
9.1 KiB
Python
"""Palier 20 (0.30.0): colonnes personnalisables par
|
|
espace et mapping étiquette ↔ colonne configurable."""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.main as main
|
|
from app import board_columns as bcols
|
|
from app import models
|
|
from tests.conftest import csrf_from
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_client(tmp_path, monkeypatch, fake_gitea):
|
|
monkeypatch.setenv("KANBANTE_DB_PATH", str(tmp_path / "cols.db"))
|
|
monkeypatch.setenv("KANBANTE_AUTH_USER", "bruno")
|
|
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", "motdepasse")
|
|
with TestClient(main.app) as client:
|
|
client.get("/login")
|
|
token = client.cookies.get("kanbante_csrf") or ""
|
|
client.post(
|
|
"/login",
|
|
data={
|
|
"username": "bruno",
|
|
"password": "motdepasse",
|
|
"next": "/",
|
|
"csrf_token": token,
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
yield client
|
|
|
|
|
|
@pytest.fixture
|
|
def ws_cols():
|
|
"""Un espace dédié rattaché à la connexion héritée."""
|
|
ws_id = models.create_workspace("cols-test", "ColonnesTest", "#123456")
|
|
models.set_workspace_connections(ws_id, [1])
|
|
bcols.invalidate(ws_id)
|
|
yield ws_id
|
|
models.clear_board_columns(ws_id)
|
|
bcols.invalidate(ws_id)
|
|
models.delete_workspace(ws_id)
|
|
|
|
|
|
def _issue(number, title, labels=None):
|
|
return {
|
|
"number": number,
|
|
"title": title,
|
|
"body": "",
|
|
"state": "open",
|
|
"labels": labels or [],
|
|
"assignee": None,
|
|
"assignees": [],
|
|
"milestone": None,
|
|
"due_date": None,
|
|
"created_at": "2026-09-01T09:00:00Z",
|
|
"updated_at": "2026-09-02T09:00:00Z",
|
|
"html_url": f"http://gitea.test/Projets/demo/issues/{number}",
|
|
"pull_request": None,
|
|
}
|
|
|
|
|
|
# ── Défauts et service ────────────────────────────────────
|
|
|
|
def test_defaults_match_historical_columns(ws_cols):
|
|
columns = bcols.get_columns(ws_cols)
|
|
assert [c["key"] for c in columns] == [
|
|
"backlog", "todo", "in_progress", "qa", "review", "done",
|
|
]
|
|
assert bcols.names(ws_cols)["todo"] == "📋 À faire"
|
|
assert bcols.label_for(ws_cols, "review") == "kanban:review"
|
|
assert bcols.label_for(ws_cols, "backlog") is None
|
|
assert bcols.focus_keys(ws_cols) == ["in_progress", "qa", "review"]
|
|
assert bcols.wall_keys(ws_cols) == [
|
|
"todo", "in_progress", "qa", "review",
|
|
]
|
|
|
|
|
|
def test_custom_column_at_model_level(ws_cols):
|
|
columns = bcols.get_columns(ws_cols)
|
|
models.upsert_board_column(
|
|
ws_cols,
|
|
{
|
|
"key": "dev", "name": "🛠️ Dev", "color": "#123123",
|
|
"position": 4, "label": "etape:dev", "aliases": "",
|
|
"kind": "flow", "in_focus": 1,
|
|
},
|
|
)
|
|
bcols.invalidate(ws_cols)
|
|
assert "dev" in models.column_keys(ws_cols)
|
|
models.move_issue(
|
|
"Projets/demo", 5, "dev", workspace_id=ws_cols, connection_id=1
|
|
)
|
|
state = models.get_board_state(
|
|
"Projets/demo", workspace_id=ws_cols, connection_id=1
|
|
)
|
|
assert [c["issue_number"] for c in state["dev"]] == [5]
|
|
assert bcols.label_for(ws_cols, "dev") == "etape:dev"
|
|
assert "dev" in bcols.focus_keys(ws_cols)
|
|
# Un autre espace n'est pas affecté.
|
|
other = models.create_workspace("cols-temoin", "Témoin", "#000000")
|
|
try:
|
|
assert "dev" not in models.column_keys(other)
|
|
finally:
|
|
models.delete_workspace(other)
|
|
|
|
|
|
# ── Panneau d'administration ──────────────────────────────
|
|
|
|
def test_admin_columns_crud(auth_client, ws_cols):
|
|
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
|
|
page = auth_client.get("/admin/columns")
|
|
assert page.status_code == 200
|
|
assert "Colonnes du board" in page.text
|
|
|
|
token = csrf_from(auth_client)
|
|
r = auth_client.post(
|
|
"/admin/columns",
|
|
data={
|
|
"name": "🛠️ En développement",
|
|
"color": "#224466",
|
|
"label": "etape:dev",
|
|
"aliases": "en-dev",
|
|
"csrf_token": token,
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
assert r.status_code == 303
|
|
keys = bcols.keys(ws_cols)
|
|
assert "en_developpement" in keys
|
|
# Insérée avant « done ».
|
|
assert keys.index("en_developpement") == keys.index("done") - 1
|
|
|
|
# Renommage + mapping via le formulaire de la colonne.
|
|
token = csrf_from(auth_client)
|
|
auth_client.post(
|
|
"/admin/columns/en_developpement",
|
|
data={
|
|
"name": "🚀 Dev",
|
|
"color": "#224466",
|
|
"label": "etape:dev",
|
|
"aliases": "en-dev, dev",
|
|
"in_focus": "1",
|
|
"csrf_token": token,
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
assert bcols.names(ws_cols)["en_developpement"] == "🚀 Dev"
|
|
lookup = bcols.label_lookup(ws_cols)
|
|
assert lookup["etape:dev"] == "en_developpement"
|
|
assert lookup["en-dev"] == "en_developpement"
|
|
|
|
# Le board affiche la nouvelle colonne.
|
|
board = auth_client.get("/board/Projets/demo")
|
|
assert 'data-column="en_developpement"' in board.text
|
|
assert "🚀 Dev" in board.text
|
|
|
|
|
|
def test_admin_column_delete_migrates_cards(auth_client, ws_cols):
|
|
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
|
|
token = csrf_from(auth_client)
|
|
auth_client.post(
|
|
"/admin/columns",
|
|
data={
|
|
"name": "Attente client", "color": "#445566", "label": "",
|
|
"aliases": "", "csrf_token": token,
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
assert "attente_client" in bcols.keys(ws_cols)
|
|
models.move_issue(
|
|
"Projets/demo", 5, "attente_client",
|
|
workspace_id=ws_cols, connection_id=1,
|
|
)
|
|
token = csrf_from(auth_client)
|
|
r = auth_client.post(
|
|
"/admin/columns/attente_client/delete",
|
|
data={"target": "backlog", "csrf_token": token},
|
|
follow_redirects=False,
|
|
)
|
|
assert r.status_code == 303
|
|
assert "attente_client" not in bcols.keys(ws_cols)
|
|
state = models.get_board_state(
|
|
"Projets/demo", workspace_id=ws_cols, connection_id=1
|
|
)
|
|
assert any(c["issue_number"] == 5 for c in state["backlog"])
|
|
# Les colonnes système ne se suppriment pas.
|
|
token = csrf_from(auth_client)
|
|
auth_client.post(
|
|
"/admin/columns/backlog/delete",
|
|
data={"target": "todo", "csrf_token": token},
|
|
follow_redirects=False,
|
|
)
|
|
assert "backlog" in bcols.keys(ws_cols)
|
|
|
|
|
|
def test_admin_columns_reset(auth_client, ws_cols):
|
|
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
|
|
token = csrf_from(auth_client)
|
|
auth_client.post(
|
|
"/admin/columns",
|
|
data={
|
|
"name": "Temporaire", "color": "#445566", "label": "",
|
|
"aliases": "", "csrf_token": token,
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
assert "temporaire" in bcols.keys(ws_cols)
|
|
token = csrf_from(auth_client)
|
|
auth_client.post(
|
|
"/admin/columns/reset",
|
|
data={"csrf_token": token},
|
|
follow_redirects=False,
|
|
)
|
|
assert bcols.keys(ws_cols) == [
|
|
"backlog", "todo", "in_progress", "qa", "review", "done",
|
|
]
|
|
|
|
|
|
# ── Mapping : adoption par alias et miroir personnalisé ──
|
|
|
|
def test_label_alias_adoption(auth_client, ws_cols, fake_gitea):
|
|
"""L'étiquette Flowdeck kanban_a_faire, déclarée alias de la
|
|
colonne todo, fait adopter À faire par le board."""
|
|
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
|
|
token = csrf_from(auth_client)
|
|
auth_client.post(
|
|
"/admin/columns/todo",
|
|
data={
|
|
"name": "📋 À faire", "color": "#3b82f6",
|
|
"label": "kanban:todo", "aliases": "kanban_a_faire",
|
|
"csrf_token": token,
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
fake_gitea.issues["Projets/demo"].append(
|
|
_issue(42, "Carte Flowdeck", labels=[
|
|
{"id": 950, "name": "kanban_a_faire", "color": "aaaaaa"},
|
|
])
|
|
)
|
|
payload = auth_client.get("/api/board/Projets/demo").json()
|
|
assert "42" in payload["columns"]["todo"]["html"]
|
|
state = models.get_board_state(
|
|
"Projets/demo", workspace_id=ws_cols, connection_id=1
|
|
)
|
|
assert any(c["issue_number"] == 42 for c in state["todo"])
|
|
|
|
|
|
def test_custom_label_mirror_on_move(auth_client, ws_cols, fake_gitea):
|
|
"""Déplacer une carte pose l'étiquette mappée de SA colonne."""
|
|
auth_client.cookies.set("kanbante_workspace", str(ws_cols))
|
|
token = csrf_from(auth_client)
|
|
auth_client.post(
|
|
"/admin/columns",
|
|
data={
|
|
"name": "En développement", "color": "#224466",
|
|
"label": "etape:dev", "aliases": "", "csrf_token": token,
|
|
},
|
|
follow_redirects=False,
|
|
)
|
|
token = csrf_from(auth_client)
|
|
r = auth_client.post(
|
|
"/api/move",
|
|
data={
|
|
"repo_full_name": "Projets/demo",
|
|
"issue_number": "5",
|
|
"to_column": "en_developpement",
|
|
"csrf_token": token,
|
|
},
|
|
)
|
|
assert r.status_code == 200
|
|
issue = next(
|
|
i for i in fake_gitea.issues["Projets/demo"] if i["number"] == 5
|
|
)
|
|
names = [l["name"] for l in issue.get("labels") or []]
|
|
assert "etape:dev" in names
|