Compare commits
39 commits
Author | SHA1 | Date | |
---|---|---|---|
76de321d41 | |||
4d5a423c78 | |||
322d37ce48 | |||
24ab2fc5f2 | |||
19898e1b09 | |||
d79375365d | |||
fb115e1bba | |||
198be70a99 | |||
f7efe0b66c | |||
072889173f | |||
4e1e9facc6 | |||
1b4a934398 | |||
1419bb6d51 | |||
b1f37a4e4a | |||
afdac98dea | |||
36b2e61e26 | |||
222acbb4f8 | |||
120472354c | |||
0c162d3b42 | |||
c4e252dbc2 | |||
36be6b51ae | |||
07d8cf42d7 | |||
ba98c3be84 | |||
4e1b13dc85 | |||
5ec38ee44f | |||
a0b79a17ea | |||
e08a46af3a | |||
635716c04b | |||
f41f5142c9 | |||
dfdd079ac4 | |||
207ce6c1d2 | |||
2b3dd28fed | |||
f7e1218f21 | |||
f74ed20e80 | |||
4c89a0783d | |||
d0843d600e | |||
8fd8ce8220 | |||
982a7ffd65 | |||
8547312a78 |
23 changed files with 1116 additions and 278 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -578,7 +578,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fabula_votes_server"
|
name = "fabula_votes_server"
|
||||||
version = "1.1.1"
|
version = "1.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"blake2",
|
"blake2",
|
||||||
|
|
|
@ -3,7 +3,7 @@ name = "fabula_votes_server"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = ["trotFunky"]
|
authors = ["trotFunky"]
|
||||||
version = "1.1.1"
|
version = "1.4.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
21
README.md
21
README.md
|
@ -7,25 +7,32 @@ 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
|
||||||
- [ ] Add introduction to the weekly truths
|
- [ ] Correctly handle non-existing week number
|
||||||
|
- [x] Add introduction to the weekly truths
|
||||||
- [ ] Bundle static assets in the binary
|
- [ ] Bundle static assets in the binary
|
||||||
- [ ] Move the databse 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 ?
|
- [x] Use fairings for the different elements
|
||||||
|
- [ ] 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 :
|
This project currently uses, for the backend :
|
||||||
- [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.
|
||||||
|
|
13
db/04_create-tags-tables.sql
Normal file
13
db/04_create-tags-tables.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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
|
||||||
|
);
|
78
src/auth.rs
78
src/auth.rs
|
@ -8,8 +8,9 @@ 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 crate::database_records::{AuthTokens, PlayerLoginInfo, Vote};
|
use rocket::fairing::AdHoc;
|
||||||
use crate::database;
|
use crate::database_records::{AuthTokens, PlayerLoginInfo};
|
||||||
|
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
|
||||||
|
@ -20,11 +21,9 @@ 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(week: u8, db: &mut Connection<Db>, cookies: &CookieJar<'_>) -> User {
|
pub async fn get_user(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
|
||||||
|
@ -86,26 +85,12 @@ 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 {
|
|
||||||
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 {
|
||||||
|
@ -113,8 +98,6 @@ pub async fn get_user(week: u8, db: &mut Connection<Db>, cookies: &CookieJar<'_>
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
id: 0,
|
id: 0,
|
||||||
name,
|
name,
|
||||||
has_week_vote: false,
|
|
||||||
votes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,8 +108,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 +118,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 +126,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 +150,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,24 +160,41 @@ 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<'_>) {
|
#[post("/<week>/logout")]
|
||||||
if cookies.get_private("auth_token").is_some() {
|
pub async fn logout(week: u8, mut db: Connection<Db>, cookies: &CookieJar<'_>) -> Redirect {
|
||||||
return
|
let auth_token: Option<String> = match cookies.get_private("auth_token") {
|
||||||
|
Some(cookie) => Some(cookie.value().to_string()),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should not be able to log out ?
|
||||||
|
if auth_token.is_none() {
|
||||||
|
return Redirect::to(uri!(week::week(week)))
|
||||||
}
|
}
|
||||||
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}");
|
match sqlx::query("DELETE FROM AuthTokens WHERE token == $1;")
|
||||||
|
.bind(auth_token)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await {
|
||||||
|
Ok(_) => debug!("Auth token deletion successful"),
|
||||||
|
Err(error) => debug!("Auth token could not be removed ({error}), proceeding anyway.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies.remove_private("auth_token");
|
||||||
|
cookies.remove_private("auth_id");
|
||||||
|
|
||||||
|
Redirect::to(uri!(week::week(week)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage() -> AdHoc {
|
||||||
|
AdHoc::on_ignite("Auth stage", |rocket| async {
|
||||||
|
rocket.mount("/", routes![login, logout])
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -19,21 +19,26 @@ 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 {
|
||||||
id: u32,
|
pub 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 {
|
||||||
id: u32,
|
pub 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)]
|
||||||
|
@ -67,3 +72,11 @@ 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
|
||||||
|
}
|
||||||
|
|
31
src/main.rs
31
src/main.rs
|
@ -1,17 +1,14 @@
|
||||||
#[macro_use] extern crate rocket;
|
#[macro_use] extern crate rocket;
|
||||||
|
|
||||||
use rocket::{Rocket, Build, futures};
|
|
||||||
use rocket::fs::{FileServer, relative};
|
use rocket::fs::{FileServer, relative};
|
||||||
use rocket::http::CookieJar;
|
|
||||||
use rocket::response::Redirect;
|
use rocket::response::Redirect;
|
||||||
use rocket::serde::{Serialize, Deserialize, json::Json};
|
|
||||||
|
|
||||||
use rocket_dyn_templates::{Template, context};
|
use rocket_dyn_templates::Template;
|
||||||
|
|
||||||
use rocket_db_pools::{sqlx, sqlx::Row, Database, Connection};
|
use rocket_db_pools::Connection;
|
||||||
use sqlx::Error;
|
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod tag;
|
||||||
mod truth;
|
mod truth;
|
||||||
mod vote;
|
mod vote;
|
||||||
mod week;
|
mod week;
|
||||||
|
@ -20,19 +17,10 @@ mod week;
|
||||||
mod database;
|
mod database;
|
||||||
mod database_records;
|
mod database_records;
|
||||||
use database::Db;
|
use database::Db;
|
||||||
use database_records::*;
|
|
||||||
use auth::User;
|
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index(mut db: Connection<Db>) -> Redirect {
|
async fn index(mut db: Connection<Db>) -> Redirect {
|
||||||
let current_week: u8 = match sqlx::query("SELECT number FROM Weeks WHERE is_last_week == 1;")
|
let current_week: u8 = week::get_last_week(&mut db).await;
|
||||||
.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})))
|
||||||
}
|
}
|
||||||
|
@ -40,10 +28,13 @@ async fn index(mut db: Connection<Db>) -> Redirect {
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", FileServer::from(relative!("static_files")))
|
.mount("/static/", 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])
|
.attach(auth::stage())
|
||||||
|
.attach(week::stage())
|
||||||
|
.attach(truth::stage())
|
||||||
|
.attach(vote::stage())
|
||||||
|
.attach(tag::stage())
|
||||||
|
.mount("/", routes![index])
|
||||||
.attach(database::stage())
|
.attach(database::stage())
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Random Row ID
|
|
295
src/tag.rs
Normal file
295
src/tag.rs
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
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])
|
||||||
|
})
|
||||||
|
}
|
25
src/truth.rs
25
src/truth.rs
|
@ -5,8 +5,9 @@ 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};
|
use crate::{auth, database, week};
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
pub struct TruthUpdateForm {
|
pub struct TruthUpdateForm {
|
||||||
|
@ -17,10 +18,10 @@ 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(week, &mut db, cookies).await;
|
let user = auth::get_user(&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 +51,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(&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 +77,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 +109,11 @@ 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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage() -> AdHoc {
|
||||||
|
AdHoc::on_ignite("Truth stage", |rocket| async {
|
||||||
|
rocket.mount("/", routes![create_truth, edit_truth])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
67
src/vote.rs
67
src/vote.rs
|
@ -9,9 +9,32 @@ 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(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>
|
||||||
|
@ -20,11 +43,11 @@ 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(week, &mut db, cookies).await;
|
let user = auth::get_user(&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."));
|
||||||
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(
|
||||||
|
@ -37,9 +60,11 @@ 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 user.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) {
|
match existing_votes.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;
|
||||||
|
@ -61,7 +86,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,18 +109,20 @@ 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)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct VoteData {
|
pub struct VoteData {
|
||||||
|
truth_count: u8,
|
||||||
votes: HashMap<String, Vec<u8>>,
|
votes: HashMap<String, Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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>) -> 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 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
|
||||||
|
@ -109,12 +136,31 @@ pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>) -> Opti
|
||||||
Vec::<VotingData>::new()
|
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});
|
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.
|
||||||
|
if !user.is_admin && (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;
|
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 {
|
||||||
|
@ -140,12 +186,11 @@ pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>) -> Opti
|
||||||
next_truth_number = raw_vote.truth_number + 1;
|
next_truth_number = raw_vote.truth_number + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Json(VoteData{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("SQLx Stage", |rocket| async {
|
AdHoc::on_ignite("Vote stage", |rocket| async {
|
||||||
rocket.mount("/", routes![vote, fetch_vote_data])
|
rocket.mount("/", routes![vote, fetch_vote_data])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
154
src/week.rs
154
src/week.rs
|
@ -4,16 +4,30 @@ 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, Connection};
|
use rocket_db_pools::{sqlx, sqlx::Row, Connection};
|
||||||
use rocket_dyn_templates::{context, Template};
|
use rocket_dyn_templates::{context, Template};
|
||||||
use crate::auth;
|
use sqlx::{Acquire, Executor};
|
||||||
|
use crate::{auth, vote};
|
||||||
use crate::auth::User;
|
use crate::auth::User;
|
||||||
use crate::database::Db;
|
use crate::database::Db;
|
||||||
use crate::database_records::{DisplayTruth, Player, Truth, Week};
|
use crate::database_records::{DisplayTruth, Player, Tag, 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(week_number, &mut db, cookies).await;
|
let user: User = auth::get_user(&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")
|
||||||
|
@ -21,7 +35,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,9 +54,11 @@ 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 truths: Vec<Truth> = match sqlx::query_as("SELECT * FROM Truths WHERE week == $1 ORDER BY number")
|
let mut 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,
|
||||||
|
@ -52,14 +68,22 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Template::render("index", context! {
|
for truth in &mut truths {
|
||||||
|
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 truths: Vec<DisplayTruth> = match sqlx::query_as("SELECT id, number, author_id, rendered_text FROM Truths WHERE week == $1 ORDER BY number")
|
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")
|
||||||
.bind(week_number)
|
.bind(week_number)
|
||||||
.fetch_all(&mut **db).await {
|
.fetch_all(&mut **db).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
@ -69,11 +93,16 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Template::render("index", context! {
|
for truth in &mut truths {
|
||||||
|
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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,10 +110,10 @@ 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(week, &mut db, cookies).await;
|
let user = auth::get_user(&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 +141,106 @@ 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(&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(&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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage() -> AdHoc {
|
||||||
|
AdHoc::on_ignite("Week stage", |rocket| async {
|
||||||
|
rocket.mount("/", routes![week, create_week, update_week, set_last_week])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,22 @@
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page_body {
|
.top_bar_side {
|
||||||
|
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;
|
||||||
|
@ -23,14 +35,28 @@
|
||||||
padding-bottom: 2em;
|
padding-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.individual_truth h3 {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: slategray solid 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.individual_truth p {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.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;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.truth_edit_form {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.graph {
|
.graph {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -47,10 +73,131 @@
|
||||||
top: 25%;
|
top: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all .2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 */
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 2px solid #9ec5fe;
|
border-top: 2px dotted slategray;
|
||||||
color: #9ec5fe;
|
color: slategray;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
@ -75,12 +222,9 @@ body > h2 {
|
||||||
padding-left: 0.25eM;
|
padding-left: 0.25eM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: portrait) {
|
nav {
|
||||||
.truth_list {
|
height: fit-content;
|
||||||
width: 60vw;
|
margin: 0 0.25em;
|
||||||
}
|
border-left: 0.15em solid slategray;
|
||||||
|
border-right: 0.15em solid slategray;
|
||||||
.graph {
|
|
||||||
width: 35vw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,67 @@
|
||||||
|
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");
|
let current_url = new URL(document.URL)
|
||||||
|
current_url.hash = '' // Strip the local part of the URL, for example if we select a Truth
|
||||||
|
const vote_response = await fetch(current_url.toString()+"/votes");
|
||||||
if (!vote_response.ok) {
|
if (!vote_response.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = ["Vérité 1", "Vérité 2", "Vérité 3", "Vérité 4", "Vérité 5", "Vérité 6", "Vérité 7"]
|
let keys = []
|
||||||
let datasets = []
|
let datasets = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const vote_data = (await vote_response.json()).votes;
|
const vote_data = (await vote_response.json());
|
||||||
for (let player in vote_data) {
|
for (let player in vote_data.votes) {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
parsing: true,
|
parsing: true,
|
||||||
label: player,
|
label: player,
|
||||||
data: vote_data[player],
|
data: vote_data.votes[player],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
for (let i = 1; i <= vote_data.truth_count; i++) {
|
||||||
|
keys.push("Vérité " + i)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse vote data : \n\t" + error.message);
|
console.error("Failed to parse vote data : \n\t" + error.message);
|
||||||
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 +83,13 @@ async function main() {
|
||||||
y: {
|
y: {
|
||||||
stacked: true
|
stacked: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
position: 'bottom',
|
||||||
|
text: 'Répartition des suppositions'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -75,7 +99,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)
|
||||||
|
|
50
templates/base_page.html.tera
Normal file
50
templates/base_page.html.tera
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<!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>
|
|
@ -1,8 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,109 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
||||||
{# Check if the user has a vote in advance, for readability #}
|
|
||||||
{% if user.logged_in == true and user.has_week_vote == true%}
|
|
||||||
{% set has_vote = true %}
|
|
||||||
{% else %}
|
|
||||||
{% set has_vote = false %}
|
|
||||||
{% endif -%}
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="top_bar">
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
{% if user.logged_in == true %}
|
|
||||||
<p>Connecté en tant que <b>{{ user.name }}</b></p>
|
|
||||||
{% else %}
|
|
||||||
<form class="login" id="login" action="/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>Semaine {{ week_data.number }}</h2>
|
|
||||||
<div class="page_body">
|
|
||||||
<div class="truth_list">
|
|
||||||
<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 %}
|
|
||||||
<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 %}
|
|
||||||
<br/>
|
|
||||||
<button form="truths">
|
|
||||||
{%- if has_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" method="POST">
|
|
||||||
{% include "truth_editor" %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="graph">
|
|
||||||
<div>
|
|
||||||
<canvas id="vote_chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
28
templates/tags/filtered_truths.html.tera
Normal file
28
templates/tags/filtered_truths.html.tera
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% 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 %}
|
42
templates/tags/index.html.tera
Normal file
42
templates/tags/index.html.tera
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{% 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 %}
|
|
@ -1,26 +0,0 @@
|
||||||
<div class="individual_truth">
|
|
||||||
<h3>Vérité {{ truth.number }}</h3>
|
|
||||||
<p>{{ truth.rendered_text | safe }}</p>
|
|
||||||
{% if user.logged_in %}
|
|
||||||
<hr/>
|
|
||||||
<label>
|
|
||||||
{%- if truth.author_id == user.id -%}
|
|
||||||
Tu l'as fait :)
|
|
||||||
{%- else -%}
|
|
||||||
Qui l'a fait ?
|
|
||||||
<select form="truths" name="truth_votes[{{ truth.id }}]">
|
|
||||||
<option value="0">---</option>
|
|
||||||
{% for player in other_players %}
|
|
||||||
{# Check if we should pre-select an existing vote #}
|
|
||||||
{% if has_vote == true and player.id == user.votes[truth_index].voted_id %}
|
|
||||||
{% set is_selected = "selected" %}
|
|
||||||
{% else %}
|
|
||||||
{% set is_selected = "" %}
|
|
||||||
{% endif %}
|
|
||||||
<option value="{{ player.id }}" {{- is_selected -}}>{{ player.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{%- endif -%}
|
|
||||||
</label>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
32
templates/weeks/editable_truth.tera
Normal file
32
templates/weeks/editable_truth.tera
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<div class="individual_truth">
|
||||||
|
<a href="/{{ truth.week }}#{{ truth.number }}"><h3 id="{{ truth.number }}">Vérité {{ truth.number }}</h3></a>
|
||||||
|
<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>
|
||||||
|
{% if tags | length > 0 and tags | length > truth.tags | length %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
120
templates/weeks/index.html.tera
Normal file
120
templates/weeks/index.html.tera
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{% 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 %}
|
40
templates/weeks/truth.html.tera
Normal file
40
templates/weeks/truth.html.tera
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{%- 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 vote_data.votes | filter(attribute="truth_id", value=truth.id) -%}
|
||||||
|
{%- set is_disabled = "disabled" -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
<div class="individual_truth">
|
||||||
|
<a href="/{{ truth.week }}#{{ truth.number }}"><h3 id="{{ truth.number }}">Vérité {{ truth.number }}</h3></a>
|
||||||
|
<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 %}
|
||||||
|
<hr/>
|
||||||
|
<label>
|
||||||
|
{%- if truth.author_id == user.id -%}
|
||||||
|
Tu l'as fait :)
|
||||||
|
{%- else -%}
|
||||||
|
Qui l'a fait ?
|
||||||
|
<select form="truths" name="truth_votes[{{ truth.id }}]" {{ is_disabled }}>
|
||||||
|
<option value="0">---</option>
|
||||||
|
{% for player in other_players %}
|
||||||
|
{# Check if we should pre-select an existing vote. #}
|
||||||
|
{% set_global is_selected = "" %}
|
||||||
|
{% for vote in vote_data.votes %}
|
||||||
|
{% if truth.id == vote.truth_id and player.id == vote.voted_id %}
|
||||||
|
{% set_global is_selected = "selected" %}
|
||||||
|
{% break %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<option value="{{ player.id }}" {{- is_selected -}}>{{ player.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{%- endif -%}
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
Loading…
Add table
Add a link
Reference in a new issue