From ff31ad83fe5709892cf7ee15eb289219a4654889 Mon Sep 17 00:00:00 2001 From: philipp Date: Wed, 2 Aug 2023 14:29:19 +0200 Subject: [PATCH] add boatdamages functionality --- migration.sql | 7 +- src/model/boatdamage.rs | 167 +++++++++++++++++++++++++++ src/model/mod.rs | 1 + src/model/rower.rs | 2 +- src/model/user.rs | 64 ++++++++-- src/tera/admin/user.rs | 11 +- src/tera/boatdamage.rs | 114 ++++++++++++++++++ src/tera/mod.rs | 2 + templates/admin/user/index.html.tera | 1 + templates/boatdamages.html.tera | 59 ++++++++++ templates/includes/macros.html.tera | 7 ++ 11 files changed, 419 insertions(+), 16 deletions(-) create mode 100644 src/model/boatdamage.rs create mode 100644 src/tera/boatdamage.rs create mode 100644 templates/boatdamages.html.tera diff --git a/migration.sql b/migration.sql index 6faf5bb..c1214f9 100644 --- a/migration.sql +++ b/migration.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS "user" ( "is_cox" boolean NOT NULL DEFAULT FALSE, "is_admin" boolean NOT NULL DEFAULT FALSE, "is_guest" boolean NOT NULL DEFAULT TRUE, + "is_tech" boolean NOT NULL DEFAULT FALSE, "deleted" boolean NOT NULL DEFAULT FALSE, "last_access" DATETIME ); @@ -112,11 +113,11 @@ CREATE TABLE IF NOT EXISTS "boat_damage" ( "boat_id" INTEGER NOT NULL REFERENCES boat(id), "desc" text not null, "user_id_created" INTEGER NOT NULL REFERENCES user(id), - "created_at" text not null default CURRENT_TIMESTAMP, + "created_at" datetime not null default CURRENT_TIMESTAMP, "user_id_fixed" INTEGER REFERENCES user(id), -- none: not fixed yet - "fixed_at" text, + "fixed_at" datetime, "user_id_verified" INTEGER REFERENCES user(id), - "verified_at" text, + "verified_at" datetime, "lock_boat" boolean not null default false -- if true: noone can use the boat ); diff --git a/src/model/boatdamage.rs b/src/model/boatdamage.rs new file mode 100644 index 0000000..3e60085 --- /dev/null +++ b/src/model/boatdamage.rs @@ -0,0 +1,167 @@ +use crate::model::{boat::Boat, user::User}; +use chrono::NaiveDateTime; +use rocket::serde::{Deserialize, Serialize}; +use rocket::FromForm; +use sqlx::{FromRow, SqlitePool}; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct BoatDamage { + pub id: i64, + pub boat_id: i64, + pub desc: String, + pub user_id_created: i64, + pub created_at: NaiveDateTime, + pub user_id_fixed: Option, + pub fixed_at: Option, + pub user_id_verified: Option, + pub verified_at: Option, + pub lock_boat: bool, +} + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct BoatDamageWithDetails { + #[serde(flatten)] + boat_damage: BoatDamage, + user_created: User, + user_fixed: Option, + user_verified: Option, + boat: Boat, +} + +pub struct BoatDamageToAdd<'r> { + pub boat_id: i64, + pub desc: &'r str, + pub user_id_created: i32, + pub lock_boat: bool, +} + +#[derive(FromForm)] +pub struct BoatDamageFixed<'r> { + pub desc: &'r str, + pub user_id_fixed: i32, +} + +#[derive(FromForm)] +pub struct BoatDamageVerified<'r> { + pub desc: &'r str, + pub user_id_verified: i32, +} + +impl BoatDamage { + pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + sqlx::query_as!( + Self, + "SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat + FROM boat_damage + WHERE id like ?", + id + ) + .fetch_one(db) + .await + .ok() + } + + pub async fn all(db: &SqlitePool) -> Vec { + let boatdamages = sqlx::query_as!( + BoatDamage, + " +SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat +FROM boat_damage +ORDER BY created_at DESC + " + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + let mut res = Vec::new(); + for boat_damage in boatdamages { + let user_fixed = match boat_damage.user_id_fixed { + Some(id) => { + let user = User::find_by_id(db, id as i32).await; + Some(user.unwrap()) + } + None => None, + }; + let user_verified = match boat_damage.user_id_verified { + Some(id) => { + let user = User::find_by_id(db, id as i32).await; + Some(user.unwrap()) + } + None => None, + }; + + res.push(BoatDamageWithDetails { + boat: Boat::find_by_id(db, boat_damage.boat_id as i32) + .await + .unwrap(), + user_created: User::find_by_id(db, boat_damage.user_id_created as i32) + .await + .unwrap(), + user_fixed, + user_verified, + boat_damage, + }) + } + res + } + + pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> { + sqlx::query!( + "INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)", + boatdamage.boat_id, + boatdamage.desc, + boatdamage.user_id_created, + boatdamage.lock_boat + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn fixed(&self, db: &SqlitePool, boat: BoatDamageFixed<'_>) -> Result<(), String> { + sqlx::query!( + "UPDATE boat_damage SET desc=?, user_id_fixed=?, fixed_at=CURRENT_TIMESTAMP WHERE id=?", + boat.desc, + boat.user_id_fixed, + self.id + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + + let user = User::find_by_id(db, boat.user_id_fixed).await.unwrap(); + if user.is_tech { + return self + .verified( + db, + BoatDamageVerified { + desc: boat.desc, + user_id_verified: user.id as i32, + }, + ) + .await; + } + + Ok(()) + } + + pub async fn verified( + &self, + db: &SqlitePool, + boat: BoatDamageVerified<'_>, + ) -> Result<(), String> { + //TODO: Check if user is allowed to verify + + sqlx::query!( + "UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?", + boat.desc, + boat.user_id_verified, + self.id + ) + .execute(db) + .await.map_err(|e| e.to_string())?; + Ok(()) + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index ba70619..b18a4cd 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -8,6 +8,7 @@ use self::{ }; pub mod boat; +pub mod boatdamage; pub mod location; pub mod log; pub mod logbook; diff --git a/src/model/rower.rs b/src/model/rower.rs index f54b7a3..99065ef 100644 --- a/src/model/rower.rs +++ b/src/model/rower.rs @@ -14,7 +14,7 @@ impl Rower { sqlx::query_as!( User, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) ", diff --git a/src/model/user.rs b/src/model/user.rs index ca0e1ba..b8363d4 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -23,6 +23,7 @@ pub struct User { pub is_cox: bool, pub is_admin: bool, pub is_guest: bool, + pub is_tech: bool, pub deleted: bool, pub last_access: Option, } @@ -41,6 +42,7 @@ pub enum LoginError { NotLoggedIn, NotAnAdmin, NotACox, + NotATech, NoPasswordSet(User), DeserializationError, } @@ -73,7 +75,7 @@ impl User { sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE id like ? ", @@ -88,7 +90,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE name like ? ", @@ -130,7 +132,7 @@ WHERE name like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE deleted = 0 ORDER BY last_access DESC @@ -145,7 +147,7 @@ ORDER BY last_access DESC sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech FROM user WHERE deleted = 0 AND is_cox=true ORDER BY last_access DESC @@ -167,12 +169,20 @@ ORDER BY last_access DESC .is_ok() } - pub async fn update(&self, db: &SqlitePool, is_cox: bool, is_admin: bool, is_guest: bool) { + pub async fn update( + &self, + db: &SqlitePool, + is_cox: bool, + is_admin: bool, + is_guest: bool, + is_tech: bool, + ) { sqlx::query!( - "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ? where id = ?", + "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ?, is_tech = ? where id = ?", is_cox, is_admin, is_guest, + is_tech, self.id ) .execute(db) @@ -325,6 +335,46 @@ impl<'r> FromRequest<'r> for User { } } +pub struct TechUser { + pub(crate) user: User, +} + +impl Deref for TechUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.user + } +} + +impl TryFrom for TechUser { + type Error = LoginError; + + fn try_from(user: User) -> Result { + if user.is_tech { + Ok(TechUser { user }) + } else { + Err(LoginError::NotATech) + } + } +} + +#[async_trait] +impl<'r> FromRequest<'r> for TechUser { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + match User::from_request(req).await { + Outcome::Success(user) => match user.try_into() { + Ok(user) => Outcome::Success(user), + Err(_) => Outcome::Failure((Status::Unauthorized, LoginError::NotACox)), + }, + Outcome::Failure(f) => Outcome::Failure(f), + Outcome::Forward(f) => Outcome::Forward(f), + } + } +} + pub struct CoxUser { user: User, } @@ -469,7 +519,7 @@ mod test { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); - user.update(&pool, false, false, false).await; + user.update(&pool, false, false, false, false).await; let user = User::find_by_id(&pool, 1).await.unwrap(); assert_eq!(user.is_admin, false); diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index b4d9cd5..dd17918 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -63,6 +63,7 @@ struct UserEditForm { is_guest: bool, is_cox: bool, is_admin: bool, + is_tech: bool, } #[post("/user", data = "")] @@ -73,13 +74,13 @@ async fn update( ) -> Flash { let user = User::find_by_id(db, data.id).await; let Some(user) = user else { - return Flash::error( - Redirect::to("/admin/user"), - format!("User with ID {} does not exist!", data.id), - ) + return Flash::error( + Redirect::to("/admin/user"), + format!("User with ID {} does not exist!", data.id), + ); }; - user.update(db, data.is_cox, data.is_admin, data.is_guest) + user.update(db, data.is_cox, data.is_admin, data.is_guest, data.is_tech) .await; Flash::success(Redirect::to("/admin/user"), "Successfully updated user") diff --git a/src/tera/boatdamage.rs b/src/tera/boatdamage.rs new file mode 100644 index 0000000..be42d96 --- /dev/null +++ b/src/tera/boatdamage.rs @@ -0,0 +1,114 @@ +use rocket::{ + form::Form, + get, post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::Template; +use sqlx::SqlitePool; +use tera::Context; + +use crate::model::{ + boat::Boat, + boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, + user::{CoxUser, TechUser, User}, +}; + +#[get("/")] +async fn index(db: &State, flash: Option>, user: User) -> Template { + let boatdamages = BoatDamage::all(db).await; + let boats = Boat::all(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + context.insert("boatdamages", &boatdamages); + context.insert("boats", &boats); + context.insert("loggedin_user", &user); + + Template::render("boatdamages", context.into_json()) +} + +#[derive(FromForm)] +pub struct FormBoatDamageToAdd<'r> { + pub boat_id: i64, + pub desc: &'r str, + pub lock_boat: bool, +} + +#[post("/", data = "")] +async fn create<'r>( + db: &State, + data: Form>, + coxuser: CoxUser, +) -> Flash { + let boatdamage_to_add = BoatDamageToAdd { + boat_id: data.boat_id, + desc: data.desc, + lock_boat: data.lock_boat, + user_id_created: coxuser.id as i32, + }; + match BoatDamage::create(db, boatdamage_to_add).await { + Ok(_) => Flash::success( + Redirect::to("/boatdamage"), + "Ausfahrt erfolgreich hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")), + } +} + +#[derive(FromForm)] +pub struct FormBoatDamageFixed<'r> { + pub desc: &'r str, +} + +#[post("//fixed", data = "")] +async fn fixed<'r>( + db: &State, + data: Form>, + boatdamage_id: i32, + coxuser: CoxUser, +) -> Flash { + let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix + let boatdamage_fixed = BoatDamageFixed { + desc: data.desc, + user_id_fixed: coxuser.id as i32, + }; + match boatdamage.fixed(db, boatdamage_fixed).await { + Ok(_) => Flash::success(Redirect::to("/boatdamage"), "Successfully fixed the boat."), + Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")), + } +} + +#[derive(FromForm)] +pub struct FormBoatDamageVerified<'r> { + pub desc: &'r str, +} + +#[post("//verified", data = "")] +async fn verified<'r>( + db: &State, + data: Form>, + boatdamage_id: i32, + techuser: TechUser, +) -> Flash { + let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix + let boatdamage_verified = BoatDamageVerified { + desc: data.desc, + user_id_verified: techuser.id as i32, + }; + match boatdamage.verified(db, boatdamage_verified).await { + Ok(_) => Flash::success( + Redirect::to("/boatdamage"), + "Successfully verified the boat.", + ), + Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")), + } +} + +pub fn routes() -> Vec { + routes![index, create, fixed, verified] +} diff --git a/src/tera/mod.rs b/src/tera/mod.rs index b14b70c..bcbee30 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -21,6 +21,7 @@ use crate::model::{ mod admin; mod auth; +mod boatdamage; mod cox; mod log; mod misc; @@ -120,6 +121,7 @@ pub fn config(rocket: Rocket) -> Rocket { .mount("/auth", auth::routes()) .mount("/log", log::routes()) .mount("/stat", stat::routes()) + .mount("/boatdamage", boatdamage::routes()) .mount("/cox", cox::routes()) .mount("/admin", admin::routes()) .mount("/", misc::routes()) diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 68cdc98..76d26f3 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -51,6 +51,7 @@
{{ macros::checkbox(label='Gast', name='is_guest', id=loop.index , checked=user.is_guest) }} {{ macros::checkbox(label='Steuerberechtigter', name='is_cox', id=loop.index , checked=user.is_cox) }} + {{ macros::checkbox(label='Technical', name='is_tech', id=loop.index , checked=user.is_tech) }} {{ macros::checkbox(label='Admin', name='is_admin', id=loop.index , checked=user.is_admin) }}
{% if user.pw %} diff --git a/templates/boatdamages.html.tera b/templates/boatdamages.html.tera new file mode 100644 index 0000000..c5a0b22 --- /dev/null +++ b/templates/boatdamages.html.tera @@ -0,0 +1,59 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/log" as log %} + +{% extends "base" %} + +{% block content %} + +
+

Bootschäden

+ + {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} + {% endif %} +

Neuen Schaden eintragen

+
+ {{ log::boat_select(only_ones=false) }} + {{ macros::input(label='Beschreibung des Schadens', name='desc', type='text', required=true) }} + +
+ + + +
+ + + {% for boatdamage in boatdamages %} + Boat: {{ boatdamage.boat.name }} + Desc: {{ boatdamage.desc }} + Schaden eingetragen von {{ boatdamage.user_created.name }} am/um {{ boatdamage.created_at | date(format='%d. %m. %Y %H:%M') }} + {% if boatdamage.is_lock %} + Boot gesperrt + {% endif %} + {% if boatdamage.fixed_at %} + Repariert von {{ boatdamage.user_fixed.name }} am/um {{ boatdamage.fixed_at | date(format='%d. %m. %Y %H:%M') }} + {% elif loggedin_user.is_cox %} +
+ + {% if loggedin_user.is_tech %} + + {% else %} + + {% endif %} +
+ {% endif %} + {% if boatdamage.verified_at %} + Verifziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d. %m. %Y %H:%M') }} + {% elif loggedin_user.is_tech and boatdamage.fixed_at %} +
+ + +
+ {% endif %} +
+ {% endfor %} + +
+ +{% include "dynamics/sidebar" %} +{% endblock content%} diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 0c1f399..3a2567a 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -13,6 +13,13 @@ {% include "includes/question-icon" %} FAQs + {% if loggedin_user.is_admin%} + + Bootsschaden + Bootsschaden + + + {% endif %} {% if loggedin_user.is_admin %} STATS