forked from yvv/decision
v3 : le vote déplié sous l'article, le panier, la signature par le portefeuille Ğ1
- Le vote se déplie sous l'article, façon accordéon, l'URL pour état (?vote=) :
niveau d'eau, totaux, seuil et voix manquantes, ton vote et où il en est
(enregistré, inscrit sur la chaîne, en attente, à reconfirmer, non compté),
inertie et formule. Le reste d'une version (coquilles, traductions,
signalements, historique) passe dans l'onglet « Détails » du panneau.
- Le panier : article par article, « tout pour / tout contre » par section ou
pour tout le document, jamais pour un article à plusieurs versions ; un seul
geste (une écriture, ou une transaction).
- La signature : le portefeuille de la page (injectedWeb3), polkadot-api chargé
au moment de signer ; sans portefeuille, Ğecko par lien et QR ; la page
attend la confirmation. Un bulletin que le portefeuille n'a pas signé est
lâché aussitôt (DELETE /api/ballots/{id}).
- Pastilles d'ambiance : flèches dans le groupe, anneau de focus pointillé à
la couleur d'accent de la page (3:1 au moins), distinct du choix.
- e2e : chaîne simulée (indexeur et nœud), faux portefeuille ; C21–C24, et
C4, C5, C6, C11, C17, C19, C20, M1 mis à jour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -310,6 +310,29 @@ pub async fn status(
|
||||
Ok(Json(json!({ "ballot": view(&state, &row).await? })))
|
||||
}
|
||||
|
||||
/// `DELETE /api/ballots/{id}` — the wallet refused to sign, or could not pay:
|
||||
/// nothing left for the chain, so the ballot is let go at once rather than shown
|
||||
/// as waiting for a day. Only a pending ballot, only by the session that asked.
|
||||
pub async fn drop(
|
||||
State(state): State<AppState>,
|
||||
User(user): User,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let row = load(&state, &id).await?;
|
||||
if row.requested_by != user.id {
|
||||
return Err(AppError::NotFound);
|
||||
}
|
||||
sqlx::query(
|
||||
"UPDATE ballots SET status = 'expired', reason = 'dropped', settled_at = ? WHERE id = ? AND status = 'pending'",
|
||||
)
|
||||
.bind(util::now())
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
let row = load(&state, &id).await?;
|
||||
Ok(Json(json!({ "ballot": view(&state, &row).await? })))
|
||||
}
|
||||
|
||||
async fn view(state: &AppState, row: &BallotRow) -> AppResult<serde_json::Value> {
|
||||
let voter: Option<String> = match &row.voter_id {
|
||||
Some(v) => {
|
||||
@@ -579,6 +602,7 @@ pub struct MyBallot {
|
||||
|
||||
/// Per version: the most recent pending ballot of `me`; else its most recent
|
||||
/// ballot if that one was refused or let go (a confirmed one shows as its vote).
|
||||
/// A ballot the wallet never signed (dropped) says nothing.
|
||||
pub async fn my_ballots(state: &AppState, me: &str, doc_id: &str) -> AppResult<HashMap<String, MyBallot>> {
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row {
|
||||
@@ -591,7 +615,8 @@ pub async fn my_ballots(state: &AppState, me: &str, doc_id: &str) -> AppResult<H
|
||||
let rows = sqlx::query_as::<_, Row>(
|
||||
"SELECT l.proposal_id, b.id, b.status, b.reason, l.choice FROM ballots b
|
||||
JOIN ballot_lines l ON l.ballot_id = b.id
|
||||
WHERE b.doc_id = ? AND b.requested_by = ? ORDER BY b.created_at, b.rowid",
|
||||
WHERE b.doc_id = ? AND b.requested_by = ? AND IFNULL(b.reason, '') <> 'dropped'
|
||||
ORDER BY b.created_at, b.rowid",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.bind(me)
|
||||
|
||||
@@ -121,7 +121,7 @@ pub fn api_router() -> Router<AppState> {
|
||||
.route("/docs/{id}/proposals", post(proposals::create))
|
||||
.route("/docs/{id}/votes", post(ballots::vote_many))
|
||||
.route("/docs/{id}/ballots", post(ballots::create))
|
||||
.route("/ballots/{id}", get(ballots::status))
|
||||
.route("/ballots/{id}", get(ballots::status).delete(ballots::drop))
|
||||
.route("/proposals/{id}", get(proposals::detail).patch(proposals::edit))
|
||||
.route("/proposals/{id}/submit", post(proposals::submit))
|
||||
.route("/proposals/{id}/withdraw", post(proposals::withdraw))
|
||||
|
||||
@@ -568,6 +568,37 @@ async fn a_ballot_not_seen_in_time_is_let_go() {
|
||||
assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["status"], "expired");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_ballot_the_wallet_refused_to_sign_is_let_go_at_once() {
|
||||
let indexer = Indexer::start().await;
|
||||
let (mut c, state) = Client::new(chain_config(&indexer)).await;
|
||||
c.login("Chiara").await;
|
||||
let doc = c.doc("monnaie-libre").await;
|
||||
let i1 = version(&doc, "I1", "inForce");
|
||||
let first = c.ballot(&doc, &[(&i1, Some("against"))], Some(ALICE)).await;
|
||||
let b = c.ballot(&doc, &[(&i1, Some("for"))], Some(ALICE)).await;
|
||||
assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["id"], b["id"]);
|
||||
// Another session may not let it go.
|
||||
let mut other = c.other();
|
||||
other.login("Yvv").await;
|
||||
let (s, _) = other.call("DELETE", &format!("/api/ballots/{}", b["id"].as_str().expect("id")), None).await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
// Its own session does: no longer waiting, and it says nothing under the article.
|
||||
let dropped = c.ok("DELETE", &format!("/api/ballots/{}", b["id"].as_str().expect("id")), None).await;
|
||||
assert_eq!(
|
||||
(dropped["ballot"]["status"].as_str(), dropped["ballot"]["reason"].as_str()),
|
||||
(Some("expired"), Some("dropped"))
|
||||
);
|
||||
assert_eq!(find(&c.doc("monnaie-libre").await, &i1)["myBallot"]["id"], first["id"]);
|
||||
// A confirmed ballot is never let go.
|
||||
indexer.pay(100, first["comment"].as_str().expect("c"), alice());
|
||||
ballots::sweep(&state).await.expect("sweep");
|
||||
let kept = c.ok("DELETE", &format!("/api/ballots/{}", first["id"].as_str().expect("id")), None).await;
|
||||
assert_eq!(kept["ballot"]["status"], "confirmed");
|
||||
let now = c.doc("monnaie-libre").await;
|
||||
assert_eq!((find(&now, &i1)["myVote"].as_str(), find(&now, &i1)["myBallot"].is_null()), (Some("against"), true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_session_sees_its_own_ballots_only() {
|
||||
let indexer = Indexer::start().await;
|
||||
|
||||
Reference in New Issue
Block a user