feat: composants d'affichage de la roadmap #94
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import dash_bootstrap_components as dbc
|
||||||
|
from dash import dcc, html
|
||||||
|
|
||||||
|
from src.roadmap import db as roadmap_db
|
||||||
|
from src.roadmap import github
|
||||||
|
|
||||||
|
_CHANGELOG_PATH = Path(__file__).resolve().parents[2] / "CHANGELOG.md"
|
||||||
|
|
||||||
|
|
||||||
|
def balance_text(balance: int) -> str:
|
||||||
|
mot = "vote" if balance == 1 else "votes"
|
||||||
|
return f"Il te reste {balance} {mot}."
|
||||||
|
|
||||||
|
|
||||||
|
def changelog_markdown() -> dcc.Markdown:
|
||||||
|
try:
|
||||||
|
text = _CHANGELOG_PATH.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
text = "_Changelog indisponible._"
|
||||||
|
return dcc.Markdown(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _vote_item(issue: dict, count: int, editable: bool):
|
||||||
|
mot = "vote" if count == 1 else "votes"
|
||||||
|
children = [
|
||||||
|
html.A(issue["title"], href=issue["html_url"], target="_blank"),
|
||||||
|
html.Span(f" — {count} {mot}", className="text-muted ms-1"),
|
||||||
|
]
|
||||||
|
if editable:
|
||||||
|
children.append(
|
||||||
|
dbc.Button(
|
||||||
|
"Voter",
|
||||||
|
id={"type": "roadmap-vote", "index": issue["number"]},
|
||||||
|
size="sm",
|
||||||
|
color="primary",
|
||||||
|
className="ms-2",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return dbc.ListGroupItem(children)
|
||||||
|
|
||||||
|
|
||||||
|
def vote_items(au_vote: list[dict], counts: dict[int, int], editable: bool) -> list:
|
||||||
|
ordered = sorted(au_vote, key=lambda i: counts.get(i["number"], 0), reverse=True)
|
||||||
|
if not ordered:
|
||||||
|
return [dbc.ListGroupItem("Aucune fonctionnalité au vote pour le moment.")]
|
||||||
|
return [_vote_item(i, counts.get(i["number"], 0), editable) for i in ordered]
|
||||||
|
|
||||||
|
|
||||||
|
def _en_cours_items(en_cours: list[dict]) -> list:
|
||||||
|
if not en_cours:
|
||||||
|
return [dbc.ListGroupItem("Rien en cours pour le moment.")]
|
||||||
|
return [
|
||||||
|
dbc.ListGroupItem(html.A(i["title"], href=i["html_url"], target="_blank"))
|
||||||
|
for i in en_cours
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def roadmap_content(editable: bool, balance: int | None = None) -> html.Div:
|
||||||
|
try:
|
||||||
|
issues = github.fetch_roadmap_issues()
|
||||||
|
except Exception:
|
||||||
|
issues = {"en_cours": [], "au_vote": []}
|
||||||
|
counts = roadmap_db.vote_counts()
|
||||||
|
|
||||||
|
body: list = []
|
||||||
|
if editable and balance is not None:
|
||||||
|
body.append(
|
||||||
|
dbc.Alert(balance_text(balance), color="info", id="roadmap-balance")
|
||||||
|
)
|
||||||
|
body.append(html.H3("En cours", className="mt-3"))
|
||||||
|
body.append(dbc.ListGroup(_en_cours_items(issues["en_cours"]), className="mb-4"))
|
||||||
|
body.append(html.H3("Au vote"))
|
||||||
|
body.append(
|
||||||
|
dbc.ListGroup(
|
||||||
|
vote_items(issues["au_vote"], counts, editable),
|
||||||
|
id="roadmap-vote-list",
|
||||||
|
className="mb-4",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
body.append(html.Hr())
|
||||||
|
body.append(html.H3("Changelog"))
|
||||||
|
body.append(changelog_markdown())
|
||||||
|
return html.Div(body)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from dash import dcc, html
|
||||||
|
|
||||||
|
from src.roadmap import ui
|
||||||
|
|
||||||
|
|
||||||
|
def test_balance_text_singular_plural():
|
||||||
|
assert ui.balance_text(1) == "Il te reste 1 vote."
|
||||||
|
assert ui.balance_text(3) == "Il te reste 3 votes."
|
||||||
|
|
||||||
|
|
||||||
|
def test_vote_items_sorted_by_count_desc():
|
||||||
|
au_vote = [
|
||||||
|
{"number": 1, "title": "A", "html_url": "u1"},
|
||||||
|
{"number": 2, "title": "B", "html_url": "u2"},
|
||||||
|
]
|
||||||
|
counts = {1: 1, 2: 5}
|
||||||
|
items = ui.vote_items(au_vote, counts, editable=False)
|
||||||
|
# le plus voté (numéro 2) vient en premier
|
||||||
|
assert "B" in str(items[0])
|
||||||
|
assert "A" in str(items[1])
|
||||||
|
|
||||||
|
|
||||||
|
def test_vote_items_buttons_only_when_editable():
|
||||||
|
au_vote = [{"number": 1, "title": "A", "html_url": "u1"}]
|
||||||
|
assert "Voter" in str(ui.vote_items(au_vote, {1: 0}, editable=True))
|
||||||
|
assert "Voter" not in str(ui.vote_items(au_vote, {1: 0}, editable=False))
|
||||||
|
|
||||||
|
|
||||||
|
def test_changelog_markdown_returns_component():
|
||||||
|
comp = ui.changelog_markdown()
|
||||||
|
assert isinstance(comp, dcc.Markdown)
|
||||||
|
assert "##" in comp.children # le CHANGELOG.md contient des titres markdown
|
||||||
|
|
||||||
|
|
||||||
|
def test_roadmap_content_renders(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
ui.github,
|
||||||
|
"fetch_roadmap_issues",
|
||||||
|
lambda: {
|
||||||
|
"en_cours": [{"number": 9, "title": "En cours X", "html_url": "u9"}],
|
||||||
|
"au_vote": [{"number": 5, "title": "Au vote Y", "html_url": "u5"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(ui.roadmap_db, "vote_counts", lambda: {5: 3})
|
||||||
|
content = ui.roadmap_content(editable=True, balance=2)
|
||||||
|
s = str(content)
|
||||||
|
assert isinstance(content, html.Div)
|
||||||
|
assert "En cours X" in s
|
||||||
|
assert "Au vote Y" in s
|
||||||
|
assert "Il te reste 2 votes." in s
|
||||||
Reference in New Issue
Block a user