tag: Introduce tag system
Create a new table representing tags, composed of a name and color. They can be used to tag truths and filter them later. Tags display under the truths they correspond to and can be clicked to access all truths matching this tag. Introduce a new element in the top bar to allow navigating to the tag list, which can be used to create and edit tags for the admin and used to select a list of tags to filter against for everyone. Update the database records of the truths to include the tag vector. As the database query result is a multi-row result, it cannot be parsed automatically so it needs to be skipped and retrieved manually.
This commit is contained in:
parent
d79375365d
commit
19898e1b09
13 changed files with 551 additions and 25 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -578,7 +578,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
|||
|
||||
[[package]]
|
||||
name = "fabula_votes_server"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"blake2",
|
||||
|
|
|
@ -3,7 +3,7 @@ name = "fabula_votes_server"
|
|||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
authors = ["trotFunky"]
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
|
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
|
||||
);
|
|
@ -19,22 +19,26 @@ pub struct PlayerLoginInfo {
|
|||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Truth {
|
||||
id: u32,
|
||||
pub id: u32,
|
||||
week: u8,
|
||||
number: u8,
|
||||
author_id: u16,
|
||||
rendered_text: String,
|
||||
raw_text: String,
|
||||
#[sqlx(skip)]
|
||||
pub tags: Vec<Tag>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct DisplayTruth {
|
||||
id: u32,
|
||||
pub id: u32,
|
||||
week: u8,
|
||||
number: u8,
|
||||
author_id: u16,
|
||||
rendered_text: String,
|
||||
#[sqlx(skip)]
|
||||
pub tags: Vec<Tag>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize, Serialize)]
|
||||
|
@ -68,3 +72,11 @@ pub struct Week {
|
|||
pub rendered_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
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use rocket_dyn_templates::Template;
|
|||
use rocket_db_pools::Connection;
|
||||
|
||||
mod auth;
|
||||
|
||||
mod tag;
|
||||
mod truth;
|
||||
mod vote;
|
||||
mod week;
|
||||
|
@ -33,6 +33,7 @@ fn rocket() -> _ {
|
|||
.attach(week::stage())
|
||||
.attach(truth::stage())
|
||||
.attach(vote::stage())
|
||||
.attach(tag::stage())
|
||||
.mount("/", routes![index])
|
||||
.attach(database::stage())
|
||||
.attach(Template::fairing())
|
||||
|
|
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])
|
||||
})
|
||||
}
|
20
src/week.rs
20
src/week.rs
|
@ -10,7 +10,8 @@ use sqlx::{Acquire, Executor};
|
|||
use crate::{auth, vote};
|
||||
use crate::auth::User;
|
||||
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 {
|
||||
|
@ -57,7 +58,7 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
|
|||
|
||||
// FIXME : This is fucking *trash* but fucking hell mate
|
||||
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)
|
||||
.fetch_all(&mut **db).await {
|
||||
Ok(v) => v,
|
||||
|
@ -67,15 +68,22 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
|
|||
}
|
||||
};
|
||||
|
||||
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,
|
||||
truths: truths,
|
||||
user: user,
|
||||
other_players: other_players,
|
||||
vote_data: vote_data
|
||||
vote_data: vote_data,
|
||||
tags: tags
|
||||
})
|
||||
} else {
|
||||
let truths: Vec<DisplayTruth> = match sqlx::query_as("SELECT id, week, 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)
|
||||
.fetch_all(&mut **db).await {
|
||||
Ok(v) => v,
|
||||
|
@ -85,6 +93,10 @@ pub async fn week(week_number: u8, mut db: Connection<Db>, cookies: &CookieJar<'
|
|||
}
|
||||
};
|
||||
|
||||
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,
|
||||
truths: truths,
|
||||
|
|
|
@ -74,14 +74,18 @@
|
|||
}
|
||||
|
||||
.week_change:link, .week_change:visited,
|
||||
.individual_truth > a, .individual_truth > a:link, .individual_truth > a: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 {
|
||||
.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;
|
||||
}
|
||||
|
@ -101,6 +105,76 @@
|
|||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -147,3 +221,10 @@ body {
|
|||
body > h2 {
|
||||
padding-left: 0.25eM;
|
||||
}
|
||||
|
||||
nav {
|
||||
height: fit-content;
|
||||
margin: 0 0.25em;
|
||||
border-left: 0.15em solid slategray;
|
||||
border-right: 0.15em solid slategray;
|
||||
}
|
||||
|
|
|
@ -13,19 +13,33 @@
|
|||
<body>
|
||||
{% block top_bar %}
|
||||
<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 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 %}
|
||||
|
||||
|
|
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,8 +1,30 @@
|
|||
<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 action="/{{ week_data.number }}/edit/{{ truth.number }}" method="POST">
|
||||
<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>
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
<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>
|
||||
|
|
Loading…
Add table
Reference in a new issue