Compare commits

..

No commits in common. "b401aa7f5d529c8ff9dc5188fc8e9a4c241fa537" and "0c162d3b423f68579f803df1d456e126940612b6" have entirely different histories.

19 changed files with 231 additions and 834 deletions

View file

@ -16,23 +16,18 @@ A list of things that could be implemented/added to the application, some of the
- [ ] 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 ?
- [x] Use fairings for the different elements - [ ] Use fairings for the different elements ?
- [ ] Use guards for User calls ? - [ ] Use guards for User calls ?
- [ ] Use SQLite Row ID for User IDs rather than regular IDs, for randomness ?
- [x] Split user from vote data
# Dependencies # Dependencies
This project currently uses, for the backend : This project currently uses :
- [Rocket](https://docs.rs/rocket/0.5.1/rocket/), for the web application backend - [Rocket](https://docs.rs/rocket/0.5.1/rocket/), for the web application backend
- [SQLX](https://docs.rs/sqlx/0.7.4/sqlx/), for database access (this is only expeceted to be used with SQLite) - [SQLX](https://docs.rs/sqlx/0.7.4/sqlx/), for database access (this is only expeceted to be used with SQLite)
- [Tera](https://docs.rs/tera/latest/tera/), for templating - [Tera](https://docs.rs/tera/latest/tera/), for templating
- [Argon2](https://docs.rs/argon2/latest/argon2/), for password hashing - [Argon2](https://docs.rs/argon2/latest/argon2/), for password hashing
- [Pull_down CMark](https://docs.rs/pulldown-cmark/0.11.0/pulldown_cmark/), for markdown rendering - [Pull_down CMark](https://docs.rs/pulldown-cmark/0.11.0/pulldown_cmark/), for markdown rendering
For the frontend :
- [Chart.js](https://www.chartjs.org/), for rendering the vote graph.
# License # License
The code present in this repository is licensed under the Mozilla Public License 2.0. The code present in this repository is licensed under the Mozilla Public License 2.0.

View file

@ -1,13 +0,0 @@
CREATE TABLE IF NOT EXISTS Tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL UNIQUE,
color VARCHAR NOT NULL
);
CREATE TABLE IF NOT EXISTS TaggedTruths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
truth_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
FOREIGN KEY (truth_id) REFERENCES Truths(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES Tags(id) ON DELETE CASCADE
);

View file

@ -8,8 +8,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use argon2::{Argon2, PasswordHash, PasswordVerifier}; use argon2::{Argon2, PasswordHash, PasswordVerifier};
use blake2::{Blake2b512, Digest}; use blake2::{Blake2b512, Digest};
use blake2::digest::FixedOutput; use blake2::digest::FixedOutput;
use rocket::fairing::AdHoc; use crate::database_records::{AuthTokens, PlayerLoginInfo, Vote};
use crate::database_records::{AuthTokens, PlayerLoginInfo};
use crate::{database, week}; use crate::{database, week};
use database::Db; use database::Db;
@ -21,9 +20,11 @@ pub struct User {
pub is_admin: bool, pub is_admin: bool,
pub id: u16, pub id: u16,
pub name: String, pub name: String,
pub has_week_vote: bool,
pub votes: Vec<Vote>
} }
pub async fn get_user(db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> User { pub async fn get_user(week: u8, db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> User {
let auth_token: Option<String> = match cookies.get_private("auth_token") { let auth_token: Option<String> = match cookies.get_private("auth_token") {
Some(cookie) => Some(cookie.value().to_string()), Some(cookie) => Some(cookie.value().to_string()),
None => None None => None
@ -85,12 +86,27 @@ pub async fn get_user(db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> User
(String::new(), false) (String::new(), false)
}; };
// 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;")
.bind(week)
.bind(&id_str)
.fetch_all(&mut ***db).await.unwrap_or_else(|error| {
error!("Error while getting votes : {error}");
Vec::<Vote>::new()
})
} else {
Vec::<Vote>::new()
};
if logged_in { if logged_in {
User { User {
logged_in, logged_in,
is_admin, is_admin,
id: id_str.parse::<u16>().unwrap(), id: id_str.parse::<u16>().unwrap(),
name, name,
has_week_vote: if votes.is_empty() { false } else { true },
votes
} }
} else { } else {
User { User {
@ -98,6 +114,8 @@ pub async fn get_user(db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> User
is_admin: false, is_admin: false,
id: 0, id: 0,
name, name,
has_week_vote: false,
votes
} }
} }
} }
@ -193,8 +211,17 @@ pub async fn logout(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -
Redirect::to(uri!(week::week(week))) Redirect::to(uri!(week::week(week)))
} }
pub fn stage() -> AdHoc { pub fn bypass_auth_debug(cookies: &CookieJar<'_>) {
AdHoc::on_ignite("Auth stage", |rocket| async { if cookies.get_private("auth_token").is_some() {
rocket.mount("/", routes![login, logout]) return
}) }
let mut hasher = Blake2b512::new();
hasher.update(b"8");
hasher.update(SystemTime::now().duration_since(UNIX_EPOCH).expect("Non monotonous time event").as_secs().to_le_bytes());
let hash = hasher.finalize_fixed().to_ascii_lowercase();
let hash_str = String::from_utf8_lossy(hash.as_slice()).to_ascii_lowercase();
cookies.add_private(("auth_token", hash_str.clone()));
cookies.add_private(("auth_id", 8.to_string()));
println!("Generated hash string : {hash_str}");
} }

View file

@ -19,26 +19,21 @@ pub struct PlayerLoginInfo {
#[derive(sqlx::FromRow, Deserialize, Serialize)] #[derive(sqlx::FromRow, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct Truth { pub struct Truth {
pub id: u32, id: u32,
week: u8, week: u8,
number: u8, number: u8,
author_id: u16, author_id: u16,
rendered_text: String, rendered_text: String,
raw_text: String, raw_text: String,
#[sqlx(skip)]
pub tags: Vec<Tag>,
} }
#[derive(sqlx::FromRow, Deserialize, Serialize)] #[derive(sqlx::FromRow, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct DisplayTruth { pub struct DisplayTruth {
pub id: u32, id: u32,
week: u8,
number: u8, number: u8,
author_id: u16, author_id: u16,
rendered_text: String, rendered_text: String,
#[sqlx(skip)]
pub tags: Vec<Tag>,
} }
#[derive(sqlx::FromRow, Deserialize, Serialize)] #[derive(sqlx::FromRow, Deserialize, Serialize)]
@ -72,11 +67,3 @@ pub struct Week {
pub rendered_text: String, pub rendered_text: String,
pub raw_text: String, pub raw_text: String,
} }
#[derive(FromForm)]
#[derive(sqlx::FromRow, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Tag {
pub name: String,
pub color: String
}

View file

@ -5,10 +5,10 @@ use rocket::response::Redirect;
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use rocket_db_pools::Connection; use rocket_db_pools::{sqlx, sqlx::Row, Connection};
mod auth; mod auth;
mod tag;
mod truth; mod truth;
mod vote; mod vote;
mod week; mod week;
@ -20,7 +20,14 @@ use database::Db;
#[get("/")] #[get("/")]
async fn index(mut db: Connection<Db>) -> Redirect { async fn index(mut db: Connection<Db>) -> Redirect {
let current_week: u8 = week::get_last_week(&mut db).await; let current_week: u8 = match sqlx::query("SELECT number FROM Weeks WHERE is_last_week == 1;")
.fetch_one(&mut **db).await {
Ok(v) => v.try_get(0).ok().unwrap_or_else(|| 1), // If error, go back to 1
Err(error) => {
error!("Error while getting current week : {error:?}");
1
}
};
Redirect::to(uri!(week::week(week_number = if current_week == 0 {1} else {current_week}))) Redirect::to(uri!(week::week(week_number = if current_week == 0 {1} else {current_week})))
} }
@ -28,13 +35,14 @@ async fn index(mut db: Connection<Db>) -> Redirect {
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {
rocket::build() rocket::build()
.mount("/static/", FileServer::from(relative!("static_files"))) .mount("/", FileServer::from(relative!("static_files")))
.attach(auth::stage()) .mount("/", routes![index,
.attach(week::stage()) vote::fetch_vote_data, vote::vote,
.attach(truth::stage()) truth::create_truth, truth::edit_truth,
.attach(vote::stage()) week::week, week::update_week, week::set_last_week, week::create_week,
.attach(tag::stage()) auth::login, auth::logout])
.mount("/", routes![index])
.attach(database::stage()) .attach(database::stage())
.attach(Template::fairing()) .attach(Template::fairing())
} }
// TODO: Random Row ID

View file

@ -1,295 +0,0 @@
use rocket::fairing::AdHoc;
use rocket::form::Form;
use rocket::http::CookieJar;
use rocket::response::Redirect;
use rocket_db_pools::Connection;
use rocket_dyn_templates::{context, Template};
use sqlx::{Sqlite, QueryBuilder};
use crate::database::Db;
use crate::database_records::{DisplayTruth, Tag};
use crate::auth;
use crate::week;
pub async fn get_all_tags(db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> Vec<Tag> {
match sqlx::query_as("SELECT name, color FROM Tags")
.fetch_all(&mut ***db)
.await {
Ok(tags) => tags,
Err(error) => {
error!("Database error while fetching tags : {error}");
cookies.add(("toast_error", "Erreur lors du chargement des tags."));
Vec::<Tag>::new()
}
}
}
pub async fn get_truth_tags(truth_id: u32, db: &mut Connection<Db>) -> Vec<Tag> {
match sqlx::query_as("
SELECT Tags.name, Tags.color FROM Tags
JOIN TaggedTruths ON TaggedTruths.tag_id == Tags.id AND TaggedTruths.truth_id == $1
ORDER BY Tags.id;")
.bind(truth_id)
.fetch_all(&mut ***db)
.await {
Ok(tags) => tags,
Err(error) => {
error!("Could not fetch tags for truth {:?} : {error}", truth_id);
Vec::<Tag>::new()
}
}
}
#[get("/")]
pub async fn tag_index(mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Template {
let user = auth::get_user(&mut db, cookies).await;
let tags: Vec<Tag> = get_all_tags(&mut db, cookies).await;
Template::render("tags/index", context!{
user: user,
tags: tags
})
}
#[get("/filter?<tags>")]
pub async fn filtered_by_tags(tags: Vec<String>, mut db: Connection<Db>) -> Template {
let last_week = week::get_last_week(&mut db).await;
let filtered_truths: Vec<DisplayTruth> = if tags.len() == 0 {
match sqlx::query_as("SELECT id, week, number, author_id, rendered_text FROM Truths WHERE week <= $1 ORDER BY week, number;")
.bind(last_week)
.fetch_all(&mut **db)
.await {
Ok(all_truths) => all_truths,
Err(error) => {
error!("Error while fetching all truths for the filter : {error}");
Vec::<DisplayTruth>::new()
}
}
} else {
let mut query_builder: QueryBuilder<Sqlite> = QueryBuilder::new("
SELECT Truths.id, Truths.week, Truths.number, Truths.author_id, Truths.rendered_text FROM Truths
JOIN TaggedTruths ON Truths.id == TaggedTruths.truth_id
JOIN Tags ON Tags.id == TaggedTruths.tag_id AND Tags.name IN (
");
let mut separated_args = query_builder.separated(", ");
for tag in &tags {
separated_args.push_bind(tag);
}
// Now that the comma separated list of strings is built, finish the query normally.
query_builder.push(") WHERE Truths.week <= ");
query_builder.push_bind(last_week);
query_builder.push("GROUP BY Truths.id ORDER BY Truths.week, Truths.number;");
match query_builder.build_query_as::<DisplayTruth>().fetch_all(&mut **db).await {
Ok(truths) => {
debug!("Got filtered truths by tags");
truths
},
Err(error) => {
error!("Error while fetching filtered truths : {error}");
Vec::<DisplayTruth>::new()
}
}
};
let filter_tags: Vec::<Tag> = match sqlx::query_as("SELECT name, color FROM Tags;")
.fetch_all(&mut **db)
.await {
Ok(all_tags) => {
all_tags.into_iter().filter(|tag: &Tag| tags.contains(&tag.name)).collect()
},
Err(error) => {
error!("Error getting tags to show on filter list : {error}");
Vec::<Tag>::new()
}
};
Template::render("tags/filtered_truths", context!{
tags: filter_tags,
truths: filtered_truths
})
}
#[post("/create", data="<new_tag>")]
pub async fn create_tag(new_tag: Form<Tag>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&mut db, cookies).await;
if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de créer une étiquette"));
return Redirect::to(uri!("/tags", tag_index));
}
match sqlx::query("INSERT INTO Tags (name, color) VALUES ($1, $2);")
.bind(&new_tag.name)
.bind(&new_tag.color)
.execute(&mut **db)
.await {
Ok(affected_lines) => if affected_lines.rows_affected() != 1 {
error!("Did not create tag : {:?}", &new_tag.name);
cookies.add(("toast_error", "L'étiquette n'a pas pû être créer"));
} else {
debug!("Successfully created new tag {:?}", &new_tag.name);
},
Err(error) => {
error!("Error whilea adding tag {:?} : {error}", &new_tag.name);
cookies.add(("toast_error", "Erreur à la création de l'étiquette"));
}
};
Redirect::to(uri!("/tags", tag_index))
}
#[post("/delete/<tag_name>")]
pub async fn delete_tag(tag_name: &str, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&mut db, cookies).await;
if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de supprimer une étiquette"));
return Redirect::to(uri!("/tags", tag_index));
}
match sqlx::query("DELETE FROM Tags WHERE name == $1;")
.bind(&tag_name)
.execute(&mut **db)
.await {
Ok(affected_lines) => if affected_lines.rows_affected() != 1 {
error!("Did not remove {tag_name}");
cookies.add(("toast_error", "L'étiquette n'a pas pû être supprimée."));
}
else {
debug!("Tag {tag_name} successfully removed.")
},
Err(error) => {
error!("Error trying to remove {tag_name} : {error}");
cookies.add(("toast_error", "Erreur lors de la suppression de l'étiquette"));
}
};
Redirect::to(uri!("/tags", tag_index))
}
#[post("/update/<tag_name>", data="<updated_tag>")]
pub async fn update_tag(tag_name: &str, updated_tag: Form<Tag>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&mut db, cookies).await;
if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de modifier une étiquette"));
return Redirect::to(uri!("/tags", tag_index));
}
match sqlx::query("UPDATE Tags SET name = $1, color = $2 WHERE name == $3;")
.bind(&updated_tag.name)
.bind(&updated_tag.color)
.bind(&tag_name)
.execute(&mut **db)
.await {
Ok(affected_lines) => if affected_lines.rows_affected() != 1 {
error!("Did not update tag {tag_name}");
cookies.add(("toast_error", "L'étiquette n'a pas pû être modifiée."));
} else {
debug!("Updated {tag_name} successfully.");
}
Err(error) => {
error!("Error while updating {tag_name} : {error}");
cookies.add(("toast_error", "Erreur lors de la modification de l'étiquette."));
}
};
Redirect::to(uri!("/tags", tag_index))
}
#[derive(FromForm)]
pub struct TagForm {
name: String,
}
#[post("/<week>/tag/<truth_id>", data="<tag_form>")]
pub async fn tag_truth(week: u8, truth_id: u32, tag_form: Form<TagForm>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&mut db, cookies).await;
if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission d'ajouter une étiquette"));
return Redirect::to(uri!("/tags", tag_index));
}
let error_cookie = ||
cookies.add(("toast_error", "Erreur lors de l'ajout de l'étiquette."));
let tag_id: u32 = sqlx::query_scalar("SELECT id FROM Tags WHERE name == $1;")
.bind(&tag_form.name)
.fetch_one(&mut **db)
.await.unwrap_or(0);
if tag_id == 0 {
error_cookie();
error!("Error while trying to figure tag ID of {:?} to create the relation with {:?}.", &tag_form.name, &truth_id);
return Redirect::to(uri!(week::week(week)));
}
match sqlx::query("INSERT INTO TaggedTruths (truth_id, tag_id) VALUES ($1, $2);")
.bind(&truth_id)
.bind(&tag_id)
.execute(&mut **db)
.await {
Ok(affected_lines) => if affected_lines.rows_affected() != 1 {
error_cookie();
error!("Tag relation not added.");
} else {
debug!("Created tag relation with truth {truth_id} and tag {tag_id}");
}
Err(error) => {
error_cookie();
error!("Error while trying to add tag relation : {error}");
}
}
Redirect::to(uri!(week::week(week)))
}
#[post("/<week>/untag/<truth_id>", data="<tag_form>")]
pub async fn untag_truth(week: u8, truth_id: u32, tag_form: Form<TagForm>, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&mut db, cookies).await;
if !user.is_admin {
cookies.add(("toast_error", "Vous n'avez pas la permission de retirer une étiquette"));
return Redirect::to(uri!("/tags", tag_index));
}
let error_cookie = ||
cookies.add(("toast_error", "Erreur lors de la suppression de l'étiquette."));
let tag_id: u32 = sqlx::query_scalar("SELECT id FROM Tags WHERE name == $1;")
.bind(&tag_form.name)
.fetch_one(&mut **db)
.await.unwrap_or(0);
if tag_id == 0 {
error_cookie();
error!("Error while trying to figure tag ({:?}) ID to remove the relation with {:?}.", &tag_id, &truth_id);
return Redirect::to(uri!(week::week(week)));
}
match sqlx::query("DELETE FROM TaggedTruths WHERE truth_id == $1 AND tag_id == $2;")
.bind(&truth_id)
.bind(&tag_id)
.execute(&mut **db)
.await {
Ok(affected_lines) => if affected_lines.rows_affected() != 1 {
error_cookie();
error!("Tag relation not deleted.");
} else {
debug!("Removed tag relation with truth {truth_id} and tag {tag_id}");
}
Err(error) => {
error_cookie();
error!("Error while trying to remove tag relation : {error}");
}
}
Redirect::to(uri!(week::week(week)))
}
pub fn stage() -> AdHoc {
AdHoc::on_ignite("Tag stage", |rocket| async {
rocket
.mount("/tags", routes![tag_index, filtered_by_tags, create_tag, update_tag, delete_tag])
.mount("/", routes![tag_truth, untag_truth])
})
}

