Add a logout button that clears the auth cookies, logging out the user. It also tries to remove the auth token from the databse, but will ignore any error during the database operation. Do some include clean-ups as well.
171 lines
6.2 KiB
Rust
171 lines
6.2 KiB
Rust
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<u32, u16>
|
|
}
|
|
|
|
#[post("/<week>/vote", data="<form>")]
|
|
pub async fn vote(week: u8, form: Form<VoteForm>,
|
|
mut db: Connection<database::Db>, 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<String, Vec<u8>>,
|
|
}
|
|
|
|
// TODO: Cache vote count ? Maintain in state ?
|
|
#[get("/<week>/votes", format = "application/json")]
|
|
pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>) -> Option<Json<VoteData>> {
|
|
let raw_votes: Vec<VotingData> = 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::<VotingData>::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::<String, Vec<u8>>::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<u8> = 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::<u8>::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])
|
|
})
|
|
}
|