diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3899fda..affa1d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,7 +42,7 @@ deploy-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 && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql' + - 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 && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen && && 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: @@ -62,7 +62,7 @@ deploy-main: - scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing/ - scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing/ - scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing/ - - ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build' + - ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen' - ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot' - ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing/rot-updating /home/k004373/rowing/rot' - ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot' diff --git a/Rocket.toml b/Rocket.toml index f774acf..b16f20f 100644 --- a/Rocket.toml +++ b/Rocket.toml @@ -1,3 +1,4 @@ [default] secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" rss_key = "rss-key-for-ci" +limits = { file = "10 MiB"} diff --git a/db.sqlite.bkp b/db.sqlite.bkp deleted file mode 100644 index 0ff20bc..0000000 Binary files a/db.sqlite.bkp and /dev/null differ diff --git a/migration.sql b/migration.sql index f81938d..671a644 100644 --- a/migration.sql +++ b/migration.sql @@ -7,7 +7,12 @@ CREATE TABLE IF NOT EXISTS "user" ( "is_guest" boolean NOT NULL DEFAULT TRUE, "is_tech" boolean NOT NULL DEFAULT FALSE, "deleted" boolean NOT NULL DEFAULT FALSE, - "last_access" DATETIME + "last_access" DATETIME, + "dob" text, + "weight" text, + "sex" text, + "dirty_thirty" text, + "dirty_dozen" text ); CREATE TABLE IF NOT EXISTS "trip_type" ( @@ -115,4 +120,3 @@ CREATE TABLE IF NOT EXISTS "boat_damage" ( "verified_at" datetime, "lock_boat" boolean not null default false -- if true: noone can use the boat ); - diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 85e4007..f03f209 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -389,7 +389,7 @@ ORDER BY departure DESC .await .unwrap() .id - .unwrap() + .unwrap() as i32 } pub async fn home( diff --git a/src/model/rower.rs b/src/model/rower.rs index 707c5d6..c8654cb 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, is_tech +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex 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 8a6366d..90061c7 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{log::Log, tripdetails::TripDetails, Day}; +use crate::tera::admin::user::UserEditForm; #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct User { @@ -24,6 +25,9 @@ pub struct User { pub is_admin: bool, pub is_guest: bool, pub is_tech: bool, + pub dob: Option, + pub weight: Option, + pub sex: Option, pub deleted: bool, pub last_access: Option, } @@ -92,7 +96,7 @@ impl User { sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex FROM user WHERE id like ? ", @@ -107,7 +111,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex FROM user WHERE id like ? ", @@ -122,7 +126,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex FROM user WHERE name like ? ", @@ -164,7 +168,7 @@ WHERE name like ? sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex FROM user WHERE deleted = 0 ORDER BY last_access DESC @@ -175,11 +179,26 @@ ORDER BY last_access DESC .unwrap() } + pub async fn ergo(db: &SqlitePool) -> Vec { + sqlx::query_as!( + Self, + " +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex +FROM user +WHERE deleted = 0 AND dob is not null and weight is not null and sex is not null +ORDER BY last_access DESC + " + ) + .fetch_all(db) + .await + .unwrap() + } + pub async fn cox(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " -SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex FROM user WHERE deleted = 0 AND is_cox=true ORDER BY last_access DESC @@ -201,20 +220,16 @@ ORDER BY last_access DESC .is_ok() } - pub async fn update( - &self, - db: &SqlitePool, - is_cox: bool, - is_admin: bool, - is_guest: bool, - is_tech: bool, - ) { + pub async fn update(&self, db: &SqlitePool, data: UserEditForm) { sqlx::query!( - "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ?, is_tech = ? where id = ?", - is_cox, - is_admin, - is_guest, - is_tech, + "UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ?, is_tech = ?, dob = ?, weight = ?, sex = ? where id = ?", + data.is_cox, + data.is_admin, + data.is_guest, + data.is_tech, + data.dob, + data.weight, + data.sex, self.id ) .execute(db) @@ -515,7 +530,7 @@ impl<'r> FromRequest<'r> for NonGuestUser { #[cfg(test)] mod test { - use crate::testdb; + use crate::{tera::admin::user::UserEditForm, testdb}; use super::User; use sqlx::SqlitePool; @@ -584,7 +599,20 @@ mod test { let pool = testdb!(); let user = User::find_by_id(&pool, 1).await.unwrap(); - user.update(&pool, false, false, false, false).await; + user.update( + &pool, + UserEditForm { + id: 1, + is_guest: false, + is_cox: false, + is_admin: false, + is_tech: false, + dob: None, + weight: None, + sex: None, + }, + ) + .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 7a94196..f0a719c 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -58,12 +58,15 @@ async fn delete(db: &State, _admin: AdminUser, user: i32) -> Flash, + pub(crate) weight: Option, + pub(crate) sex: Option, } #[post("/user", data = "")] @@ -80,8 +83,7 @@ async fn update( ); }; - user.update(db, data.is_cox, data.is_admin, data.is_guest, data.is_tech) - .await; + user.update(db, data.into_inner()).await; Flash::success(Redirect::to("/admin/user"), "Successfully updated user") } diff --git a/src/tera/ergo.rs b/src/tera/ergo.rs new file mode 100644 index 0000000..81af3b5 --- /dev/null +++ b/src/tera/ergo.rs @@ -0,0 +1,191 @@ +use std::env; + +use rocket::{ + form::Form, + fs::TempFile, + get, + http::ContentType, + post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::{context, Template}; +use serde::Serialize; +use sqlx::SqlitePool; +use tera::Context; + +use crate::model::{ + log::Log, + user::{AdminUser, NonGuestUser, User}, +}; + +#[derive(Serialize)] +struct ErgoStat { + name: String, + dob: Option, + weight: Option, + sex: Option, + result: Option, +} + +#[get("/final")] +async fn send(db: &State, _user: AdminUser) -> Template { + let thirty = sqlx::query_as!( + ErgoStat, + "SELECT name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + let dozen= sqlx::query_as!( + ErgoStat, + "SELECT name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + Template::render( + "ergo.final", + context!(loggedin_user: &_user.user, thirty, dozen), + ) +} + +#[get("/reset")] +async fn reset(db: &State, _user: AdminUser) -> Flash { + sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;") + .execute(db.inner()) + .await + .unwrap(); + + Flash::success( + Redirect::to("/ergo"), + "Erfolgreich zurückgesetzt (Bilder müssen manuell gelöscht werden!)", + ) +} + +#[get("/")] +async fn index( + db: &State, + user: NonGuestUser, + flash: Option>, +) -> Template { + let users = User::ergo(db).await; + + let thirty = sqlx::query_as!( + ErgoStat, + "SELECT name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + let dozen= sqlx::query_as!( + ErgoStat, + "SELECT name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("loggedin_user", &user.user); + context.insert("users", &users); + context.insert("thirty", &thirty); + context.insert("dozen", &dozen); + + Template::render("ergo", context.into_json()) +} + +#[derive(FromForm, Debug)] +pub struct ErgoToAdd<'a> { + user: i64, + result: String, + proof: TempFile<'a>, +} + +#[post("/thirty", data = "", format = "multipart/form-data")] +async fn new_thirty( + db: &State, + mut data: Form>, + created_by: NonGuestUser, +) -> Flash { + let user = User::find_by_id(db, data.user as i32).await.unwrap(); + + let extension = if data.proof.content_type() == Some(&ContentType::JPEG) { + "jpg" + } else { + return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert"); + }; + let base_dir = env::current_dir().unwrap(); + let file_path = base_dir.join(format!("data-ergo/thirty/{}.{extension}", user.name)); + if let Err(e) = data.proof.move_copy_to(file_path).await { + eprintln!("Failed to persist file: {:?}", e); + } + + sqlx::query!( + "UPDATE user SET dirty_thirty = ? where id = ?", + data.result, + data.user + ) + .execute(db.inner()) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + Log::create( + db, + format!("{created_by:?} created thirty-ergo entry: {data:?}"), + ) + .await; + + Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen") +} + +#[post("/dozen", data = "", format = "multipart/form-data")] +async fn new_dozen( + db: &State, + mut data: Form>, + created_by: NonGuestUser, +) -> Flash { + let user = User::find_by_id(db, data.user as i32).await.unwrap(); + + let extension = if data.proof.content_type() == Some(&ContentType::JPEG) { + "jpg" + } else { + return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert"); + }; + let base_dir = env::current_dir().unwrap(); + let file_path = base_dir.join(format!("data-ergo/dozen/{}.{extension}", user.name)); + if let Err(e) = data.proof.move_copy_to(file_path).await { + eprintln!("Failed to persist file: {:?}", e); + } + + sqlx::query!( + "UPDATE user SET dirty_dozen = ? where id = ?", + data.result, + data.user + ) + .execute(db.inner()) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + Log::create( + db, + format!("{created_by:?} created dozen-ergo entry: {data:?}"), + ) + .await; + + Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen") +} + +pub fn routes() -> Vec { + routes![index, new_thirty, new_dozen, send, reset] +} + +#[cfg(test)] +mod test {} diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 799ae24..8cd4612 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -20,10 +20,11 @@ use crate::model::{ usertrip::{UserTrip, UserTripDeleteError, UserTripError}, }; -mod admin; +pub(crate) mod admin; mod auth; mod boatdamage; mod cox; +mod ergo; mod log; mod misc; mod stat; @@ -56,6 +57,7 @@ async fn index(db: &State, user: User, flash: Option) -> Rocket { .mount("/auth", auth::routes()) .mount("/wikiauth", routes![wikiauth]) .mount("/log", log::routes()) + .mount("/ergo", ergo::routes()) .mount("/stat", stat::routes()) .mount("/boatdamage", boatdamage::routes()) .mount("/cox", cox::routes()) diff --git a/staging-diff.sql b/staging-diff.sql index f2786a1..59dcf12 100644 --- a/staging-diff.sql +++ b/staging-diff.sql @@ -18,3 +18,13 @@ DROP TABLE logbook; ALTER TABLE logbook_temp RENAME TO logbook; INSERT INTO rower(rower_id, logbook_id) SELECT shipmaster, id FROM logbook; + + + +-- tmp ergo challenge stuff +ALTER TABLE user ADD COLUMN dob text; +ALTER TABLE user ADD COLUMN weight text; +ALTER TABLE user ADD COLUMN sex text; +ALTER TABLE user ADD COLUMN dirty_thirty text; +ALTER TABLE user ADD COLUMN dirty_dozen text; + diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index bfdd70d..5fbb47b 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -54,6 +54,9 @@ {{ 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) }} + {{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob) }} + {{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight) }} + {{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex) }} {% if user.pw %} Passwort zurücksetzen diff --git a/templates/ergo.final.html.tera b/templates/ergo.final.html.tera new file mode 100644 index 0000000..a996e15 --- /dev/null +++ b/templates/ergo.final.html.tera @@ -0,0 +1,31 @@ +{% 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 %} + +

