Compare commits

..

13 commits

Author SHA1 Message Date
a0b79a17ea Redirects: properly redirect to the current week
Previously, most redirects targeted the root of the application.
This was okay for the first part of development where only one week would be
active, but would be annoying when using multiple weeks.

Change those redirects to call week::week.

Change the login path to be dependant on the current week as well,
so it can be correctly redirected.
2024-07-26 01:08:47 +01:00
e08a46af3a week: Create new weeks and change active one
Implement a way for admins to introduce new weeks and set them as the last active one.
The last active week is the last one that is accessible to players, and will also prevent
the graph from being displayed.

Implement arrows for navigating between the weeks in the HTML.
2024-07-26 00:52:41 +01:00
635716c04b vote: Change graph display condition, don't fetch for admin
Now, the graph will only show when everyone has voted and the week is no longer
the last one active.
This is to minimize information gathering by looking during voting.

Don't fetch the vote data for the admin : it is not used, so no need to
query the database.

Update README with new TODOs and clean up main includes a bit.
2024-07-26 00:47:10 +01:00
f41f5142c9 styles: Minor reorganization
Properly split what is custom classes and regular HTML tags.
2024-07-26 00:41:05 +01:00
dfdd079ac4 templates/truth: Prevent changing vote if not last week
If the week is not active anymore, prevent changing any vote but allow adding missing ones.
2024-07-25 23:44:00 +01:00
207ce6c1d2 templates: Check vote against truth id, not number
I made a choice to only log confirmed votes : there is no blank vote in the database.
This means that when fetching a user's vote, if they have not voted for everyone
there will be votes missing.
As this is sent to the templating engine via a Vector, the ordering of the votes
will be incorrect : all existing votes will follow each other, and there will be
missing votes at the end.

Update the select logic in the truth template to account for that by checking
the truth_id directly, rather than via the index of the array. (O(N²)...)
Remove 'has_vote' as this is not useful anymore.
2024-07-25 23:30:02 +01:00
2b3dd28fed style: Fix edit box clipping the graph
It looks like the width of the element is calculated *inside* the padding, so the
outer width is really width+2*padding.
This leads to the edit box going outside of the truth column and clipping the graph.

Compute the width to take the padding into account.
2024-07-25 18:39:07 +01:00
f7e1218f21 truths: Change truth creation URL
As we will be able to create a new week, <week>/new to <week>/new_truth to
differentiate it from the week creation URL.
2024-07-25 16:54:19 +01:00
f74ed20e80 vote_chart: Add custom colors
I didn't like the default colors, so introduce a new set of them and
set them to the datasets.
Do it after the sort, otherwise the colors wouldn't stay consistent.
2024-07-24 18:11:37 +01:00
4c89a0783d vote_chart: Add title to the graph 2024-07-24 18:08:13 +01:00
d0843d600e vote_chart: Control the graph ratio switch
The media query used to switch the ratio of the graph would only fire when
switching from landscape to portrait, or the other way around.
This makes controlling the point where the graph switches to vertical is
only possible at this exact point.

Introduce a limit aspect-ratio that controls the change and use it for the
MediaQuery as well.

Make sure we don't delete/recreate the chart for no reason as well.
2024-07-24 16:56:08 +01:00
8fd8ce8220 vote_chart: Remove test code
This was written for testing the chart before plugging in to the database,
remove it now that is not useful anymore.
2024-07-24 16:27:43 +01:00
982a7ffd65 vote_chart: sort parsed votes
The SQL query retrieving the votes is deterministicly sorted, but goes through
a HashMap, for ease of processing and transfer, which does not maintaing order.

Sort the resulting object in the Javascript to keep a consistent order in the graph.
2024-07-24 15:23:52 +01:00
10 changed files with 254 additions and 70 deletions

View file

