From 8840f0ab4868a5f0b7f5ba8fa2576438c80cbb4d Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Fri, 9 Feb 2024 00:07:43 +0100 Subject: [PATCH] feat: creation and display of availabillies work --- Cargo.lock | 30 +++++++++--- Cargo.toml | 1 + migrations/20230609121618_initial.sql | 3 +- src/auth/mod.rs | 1 + src/auth/redirect.rs | 70 +++++++++++++++++++++++++++ src/calendar/mod.rs | 1 + src/calendar/post_availabillity.rs | 34 +++++++++++++ src/calendar/routes.rs | 60 +++++++++++------------ src/main.rs | 4 +- src/models/availabillity.rs | 41 +++++++++++++--- src/models/function.rs | 13 ++++- src/models/role.rs | 2 +- src/models/user.rs | 17 +++---- templates/availabillity_new.html | 37 +++++++------- templates/base.html | 2 +- templates/index.html | 10 +++- 16 files changed, 243 insertions(+), 83 deletions(-) create mode 100644 src/auth/redirect.rs create mode 100644 src/calendar/post_availabillity.rs diff --git a/Cargo.lock b/Cargo.lock index 36536747..f575acc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,7 @@ dependencies = [ "askama_actix", "chrono", "dotenv", + "futures-util", "serde", "sqlx", ] @@ -995,9 +996,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-intrusive" @@ -1031,6 +1032,17 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "futures-rustls" version = "0.22.2" @@ -1044,27 +1056,29 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", + "futures-macro", "futures-sink", "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6ae46af8..d66581e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ actix-identity = "0.5.2" chrono = { version = "0.4.33", features = ["serde"] } actix-files = "0.6.5" askama_actix = "0.14.0" +futures-util = "0.3.30" diff --git a/migrations/20230609121618_initial.sql b/migrations/20230609121618_initial.sql index e5346086..e312ba72 100644 --- a/migrations/20230609121618_initial.sql +++ b/migrations/20230609121618_initial.sql @@ -37,7 +37,8 @@ CREATE TABLE availabillity userId INTEGER NOT NULL REFERENCES user_ (id), date DATE NOT NULL, startTime TIME, - endTime TIME + endTime TIME, + comment TEXT ); CREATE TABLE event diff --git a/src/auth/mod.rs b/src/auth/mod.rs index e8217bc8..e8fa73de 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,4 +1,5 @@ pub mod routes; pub mod utils; +pub mod redirect; pub use routes::init; diff --git a/src/auth/redirect.rs b/src/auth/redirect.rs new file mode 100644 index 00000000..b6e9c60a --- /dev/null +++ b/src/auth/redirect.rs @@ -0,0 +1,70 @@ +use std::future::{ready, Ready}; + +use actix_identity::IdentityExt; +use actix_web::{ + body::EitherBody, + dev::{self, Service, ServiceRequest, ServiceResponse, Transform}, + http, Error, HttpResponse, +}; +use futures_util::future::LocalBoxFuture; + +pub struct CheckLogin; + +impl Transform for CheckLogin +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type InitError = (); + type Transform = CheckLoginMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(CheckLoginMiddleware { service })) + } +} +pub struct CheckLoginMiddleware { + service: S, +} + +impl Service for CheckLoginMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + dev::forward_ready!(service); + + fn call(&self, request: ServiceRequest) -> Self::Future { + // Change this to see the change in outcome in the browser. + // Usually this boolean would be acquired from a password check or other auth verification. + let is_logged_in = request.get_identity().is_ok(); + + // Don't forward to `/login` if we are already on `/login`. + if !is_logged_in && request.path() != "/login" && !request.path().starts_with("/static") { + let (request, _pl) = request.into_parts(); + + let response = HttpResponse::Found() + .insert_header((http::header::LOCATION, "/login")) + .finish() + // constructed responses map to "right" body + .map_into_right_body(); + + return Box::pin(async { Ok(ServiceResponse::new(request, response)) }); + } + + let res = self.service.call(request); + + Box::pin(async move { + // forwarded responses map to "left" body + res.await.map(ServiceResponse::map_into_left_body) + }) + } +} diff --git a/src/calendar/mod.rs b/src/calendar/mod.rs index 77f537af..62851439 100644 --- a/src/calendar/mod.rs +++ b/src/calendar/mod.rs @@ -1,4 +1,5 @@ pub mod routes; mod get_availabillity_new; +mod post_availabillity; pub use routes::init; diff --git a/src/calendar/post_availabillity.rs b/src/calendar/post_availabillity.rs new file mode 100644 index 00000000..64b94361 --- /dev/null +++ b/src/calendar/post_availabillity.rs @@ -0,0 +1,34 @@ +use actix_identity::Identity; +use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use chrono::{NaiveDate, NaiveTime}; +use serde::Deserialize; +use sqlx::PgPool; + +use crate::models::{availabillity::Availabillity, user::User}; + +#[derive(Deserialize)] +pub struct AvailabillityForm { + date: NaiveDate, + start_time: Option, + end_time: Option, + comment: Option +} + +#[actix_web::post("/availabillity/new")] +pub async fn post_availabillity( + user: Identity, + pool: web::Data, + form: web::Form, +) -> impl Responder { + let current_user = User::read_by_id(pool.as_ref(), user.id().unwrap().parse().unwrap()) + .await + .unwrap(); + + if let Ok(_) = Availabillity::create(pool.get_ref(), current_user.id, form.date, form.start_time, form.end_time, form.comment.clone()).await { + HttpResponse::Found() + .insert_header((LOCATION, "/")) + .finish() + } else { + HttpResponse::BadRequest().body("Fehler beim erstellen") + } +} diff --git a/src/calendar/routes.rs b/src/calendar/routes.rs index 24f418b5..e0d74aca 100644 --- a/src/calendar/routes.rs +++ b/src/calendar/routes.rs @@ -9,12 +9,12 @@ use crate::models::{ area::Area, availabillity::Availabillity, event::Event, role::Role, user::User, }; -use super::get_availabillity_new::get_availabillity_new; - +use super::{get_availabillity_new::get_availabillity_new, post_availabillity::post_availabillity}; pub fn init(cfg: &mut web::ServiceConfig) { cfg.service(get_index); cfg.service(get_availabillity_new); + cfg.service(post_availabillity); } #[derive(Deserialize)] @@ -34,38 +34,36 @@ struct CalendarTemplate { #[actix_web::get("/")] async fn get_index( - user: Option, + user: Identity, pool: web::Data, query: web::Query, ) -> impl Responder { - if let Some(user) = user { - let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()) - .await - .unwrap(); - let date = match query.date { - Some(given_date) => given_date, - None => Utc::now().date_naive(), - }; - let area = Area::read_by_id(pool.get_ref(), current_user.area_id) - .await - .unwrap(); - let events = Event::read_by_date(pool.get_ref(), date).await.unwrap(); - let availabillities = Availabillity::read_by_date(pool.get_ref(), date) - .await - .unwrap(); + let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap()) + .await + .unwrap(); + let date = match query.date { + Some(given_date) => given_date, + None => Utc::now().date_naive(), + }; + let area = Area::read_by_id(pool.get_ref(), current_user.area_id) + .await + .unwrap(); + let events = Event::read_by_date(pool.get_ref(), date).await.unwrap(); + let mut availabillities = Availabillity::read_by_date(pool.get_ref(), date) + .await + .unwrap(); - let template = CalendarTemplate { - user_role: current_user.role, - date, - area, - events, - availabillities, - }; - - HttpResponse::Ok().body(template.render().unwrap()) - } else { - HttpResponse::PermanentRedirect() - .insert_header((LOCATION, "/login")) - .finish() + for avl in availabillities.iter_mut() { + avl.load_user(pool.get_ref()).await.unwrap() } + + let template = CalendarTemplate { + user_role: current_user.role, + date, + area, + events, + availabillities, + }; + + HttpResponse::Ok().body(template.render().unwrap()) } diff --git a/src/main.rs b/src/main.rs index 84a9faf8..103d9c3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use actix_web::{web, App, HttpServer}; use dotenv::dotenv; use sqlx::postgres::PgPool; +use crate::auth::redirect; mod auth; mod calendar; mod models; @@ -24,12 +25,13 @@ async fn main() -> anyhow::Result<()> { .app_data(web::Data::new(pool.clone())) .configure(auth::init) .configure(calendar::init) + .wrap(redirect::CheckLogin) .wrap(IdentityMiddleware::default()) .wrap(SessionMiddleware::new( CookieSessionStore::default(), secret_key.clone(), )) - .service(actix_files::Files::new("", "./static").show_files_listing()) + .service(actix_files::Files::new("/static", "./static").show_files_listing()) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/src/models/availabillity.rs b/src/models/availabillity.rs index c40a21ce..549fd3c5 100644 --- a/src/models/availabillity.rs +++ b/src/models/availabillity.rs @@ -1,25 +1,41 @@ use chrono::{NaiveDate, NaiveTime}; use sqlx::{query, PgPool}; +use super::user::User; + pub struct Availabillity { pub id: i32, pub user_id: i32, + pub user: Option, pub date: NaiveDate, pub start_time: Option, - pub end_time: Option + pub end_time: Option, + pub comment: Option, } impl Availabillity { - pub async fn create(pool: &PgPool, user_id: i32, date: NaiveDate, start_time: Option, end_time: Option) -> anyhow::Result { - let mut result = match (start_time, end_time) { - (Some(start_time), Some(end_time)) => query!("INSERT INTO availabillity (userId, date, startTime, endTime) VALUES ($1, $2, $3, $4) RETURNING id;", user_id, date, start_time, end_time).fetch_one(pool).await?.id, - (_, _) => query!("INSERT INTO availabillity (userId, date) VALUES ($1, $2) RETURNING id;", user_id, date).fetch_one(pool).await?.id + pub async fn create( + pool: &PgPool, + user_id: i32, + date: NaiveDate, + start_time: Option, + end_time: Option, + comment: Option, + ) -> anyhow::Result { + let result = match (start_time, end_time, comment) { + (Some(start_time), Some(end_time), Some(comment)) => query!("INSERT INTO availabillity (userId, date, startTime, endTime, comment) VALUES ($1, $2, $3, $4, $5) RETURNING id;", user_id, date, start_time, end_time, comment).fetch_one(pool).await?.id, + (Some(start_time), Some(end_time), None) => query!("INSERT INTO availabillity (userId, date, startTime, endTime) VALUES ($1, $2, $3, $4) RETURNING id;", user_id, date, start_time, end_time).fetch_one(pool).await?.id, + (None, None, Some(comment)) => query!("INSERT INTO availabillity (userId, date, comment) VALUES ($1, $2, $3) RETURNING id;", user_id, date, comment).fetch_one(pool).await?.id, + (_, _, _) => query!("INSERT INTO availabillity (userId, date) VALUES ($1, $2) RETURNING id;", user_id, date).fetch_one(pool).await?.id }; Ok(result) } - pub async fn read_by_date(pool: &PgPool, date: NaiveDate) -> anyhow::Result> { + pub async fn read_by_date( + pool: &PgPool, + date: NaiveDate, + ) -> anyhow::Result> { let records = query!("SELECT * FROM availabillity WHERE date = $1", date) .fetch_all(pool) .await?; @@ -29,12 +45,21 @@ impl Availabillity { .map(|a| Availabillity { id: a.id, user_id: a.userid, + user: None, date: a.date, start_time: a.starttime, - end_time: a.endtime + end_time: a.endtime, + comment: a.comment.clone(), }) - .collect(); + .collect(); Ok(availabillities) } + + pub async fn load_user(&mut self, pool: &PgPool) -> anyhow::Result<()> { + let user = User::read_by_id(pool, self.user_id).await?; + self.user = Some(user); + + Ok(()) + } } diff --git a/src/models/function.rs b/src/models/function.rs index b248dc07..2b04e899 100644 --- a/src/models/function.rs +++ b/src/models/function.rs @@ -1,10 +1,21 @@ -#[derive(sqlx::Type, Debug)] +use std::fmt::Display; + +#[derive(sqlx::Type, Debug, Clone)] #[sqlx(type_name = "function", rename_all = "lowercase")] pub enum Function { Posten = 1, Wachhabender = 10, } +impl Display for Function { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Function::Posten => write!(f, "Posten"), + Function::Wachhabender => write!(f, "Wachhabender") + } + } +} + impl TryFrom for Function { type Error = (); diff --git a/src/models/role.rs b/src/models/role.rs index e2443b15..0d10a50b 100644 --- a/src/models/role.rs +++ b/src/models/role.rs @@ -1,4 +1,4 @@ -#[derive(sqlx::Type, Debug)] +#[derive(sqlx::Type, Debug, Clone)] #[sqlx(type_name = "role", rename_all = "lowercase")] pub enum Role { Staff = 1, diff --git a/src/models/user.rs b/src/models/user.rs index 9d76de78..964e1c34 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -3,6 +3,7 @@ use sqlx::PgPool; use super::{function::Function, role::Role}; +#[derive(Clone)] pub struct User { pub id: i32, pub name: String, @@ -51,7 +52,7 @@ impl User { } } - pub async fn read_by_id(pool: &PgPool, id: i32) -> Option { + pub async fn read_by_id(pool: &PgPool, id: i32) -> anyhow::Result { let record = sqlx::query!( r#" SELECT id, @@ -71,10 +72,9 @@ impl User { id, ) .fetch_one(pool) - .await; + .await?; - match record { - Ok(record) => Some(User { + let user = User { id: record.id, name: record.name, email: record.email, @@ -86,12 +86,9 @@ impl User { locked: record.locked, last_login: record.lastlogin, receive_notifications: record.receivenotifications, - }), - Err(err) => { - println!("User.read({id}): {err}"); - None - } - } + }; + + Ok(user) } pub async fn read_for_login(pool: &PgPool, email: &str) -> anyhow::Result> { diff --git a/templates/availabillity_new.html b/templates/availabillity_new.html index c6bca6a2..5eef5d85 100644 --- a/templates/availabillity_new.html +++ b/templates/availabillity_new.html @@ -3,29 +3,14 @@ {% block content %}
-
+

Neue Vefügbarkeit für den {{ date.format("%d.%m.%Y") }}

-
-
- -
-
-
-
-
- -
-
-
-
-
+ +
- +
@@ -56,6 +41,19 @@
+
+
+ +
+
+
+
+ +
+
+
+
+
@@ -69,6 +67,7 @@
+
diff --git a/templates/base.html b/templates/base.html index dc084dc6..e94f3af4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,7 +5,7 @@ Brass - Brasiwa Leipzig - + diff --git a/templates/index.html b/templates/index.html index 275d0bef..36a21a94 100644 --- a/templates/index.html +++ b/templates/index.html @@ -57,14 +57,20 @@ - {% if events.len() == 0 %} + {% if availabillities.len() == 0 %}
keine Verfügbarkeiten eingetragen
{% else %} {% for availabillity in availabillities %} + {% let user = availabillity.user.as_ref().unwrap() %} +
-
{{ availabillity.user_id }}
+

{{ user.name }}

+

{{ user.function }}

+ {% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %} +

{{ availabillity.start_time.as_ref().unwrap() }}

+ {% endif %}
{% endfor %} {% endif %}