Aktuelle Woche

+
+ Dirty Thirty +

+

{% for stat in thirty %}{{ stat.name }} {{ stat.dob }} {{ stat.weight }} {{ stat.sex }} Donau Linz {{ stat.result }} + {% endfor %} +
+

+
+ +
+ Dirty Dozen +

+

{% for stat in dozen %}{{ stat.name }} {{ stat.dob }} {{ stat.weight }} {{ stat.sex }} Donau Linz {{ stat.result }} + {% endfor %} +
+

+
+
+ +{% endblock content%} diff --git a/templates/ergo.html.tera b/templates/ergo.html.tera new file mode 100644 index 0000000..2ecc90f --- /dev/null +++ b/templates/ergo.html.tera @@ -0,0 +1,80 @@ +{% 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 %} +

Neuer Eintrag

+
+ Dirty Thirty +

+

+
+ + + {{ macros::input(label="Zeit [(hh:)mm:ss]/Distanz [m]", name="result", required=true, type="text", class="input") }} + + +
+
+

+
+ +
+ Dirty Dozen +

+

+
+ + + {{ macros::input(label="Zeit [(hh:)mm:ss]/Distanz [m]", name="result", required=true, type="text", class="input") }} + + +
+
+

+
+ + + +

Aktuelle Woche

+
+ Dirty Thirty +

+

+
    + {% for stat in thirty %} +
  1. {{ stat.name }}: {{ stat.result }}
  2. + {% endfor %} +
+
+

+
+ +
+ Dirty Dozen +

+

+
    + {% for stat in dozen%} +
  1. {{ stat.name }}: {{ stat.result }}
  2. + {% endfor %} +
+
+

+
+
+ +{% endblock content%} diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 470d734..1781a53 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -1,70 +1,129 @@ {% macro header(loggedin_user) %} -
-
- - -
- - {% include "includes/question-icon" %} - FAQs - - {% if not loggedin_user.is_guest %} - - {% include "includes/book" %} - Logbuch - - + {% endif %} {% if loggedin_user.is_admin %} + + + + + Userverwaltung + + {% endif %} + + + + + + + Ausloggen + +
+
+
+
{% endmacro header %} {% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='') %}