From fb45c8e7d578b4c7cef7d579c55d1d4fdc691b91 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 30 Jun 2026 13:55:28 +0200 Subject: [PATCH] feat: registre des votes feature_votes #94 Co-Authored-By: Claude Sonnet 4.6 --- src/app.py | 11 ++++++++++ src/roadmap/__init__.py | 0 src/roadmap/db.py | 42 +++++++++++++++++++++++++++++++++++++++ tests/roadmap/__init__.py | 0 tests/roadmap/conftest.py | 18 +++++++++++++++++ tests/roadmap/test_db.py | 31 +++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+) create mode 100644 src/roadmap/__init__.py create mode 100644 src/roadmap/db.py create mode 100644 tests/roadmap/__init__.py create mode 100644 tests/roadmap/conftest.py create mode 100644 tests/roadmap/test_db.py diff --git a/src/app.py b/src/app.py index 8802592..8147238 100644 --- a/src/app.py +++ b/src/app.py @@ -76,6 +76,13 @@ cache.init_app( app: Dash = Dash( server=server, + # name="src" (et non "src.app") pour que use_pages enregistre les pages sous + # le namespace `src.pages.*`, identique aux imports inter-pages (ex. + # inscription.py: `from src.pages.connexion import linkedin_button`). Sinon + # Dash découvre `pages.connexion` tandis que l'import explicite crée + # `src.pages.connexion` : deux identités, même URL → "duplicate paths" → + # check_for_duplicate_pathnames lève à chaque requête → 500 sur toute l'app. + name="src", title="decp.info", use_pages=True, suppress_callback_exceptions=True, @@ -109,6 +116,10 @@ from src.saved_views import db as saved_views_db # noqa: E402 saved_views_db.init_schema() +from src.roadmap import db as roadmap_db # noqa: E402 + +roadmap_db.init_schema() + # robots.txt @app.server.route("/robots.txt") diff --git a/src/roadmap/__init__.py b/src/roadmap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/roadmap/db.py b/src/roadmap/db.py new file mode 100644 index 0000000..8f1c37e --- /dev/null +++ b/src/roadmap/db.py @@ -0,0 +1,42 @@ +from datetime import datetime, timezone + +from src.auth.db import get_conn + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS feature_votes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + issue_number INTEGER NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_feature_votes_issue + ON feature_votes(issue_number); +""" + + +def _now() -> str: + return datetime.now(timezone.utc).isoformat() + + +def init_schema() -> None: + get_conn().executescript(SCHEMA) + + +def record_vote(user_id: int, issue_number: int) -> None: + get_conn().execute( + "INSERT INTO feature_votes (user_id, issue_number, created_at) " + "VALUES (?, ?, ?)", + (user_id, issue_number, _now()), + ) + + +def vote_counts() -> dict[int, int]: + rows = ( + get_conn() + .execute( + "SELECT issue_number, COUNT(*) FROM feature_votes GROUP BY issue_number" + ) + .fetchall() + ) + return {row[0]: row[1] for row in rows} diff --git a/tests/roadmap/__init__.py b/tests/roadmap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/roadmap/conftest.py b/tests/roadmap/conftest.py new file mode 100644 index 0000000..adff4b2 --- /dev/null +++ b/tests/roadmap/conftest.py @@ -0,0 +1,18 @@ +import pytest + +# Dash minimal pour que register_page() fonctionne dans les tests de pages de ce +# répertoire (CONFIG peuplé). Instancié une seule fois, ici, avant tout import de page. +from dash import Dash as _Dash + +_Dash(__name__, use_pages=True, pages_folder="", assets_folder="assets") + + +@pytest.fixture +def users_db_path(monkeypatch, tmp_path): + from src.auth.db import reset_conn_for_tests + + db_path = tmp_path / "users.test.sqlite" + monkeypatch.setenv("USERS_DB_PATH", str(db_path)) + reset_conn_for_tests() + yield db_path + reset_conn_for_tests() diff --git a/tests/roadmap/test_db.py b/tests/roadmap/test_db.py new file mode 100644 index 0000000..2d454ca --- /dev/null +++ b/tests/roadmap/test_db.py @@ -0,0 +1,31 @@ +from src.auth import db as auth_db +from src.roadmap import db as roadmap_db + + +def _make_user(email="u@ex.fr"): + auth_db.init_schema() + return auth_db.create_user(email, "hash") + + +def test_init_schema_creates_feature_votes(users_db_path): + roadmap_db.init_schema() + conn = auth_db.get_conn() + tables = { + row[0] + for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'") + } + assert "feature_votes" in tables + + +def test_record_vote_and_counts(users_db_path): + roadmap_db.init_schema() + uid = _make_user() + roadmap_db.record_vote(uid, 42) + roadmap_db.record_vote(uid, 42) # vote multiple autorisé + roadmap_db.record_vote(uid, 7) + assert roadmap_db.vote_counts() == {42: 2, 7: 1} + + +def test_vote_counts_empty(users_db_path): + roadmap_db.init_schema() + assert roadmap_db.vote_counts() == {}