View file

@ -5,7 +5,6 @@ use rocket::response::Redirect;
use rocket_db_pools::{sqlx, Connection}; use rocket_db_pools::{sqlx, Connection};
use pulldown_cmark::{Parser, Options}; use pulldown_cmark::{Parser, Options};
use rocket::fairing::AdHoc;
use sqlx::Row; use sqlx::Row;
use crate::{auth, database, week}; use crate::{auth, database, week};
@ -18,7 +17,7 @@ pub struct TruthUpdateForm {
#[post("/<week>/edit/<truth_number>", data="<form>")] #[post("/<week>/edit/<truth_number>", data="<form>")]
pub async fn edit_truth(week: u8, truth_number: u8, form: Form<TruthUpdateForm>, pub async fn edit_truth(week: u8, truth_number: 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(&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!(week::week(week))); return Redirect::to(uri!(week::week(week)));
@ -57,7 +56,7 @@ pub async fn edit_truth(week: u8, truth_number: u8, form: Form<TruthUpdateForm>,
#[post("/<week>/new_truth", 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(&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!(week::week(week))); return Redirect::to(uri!(week::week(week)));
@ -111,9 +110,3 @@ pub async fn create_truth(week: u8, form: Form<TruthUpdateForm>,
Redirect::to(uri!(week::week(week))) Redirect::to(uri!(week::week(week)))
} }
pub fn stage() -> AdHoc {
AdHoc::on_ignite("Truth stage", |rocket| async {
rocket.mount("/", routes![create_truth, edit_truth])
})
}

View file

@ -12,29 +12,6 @@ use rocket_db_pools::{sqlx, Connection};
use crate::{auth, database, week}; use crate::{auth, database, week};
use crate::database_records::{Vote, VotingData}; use crate::database_records::{Vote, VotingData};
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct WeeklyUserVotes {
pub has_week_vote: bool,
pub votes: Vec<Vote>
}
pub async fn get_weekly_user_votes(week: u8, user: &auth::User, db: &mut Connection<database::Db>) -> WeeklyUserVotes {
let votes: Vec<Vote> = if user.logged_in && !user.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;")
.bind(week)
.bind(user.id)
.fetch_all(&mut ***db).await.unwrap_or_else(|error| {
error!("Error while getting votes : {error}");
Vec::<Vote>::new()
})
} else {
Vec::<Vote>::new()
};
WeeklyUserVotes {has_week_vote: if votes.is_empty() { false } else { true }, votes}
}
#[derive(FromForm)] #[derive(FromForm)]
pub struct VoteForm { pub struct VoteForm {
truth_votes: HashMap<u32, u16> truth_votes: HashMap<u32, u16>
@ -43,7 +20,7 @@ pub struct VoteForm {
#[post("/<week>/vote", data="<form>")] #[post("/<week>/vote", data="<form>")]
pub async fn vote(week: u8, form: Form<VoteForm>, pub async fn vote(week: u8, form: Form<VoteForm>,
mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect { mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&mut db, cookies).await; let user = auth::get_user(week, &mut db, cookies).await;
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."));
@ -60,11 +37,9 @@ pub async fn vote(week: u8, form: Form<VoteForm>,
} }
); );
let existing_votes = get_weekly_user_votes(week, &user, &mut db).await;
let mut had_error = false; let mut had_error = false;
for (truth_id, voted_id) in filtered_votes { for (truth_id, voted_id) in filtered_votes {
match existing_votes.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) { match user.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) {
Some(vote) => { Some(vote) => {
if *voted_id == vote.voted_id { if *voted_id == vote.voted_id {
continue; continue;
@ -122,7 +97,7 @@ pub struct VoteData {
// TODO: Cache vote count ? Maintain in state ? // TODO: Cache vote count ? Maintain in state ?
#[get("/<week>/votes", format = "application/json")] #[get("/<week>/votes", format = "application/json")]
pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Option<Json<VoteData>> { pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Option<Json<VoteData>> {
let user = auth::get_user(&mut db, cookies).await; let user = auth::get_user(week, &mut db, cookies).await;
let raw_votes: Vec<VotingData> = sqlx::query_as(" let raw_votes: Vec<VotingData> = sqlx::query_as("
SELECT Players.name as votes_for, Truths.number as truth_number, count(*) as votes FROM Votes 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 Players ON Votes.voted_id == Players.id
@ -189,8 +164,9 @@ pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>, cookies
Some(Json(VoteData{truth_count: truth_count, votes: vote_data})) Some(Json(VoteData{truth_count: truth_count, votes: vote_data}))
} }
// FIXME:
pub fn stage() -> AdHoc { pub fn stage() -> AdHoc {
AdHoc::on_ignite("Vote stage", |rocket| async { AdHoc::on_ignite("SQLx Stage", |rocket| async {
rocket.mount("/", routes![vote, fetch_vote_data]) rocket.mount("/", routes![vote, fetch_vote_data])
}) })
} }

View file

@ -4,30 +4,17 @@ use rocket::form::Form;
use rocket::http::CookieJar; use rocket::http::CookieJar;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket_db_pools::{sqlx, sqlx::Row, Connection}; use rocket_db_pools::{sqlx, Connection};
use rocket_dyn_templates::{context, Template}; use rocket_dyn_templates::{context, Template};
use sqlx::{Acquire, Executor}; use sqlx::{Acquire, Executor};
use crate::{auth, vote}; use crate::auth;
use crate::auth::User; use crate::auth::User;
use crate::database::Db; use crate::database::Db;
use crate::database_records::{DisplayTruth, Player, Tag, Truth, Week}; use crate::database_records::{DisplayTruth, Player, Truth, Week};
use crate::tag;
use crate::vote::WeeklyUserVotes;
pub async fn get_last_week(db: &mut Connection<Db>) -> u8 {
match sqlx::query("SELECT number FROM Weeks WHERE is_last_week == 1;")
.fetch_one(&mut ***db).await {
Ok(row) => row.try_get(0).ok().unwrap_or_else(|| 1), // If error, go back to 1
Err(error) => {
error!("Error while getting current week : {error:?}");
1
}
}
}
#[get("/<week_number>")] #[get("/<week_number>")]
pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Template { pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Template {
let user: User = auth::get_user(&mut db, cookies).await; let user: User = auth::get_user(week_number, &mut db, cookies).await;
let other_players = if user.logged_in { let other_players = if user.logged_in {
match sqlx::query_as("SELECT id, name FROM Players WHERE id <> $1 AND is_admin == 0 ORDER BY name") match sqlx::query_as("SELECT id, name FROM Players WHERE id <> $1 AND is_admin == 0 ORDER BY name")
@ -54,11 +41,9 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
} }
}; };
let vote_data: WeeklyUserVotes = vote::get_weekly_user_votes(week_number,&user, &mut db).await;
// FIXME : This is fucking *trash* but fucking hell mate // FIXME : This is fucking *trash* but fucking hell mate
if user.is_admin { if user.is_admin {
let mut truths: Vec<Truth> = match sqlx::query_as("SELECT * FROM Truths WHERE week == $1 ORDER BY number") let truths: Vec<Truth> = match sqlx::query_as("SELECT * FROM Truths WHERE week == $1 ORDER BY number")
.bind(week_number) .bind(week_number)
.fetch_all(&mut **db).await { .fetch_all(&mut **db).await {
Ok(v) => v, Ok(v) => v,
@ -68,22 +53,14 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
} }
}; };
for truth in &mut truths { Template::render("index", context! {
truth.tags = tag::get_truth_tags(truth.id, &mut db).await;
}
let tags: Vec<Tag> = tag::get_all_tags(&mut db, cookies).await;
Template::render("weeks/index", context! {
week_data: week_data, week_data: week_data,
truths: truths, truths: truths,
user: user, user: user,
other_players: other_players, other_players: other_players,
vote_data: vote_data,
tags: tags
}) })
} else { } else {
let mut truths: Vec<DisplayTruth> = match sqlx::query_as("SELECT id, week, number, author_id, rendered_text FROM Truths WHERE week == $1 ORDER BY number") let truths: Vec<DisplayTruth> = match sqlx::query_as("SELECT id, number, author_id, rendered_text FROM Truths WHERE week == $1 ORDER BY number")
.bind(week_number) .bind(week_number)
.fetch_all(&mut **db).await { .fetch_all(&mut **db).await {
Ok(v) => v, Ok(v) => v,
@ -93,16 +70,11 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
} }
}; };
for truth in &mut truths { Template::render("index", context! {
truth.tags = tag::get_truth_tags(truth.id, &mut db).await;
}
Template::render("weeks/index", context! {
week_data: week_data, week_data: week_data,
truths: truths, truths: truths,
user: user, user: user,
other_players: other_players, other_players: other_players,
vote_data: vote_data
}) })
} }
} }
@ -110,7 +82,7 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
#[post("/<week>/edit", data="<raw_intro>")] #[post("/<week>/edit", data="<raw_intro>")]
pub async fn update_week(week: u8, raw_intro: Form<String>, pub async fn update_week(week: u8, raw_intro: Form<String>,
mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect { mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&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!(week(week))); return Redirect::to(uri!(week(week)));
@ -146,7 +118,7 @@ pub async fn update_week(week: u8, raw_intro: Form<String>,
#[post("/<week>/set_last")] #[post("/<week>/set_last")]
pub async fn set_last_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect { pub async fn set_last_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&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!(week(week))); return Redirect::to(uri!(week(week)));
@ -201,7 +173,7 @@ pub async fn set_last_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar
#[get("/<week>/create")] #[get("/<week>/create")]
pub async fn create_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect { pub async fn create_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
let user = auth::get_user(&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!(week(week - 1))); return Redirect::to(uri!(week(week - 1)));
@ -238,9 +210,3 @@ pub async fn create_week(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'
} }
} }
} }
pub fn stage() -> AdHoc {
AdHoc::on_ignite("Week stage", |rocket| async {
rocket.mount("/", routes![week, create_week, update_week, set_last_week])
})
}

View file

@ -2,22 +2,10 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center;
margin-right: 2em; margin-right: 2em;
} }
.top_bar_side { .page_body {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
}
.top_bar_side > * {
padding: 0 0.5em;
}
.truth_page_body {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
@ -35,16 +23,12 @@
padding-bottom: 2em; padding-bottom: 2em;
} }
.individual_truth h3 { .individual_truth > h3 {
text-align: center; text-align: center;
padding-bottom: 4px; padding-bottom: 4px;
border-bottom: slategray solid 0.15em; border-bottom: slategray solid 0.15em;
} }
.individual_truth p {
padding: 0 0.5em;
}
.editor { .editor {
width: calc(100% - 1.25em); /* The width is calculated *inside* the padding, so adjust for it. */ width: calc(100% - 1.25em); /* The width is calculated *inside* the padding, so adjust for it. */
height: 6eM; height: 6eM;
@ -53,10 +37,6 @@
margin-bottom: 1em; margin-bottom: 1em;
} }
.truth_edit_form {
margin-bottom: 1em;
}
.graph { .graph {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -73,19 +53,13 @@
top: 25%; top: 25%;
} }
.week_change:link, .week_change:visited, .week_change:link, .week_change:visited {
.individual_truth > a, .individual_truth > a:link, .individual_truth > a:visited,
nav > a, nav > a:link, nav > a:visited,
h1 > a, h1 > a:link, h1 > a:visited {
color: black; color: black;
text-decoration: none; text-decoration: none;
transition: all .2s ease-in-out; transition: all .2s ease-in-out;
} }
.week_change:hover, .week_change:focus, .week_change:hover, .week_change:focus {
.individual_truth > a:hover, .individual_truth > a:focus,
nav > a:hover, nav > a:focus,
h1 > a:hover, h1 > a:focus {
color: mediumpurple; color: mediumpurple;
text-shadow: 0 2px 2px slategray; text-shadow: 0 2px 2px slategray;
} }
@ -105,93 +79,6 @@ h1 > a:hover, h1 > a:focus {
} }
} }
.tag_list {
display: flex;
flex-flow: column wrap;
align-content: flex-start;
max-height: 40vh;
margin-bottom: 1em;
}
@media (orientation: portrait) {
.tag_list {
max-height: none;
}
}
.new_tag {
display: flex;
flex-direction: column;
border: black dashed 1px;
padding: 0.5em;
margin-top: 0.5em;
width: fit-content;
}
.new_tag > button {
margin-top: 1em;
}
.tag {
display: flex;
justify-content: center;
margin: 0.25em;
width: fit-content;
text-decoration: none;
text-shadow:
-1px 0 3px black,
1px 0 3px black,
0 -1px 3px black,
0 1px 3px black;
color: white;
padding: 0.25em 0.75em 0.25em 0.25em;
border-bottom-left-radius: 0.4em;
border-top-left-radius: 0.4em;
border-bottom-right-radius: 1.5em 0.8em;
border-top-right-radius: 1.5em 0.8em;
transition: all .2s ease-in-out;
}
.tag:hover, .tag:focus {
color: mediumpurple;
}
.truth_tags {
display: flex;
flex-flow: wrap;
}
.truth_tags > .tag { /* We want smaller tags, the shadows must follow as well */
font-size: smaller;
text-shadow:
-1px 0 2px black,
1px 0 2px black,
0 -1px 2px black,
0 1px 2px black;
}
.truth_tags > form {
display: contents;
}
.login {
display: flex;
flex-direction: column;
}
.login > * {
padding-bottom: 0.1em;
}
.login > label {
align-self: flex-end;
}
.login > b {
align-self: center;
}
/* Global styling */ /* Global styling */
hr { hr {
@ -221,10 +108,3 @@ body {
body > h2 { body > h2 {
padding-left: 0.25eM; padding-left: 0.25eM;
} }
nav {
height: fit-content;
margin: 0 0.25em;
border-left: 0.15em solid slategray;
border-right: 0.15em solid slategray;
}

View file

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
{% set title = "Vérités Nova Borealis" %}
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<link href="/static/style.css" rel="stylesheet"/>
<link href="/static/favicon.ico" rel="icon"/>
{% block scripts %}
{% endblock %}
</head>
<body>
{% block top_bar %}
<div class="top_bar">
<div class="top_bar_side">
<h1><a href="/">{{ title }}</a></h1>
<nav>
Aller à :
<a href="/">Vérités</a>
-
<a href="/tags">Thèmes</a>
</nav>
</div>
<div class="top_bar_side">
{% block top_bar_side %}
<div>
{% if user.logged_in == true %}
<form class="login" id="logout" action="/{{ week_data.number }}/logout" method="POST">
Connecté en tant que <b>{{ user.name }}</b>
<button form="logout">Déconnecter</button>
</form>
{% else %}
<form class="login" id="login" action="/{{ week_data.number }}/login" method="POST">
<label>Pseudo <input form="login" type="text" name="name"/></label>
<label>Mot de passe <input form="login" type="password" name="password"/></label>
<button form="login">Se connecter</button>
</form>
{% endif %}
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block body %}
{% endblock %}
</body>
</html>

View file

@ -0,0 +1,8 @@
<div class="individual_truth">
<h3>Vérité {{ truth.number }}</h3>
<p>{{ truth.rendered_text | safe }}</p>
<hr/>
<form action="/{{ week_data.number }}/edit/{{ truth.number }}" method="POST">
{% include "truth_editor" %}
</form>
</div>

141
templates/index.html.tera Normal file
View file

@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="en">
{% set title = "Vérités Fabula Ultima" %}
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<link href="/style.css" rel="stylesheet"/>
{% if user.logged_in == true and not user.is_admin %}
<script defer="defer" type="text/javascript" src="/vote_handler.js"></script>
{% endif %}
<script defer="defer" type="application/javascript" src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script defer="defer" type="text/javascript" src="/vote_chart.js"></script>
</head>
{#{% import "week_change_arrows" as week_macro %}#}
{# For some reason the import does not work ? Figure it out at some point... #}
{%- 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 %}
{% set next_arrow_chara = '⟹' %}
{% endif %}
{# Remove the form if all votes are locked, to reduce confusion. #}
{% set lock_truth_form = user.votes | length + 1 == truths | length and week_data.is_last_week != true %}
<body>
<div class="top_bar">
<h1>{{ title }}</h1>
{% if user.logged_in == true %}
<form class="login" id="logout" action="/{{ week_data.number }}/logout" method="POST">
Connecté en tant que <b>{{ user.name }}</b>
<button form="logout">Déconnecter</button>
</form>
{% else %}
<form class="login" id="login" action="/{{ week_data.number }}/login" method="POST">
<label>Pseudo <input form="login" type="text" name="name"/></label>
<label>Mot de passe <input form="login" type="password" name="password"/></label>
<button form="login">Se connecter</button>
</form>
{% endif %}
</div>
<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="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">
{{ week_data.rendered_text | safe }}
{%- if user.is_admin == true -%}
<hr/>
<form action="/{{ week_data.number }}/edit" method="post">
<textarea class="editor" name="raw_intro">
{{- week_data.raw_text -}}
</textarea>
<button>
Modifier l'introduction
</button>
</form>
{% endif %}
</div>
{% if user.logged_in == true and user.is_admin == false and not lock_truth_form %}
<form id="truths" action="/{{ week_data.number }}/vote" method="POST">
{% endif %}
{# Truths start at 1 but the array starts at 0 #}
{% set index_delta = 1 %}
{% for truth in truths %}
{#
The truths are in an ordered array, but one of them might be the user's.
In this case, we need to stop the array index from incrementing if the current
truth is the user's, as they cannot have voted for themselves, leading to one
less votes than there are truths.
#}
{%- if truth.author_id == user.id -%}
{%- set_global index_delta = 2 -%}
{% endif %}
{% set truth_index = truth.number - index_delta %}
{% if user.is_admin == true %}
{% include "editable_truth" %}
{% else %}
{% include "truth" %}
{% endif %}
{% endfor %}
{% if user.logged_in == true and user.is_admin == false and not lock_truth_form %}
<br/>
<button form="truths">
{%- if user.logged_in == true and user.has_week_vote == true -%}
Changer de vote
{% else %}
À voter !
{% endif %}
</button>
</form>
{% endif %}
{# If admin, show an additional box for creating a new Truth. #}
{% if user.is_admin == true %}
<div class="individual_truth">
<h3>Nouvelle vérité</h3>
<form action="/{{ week_data.number }}/new_truth" method="POST">
{% include "truth_editor" %}
</form>
</div>
{% endif %}
</div>
<div class="graph">
<div>
<canvas id="vote_chart"></canvas>
</div>
</div>
</div>
</body>
</html>

View file

@ -1,28 +0,0 @@
{% extends "base_page" %}
{% block top_bar_side %}
{% endblock %}
{% block body %}
{% if tags | length == 0 %}
<h2>Ensemble des vérités à ce jour</h2>
{% elif tags | length == 1 %}
<h2>Vérités correponsdantes au thème</h2>
{% else %}
<h2>Vérités correponsdantes aux thèmes</h2>
{% endif %}
<div class="truth_tags">
{% for tag in tags %}
<a class="tag" href="/tags/filter?tags={{ tag.name }}"
style="background-color: {{ tag.color }}">{{ tag.name }}</a>
{% endfor %}
</div>
{% for truth in truths %}
<div class="individual_truth">
<a href="/{{ truth.week }}/#{{ truth.number }}"><h3 id="{{ truth.number }}">Semaine {{ truth.week }} - Vérité {{ truth.number }}</h3></a>
<p>{{ truth.rendered_text | safe }}</p>
</div>
{% endfor %}
{% endblock %}

View file

@ -1,42 +0,0 @@
{% extends "base_page" %}
{% block top_bar_side %}
{% endblock %}
{% block body %}
<h2>Liste des thèmes de Vérité</h2>
<form class="tag_list" id="filter_by_tags" action="/tags/filter">
{% for tag in tags %}
<div class="tag" style="background-color: {{ tag.color }}">
<label><input type="checkbox" form="filter_by_tags" name="tags" value="{{ tag.name }}">{{ tag.name }}</label>
</div>
{% endfor %}
</form>
<button form="filter_by_tags">Voir les vérités correspondantes</button>
{% if user.is_admin %}
<hr>
<h2>Éditer les thèmes</h2>
<div class="tag_list">
{% for tag in tags %}
<form class="tag" style="background-color: {{ tag.color }}" id="edit_tag[{{ tag.name }}]" method="post" action="/tags/update/{{ tag.name }}">
<input type="text" form="edit_tag[{{ tag.name }}]" name="name" value="{{ tag.name }}">
<input type="color" form="edit_tag[{{ tag.name }}]" name="color" value="{{ tag.color }}">
<button>Mettre à jour</button>
<button formaction="/tags/delete/{{ tag.name }}">Supprimer</button>
</form>
{% endfor %}
</div>
<form class="new_tag" id="new_tag" action="/tags/create" method="post">
<div>
<label>Nom de l'étiquette: <input type="text" form="new_tag" name="name"/></label>
<label>Couleur: <input type="color" form="new_tag" name="color"></label>
</div>
<button>Ajouter une étiquette</button>
</form>
{% endif %}
{% endblock %}

View file

@ -1,18 +1,12 @@
{%- set is_disabled = "" -%} {%- set is_disabled = "" -%}
{# If we are not during the active week, prevent changing vote but not sending a missing one. #} {# 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 vote_data.votes | filter(attribute="truth_id", value=truth.id) -%} {%- if week_data.is_last_week != true and user.votes | filter(attribute="truth_id", value=truth.id) -%}
{%- set is_disabled = "disabled" -%} {%- set is_disabled = "disabled" -%}
{%- endif -%} {%- endif -%}
<div class="individual_truth"> <div class="individual_truth">
<a href="/{{ truth.week }}/#{{ truth.number }}"><h3 id="{{ truth.number }}">Vérité {{ truth.number }}</h3></a> <h3>Vérité {{ truth.number }}</h3>
<p>{{ truth.rendered_text | safe }}</p> <p>{{ truth.rendered_text | safe }}</p>
<hr>
<div class="truth_tags">
{% for tag in truth.tags %}
<a class="tag" href="/tags/filter?tags={{ tag.name }}" style="background-color: {{ tag.color }}">{{ tag.name }}</a>
{% endfor %}
</div>
{% if user.logged_in %} {% if user.logged_in %}
<hr/> <hr/>
<label> <label>
@ -25,7 +19,7 @@
{% 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. #}
{% set_global is_selected = "" %} {% set_global is_selected = "" %}
{% for vote in vote_data.votes %} {% for vote in user.votes %}
{% if truth.id == vote.truth_id and player.id == vote.voted_id %} {% if truth.id == vote.truth_id and player.id == vote.voted_id %}
{% set_global is_selected = "selected" %} {% set_global is_selected = "selected" %}
{% break %} {% break %}

View file

@ -1,30 +0,0 @@
<div class="individual_truth">
<h3>Vérité {{ truth.number }}</h3>
<p>{{ truth.rendered_text | safe }}</p>
<div class="truth_tags">
{% for tag in truth.tags %}
<form action="/{{ week_data.number }}/untag/{{ truth.id }}" method="post">
<button name="name" value="{{ tag.name }}">❌</button>
</form>
<a class="tag" href="/tags/filter?tags={{ tag.name }}" style="background-color: {{ tag.color }}">
{{ tag.name }}
</a>
{% endfor %}
</div>
<hr/>
<form class="truth_edit_form" action="/{{ week_data.number }}/edit/{{ truth.number }}" method="POST">
{% include "weeks/truth_editor" %}
</form>
<form class="truth_edit_form" action="/{{ week_data.number }}/tag/{{ truth.id }}" method="post">
<label>Thème supplémentaire:
<select name="name">
{% for tag in tags %}
{% if not truth.tags is containing(tag) %}
<option value="{{ tag.name }}" style="background-color: {{ tag.color }}">{{ tag.name }}</option>
{% endif %}
{% endfor %}
</select>
</label>
<button>Ajouter un thème</button>
</form>
</div>

View file

@ -1,120 +0,0 @@
{% extends "base_page" %}
{% block scripts %}
{% if user.logged_in == true and not user.is_admin %}
<script defer="defer" type="text/javascript" src="/static/vote_handler.js"></script>
{% endif %}
<script defer="defer" type="application/javascript" src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<script defer="defer" type="text/javascript" src="/static/vote_chart.js"></script>
{% endblock %}
{#{% import "week_change_arrows" as week_macro %}#}
{# For some reason the import does not work ? Figure it out at some point... #}
{%- 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 -%}
{# Remove the form if all votes are locked, to reduce confusion. #}
{% set lock_truth_form = vote_data.votes | length + 1 == truths | length and week_data.is_last_week != true %}
{% block body %}
{# Apparently needs to be inside the block ? #}
{% 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 %}
{% set next_arrow_chara = '⟹' %}
{% endif %}
<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="truth_page_body">
<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">
{{ week_data.rendered_text | safe }}
{%- if user.is_admin == true -%}
<hr/>
<form action="/{{ week_data.number }}/edit" method="post">
<textarea class="editor" name="raw_intro">
{{- week_data.raw_text -}}
</textarea>
<button>
Modifier l'introduction
</button>
</form>
{% endif %}
</div>
{% if user.logged_in == true and user.is_admin == false and not lock_truth_form %}
<form id="truths" action="/{{ week_data.number }}/vote" method="POST">
{% endif %}
{# Truths start at 1 but the array starts at 0 #}
{% set index_delta = 1 %}
{% for truth in truths %}
{#
The truths are in an ordered array, but one of them might be the user's.
In this case, we need to stop the array index from incrementing if the current
truth is the user's, as they cannot have voted for themselves, leading to one
less votes than there are truths.
#}
{%- if truth.author_id == user.id -%}
{%- set_global index_delta = 2 -%}
{% endif %}
{% set truth_index = truth.number - index_delta %}
{% if user.is_admin == true %}
{% include "weeks/editable_truth" %}
{% else %}
{% include "weeks/truth" %}
{% endif %}
{% endfor %}
{% if user.logged_in == true and user.is_admin == false and not lock_truth_form %}
<br/>
<button form="truths">
{%- if user.logged_in == true and vote_data.has_week_vote == true -%}
Changer de vote
{% else %}
À voter !
{% endif %}
</button>
</form>
{% endif %}
{# If admin, show an additional box for creating a new Truth. #}
{% if user.is_admin == true %}
<div class="individual_truth">
<h3>Nouvelle vérité</h3>
<form action="/{{ week_data.number }}/new_truth" method="POST">
{% include "weeks/truth_editor" %}
</form>
</div>
{% endif %}
</div>
<div class="graph">
<div>
<canvas id="vote_chart"></canvas>
</div>
</div>
</div>
{% endblock %}