diff --git a/README.md b/README.md index d199a1f..e9e966b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ ## New large features ### Logbuch -- Log with activities +Next: +- Make boats updateable (incl. rower + location) +- Write tests for model/boat.rs ### Guest-Scheckbuch - guest_trip @@ -100,4 +102,4 @@ user_details - [ ] (delete) GET /admin/planned-event//delete - [ ] (FileServer: static/) GET /public/ [10] - [ ] (login) POST /api/login/ - +- [ ] /tera/admin/boat.rs diff --git a/migration.sql b/migration.sql index 479350d..8414ee0 100644 --- a/migration.sql +++ b/migration.sql @@ -74,7 +74,7 @@ CREATE TABLE IF NOT EXISTS "boat" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" text NOT NULL UNIQUE, "amount_seats" integer NOT NULL, - "location_id" INTEGER NOT NULL REFERENCES location(id), + "location_id" INTEGER NOT NULL REFERENCES location(id) DEFAULT 1, "owner" INTEGER REFERENCES user(id), -- null: club is owner "year_built" INTEGER, "boatbuilder" TEXT, @@ -111,7 +111,7 @@ 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, + "created_at" text not null default CURRENT_TIMESTAMP, "user_id_fixed" INTEGER REFERENCES user(id), -- none: not fixed yet "fixed_at" text, "user_id_verified" INTEGER REFERENCES user(id), diff --git a/src/model/boat.rs b/src/model/boat.rs new file mode 100644 index 0000000..0db76c1 --- /dev/null +++ b/src/model/boat.rs @@ -0,0 +1,232 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct Boat { + pub id: i64, + pub name: String, + pub amount_seats: i64, + pub location_id: i64, + pub owner: Option, + pub year_built: Option, + pub boatbuilder: Option, + #[serde(default = "bool::default")] + default_shipmaster_only_steering: bool, + #[serde(default = "bool::default")] + skull: bool, + #[serde(default = "bool::default")] + external: bool, +} + +impl Boat { + pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + sqlx::query_as!( + Self, + " + SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, skull, external + FROM boat + WHERE id like ? + ", + id + ) + .fetch_one(db) + .await + .ok() + } + // + // pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option { + // sqlx::query_as!( + // User, + // " + //SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access + //FROM user + //WHERE name like ? + // ", + // name + // ) + // .fetch_one(db) + // .await + // .ok() + // } + // + pub async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as!( + Boat, + " +SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, skull, external +FROM boat +ORDER BY amount_seats DESC + " + ) + .fetch_all(db) + .await + .unwrap() //TODO: fixme + } + + pub async fn create( + db: &SqlitePool, + name: &str, + amount_seats: i64, + year_built: Option, + boatbuilder: Option<&str>, + default_shipmaster_only_steering: bool, + skull: bool, + external: bool, + ) -> bool { + sqlx::query!( + "INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, skull, external) VALUES (?,?,?,?,?,?,?)", + name, + amount_seats, + year_built, + boatbuilder, + default_shipmaster_only_steering, + skull, + external + ) + .execute(db) + .await + .is_ok() + } + + // pub async fn update(&self, db: &SqlitePool, is_cox: bool, is_admin: bool, is_guest: bool) { + // sqlx::query!( + // "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ? where id = ?", + // is_cox, + // is_admin, + // is_guest, + // self.id + // ) + // .execute(db) + // .await + // .unwrap(); //Okay, because we can only create a User of a valid id + // } + // + pub async fn delete(&self, db: &SqlitePool) { + sqlx::query!("DELETE FROM boat WHERE id=?", self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + } +} + +//#[cfg(test)] +//mod test { +// use crate::testdb; +// +// use super::User; +// use sqlx::SqlitePool; +// +// #[sqlx::test] +// fn test_find_correct_id() { +// let pool = testdb!(); +// let user = User::find_by_id(&pool, 1).await.unwrap(); +// assert_eq!(user.id, 1); +// } +// +// #[sqlx::test] +// fn test_find_wrong_id() { +// let pool = testdb!(); +// let user = User::find_by_id(&pool, 1337).await; +// assert!(user.is_none()); +// } +// +// #[sqlx::test] +// fn test_find_correct_name() { +// let pool = testdb!(); +// let user = User::find_by_name(&pool, "admin".into()).await.unwrap(); +// assert_eq!(user.id, 1); +// } +// +// #[sqlx::test] +// fn test_find_wrong_name() { +// let pool = testdb!(); +// let user = User::find_by_name(&pool, "name-does-not-exist".into()).await; +// assert!(user.is_none()); +// } +// +// #[sqlx::test] +// fn test_all() { +// let pool = testdb!(); +// let res = User::all(&pool).await; +// assert!(res.len() > 3); +// } +// +// #[sqlx::test] +// fn test_succ_create() { +// let pool = testdb!(); +// +// assert_eq!( +// User::create(&pool, "new-user-name".into(), false).await, +// true +// ); +// } +// +// #[sqlx::test] +// fn test_duplicate_name_create() { +// let pool = testdb!(); +// +// assert_eq!(User::create(&pool, "admin".into(), false).await, false); +// } +// +// #[sqlx::test] +// fn test_update() { +// let pool = testdb!(); +// +// let user = User::find_by_id(&pool, 1).await.unwrap(); +// user.update(&pool, false, false, false).await; +// +// let user = User::find_by_id(&pool, 1).await.unwrap(); +// assert_eq!(user.is_admin, false); +// } +// +// #[sqlx::test] +// fn succ_login_with_test_db() { +// let pool = testdb!(); +// User::login(&pool, "admin".into(), "admin".into()) +// .await +// .unwrap(); +// } +// +// #[sqlx::test] +// fn wrong_pw() { +// let pool = testdb!(); +// assert!(User::login(&pool, "admin".into(), "admi".into()) +// .await +// .is_err()); +// } +// +// #[sqlx::test] +// fn wrong_username() { +// let pool = testdb!(); +// assert!(User::login(&pool, "admi".into(), "admin".into()) +// .await +// .is_err()); +// } +// +// #[sqlx::test] +// fn reset() { +// let pool = testdb!(); +// let user = User::find_by_id(&pool, 1).await.unwrap(); +// +// user.reset_pw(&pool).await; +// +// let user = User::find_by_id(&pool, 1).await.unwrap(); +// assert_eq!(user.pw, None); +// } +// +// #[sqlx::test] +// fn update_pw() { +// let pool = testdb!(); +// let user = User::find_by_id(&pool, 1).await.unwrap(); +// +// assert!(User::login(&pool, "admin".into(), "abc".into()) +// .await +// .is_err()); +// +// user.update_pw(&pool, "abc".into()).await; +// +// User::login(&pool, "admin".into(), "abc".into()) +// .await +// .unwrap(); +// } +//} diff --git a/src/model/mod.rs b/src/model/mod.rs index 76e92fb..6d5c012 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -7,6 +7,7 @@ use self::{ trip::{Trip, TripWithUserAndType}, }; +pub mod boat; pub mod log; pub mod planned_event; pub mod trip; diff --git a/src/tera/admin/boat.rs b/src/tera/admin/boat.rs new file mode 100644 index 0000000..c1ccc32 --- /dev/null +++ b/src/tera/admin/boat.rs @@ -0,0 +1,113 @@ +use crate::model::{boat::Boat, user::AdminUser}; +use rocket::{ + form::Form, + get, post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::{tera::Context, Template}; +use sqlx::SqlitePool; + +#[get("/boat")] +async fn index( + db: &State, + admin: AdminUser, + flash: Option>, +) -> Template { + let boats = Boat::all(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("boats", &boats); + context.insert("loggedin_user", &admin.user); + + Template::render("admin/boat/index", context.into_json()) +} + +#[get("/boat//delete")] +async fn delete(db: &State, _admin: AdminUser, boat: i32) -> Flash { + let boat = Boat::find_by_id(db, boat).await; + match boat { + Some(boat) => { + boat.delete(db).await; + Flash::success( + Redirect::to("/admin/boat"), + format!("Sucessfully deleted boat {}", boat.name), + ) + } + None => Flash::error(Redirect::to("/admin/boat"), "Boat does not exist"), + } +} + +//#[derive(FromForm)] +//struct UserEditForm { +// id: i32, +// is_guest: bool, +// is_cox: bool, +// is_admin: bool, +//} +// +//#[post("/user", data = "")] +//async fn update( +// db: &State, +// data: Form, +// _admin: AdminUser, +//) -> 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), +// ) +// }; +// +// user.update(db, data.is_cox, data.is_admin, data.is_guest) +// .await; +// +// Flash::success(Redirect::to("/admin/user"), "Successfully updated user") +//} +// +#[derive(FromForm)] +struct BoatAddForm<'r> { + name: &'r str, + amount_seats: i64, + year_built: Option, + boatbuilder: Option<&'r str>, + default_shipmaster_only_steering: bool, + skull: bool, + external: bool, +} + +#[post("/boat/new", data = "")] +async fn create( + db: &State, + data: Form>, + _admin: AdminUser, +) -> Flash { + if Boat::create( + db, + data.name, + data.amount_seats, + data.year_built, + data.boatbuilder, + data.default_shipmaster_only_steering, + data.skull, + data.external, + ) + .await + { + Flash::success(Redirect::to("/admin/boat"), "Successfully created boat") + } else { + Flash::error( + Redirect::to("/admin/boat"), + format!("Error while creating boat {} in DB", data.name), + ) + } +} + +pub fn routes() -> Vec { + routes![index, create, delete] //, update] +} diff --git a/src/tera/admin/mod.rs b/src/tera/admin/mod.rs index b0cc07d..ee4f5fb 100644 --- a/src/tera/admin/mod.rs +++ b/src/tera/admin/mod.rs @@ -3,6 +3,7 @@ use sqlx::SqlitePool; use crate::{model::log::Log, tera::Config}; +pub mod boat; pub mod planned_event; pub mod user; @@ -17,6 +18,7 @@ async fn rss(db: &State, key: Option<&str>, config: &State) pub fn routes() -> Vec { let mut ret = Vec::new(); ret.append(&mut user::routes()); + ret.append(&mut boat::routes()); ret.append(&mut planned_event::routes()); ret.append(&mut routes![rss]); ret diff --git a/templates/admin/boat/index.html.tera b/templates/admin/boat/index.html.tera new file mode 100644 index 0000000..cdf01c4 --- /dev/null +++ b/templates/admin/boat/index.html.tera @@ -0,0 +1,78 @@ +{% import "includes/macros" as macros %} + +{% extends "base" %} + +{% block content %} +
+ {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} + {% endif %} + +

Boats

+ +
+
+

Neues Boot hinzufügen

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ default_shipmaster_only_steering: + + +
+
+ skull: + + +
+
+ external: + + +
+
+
+
+ +
+
+ + +
+
+ {% for boat in boats %} +
+
+ +
{{ boat.name }} +
+
+ +
+{% endfor %} +
+ +
+ +{% endblock content %} diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 7566399..951aeaa 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -13,6 +13,12 @@ FAQs {% if loggedin_user.is_admin %} + + BOATS + Bootsverwaltung + + + Userverwaltung