use std::collections::hash_map::Entry; use std::collections::HashMap; use rocket::fairing::AdHoc; use rocket::form::Form; use rocket::http::CookieJar; use rocket::response::Redirect; use rocket::serde::{Serialize, Deserialize}; use rocket::serde::json::Json; use rocket_db_pools::{sqlx, Connection}; use crate::{auth, database, week}; use crate::database_records::{Vote, VotingData}; #[derive(FromForm)] pub struct VoteForm { truth_votes: HashMap } #[post("//vote", data="
")] pub async fn vote(week: u8, form: Form, mut db: Connection, cookies: &CookieJar<'_>) -> Redirect { let user = auth::get_user(week, &mut db, cookies).await; if !user.logged_in { cookies.add(("toast_error", "Vous n'avez pas la permission de changer de vote.")); return Redirect::to(uri!(week::week(week))); } let filtered_votes = form.truth_votes.iter().filter_map( |vote| { if *vote.1 != 0 { Some(vote) } else { None } } ); let mut had_error = false; for (truth_id, voted_id) in filtered_votes { match user.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) { Some(vote) => { if *voted_id == vote.voted_id { continue; } debug!("Player {:?} updating vote {:?} for truth {truth_id} : \n\t\ Previously voted {:?}, now voted {voted_id}", user.id, vote.id, vote.voted_id); match sqlx::query("UPDATE Votes SET voted_id = $3 WHERE truth_id == $1 AND voter_id == $2;") .bind(truth_id) .bind(user.id) .bind(voted_id) .fetch_optional(&mut **db) .await { Ok(_) => {} Err(error) => { error!("Error while submitting a vote : {error}"); had_error = true; } }; } None => { debug!("Player {:?} voting {voted_id} for truth {truth_id}", user.id); // TODO: Find a way to use only one statement ? --> query_many // Cannot all launch and await because all connect to DB match sqlx::query("INSERT INTO Votes (truth_id, voter_id, voted_id) VALUES ($1, $2, $3);") .bind(truth_id) .bind(user.id) .bind(voted_id) .fetch_optional(&mut **db) .await { Ok(_) => {} Err(error) => { error!("Error while submitting a vote : {error}"); had_error = true; } }; } } } if had_error { cookies.add(("toast_error", "Il y a eu un problème lors de la soumission du vote !")); } else { debug!("Vote successful") } Redirect::to(uri!(week::week(week))) } #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] pub struct VoteData { truth_count: u8, votes: HashMap>, } // TODO: Cache vote count ? Maintain in state ? #[get("//votes", format = "application/json")] pub async fn fetch_vote_data(week: u8, mut db: Connection) -> Option> { let raw_votes: Vec = sqlx::query_as(" SELECT Players.name as votes_for, Truths.number as truth_number, count(*) as votes FROM Votes JOIN Players ON Votes.voted_id == Players.id JOIN Truths on Votes.truth_id == Truths.id AND Truths.week == $1 GROUP BY votes_for, truth_number ORDER BY votes_for, truth_number;") .bind(week) .fetch_all(&mut **db) .await.unwrap_or_else(|error| { error!("Error while fetching vote data {error}"); Vec::::new() }); let truth_count: u8 = sqlx::query_scalar("SELECT count(id) from Truths WHERE week == $1;") .bind(week) .fetch_one(&mut **db) .await.unwrap_or(0); let player_count: u8 = sqlx::query_scalar("SELECT count(id) from Players WHERE is_admin == 0;") .fetch_one(&mut **db) .await.unwrap_or(0); // Each player should have a truth assigned to them which they cannot vote on. let max_vote_count = truth_count * (player_count - 1); let vote_count = raw_votes.iter().fold(0, |count, votes| {count + votes.votes}); // Only show the graph if we have all the votes and this is not the last week. if max_vote_count == 0 || vote_count < max_vote_count || week == sqlx::query_scalar("SELECT number from Weeks WHERE is_last_week == 1;") .fetch_optional(&mut **db) .await.unwrap_or(Some(0)).unwrap_or(0) { return None; } // The hash map makes storing and retrieving the data really easy, *but* // it does lose the order of the original array (which is sorted via the SQL). let mut vote_data = HashMap::>::new(); let mut next_truth_number = 1; for raw_vote in raw_votes { // The truth_number is monotonous (sorted by the SQL request). // If the next vote's truth is lower, we have changed player. if next_truth_number > raw_vote.truth_number { next_truth_number = 1; } let votes_for_player: &mut Vec = match vote_data.entry(raw_vote.votes_for) { Entry::Occupied(existing) => {existing.into_mut()} Entry::Vacant(vacant) => { next_truth_number = 1; // We changed user, reset to 1. vacant.insert(Vec::::new())} }; // Fill up missing spaces if we are missing any. for _ in next_truth_number..raw_vote.truth_number { votes_for_player.push(0); } // Update with the vote result. votes_for_player.push(raw_vote.votes); next_truth_number = raw_vote.truth_number + 1; } Some(Json(VoteData{truth_count: truth_count, votes: vote_data})) } // FIXME: pub fn stage() -> AdHoc { AdHoc::on_ignite("SQLx Stage", |rocket| async { rocket.mount("/", routes![vote, fetch_vote_data]) }) }