diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a6879e9..2219289 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,8 +12,8 @@ build: stage: build script: - cargo build --release --target $CARGO_TARGET + - strip target/$CARGO_TARGET/release/rot - cd frontend && npm install && npm run build - # - strip target/$CARGO_TARGET/release/rot artifacts: paths: - target/$CARGO_TARGET/release/rot @@ -37,11 +37,12 @@ deploy-staging: - ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts script: - scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/rot-updating + - scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ - scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ - scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ - scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/ - ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging' - - ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build' + - ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql' - ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing-staging/rot-updating /home/k004373/rowing-staging/rot' - ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging' only: diff --git a/Cargo.lock b/Cargo.lock index c990664..e9966dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,7 +135,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -146,7 +146,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -335,7 +335,7 @@ dependencies = [ "rand", "sha2", "subtle", - "time 0.3.23", + "time 0.3.24", "version_check", ] @@ -418,6 +418,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "deranged" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" + [[package]] name = "deunicode" version = "0.4.3" @@ -454,7 +460,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -510,9 +516,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -1423,7 +1429,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1462,7 +1468,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1532,7 +1538,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1588,7 +1594,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", "version_check", "yansi 1.0.0-rc", ] @@ -1673,7 +1679,7 @@ checksum = "2dfaf0c85b766276c797f3791f5bc6d5bd116b41d53049af2789666b0c0bc9fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1684,7 +1690,7 @@ checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.3", + "regex-automata 0.3.4", "regex-syntax 0.7.4", ] @@ -1699,9 +1705,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" dependencies = [ "aho-corasick", "memchr", @@ -1764,7 +1770,7 @@ dependencies = [ "serde", "state", "tempfile", - "time 0.3.23", + "time 0.3.24", "tokio", "tokio-stream", "tokio-util", @@ -1785,7 +1791,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.27", + "syn 2.0.28", "unicode-xid", ] @@ -1824,7 +1830,7 @@ dependencies = [ "smallvec", "stable-pattern", "state", - "time 0.3.23", + "time 0.3.24", "tokio", "uncased", ] @@ -1931,22 +1937,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.177" +version = "1.0.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ba2516aa6bf82e0b19ca8b50019d52df58455d3cf9bdaf6315225fdd0c560a" +checksum = "0a5bf42b8d227d4abf38a1ddb08602e229108a517cd4e5bb28f9c7eaafdce5c0" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.177" +version = "1.0.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3" +checksum = "741e124f5485c7e60c03b043f79f320bff3527f4bbf12cf3831750dc46a0ec2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -2116,7 +2122,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.23", + "time 0.3.24", "tokio-stream", "url", "webpki-roots", @@ -2199,9 +2205,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -2270,7 +2276,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -2295,10 +2301,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" dependencies = [ + "deranged", "itoa", "serde", "time-core", @@ -2313,9 +2320,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" dependencies = [ "time-core", ] @@ -2362,7 +2369,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -2461,7 +2468,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -2726,7 +2733,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -2748,7 +2755,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2962,9 +2969,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11" +checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" dependencies = [ "memchr", ] diff --git a/README.md b/README.md index ffce0da..4bd52cb 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,3 @@ -# Backend -- [] **Create missing backend tests (see below)** -- [] trip_details -> is_locked (default: false) -- [] ics for registered trips - -## New large features -### Logbuch -- Only layout + tests missing :-) - -### Guest-Scheckbuch -- guest_trip - - guest_user_id - - amount_trips - - paid_to_user_id -- guest_trip_logbook - - guest_trip_id - - logbook_id - -### Bootsreservierungen -- Confirmation required? -- How long in advance is it possible? -- Default reservations for some regular events (A+F, USI, ...)? - -### Notifications -- notifcations - - id - - message - - category - - created_at - - confirmed_at: Option - - user_id - - link -- ideas - - created an event at the same datetime as you - -### Schnupper-Pipeline -- Mail-Adressen von Interessierten dauerhaft entgegennehmen -- Termin ausgemacht -> Interessierte kontaktieren -- X Personen können teilnehmen (bis zu 3(?) pro Person erlauben (Familie)?) -- Automatisch Bestätigung bei Anmeldung schicken, mit Detail-Infos -- Ein paar Tage vorher Erinnerungs-Mail ausschicken -- Anmeldungen können manuell wieder gelöscht werden -- Es gibt Liste mit aktuellen Anmeldungen - -### Ergochallenge -- Bilder + Dateneingabe -- Automatische Mail senden - -## Backlog (i.e. don't work on this now) -### Sync w/ nextcloud -- remove most fields (names, ...) from users and add uid -- create user_nextcloud table; to be re-created every day(?) - -user -- UID -- pw -- last_access - -user_details -- UID -- fn (formatted name) -- is_cox (if CATEGORIES = {Steuerleute, Bootsführer}) -- is_admin (if CATEGORIES = Admin) -- is_guest (if person not in nextcloud) - -### Misc -- [] Don't show events if time > 1h(?) ago -- [] exactly same time -> deny registration -- [] automatically add regular planned trip -- [] same day+time: aggregate stats (x people, of which y cox and z rower) -- [] Lock trip; noone can register anymore -- [] on delete cascade doesn't work; e.g. created planned_event/trip + delete it -> trip_details entry still there! -- [] allow users to add u2f key -- [] Möglichkeiten für Bootseinteilungen bei planned_events anzeigen - - - # Frontend Process ´cd frontend´ ´npm install´ @@ -82,7 +5,6 @@ user_details # Notes / Bugfixes ## Frontend -- [] add UI for `trip_type` - [] support esc to close sidebar - [] after an hour(?) of inactivity -> show large popup w/ "maybe old data (ignore) (reload page)" (ignore bc maybe use is actively doing something -> don't throw input away!) @@ -90,34 +12,3 @@ user_details # Nice to have ## Frontend - [] my trips for cox - -# Missing backend tests - -- [x] (index) GET / -- [x] (faq) GET /faq -- [x] (cal) GET /cal -- [x] (FileServer: svelte/build) GET / -- [x] (join) GET /join/ -- [x] (remove) GET /remove/ -- [x] (create) POST /cox/trip -- [x] (update) POST /cox/trip/ -- [x] (join) GET /cox/join/ -- [ ] (remove) GET /cox/remove/ -- [ ] (remove_trip) GET /cox/remove/trip/ -- [ ] (index) GET /auth/ -- [ ] (login) POST /auth/ -- [ ] (logout) GET /auth/logout -- [ ] (updatepw) POST /auth/set-pw -- [ ] (setpw) GET /auth/set-pw/ -- [ ] (rss) GET /admin/rss? -- [ ] (index) GET /admin/user -- [ ] (update) POST /admin/user -- [ ] (create) POST /admin/planned-event -- [ ] (update) PUT /admin/planned-event -- [ ] (create) POST /admin/user/new -- [ ] (delete) GET /admin/user//delete -- [ ] (resetpw) GET /admin/user//reset-pw -- [ ] (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 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/rot.service b/rot.service index abbec21..ba8f463 100644 --- a/rot.service +++ b/rot.service @@ -4,13 +4,12 @@ Description=Rot [Service] User=root Group=root -WorkingDirectory=/home/k004373/rot +WorkingDirectory=/home/k004373/rowing Environment="ROCKET_ENV=prod" Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_PORT=8001" -Environment="ROCKET_LOG=info" -ExecStart=/home/k004373/rot/target/release/rot +Environment="RUST_LOG=info" +ExecStart=/home/k004373/rowing/rot [Install] WantedBy=multi-user.target - diff --git a/rotstaging.service b/rotstaging.service index 8091a14..7a7e252 100644 --- a/rotstaging.service +++ b/rotstaging.service @@ -9,8 +9,7 @@ Environment="ROCKET_ENV=prod" Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_PORT=7999" Environment="ROCKET_LOG=info" -ExecStart=./rot +ExecStart=/home/k004373/rowing-staging/rot [Install] WantedBy=multi-user.target - diff --git a/src/model/boat.rs b/src/model/boat.rs index 11fa625..444dcbf 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -50,7 +50,6 @@ pub struct BoatToAdd<'r> { #[derive(FromForm)] pub struct BoatToUpdate<'r> { - pub id: i32, pub name: &'r str, pub amount_seats: i64, pub year_built: Option, @@ -58,8 +57,8 @@ pub struct BoatToUpdate<'r> { pub default_shipmaster_only_steering: bool, pub skull: bool, pub external: bool, - pub location_id: Option, - pub owner: Option, + pub location_id: i64, + pub owner_id: Option, } impl Boat { @@ -126,7 +125,7 @@ ORDER BY amount_seats DESC res } - pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> bool { + pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> { sqlx::query!( "INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, skull, external, location_id, owner) VALUES (?,?,?,?,?,?,?,?,?)", boat.name, @@ -140,10 +139,11 @@ ORDER BY amount_seats DESC boat.owner ) .execute(db) - .await.is_ok() + .await.map_err(|e| e.to_string())?; + Ok(()) } - pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> bool { + pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> { sqlx::query!( "UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, skull=?, external=?, location_id=?, owner=? WHERE id=?", boat.name, @@ -154,12 +154,12 @@ ORDER BY amount_seats DESC boat.skull, boat.external, boat.location_id, - boat.owner, + boat.owner_id, self.id ) .execute(db) - .await - .is_ok() + .await.map_err(|e| e.to_string())?; + Ok(()) } pub async fn delete(&self, db: &SqlitePool) { @@ -179,6 +179,8 @@ mod test { use sqlx::SqlitePool; + use super::BoatToUpdate; + #[sqlx::test] fn test_find_correct_id() { let pool = testdb!(); @@ -220,7 +222,7 @@ mod test { } ) .await, - true + Ok(()) ); } @@ -244,7 +246,137 @@ mod test { } ) .await, - false + Err( + "error returned from database: (code: 2067) UNIQUE constraint failed: boat.name" + .into() + ) ); } + + #[sqlx::test] + fn test_is_locked() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 5) + .await + .unwrap() + .is_locked(&pool) + .await; + assert_eq!(res, true); + } + + #[sqlx::test] + fn test_is_not_locked() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 4) + .await + .unwrap() + .is_locked(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_is_not_locked_no_damage() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 3) + .await + .unwrap() + .is_locked(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_has_minor_damage() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 4) + .await + .unwrap() + .has_minor_damage(&pool) + .await; + assert_eq!(res, true); + } + + #[sqlx::test] + fn test_has_no_minor_damage() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 5) + .await + .unwrap() + .has_minor_damage(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_on_water() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 2) + .await + .unwrap() + .on_water(&pool) + .await; + assert_eq!(res, true); + } + + #[sqlx::test] + fn test_not_on_water() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 4) + .await + .unwrap() + .on_water(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_succ_update() { + let pool = testdb!(); + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + let update = BoatToUpdate { + name: "my-new-boat-name", + amount_seats: 3, + year_built: None, + boatbuilder: None, + default_shipmaster_only_steering: false, + skull: true, + external: false, + location_id: 1, + owner_id: None, + }; + + boat.update(&pool, update).await.unwrap(); + + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + assert_eq!(boat.name, "my-new-boat-name"); + } + + #[sqlx::test] + fn test_failed_update() { + let pool = testdb!(); + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + let update = BoatToUpdate { + name: "my-new-boat-name", + amount_seats: 3, + year_built: None, + boatbuilder: None, + default_shipmaster_only_steering: false, + skull: true, + external: false, + location_id: 999, + owner_id: None, + }; + + match boat.update(&pool, update).await { + Ok(_) => panic!("Update with invalid location should not succeed"), + Err(e) => assert_eq!( + e, + "error returned from database: (code: 787) FOREIGN KEY constraint failed" + ), + }; + + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + assert_eq!(boat.name, "Haichenbach"); + } } 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/logbook.rs b/src/model/logbook.rs index c7bf488..9781476 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -5,7 +5,7 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{boat::Boat, rower::Rower, user::User}; -#[derive(FromRow, Serialize, Clone)] +#[derive(FromRow, Serialize, Clone, Debug)] pub struct Logbook { pub id: i64, pub boat_id: i64, @@ -20,6 +20,12 @@ pub struct Logbook { pub logtype: Option, } +impl PartialEq for Logbook { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + #[derive(FromForm)] pub struct LogToAdd { pub boat_id: i32, @@ -54,11 +60,14 @@ pub struct LogbookWithBoatAndRowers { pub arrival_timestamp: Option, } +#[derive(Debug, PartialEq)] pub enum LogbookUpdateError { NotYourEntry, TooManyRowers(usize, usize), + RowerCreateError(i64, String), } +#[derive(Debug, PartialEq)] pub enum LogbookCreateError { BoatAlreadyOnWater, BoatLocked, @@ -66,6 +75,7 @@ pub enum LogbookCreateError { TooManyRowers(usize, usize), ShipmasterAlreadyOnWater, RowerAlreadyOnWater(User), + RowerCreateError(i64, String), } impl Logbook { @@ -164,8 +174,8 @@ ORDER BY departure DESC pub async fn create(db: &SqlitePool, log: LogToAdd) -> Result<(), LogbookCreateError> { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { - return Err(LogbookCreateError::BoatNotFound); - }; + return Err(LogbookCreateError::BoatNotFound); + }; if boat.is_locked(db).await { return Err(LogbookCreateError::BoatLocked); @@ -217,7 +227,11 @@ ORDER BY departure DESC .await.unwrap(); for rower in &log.rower { - Rower::create(&mut tx, inserted_row.id, *rower).await; + Rower::create(&mut tx, inserted_row.id, *rower) + .await + .map_err(|e| { + return LogbookCreateError::RowerCreateError(*rower, e.to_string()); + })?; } tx.commit().await.unwrap(); @@ -289,7 +303,9 @@ ORDER BY departure DESC self.remove_rowers(&mut tx).await; for rower in &log.rower { - Rower::create(&mut tx, self.id, *rower).await; + Rower::create(&mut tx, self.id, *rower).await.map_err(|e| { + return LogbookUpdateError::RowerCreateError(*rower, e.to_string()); + })?; } tx.commit().await.unwrap(); @@ -304,75 +320,286 @@ ORDER BY departure DESC // .unwrap(); //Okay, because we can only create a User of a valid id // } } -// -//#[cfg(test)] -//mod test { -// use crate::{model::boat::Boat, testdb}; -// -// use sqlx::SqlitePool; -// -// #[sqlx::test] -// fn test_find_correct_id() { -// let pool = testdb!(); -// let boat = Boat::find_by_id(&pool, 1).await.unwrap(); -// assert_eq!(boat.id, 1); -// } -// -// #[sqlx::test] -// fn test_find_wrong_id() { -// let pool = testdb!(); -// let boat = Boat::find_by_id(&pool, 1337).await; -// assert!(boat.is_none()); -// } -// -// #[sqlx::test] -// fn test_all() { -// let pool = testdb!(); -// let res = Boat::all(&pool).await; -// assert!(res.len() > 3); -// } -// -// #[sqlx::test] -// fn test_succ_create() { -// let pool = testdb!(); -// -// assert_eq!( -// Boat::create( -// &pool, -// "new-boat-name".into(), -// 42, -// None, -// "Best Boatbuilder".into(), -// true, -// true, -// false, -// Some(1), -// None -// ) -// .await, -// true -// ); -// } -// -// #[sqlx::test] -// fn test_duplicate_name_create() { -// let pool = testdb!(); -// -// assert_eq!( -// Boat::create( -// &pool, -// "Haichenbach".into(), -// 42, -// None, -// "Best Boatbuilder".into(), -// true, -// true, -// false, -// Some(1), -// None -// ) -// .await, -// false -// ); -// } -//} + +#[cfg(test)] +mod test { + use super::{LogToAdd, Logbook, LogbookCreateError, LogbookUpdateError}; + use crate::model::user::User; + use crate::testdb; + + use sqlx::SqlitePool; + + #[sqlx::test] + fn test_find_correct_id() { + let pool = testdb!(); + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + assert_eq!(logbook.id, 1); + } + + #[sqlx::test] + fn test_find_wrong_id() { + let pool = testdb!(); + let logbook = Logbook::find_by_id(&pool, 1337).await; + assert_eq!(logbook, None); + } + + #[sqlx::test] + fn test_on_water() { + let pool = testdb!(); + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let logbook_with_details = Logbook::on_water(&pool).await; + + assert_eq!(logbook_with_details[0].logbook, logbook); + } + + #[sqlx::test] + fn test_completed() { + let pool = testdb!(); + let completed = Logbook::completed(&pool).await; + + assert_eq!( + completed[0].logbook, + Logbook::find_by_id(&pool, 3).await.unwrap() + ); + assert_eq!( + completed[1].logbook, + Logbook::find_by_id(&pool, 2).await.unwrap() + ); + } + + //#[sqlx::test] + //fn test_all() { + // let pool = testdb!(); + // let res = Boat::all(&pool).await; + // assert!(res.len() > 3); + //} + + #[sqlx::test] + fn test_succ_create() { + let pool = testdb!(); + + Logbook::create( + &pool, + LogToAdd { + boat_id: 3, + shipmaster: 5, + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rower: Vec::new(), + }, + ) + .await + .unwrap() + } + + #[sqlx::test] + fn test_create_boat_not_found() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 999, + shipmaster: 5, + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rower: Vec::new(), + }, + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::BoatNotFound)); + } + + #[sqlx::test] + fn test_create_boat_locked() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 5, + shipmaster: 5, + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rower: Vec::new(), + }, + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::BoatLocked)); + } + + #[sqlx::test] + fn test_create_boat_on_water() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 2, + shipmaster: 5, + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rower: Vec::new(), + }, + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::BoatAlreadyOnWater)); + } + + #[sqlx::test] + fn test_create_shipmaster_on_water() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 3, + shipmaster: 2, + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rower: Vec::new(), + }, + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::ShipmasterAlreadyOnWater)); + } + + #[sqlx::test] + fn test_create_too_many_rowers() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 1, + shipmaster: 5, + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rower: vec![1], + }, + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2))); + } + + #[sqlx::test] + fn test_distances() { + let pool = testdb!(); + + let res = Logbook::distances(&pool).await; + + assert_eq!( + res, + vec![ + ("Ottensheim".into(), 25 as i64), + ("Ottensheim + Regattastrecke".into(), 29 as i64), + ] + ); + } + + #[sqlx::test] + fn test_succ_home() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let user = User::find_by_id(&pool, 2).await.unwrap(); + + logbook + .home( + &pool, + &user, + super::LogToFinalize { + destination: "new-destination".into(), + distance_in_km: 42, + comments: Some("Perfect water".into()), + logtype: None, + rower: vec![], + }, + ) + .await + .unwrap(); + } + + #[sqlx::test] + fn test_home_wrong_user() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let user = User::find_by_id(&pool, 1).await.unwrap(); + + let res = logbook + .home( + &pool, + &user, + super::LogToFinalize { + destination: "new-destination".into(), + distance_in_km: 42, + comments: Some("Perfect water".into()), + logtype: None, + rower: vec![], + }, + ) + .await; + + assert_eq!(res, Err(LogbookUpdateError::NotYourEntry)); + } + + #[sqlx::test] + fn test_home_too_many_rower() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let user = User::find_by_id(&pool, 2).await.unwrap(); + + let res = logbook + .home( + &pool, + &user, + super::LogToFinalize { + destination: "new-destination".into(), + distance_in_km: 42, + comments: Some("Perfect water".into()), + logtype: None, + rower: vec![1], + }, + ) + .await; + + assert_eq!(res, Err(LogbookUpdateError::TooManyRowers(1, 2))); + } +} diff --git a/src/model/logtype.rs b/src/model/logtype.rs index 111d42d..03d1b05 100644 --- a/src/model/logtype.rs +++ b/src/model/logtype.rs @@ -2,12 +2,12 @@ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; #[derive(FromRow, Debug, Serialize, Deserialize, Clone)] -pub struct LogType{ +pub struct LogType { pub id: i64, name: String, } -impl LogType{ +impl LogType { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!( Self, @@ -45,7 +45,7 @@ mod test { #[sqlx::test] fn test_find_true() { - let pool = testdb!(); + let _ = testdb!(); } //TODO: write tests 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/planned_event.rs b/src/model/planned_event.rs index 51cd58e..3028724 100644 --- a/src/model/planned_event.rs +++ b/src/model/planned_event.rs @@ -10,7 +10,7 @@ use sqlx::{FromRow, SqlitePool}; use super::{tripdetails::TripDetails, triptype::TripType, user::User}; -#[derive(Serialize, Clone, FromRow)] +#[derive(Serialize, Clone, FromRow, Debug, PartialEq)] pub struct PlannedEvent { pub id: i64, pub name: String, @@ -19,7 +19,7 @@ pub struct PlannedEvent { pub planned_starting_time: String, max_people: i64, pub day: String, - notes: Option, + pub notes: Option, pub allow_guests: bool, trip_type_id: Option, always_show: bool, diff --git a/src/model/rower.rs b/src/model/rower.rs index f2b4f78..99065ef 100644 --- a/src/model/rower.rs +++ b/src/model/rower.rs @@ -14,10 +14,10 @@ impl Rower { sqlx::query_as!( User, " - SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access - FROM user - WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) - ", +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=?) + ", log.id ) .fetch_all(db) @@ -25,14 +25,69 @@ impl Rower { .unwrap() } - pub async fn create(db: &mut Transaction<'_, Sqlite>, logbook_id: i64, rower_id: i64) { - let _ = sqlx::query!( + pub async fn create( + db: &mut Transaction<'_, Sqlite>, + logbook_id: i64, + rower_id: i64, + ) -> Result<(), String> { + //TODO: Check if rower is allowed to row + + sqlx::query!( "INSERT INTO rower(logbook_id, rower_id) VALUES (?,?)", logbook_id, rower_id ) .execute(db) .await - .unwrap(); + .map_err(|e| return e.to_string())?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use sqlx::SqlitePool; + + use super::Logbook; + use crate::model::{rower::Rower, user::User}; + use crate::testdb; + + #[sqlx::test] + fn test_for_log() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 3).await.unwrap(); + let rowers = Rower::for_log(&pool, &logbook).await; + let expected = User::find_by_id(&pool, 3).await.unwrap(); + assert_eq!(rowers, vec![expected]); + } + + #[sqlx::test] + fn test_for_log_none() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 2).await.unwrap(); + let rowers = Rower::for_log(&pool, &logbook).await; + assert_eq!(rowers, vec![]); + } + + #[sqlx::test] + fn test_create() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 3).await.unwrap(); + + let mut tx = pool.begin().await.unwrap(); + Rower::create(&mut tx, logbook.id, 2).await.unwrap(); + tx.commit().await.unwrap(); + let rowers = Rower::for_log(&pool, &logbook).await; + assert_eq!( + rowers, + vec![ + User::find_by_id(&pool, 2).await.unwrap(), + User::find_by_id(&pool, 3).await.unwrap() + ] + ); } } diff --git a/src/model/trip.rs b/src/model/trip.rs index a7671a6..bfb7564 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -154,7 +154,7 @@ FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE i .await .unwrap(); //Okay, as trip can only be created with proper DB backing let Some(trip_details_id) = trip_details.id else { - return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? + return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? }; sqlx::query!( @@ -176,7 +176,7 @@ FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE i db: &SqlitePool, cox: &CoxUser, planned_event: &PlannedEvent, - ) { + ) -> bool { sqlx::query!( "DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?", cox.id, @@ -184,7 +184,9 @@ FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE i ) .execute(db) .await - .unwrap(); //TODO: handle case where cox is not registered + .unwrap() + .rows_affected() + > 0 } pub(crate) async fn delete( diff --git a/src/model/triptype.rs b/src/model/triptype.rs index f13e634..f7aaf94 100644 --- a/src/model/triptype.rs +++ b/src/model/triptype.rs @@ -48,7 +48,7 @@ mod test { #[sqlx::test] fn test_find_true() { - let pool = testdb!(); + let _ = testdb!(); } //TODO: write tests diff --git a/src/model/user.rs b/src/model/user.rs index 9182679..b8363d4 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -23,11 +23,17 @@ pub struct User { pub is_cox: bool, pub is_admin: bool, pub is_guest: bool, - #[serde(default = "bool::default")] + pub is_tech: bool, pub deleted: bool, pub last_access: Option, } +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + #[derive(Debug)] pub enum LoginError { InvalidAuthenticationCombo, @@ -36,6 +42,7 @@ pub enum LoginError { NotLoggedIn, NotAnAdmin, NotACox, + NotATech, NoPasswordSet(User), DeserializationError, } @@ -66,9 +73,9 @@ impl User { pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { sqlx::query_as!( - User, + 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 ? ", @@ -81,9 +88,9 @@ WHERE id like ? pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option { sqlx::query_as!( - User, + 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 ? ", @@ -123,9 +130,9 @@ WHERE name like ? pub async fn all(db: &SqlitePool) -> Vec { sqlx::query_as!( - User, + 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 @@ -133,14 +140,14 @@ ORDER BY last_access DESC ) .fetch_all(db) .await - .unwrap() //TODO: fixme + .unwrap() } pub async fn cox(db: &SqlitePool) -> Vec { sqlx::query_as!( - User, + 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 @@ -148,7 +155,7 @@ ORDER BY last_access DESC ) .fetch_all(db) .await - .unwrap() //TODO: fixme + .unwrap() } pub async fn create(db: &SqlitePool, name: &str, is_guest: bool) -> bool { @@ -162,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) @@ -320,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, } @@ -464,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/boat.rs b/src/tera/admin/boat.rs index b8007c9..d57cf19 100644 --- a/src/tera/admin/boat.rs +++ b/src/tera/admin/boat.rs @@ -50,25 +50,22 @@ async fn delete(db: &State, _admin: AdminUser, boat: i32) -> Flash", data = "")] async fn update( db: &State, data: Form>, + boat_id: i32, _admin: AdminUser, ) -> Flash { - let boat = Boat::find_by_id(db, data.id).await; + let boat = Boat::find_by_id(db, boat_id).await; let Some(boat) = boat else { - return Flash::error( - Redirect::to("/admin/boat"), - "Boat does not exist!", - ) + return Flash::error(Redirect::to("/admin/boat"), "Boat does not exist!"); }; - if !boat.update(db, data.into_inner()).await { - return Flash::error(Redirect::to("/admin/boat"), "Boat could not be updated!"); + match boat.update(db, data.into_inner()).await { + Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Successfully updated boat"), + Err(e) => Flash::error(Redirect::to("/admin/boat"), e), } - - Flash::success(Redirect::to("/admin/boat"), "Successfully updated boat") } #[post("/boat/new", data = "")] @@ -77,16 +74,236 @@ async fn create( data: Form>, _admin: AdminUser, ) -> Flash { - if Boat::create(db, data.into_inner()).await { - Flash::success(Redirect::to("/admin/boat"), "Successfully created boat") - } else { - Flash::error( - Redirect::to("/admin/boat"), - "Error while creating the boat in DB", - ) + match Boat::create(db, data.into_inner()).await { + Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Successfully created boat"), + Err(e) => Flash::error(Redirect::to("/admin/boat"), e), } } pub fn routes() -> Vec { routes![index, create, delete, update] } + +#[cfg(test)] +mod test { + use rocket::{ + http::{ContentType, Status}, + local::asynchronous::Client, + }; + use sqlx::SqlitePool; + + use crate::tera::admin::boat::Boat; + use crate::testdb; + + #[sqlx::test] + fn test_boat_index() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/admin/boat"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let text = response.into_string().await.unwrap(); + assert!(&text.contains("Neues Boot hinzufügen")); + assert!(&text.contains("Kaputtes Boot :-(")); + assert!(&text.contains("Haichenbach")); + } + + #[sqlx::test] + fn test_succ_update() { + let db = testdb!(); + + let boat = Boat::find_by_id(&db, 1).await.unwrap(); + assert_eq!(boat.name, "Haichenbach"); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/1") + .header(ContentType::Form) + .body("name=Haichiii&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successSuccessfully updated boat"); + + let boat = Boat::find_by_id(&db, 1).await.unwrap(); + assert_eq!(boat.name, "Haichiii"); + } + + #[sqlx::test] + fn test_update_wrong_boat() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/1337") + .header(ContentType::Form) + .body("name=Haichiii&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorBoat does not exist!"); + + let boat = Boat::find_by_id(&db, 1).await.unwrap(); + assert_eq!(boat.name, "Haichenbach"); + } + + #[sqlx::test] + fn test_update_wrong_foreign() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/1") + .header(ContentType::Form) + .body("name=Haichiii&amount_seats=1&location_id=999"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "5:errorerror returned from database: (code: 787) FOREIGN KEY constraint failed" + ); + } + + #[sqlx::test] + fn test_succ_create() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/new") + .header(ContentType::Form) + .body("name=completely-new-boat&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successSuccessfully created boat"); + + let boat = Boat::find_by_id(&db, 6).await.unwrap(); + assert_eq!(boat.name, "completely-new-boat"); + } + + #[sqlx::test] + fn test_create_db_error() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/new") + .header(ContentType::Form) + .body("name=Haichenbach&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "5:errorerror returned from database: (code: 2067) UNIQUE constraint failed: boat.name" + ); + } +} diff --git a/src/tera/admin/planned_event.rs b/src/tera/admin/planned_event.rs index 54e9604..87676a5 100644 --- a/src/tera/admin/planned_event.rs +++ b/src/tera/admin/planned_event.rs @@ -30,7 +30,6 @@ async fn create( let data = data.into_inner(); let trip_details_id = TripDetails::create(db, data.tripdetails).await; - let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc. we //just created //the object @@ -87,3 +86,189 @@ async fn delete(db: &State, id: i64, _admin: AdminUser) -> Flash Vec { routes![create, delete, update] } + +#[cfg(test)] +mod test { + use rocket::{ + http::{ContentType, Status}, + local::asynchronous::Client, + }; + use sqlx::SqlitePool; + + use super::*; + use crate::testdb; + + #[sqlx::test] + fn test_delete() { + let db = testdb!(); + + let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/admin/planned-event/1/delete"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successSuccessfully deleted the event" + ); + + let event = PlannedEvent::find_by_id(&db, 1).await; + assert_eq!(event, None); + } + + #[sqlx::test] + fn test_delete_invalid_id() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/admin/planned-event/1337/delete"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorPlannedEvent does not exist"); + + let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap(); + } + + #[sqlx::test] + fn test_update() { + let db = testdb!(); + + let event = PlannedEvent::find_by_id(&db, 1).await.unwrap(); + assert_eq!(event.notes, Some("trip_details for a planned event".into())); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .put("/admin/planned-event") + .header(ContentType::Form) // Set the content type to form + .body("id=1&planned_amount_cox=2&max_people=3¬es=new-planned-event-text"); // Add the form data to the request body; + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successSuccessfully edited the event" + ); + + let event = PlannedEvent::find_by_id(&db, 1).await.unwrap(); + assert_eq!(event.notes, Some("new-planned-event-text".into())); + } + + #[sqlx::test] + fn test_update_invalid_id() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .put("/admin/planned-event") + .header(ContentType::Form) // Set the content type to form + .body("id=1337&planned_amount_cox=2&max_people=3¬es=new-planned-event-text"); // Add the form data to the request body; + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorPlanned event id not found"); + } + + #[sqlx::test] + fn test_create() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/planned-event") + .header(ContentType::Form) // Set the content type to form + .body("name=my-cool-new-event&planned_amount_cox=42&tripdetails.planned_starting_time=10:01&tripdetails.max_people=3&tripdetails.day=2345-12-20"); // Add the form data to the request body; + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successSuccessfully planned the event" + ); + + let event = PlannedEvent::find_by_id(&db, 2).await.unwrap(); + assert_eq!(event.name, "my-cool-new-event"); + } +} 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/auth.rs b/src/tera/auth.rs index 329b692..bd121db 100644 --- a/src/tera/auth.rs +++ b/src/tera/auth.rs @@ -75,7 +75,7 @@ async fn login( ); } Err(_) => { - return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere Philipp H. (Tel.nr. siehe Signalgruppe) oder schreibe eine Mail an rudern@gmx.at!"); + return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere Philipp H. (Tel.nr. siehe Signalgruppe) oder schreibe eine Mail an it@rudernlinz.at!"); } }; @@ -112,11 +112,11 @@ async fn updatepw( cookies: &CookieJar<'_>, ) -> Flash { let user = User::find_by_id(db, updatepw.userid).await; - let Some(user) = user else{ - return Flash::error( - Redirect::to("/auth"), - format!("User with ID {} does not exist!", updatepw.userid), - ) + let Some(user) = user else { + return Flash::error( + Redirect::to("/auth"), + format!("User with ID {} does not exist!", updatepw.userid), + ); }; if updatepw.password != updatepw.password_confirm { 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/cox.rs b/src/tera/cox.rs index 2aecced..ad4a344 100644 --- a/src/tera/cox.rs +++ b/src/tera/cox.rs @@ -129,18 +129,20 @@ async fn remove_trip(db: &State, trip_id: i64, cox: CoxUser) -> Flas #[get("/remove/")] async fn remove(db: &State, planned_event_id: i64, cox: CoxUser) -> Flash { if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await { - Trip::delete_by_planned_event(db, &cox, &planned_event).await; + if Trip::delete_by_planned_event(db, &cox, &planned_event).await { + Log::create( + db, + format!( + "Cox {} deleted registration for planned_event.id={}", + cox.name, planned_event_id + ), + ) + .await; - Log::create( - db, - format!( - "Cox {} deleted registration for planned_event.id={}", - cox.name, planned_event_id - ), - ) - .await; - - Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") + Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") + } else { + Flash::error(Redirect::to("/"), "Steuermann hilft nicht aus...") + } } else { Flash::error(Redirect::to("/"), "Planned_event does not exist.") } @@ -425,4 +427,101 @@ mod test { assert_eq!(flash_cookie.value(), "5:errorEvent gibt's nicht"); } + + #[sqlx::test] + fn test_remove() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=cox&password=cox"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/cox/join/1"); + let response = req.dispatch().await; + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successDanke für's helfen!"); + + let req = client.get("/cox/join/1"); + let _ = req.dispatch().await; + + let req = client.get("/cox/remove/1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!"); + } + + #[sqlx::test] + fn test_remove_wrong_id() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=cox&password=cox"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/cox/remove/999"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorPlanned_event does not exist."); + } + + #[sqlx::test] + fn test_remove_cox_not_participating() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=cox&password=cox"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/cox/remove/1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorSteuermann hilft nicht aus..."); + } } diff --git a/src/tera/log.rs b/src/tera/log.rs index 6715f1e..bc8a8df 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -129,6 +129,7 @@ async fn create_logbook(db: &SqlitePool, data: Form) -> Flash Flash::error(Redirect::to("/log"), format!("Boot gesperrt")), Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), format!("Boot gibt's ned")), Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), + Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")), } } @@ -159,10 +160,10 @@ async fn home_logbook( ) -> Flash { let logbook: Option = Logbook::find_by_id(db, logbook_id).await; let Some(logbook) = logbook else { - return Flash::error( - Redirect::to("/admin/log"), - format!("Log with ID {} does not exist!", logbook_id), - ) + return Flash::error( + Redirect::to("/admin/log"), + format!("Log with ID {} does not exist!", logbook_id), + ); }; match logbook.home(db, user, data.into_inner()).await { @@ -219,4 +220,164 @@ pub fn routes() -> Vec { } #[cfg(test)] -mod test {} +mod test { + use rocket::http::ContentType; + use rocket::{http::Status, local::asynchronous::Client}; + use sqlx::SqlitePool; + + use crate::testdb; + + #[sqlx::test] + fn test_kiosk_cookie() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/auth")); + + let req = client.get("/log/kiosk/ekrv2019"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let req = client.get("/log"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Neue Ausfahrt")); + } + + #[sqlx::test] + fn test_index() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/log"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Neue Ausfahrt")); + } + + #[sqlx::test] + fn test_show() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/log/show"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Bootsname: Joe")); + } + + #[sqlx::test] + fn test_show_kiosk() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log/kiosk/ekrv2019"); + let _ = req.dispatch().await; + + let req = client.get("/log/show"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Bootsname: Joe")); + } + + #[sqlx::test] + fn test_create() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/log") + .header(ContentType::Form) + .body("boat_id=1&shipmaster=4&departure=2199-12-31T10:00"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt erfolgreich hinzugefügt" + ); + } + + #[sqlx::test] + fn test_home_kiosk() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log/kiosk/ekrv2019"); + let _ = req.dispatch().await; + + let req = client + .post("/log/1") + .header(ContentType::Form) + .body("destination=Ottensheim&distance_in_km=25"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successSuccessfully updated log"); + } +} diff --git a/src/tera/mod.rs b/src/tera/mod.rs index fea7fe8..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; @@ -47,7 +48,9 @@ async fn index(db: &State, user: User, flash: Option")] async fn join(db: &State, trip_details_id: i64, user: User) -> Flash { - let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { return Flash::error(Redirect::to("/"), "Trip_details do not exist.") }; + let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { + return Flash::error(Redirect::to("/"), "Trip_details do not exist."); + }; match UserTrip::create(db, &user, &trip_details).await { Ok(_) => { @@ -84,8 +87,8 @@ async fn join(db: &State, trip_details_id: i64, user: User) -> Flash #[get("/remove/")] async fn remove(db: &State, trip_details_id: i64, user: User) -> Flash { let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { - return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); - }; + return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); + }; UserTrip::delete(db, &user, &trip_details).await; @@ -118,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()) @@ -243,4 +247,22 @@ mod test { assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist."); } + + #[sqlx::test] + fn test_public() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + + let req = client.get("/public/main.css"); + let response = req.dispatch().await; + assert_eq!(response.status(), Status::Ok); + + let req = client.get("/public/main.js"); + let response = req.dispatch().await; + assert_eq!(response.status(), Status::Ok); + } } diff --git a/staging-diff.sql b/staging-diff.sql new file mode 100644 index 0000000..4eba322 --- /dev/null +++ b/staging-diff.sql @@ -0,0 +1 @@ +UPDATE user SET is_admin=true WHERE name IN ('Sandra Sollberger', 'Thomas Hoffelner', 'Manfred Meindl', 'Bernhard Heinemann'); 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..e60cb93 --- /dev/null +++ b/templates/boatdamages.html.tera @@ -0,0 +1,60 @@ +{% 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) }} + {{ macros::checkbox(label='Boot sperren', name='lock_boat', 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/forms/boat.html.tera b/templates/includes/forms/boat.html.tera index a17718d..f29e633 100644 --- a/templates/includes/forms/boat.html.tera +++ b/templates/includes/forms/boat.html.tera @@ -22,7 +22,7 @@ {% macro edit(boat, uuid) %} -
+