@ -7,15 +7,17 @@ vote and who we think wrote them and see some stats !
A list of things that could be implemented/added to the application, some of them are needed for "feature completeness" ! A list of things that could be implemented/added to the application, some of them are needed for "feature completeness" !
- [ ] Being able to change from one week to the next - [x] Being able to change from one week to the next
- [ ] Create new weeks for the admin - [x] Create new weeks for the admin
- [ ] Proper week redirection - [x] Proper week redirection
- [ ] Correctly handle non-existing week number
- [x] Add introduction to the weekly truths - [x] Add introduction to the weekly truths
- [ ] Bundle static assets in the binary - [ ] Bundle static assets in the binary
- [ ] Move the database queries to their own functions - [ ] Move the database queries to their own functions
- [ ] Cache those results - [ ] Cache those results
- [ ] Centralize Markdown parsing ? - [ ] Centralize Markdown parsing ?
- [ ] Use fairings for the different elements ? - [ ] Use fairings for the different elements ?
- [ ] Use guards for User calls ?
# Dependencies # Dependencies

View file

@ -9,7 +9,7 @@ use argon2::{Argon2, PasswordHash, PasswordVerifier};
use blake2::{Blake2b512, Digest}; use blake2::{Blake2b512, Digest};
use blake2::digest::FixedOutput; use blake2::digest::FixedOutput;
use crate::database_records::{AuthTokens, PlayerLoginInfo, Vote}; use crate::database_records::{AuthTokens, PlayerLoginInfo, Vote};
use crate::database; use crate::{database, week};
use database::Db; use database::Db;
// TODO: Make FromRequest guard https://api.rocket.rs/v0.5/rocket/request/trait.FromRequest and split admin // TODO: Make FromRequest guard https://api.rocket.rs/v0.5/rocket/request/trait.FromRequest and split admin
@ -86,7 +86,8 @@ pub async fn get_user(week: u8, db: &mut Connection<Db>, cookies: &CookieJar<'_>
(String::new(), false) (String::new(), false)
}; };
let votes: Vec<Vote> = if logged_in { // TODO: Move to src/vote.rs
let votes: Vec<Vote> = if logged_in && !is_admin {
sqlx::query_as("SELECT Votes.* FROM Votes JOIN Truths ON Votes.truth_id == Truths.id AND Truths.week == $1 WHERE voter_id == $2 ORDER BY Truths.number;") sqlx::query_as("SELECT Votes.* FROM Votes JOIN Truths ON Votes.truth_id == Truths.id AND Truths.week == $1 WHERE voter_id == $2 ORDER BY Truths.number;")
.bind(week) .bind(week)
.bind(&id_str) .bind(&id_str)
@ -125,8 +126,8 @@ pub struct AuthForm {
password: String password: String
} }
#[post("/login", data="<form>")] #[post("/<week>/login", data="<form>")]
pub async fn login(form: Form<AuthForm>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect { pub async fn login(week: u8, form: Form<AuthForm>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user_search: Result<PlayerLoginInfo, _> = sqlx::query_as("SELECT id, is_admin, name, pwd_hash FROM Players WHERE name == $1") let user_search: Result<PlayerLoginInfo, _> = sqlx::query_as("SELECT id, is_admin, name, pwd_hash FROM Players WHERE name == $1")
.bind(&form.name) .bind(&form.name)
.fetch_one(&mut **db) .fetch_one(&mut **db)
@ -135,7 +136,7 @@ pub async fn login(form: Form<AuthForm>, mut db: Connection<Db>, cookies: &Cooki
if user_search.is_err() { if user_search.is_err() {
error!("Login failed : invalid user {:?}, err: {:?}", form.name, user_search.err()); error!("Login failed : invalid user {:?}, err: {:?}", form.name, user_search.err());
cookies.add(("toast_error", "Impossible de se connecter !")); cookies.add(("toast_error", "Impossible de se connecter !"));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
let new_user = user_search.unwrap(); let new_user = user_search.unwrap();
@ -143,7 +144,7 @@ pub async fn login(form: Form<AuthForm>, mut db: Connection<Db>, cookies: &Cooki
if password_hash_parse.is_err() { if password_hash_parse.is_err() {
error!("Login failed : could not parse password hash {:?}", password_hash_parse.err()); error!("Login failed : could not parse password hash {:?}", password_hash_parse.err());
cookies.add(("toast_error", "Impossible de se connecter !")); cookies.add(("toast_error", "Impossible de se connecter !"));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
let password_hash = password_hash_parse.unwrap(); let password_hash = password_hash_parse.unwrap();
@ -167,7 +168,7 @@ pub async fn login(form: Form<AuthForm>, mut db: Connection<Db>, cookies: &Cooki
Err(error) => { Err(error) => {
error!("Login failed : coult not store auth token in database : {error}"); error!("Login failed : coult not store auth token in database : {error}");
cookies.add(("toast_error", "Impossible de se connecter !")); cookies.add(("toast_error", "Impossible de se connecter !"));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
} }
@ -177,11 +178,11 @@ pub async fn login(form: Form<AuthForm>, mut db: Connection<Db>, cookies: &Cooki
Err(err) => { Err(err) => {
error!("Login failed : invalid password for {:?}\nError : {err}", new_user.name); error!("Login failed : invalid password for {:?}\nError : {err}", new_user.name);
cookies.add(("toast_error", "Impossible de se connecter !")); cookies.add(("toast_error", "Impossible de se connecter !"));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
} }
Redirect::to(uri!("/")) Redirect::to(uri!(week::week(week)))
} }
pub fn bypass_auth_debug(cookies: &CookieJar<'_>) { pub fn bypass_auth_debug(cookies: &CookieJar<'_>) {

View file

@ -12,6 +12,8 @@ use rocket_db_pools::{sqlx, sqlx::Row, Database, Connection};
use sqlx::Error; use sqlx::Error;
mod auth; mod auth;
use auth::User;
mod truth; mod truth;
mod vote; mod vote;
mod week; mod week;
@ -19,9 +21,8 @@ mod week;
mod database; mod database;
mod database_records; mod database_records;
use database::Db;
use database_records::*; use database_records::*;
use auth::User; use database::Db;
#[get("/")] #[get("/")]
async fn index(mut db: Connection<Db>) -> Redirect { async fn index(mut db: Connection<Db>) -> Redirect {
@ -41,7 +42,11 @@ async fn index(mut db: Connection<Db>) -> Redirect {
fn rocket() -> _ { fn rocket() -> _ {
rocket::build() rocket::build()
.mount("/", FileServer::from(relative!("static_files"))) .mount("/", FileServer::from(relative!("static_files")))
.mount("/", routes![index, vote::fetch_vote_data, vote::vote, truth::create_truth, truth::edit_truth, week::week, week::update_week, auth::login]) .mount("/", routes![index,
vote::fetch_vote_data, vote::vote,
truth::create_truth, truth::edit_truth,
week::week, week::update_week, week::set_last_week, week::create_week,
auth::login])
.attach(database::stage()) .attach(database::stage())
.attach(Template::fairing()) .attach(Template::fairing())
} }

View file

@ -6,7 +6,7 @@ use rocket_db_pools::{sqlx, Connection};
use pulldown_cmark::{Parser, Options}; use pulldown_cmark::{Parser, Options};
use sqlx::Row; use sqlx::Row;
use crate::{auth, database}; use crate::{auth, database, week};
#[derive(FromForm)] #[derive(FromForm)]
pub struct TruthUpdateForm { pub struct TruthUpdateForm {
@ -20,7 +20,7 @@ pub async fn edit_truth(week: u8, truth_number: u8, form: Form<TruthUpdateForm>,
let user = auth::get_user(week, &mut db, cookies).await; let user = auth::get_user(week, &mut db, cookies).await;
if !user.is_admin { if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de changer la vérité.")); cookies.add(("toast_error", "Vous n'avez pas la permission de changer la vérité."));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
let mut options = Options::empty(); let mut options = Options::empty();
@ -50,16 +50,16 @@ pub async fn edit_truth(week: u8, truth_number: u8, form: Form<TruthUpdateForm>,
} }
}; };
Redirect::to(uri!("/")) Redirect::to(uri!(week::week(week)))
} }
#[post("/<week>/new", data="<form>")] #[post("/<week>/new_truth", data="<form>")]
pub async fn create_truth(week: u8, form: Form<TruthUpdateForm>, pub async fn create_truth(week: u8, form: Form<TruthUpdateForm>,
mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect { mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(week, &mut db, cookies).await; let user = auth::get_user(week, &mut db, cookies).await;
if !user.is_admin { if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission d'ajouter de vérité.")); cookies.add(("toast_error", "Vous n'avez pas la permission d'ajouter de vérité."));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
let truth_number: u8 = match sqlx::query("SELECT max(number) from Truths WHERE week == $1;") let truth_number: u8 = match sqlx::query("SELECT max(number) from Truths WHERE week == $1;")
@ -76,7 +76,7 @@ pub async fn create_truth(week: u8, form: Form<TruthUpdateForm>,
if truth_number == 0 { if truth_number == 0 {
error!("Error while getting max truth number."); error!("Error while getting max truth number.");
cookies.add(("toast_error", "Erreur lors de l'ajout de la vérité...")); cookies.add(("toast_error", "Erreur lors de l'ajout de la vérité..."));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
let mut options = Options::empty(); let mut options = Options::empty();
@ -108,5 +108,5 @@ pub async fn create_truth(week: u8, form: Form<TruthUpdateForm>,
debug!("Truth was successfully added"); debug!("Truth was successfully added");
Redirect::to(uri!("/")) Redirect::to(uri!(week::week(week)))
} }

View file

@ -2,6 +2,7 @@ use std::collections::hash_map::Entry;
use std::collections::HashMap; use std::collections::HashMap;
use rocket::fairing::AdHoc; use rocket::fairing::AdHoc;
use rocket::form::Form; use rocket::form::Form;
use rocket::futures::TryFutureExt;
use rocket::http::CookieJar; use rocket::http::CookieJar;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::serde::{Serialize, Deserialize}; use rocket::serde::{Serialize, Deserialize};
@ -9,7 +10,7 @@ use rocket::serde::json::Json;
use rocket_db_pools::{sqlx, Connection}; use rocket_db_pools::{sqlx, Connection};
use crate::{auth, database}; use crate::{auth, database, week};
use crate::database_records::{Vote, VotingData}; use crate::database_records::{Vote, VotingData};
#[derive(FromForm)] #[derive(FromForm)]
@ -24,7 +25,7 @@ pub async fn vote(week: u8, form: Form<VoteForm>,
if !user.logged_in { if !user.logged_in {
cookies.add(("toast_error", "Vous n'avez pas la permission de changer de vote.")); cookies.add(("toast_error", "Vous n'avez pas la permission de changer de vote."));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week::week(week)));
} }
let filtered_votes = form.truth_votes.iter().filter_map( let filtered_votes = form.truth_votes.iter().filter_map(
@ -61,7 +62,7 @@ pub async fn vote(week: u8, form: Form<VoteForm>,
} }
None => { None => {
debug!("Player {:?} voting {voted_id} for truth {truth_id}", user.id); debug!("Player {:?} voting {voted_id} for truth {truth_id}", user.id);
// TODO: Find a way to use only one statement ? // TODO: Find a way to use only one statement ? --> query_many
// Cannot all launch and await because all connect to DB // 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);") match sqlx::query("INSERT INTO Votes (truth_id, voter_id, voted_id) VALUES ($1, $2, $3);")
.bind(truth_id) .bind(truth_id)
@ -84,7 +85,7 @@ pub async fn vote(week: u8, form: Form<VoteForm>,
debug!("Vote successful") debug!("Vote successful")
} }
Redirect::to(uri!("/")) Redirect::to(uri!(week::week(week)))
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -110,11 +111,18 @@ pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>) -> Opti
}); });
let vote_count = raw_votes.iter().fold(0, |count, votes| {count + votes.votes}); let vote_count = raw_votes.iter().fold(0, |count, votes| {count + votes.votes});
if vote_count < 17 { // Only show the graph if we have all the votes and this is not the last week.
// FIXME: Make the 42 not hardcoded
if vote_count < 42 || week != sqlx::query_scalar("SELECT number from Weeks WHERE number == $1 AND is_last_week == 1;")
.bind(week)
.fetch_optional(&mut **db)
.await.unwrap_or(Some(0)).unwrap_or(0) {
return None; 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 vote_data = HashMap::<String, Vec<u8>>::new();
let mut next_truth_number = 1; let mut next_truth_number = 1;
for raw_vote in raw_votes { for raw_vote in raw_votes {

View file

@ -6,6 +6,7 @@ use rocket::response::Redirect;
use rocket_db_pools::{sqlx, Connection}; use rocket_db_pools::{sqlx, Connection};
use rocket_dyn_templates::{context, Template}; use rocket_dyn_templates::{context, Template};
use sqlx::{Acquire, Executor};
use crate::auth; use crate::auth;
use crate::auth::User; use crate::auth::User;
use crate::database::Db; use crate::database::Db;
@ -21,7 +22,7 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
.fetch_all(&mut **db).await { .fetch_all(&mut **db).await {
Ok(v) => v, Ok(v) => v,
Err(error) => { Err(error) => {
println!("Some error while getting players : {error}"); error!("Some error while getting players : {error}");
Vec::<Player>::new() Vec::<Player>::new()
} }
} }
@ -84,7 +85,7 @@ pub async fn update_week(week: u8, raw_intro: Form<String>,
let user = auth::get_user(week, &mut db, cookies).await; let user = auth::get_user(week, &mut db, cookies).await;
if !user.is_admin { if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de changer la semaine.")); cookies.add(("toast_error", "Vous n'avez pas la permission de changer la semaine."));
return Redirect::to(uri!("/")); return Redirect::to(uri!(week(week)));
} }
let mut options = Options::empty(); let mut options = Options::empty();
@ -112,5 +113,100 @@ pub async fn update_week(week: u8, raw_intro: Form<String>,
} }
}; };
Redirect::to(uri!("/")) Redirect::to(uri!(week(week)))
}
#[post("/<week>/set_last")]
pub async fn set_last_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(week, &mut db, cookies).await;
if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de changer la semaine."));
return Redirect::to(uri!(week(week)));
}
let add_error_cookie = ||
cookies.add(("toast_error", "Erreur lors du changement d'état de la semaine."));
let db_transaction_connection = db.begin().await;
if db_transaction_connection.is_err() {
error!("Could not start database transaction for last week change : {:?}", db_transaction_connection.unwrap_err());
add_error_cookie();
return Redirect::to(uri!(week(week)));
}
let mut db_transaction = db_transaction_connection.unwrap();
// Remove the last flag from other weeks, if set.
match db_transaction.execute("UPDATE Weeks SET is_last_week = 0 WHERE is_last_week == 1;")
.await {
Ok(_) => debug!("Succesfully cleared is_last_week"),
Err(error) => {
error!("Failed to clear last week status : {error}");
add_error_cookie();
return Redirect::to(uri!(week(week)));
}
};
// We should set one week, if not there's something wrong : rollback.
if match sqlx::query("UPDATE Weeks SET is_last_week = 1 WHERE number == $1;")
.bind(week)
.execute(&mut *db_transaction)
.await {
Ok(result) => result.rows_affected(),
Err(error) => {
error!("Error while setting last week status : {error}");
0
}
} == 1 {
db_transaction.commit().await.unwrap_or_else(|error| {
error!("Error while committing week is last transaction : {error}");
add_error_cookie();
})
} else {
db_transaction.rollback().await.unwrap_or_else(|error| {
error!("Error while rolling back week is last transaction : {error}");
add_error_cookie();
})
}
Redirect::to(uri!(week(week)))
}
#[get("/<week>/create")]
pub async fn create_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(week, &mut db, cookies).await;
if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de changer la semaine."));
return Redirect::to(uri!(week(week - 1)));
}
let week_check: Result<Option<u8>, sqlx::Error> = sqlx::query_scalar("SELECT number from Weeks WHERE number == $1;")
.bind(week)
.fetch_optional(&mut **db)
.await;
if week_check.is_err() {
error!("Error while checking week existence : {:?}", week_check.unwrap_err());
cookies.add(("toast_error", "Erreur en vérifiant que la semaine n'existe pas déjà"));
return Redirect::to(uri!(week(week - 1)));
}
if week_check.unwrap().is_some() {
debug!("Week {week} already exists, not creating.");
return Redirect::to(uri!(week(week)));
}
match sqlx::query("INSERT INTO Weeks (number, is_last_week, rendered_text, raw_text) VALUES ($1, 0, \"\", \"\");")
.bind(week)
.execute(&mut **db)
.await {
Ok(_) => {
debug!("Succesfully created new week {week}");
Redirect::to(uri!(week(week)))
},
Err(error) => {
error!("Error while creating new week {week} : {error}");
cookies.add(("toast_error", "Erreur en créant la nouvelle selmaine."));
Redirect::to(uri!(week(week - 1)))
}
}
} }

View file

@ -24,7 +24,7 @@
} }
.editor { .editor {
width: 100%; width: calc(100% - 1.25em); /* The width is calculated *inside* the padding, so adjust for it. */
height: 6eM; height: 6eM;
font-size: large; font-size: large;
padding: 0.5em; padding: 0.5em;
@ -47,6 +47,34 @@
top: 25%; top: 25%;
} }
.week_change:link, .week_change:visited {
color: black;
text-decoration: none;
transition: all .2s ease-in-out;
}
.week_change:hover, .week_change:focus {
color: mediumpurple;
text-shadow: 0 2px 2px slategray;
}
.week_change_hidden {
padding-left: 0.5eM;
padding-right: 0.5eM;
}
@media (orientation: portrait) {
.truth_list {
width: 60vw;
}
.graph {
width: 35vw;
}
}
/* Global styling */
hr { hr {
border: none; border: none;
border-top: 2px solid #9ec5fe; border-top: 2px solid #9ec5fe;
@ -74,13 +102,3 @@ body {
body > h2 { body > h2 {
padding-left: 0.25eM; padding-left: 0.25eM;
} }
@media (orientation: portrait) {
.truth_list {
width: 60vw;
}
.graph {
width: 35vw;
}
}

View file

@ -1,14 +1,5 @@
const limit_ratio = 1.3
// const names = ["Bystus", "Dory", "Fen", "Lucky", "Nico", "Peran", "trot"] const colors = ['#b6b8fc', '#b6f4fc', '#fcb6cc', '#e0fcb6', '#fcdcb6', '#b6fcc8', '#f0b6fc']
//
//
// let data = [];
// for (let i = 0; i < 7; i++) {
// data.push({
// parsing: true,
// label: names[i],
// data: Array.from(keys, () => Math.round(Math.random()*1.33))})
// }
async function main() { async function main() {
const vote_response = await fetch(document.URL+"/votes"); const vote_response = await fetch(document.URL+"/votes");
if (!vote_response.ok) { if (!vote_response.ok) {
@ -32,19 +23,41 @@ async function main() {
return; return;
} }
// Sort by label to maintain the same graph order, as it goes through a hash map in the backend.
datasets.sort((a, b) => a.label > b.label)
for (let i = 0; i < datasets.length; i++) {
datasets[i].backgroundColor = colors[i % colors.length]
}
const chart_canvas = document.getElementById("vote_chart") const chart_canvas = document.getElementById("vote_chart")
let chart let chart
let previous_orientation
function create_chart(keys, data) { function create_chart(keys, data) {
let main_axis; let main_axis;
let aspect_ratio; let aspect_ratio;
if (window.innerWidth > window.innerHeight) { let orientation
if (window.innerWidth / window.innerHeight > limit_ratio) {
orientation = "landscape"
main_axis = 'x' main_axis = 'x'
aspect_ratio = 2 aspect_ratio = 2
} else { } else {
orientation = "portrait"
main_axis = 'y' main_axis = 'y'
aspect_ratio = 0.5 aspect_ratio = 0.5
} }
// Don't re-create the chart for no reason.
if (orientation === previous_orientation) {
console.log("bijour")
return;
} else {
console.log("badour")
}
previous_orientation = orientation
if ( chart ) { if ( chart ) {
chart.destroy() chart.destroy()
} }
@ -66,6 +79,13 @@ async function main() {
y: { y: {
stacked: true stacked: true
} }
},
plugins: {
title: {
display: true,
position: 'bottom',
text: 'Répartition des suppositions'
}
} }
} }
}) })
@ -75,7 +95,7 @@ async function main() {
create_chart(keys, datasets) create_chart(keys, datasets)
} }
const orientation_query = matchMedia("screen and (orientation:portrait)"); const orientation_query = matchMedia(`(aspect-ratio < ${limit_ratio})`);
orientation_query.onchange = update_chart_ratio orientation_query.onchange = update_chart_ratio
create_chart(keys, datasets) create_chart(keys, datasets)

View file

@ -12,12 +12,29 @@
<script defer="defer" type="text/javascript" src="/vote_chart.js"></script> <script defer="defer" type="text/javascript" src="/vote_chart.js"></script>
</head> </head>
{# Check if the user has a vote in advance, for readability #} {#{% import "week_change_arrows" as week_macro %}#}
{% if user.logged_in == true and user.has_week_vote == true%} {# For some reason the import does not work ? Figure it out at some point... #}
{% set has_vote = true %} {%- macro display(display_character, to, enabled) -%}
{%- set class = "week_change" -%}
{%- if enabled == true %}
{% set target = ("href=/" ~ to) %}
{%- else -%}
{% set class = class ~ " week_change_hidden" -%}
{% set target = "" %}
{%- endif -%}
<a {{ target }} class="{{ class }}">{%- if enabled == true -%}{{- display_character -}}{%- endif -%}</a>
{%- endmacro display -%}
{% set back_arrow_enabled = week_data.number > 1 %}
{% set next_arrow_enabled = (week_data.is_last_week != true or user.is_admin == true) %}
{% set next_arrow_href = (week_data.number + 1) %}
{% if user.is_admin == true %}
{% set next_arrow_href = next_arrow_href ~ "/create" %}
{% set next_arrow_chara = '⥅' %}
{% else %} {% else %}
{% set has_vote = false %} {% set next_arrow_chara = '⟹' %}
{% endif -%} {% endif %}
<body> <body>
<div class="top_bar"> <div class="top_bar">
@ -25,7 +42,7 @@
{% if user.logged_in == true %} {% if user.logged_in == true %}
<p>Connecté en tant que <b>{{ user.name }}</b></p> <p>Connecté en tant que <b>{{ user.name }}</b></p>
{% else %} {% else %}
<form class="login" id="login" action="/login" method="POST"> <form class="login" id="login" action="/{{ week_data.number }}/login" method="POST">
<label>Pseudo <input form="login" type="text" name="name"/></label> <label>Pseudo <input form="login" type="text" name="name"/></label>
<label>Mot de passe <input form="login" type="password" name="password"/></label> <label>Mot de passe <input form="login" type="password" name="password"/></label>
<button form="login">Se connecter</button> <button form="login">Se connecter</button>
@ -33,9 +50,18 @@
{% endif %} {% endif %}
</div> </div>
<h2>Semaine {{ week_data.number }}</h2> <h2>{{- self::display(display_character='⟸', to=(week_data.number - 1), enabled=back_arrow_enabled) }}
Semaine {{ week_data.number }}
{{- self::display(display_character=next_arrow_chara, to=next_arrow_href, enabled=next_arrow_enabled) -}}</h2>
<div class="page_body"> <div class="page_body">
<div class="truth_list"> <div class="truth_list">
{% if user.is_admin == true and week_data.is_last_week != true %}
<form action="/{{ week_data.number }}/set_last" method="post">
<button>
Définir comme dernière semaine active
</button>
</form>
{% endif %}
<div class="week_intro"> <div class="week_intro">
{{ week_data.rendered_text | safe }} {{ week_data.rendered_text | safe }}
{%- if user.is_admin == true -%} {%- if user.is_admin == true -%}
@ -78,7 +104,7 @@
{% if user.logged_in == true and user.is_admin == false %} {% if user.logged_in == true and user.is_admin == false %}
<br/> <br/>
<button form="truths"> <button form="truths">
{%- if has_vote == true -%} {%- if user.logged_in == true and user.has_week_vote == true -%}
Changer de vote Changer de vote
{% else %} {% else %}
À voter ! À voter !
@ -91,7 +117,7 @@
{% if user.is_admin == true %} {% if user.is_admin == true %}
<div class="individual_truth"> <div class="individual_truth">
<h3>Nouvelle vérité</h3> <h3>Nouvelle vérité</h3>
<form action="/{{ week_data.number }}/new" method="POST"> <form action="/{{ week_data.number }}/new_truth" method="POST">
{% include "truth_editor" %} {% include "truth_editor" %}
</form> </form>
</div> </div>

View file

@ -1,3 +1,9 @@
{%- set is_disabled = "" -%}
{# If we are not during the active week, prevent changing vote but not sending a missing one. #}
{%- if week_data.is_last_week != true and user.votes | filter(attribute="truth_id", value=truth.id) -%}
{%- set is_disabled = "disabled" -%}
{%- endif -%}
<div class="individual_truth"> <div class="individual_truth">
<h3>Vérité {{ truth.number }}</h3> <h3>Vérité {{ truth.number }}</h3>
<p>{{ truth.rendered_text | safe }}</p> <p>{{ truth.rendered_text | safe }}</p>
@ -8,15 +14,17 @@
Tu l'as fait :) Tu l'as fait :)
{%- else -%} {%- else -%}
Qui l'a fait ? Qui l'a fait ?
<select form="truths" name="truth_votes[{{ truth.id }}]"> <select form="truths" name="truth_votes[{{ truth.id }}]" {{ is_disabled }}>
<option value="0">---</option> <option value="0">---</option>
{% for player in other_players %} {% for player in other_players %}
{# Check if we should pre-select an existing vote #} {# Check if we should pre-select an existing vote. #}
{% if has_vote == true and player.id == user.votes[truth_index].voted_id %} {% set_global is_selected = "" %}
{% set is_selected = "selected" %} {% for vote in user.votes %}
{% else %} {% if truth.id == vote.truth_id and player.id == vote.voted_id %}
{% set is_selected = "" %} {% set_global is_selected = "selected" %}
{% endif %} {% break %}
{% endif %}
{% endfor %}
<option value="{{ player.id }}" {{- is_selected -}}>{{ player.name }}</option> <option value="{{ player.id }}" {{- is_selected -}}>{{ player.name }}</option>
{% endfor %} {% endfor %}
</select> </select>