v3 : Décisions et « À venir » côté serveur

- ld-engine/pending : un module (article) reste à voter tant qu'aucune
  version n'a été adoptée par un vote (seed a minima non confirmé, ou pas
  de version en vigueur et au moins une version ouverte) ; les variantes
  comptent pour un ; « sans ton vote » = aucun vote valide sur aucune version.
- overview.rs : GET /api/groups/{slug} ajoute « upcoming » (docs, mandats,
  protocoles), lu au démarrage dans seeds/upcoming/<groupe>.json, trilingue,
  servi seulement à qui ouvre le groupe ; GET /api/groups/{slug}/decisions,
  une ligne par doc avec toVote et withoutMyVote (si la personne vote).
- Retour après connexion par défaut : « / » au lieu de « /groupes ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-30 09:07:39 +01:00
co-authored by Claude Opus 5.5
parent d400bfc92e
commit c640413a70
7 changed files with 596 additions and 9 deletions
+1
View File
@@ -10,6 +10,7 @@ pub mod assist;
pub mod compose;
pub mod edit;
pub mod names;
pub mod pending;
pub mod threshold;
pub use threshold::{FormulaParams, InertiaPreset, Tally};
+95
View File
@@ -0,0 +1,95 @@
//! What is left to vote in a document — the line of « Décisions ».
//!
//! A module is an article (a unit). It stays **to vote** as long as no version
//! of it was adopted by a vote: its version in force is a seed a minima not yet
//! confirmed, or it has no version in force and at least one open version.
//! Variants of an article count once. For a person who votes, a module is
//! **without their vote** when they hold no valid vote (one not « to
//! reconfirm ») on any of its versions.
/// One article, as one person sees it.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Module {
/// The version in force, if any: `Some(true)` when a vote adopted or
/// confirmed it, `Some(false)` for a seed a minima.
pub in_force: Option<bool>,
/// Open versions of the article (proposals to replace, add or remove it).
pub open: u32,
/// The person holds a valid vote on one of its versions, in force or open.
pub voted: bool,
}
impl Module {
pub fn to_vote(&self) -> bool {
match self.in_force {
Some(adopted) => !adopted,
None => self.open > 0,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Pending {
/// Modules not yet adopted by a vote.
pub to_vote: u32,
/// Among them, those where the person has no valid vote.
pub without_my_vote: u32,
}
pub fn pending(modules: &[Module]) -> Pending {
modules.iter().filter(|m| m.to_vote()).fold(Pending::default(), |acc, m| Pending {
to_vote: acc.to_vote + 1,
without_my_vote: acc.without_my_vote + u32::from(!m.voted),
})
}
#[cfg(test)]
mod tests {
use super::*;
const SEED: Option<bool> = Some(false);
const ADOPTED: Option<bool> = Some(true);
fn m(in_force: Option<bool>, open: u32, voted: bool) -> Module {
Module { in_force, open, voted }
}
#[test]
fn a_seed_a_minima_stays_to_vote_until_a_vote_confirms_it() {
assert!(m(SEED, 0, false).to_vote());
assert!(m(SEED, 2, true).to_vote());
assert!(!m(ADOPTED, 0, false).to_vote());
}
#[test]
fn an_adopted_article_is_no_longer_to_vote_even_with_rivals() {
assert!(!m(ADOPTED, 3, false).to_vote());
}
#[test]
fn an_empty_slot_is_to_vote_only_with_an_open_version() {
assert!(m(None, 1, false).to_vote());
assert!(!m(None, 0, false).to_vote());
}
#[test]
fn variants_count_once_and_my_vote_on_any_version_counts() {
// Seed of the certification act: 3 seeds a minima, 30 open articles (two with a variant).
let mut modules = vec![m(SEED, 0, false); 3];
modules.extend(vec![m(None, 1, false); 28]);
modules.extend(vec![m(None, 2, false); 2]);
assert_eq!(pending(&modules), Pending { to_vote: 33, without_my_vote: 33 });
modules[0].voted = true;
modules[31].voted = true;
assert_eq!(pending(&modules), Pending { to_vote: 33, without_my_vote: 31 });
// Once adopted, an article leaves both counts, voted or not.
modules[1].in_force = ADOPTED;
modules[31].in_force = ADOPTED;
assert_eq!(pending(&modules), Pending { to_vote: 31, without_my_vote: 30 });
}
#[test]
fn nothing_to_vote_in_an_empty_document() {
assert_eq!(pending(&[]), Pending::default());
}
}