feat: creation and display of availabillies work

This commit is contained in:
Max Hohlfeld 2024-02-09 00:07:43 +01:00
parent 497e6fbf6c
commit 8840f0ab48
16 changed files with 243 additions and 83 deletions

30
Cargo.lock generated
View File

@ -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]]

View File

@ -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"

View File

@ -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

View File

@ -1,4 +1,5 @@
pub mod routes;
pub mod utils;
pub mod redirect;
pub use routes::init;

70
src/auth/redirect.rs Normal file
View File

@ -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<S, B> Transform<S, ServiceRequest> for CheckLogin
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type InitError = ();
type Transform = CheckLoginMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(CheckLoginMiddleware { service }))
}
}
pub struct CheckLoginMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for CheckLoginMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
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)
})
}
}

View File

@ -1,4 +1,5 @@
pub mod routes;
mod get_availabillity_new;
mod post_availabillity;
pub use routes::init;

View File

@ -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<NaiveTime>,
end_time: Option<NaiveTime>,
comment: Option<String>
}
#[actix_web::post("/availabillity/new")]
pub async fn post_availabillity(
user: Identity,
pool: web::Data<PgPool>,
form: web::Form<AvailabillityForm>,
) -> 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")
}
}

View File

@ -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,11 +34,10 @@ struct CalendarTemplate {
#[actix_web::get("/")]
async fn get_index(
user: Option<Identity>,
user: Identity,
pool: web::Data<PgPool>,
query: web::Query<CalendarQuery>,
) -> impl Responder {
if let Some(user) = user {
let current_user = User::read_by_id(pool.get_ref(), user.id().unwrap().parse().unwrap())
.await
.unwrap();
@ -50,10 +49,14 @@ async fn get_index(
.await
.unwrap();
let events = Event::read_by_date(pool.get_ref(), date).await.unwrap();
let availabillities = Availabillity::read_by_date(pool.get_ref(), date)
let mut availabillities = Availabillity::read_by_date(pool.get_ref(), date)
.await
.unwrap();
for avl in availabillities.iter_mut() {
avl.load_user(pool.get_ref()).await.unwrap()
}
let template = CalendarTemplate {
user_role: current_user.role,
date,
@ -63,9 +66,4 @@ async fn get_index(
};
HttpResponse::Ok().body(template.render().unwrap())
} else {
HttpResponse::PermanentRedirect()
.insert_header((LOCATION, "/login"))
.finish()
}
}

View File

@ -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()

View File

@ -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<User>,
pub date: NaiveDate,
pub start_time: Option<NaiveTime>,
pub end_time: Option<NaiveTime>
pub end_time: Option<NaiveTime>,
pub comment: Option<String>,
}
impl Availabillity {
pub async fn create(pool: &PgPool, user_id: i32, date: NaiveDate, start_time: Option<NaiveTime>, end_time: Option<NaiveTime>) -> anyhow::Result<i32> {
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<NaiveTime>,
end_time: Option<NaiveTime>,
comment: Option<String>,
) -> anyhow::Result<i32> {
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<Vec<Availabillity>> {
pub async fn read_by_date(
pool: &PgPool,
date: NaiveDate,
) -> anyhow::Result<Vec<Availabillity>> {
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();
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(())
}
}

View File

@ -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<u8> for Function {
type Error = ();

View File

@ -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,

View File

@ -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<User> {
pub async fn read_by_id(pool: &PgPool, id: i32) -> anyhow::Result<User> {
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<Option<User>> {

View File

@ -3,29 +3,14 @@
{% block content %}
<section class="section">
<div class="container">
<form>
<form method="post" action="/availabillity/new">
<h1 class="title">Neue Vefügbarkeit für den {{ date.format("%d.%m.%Y") }}</h1>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Funktion</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select">
<select name="function">
<option value="1">Posten</option>
<option value="10">Wachhabender</option>
</select>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="date" value="{{ date }}">
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Zeitangabe</label>
<label class="label">Dauer</label>
</div>
<div class="field-body">
<div class="field">
@ -56,6 +41,19 @@
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Kommentar</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" placeholder="nur Posten, nur Wachhabender, etc.."></textarea>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label"></div>
<div class="field-body">
@ -69,6 +67,7 @@
</div>
</div>
</div>
</form>
</div>
</section>

View File

@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brass - Brasiwa Leipzig</title>
<link rel="stylesheet" href="/bulma.css">
<link rel="stylesheet" href="/static/bulma.css">
</head>
<body>

View File

@ -57,14 +57,20 @@
</div>
</div>
{% if events.len() == 0 %}
{% if availabillities.len() == 0 %}
<div class="box">
<h5 class="title is-5">keine Verfügbarkeiten eingetragen</h5>
</div>
{% else %}
{% for availabillity in availabillities %}
{% let user = availabillity.user.as_ref().unwrap() %}
<div class="box">
<h5 class="title is-5">{{ availabillity.user_id }}</h5>
<p>{{ user.name }}</p>
<p>{{ user.function }}</p>
{% if availabillity.start_time.is_some() && availabillity.end_time.is_some() %}
<p>{{ availabillity.start_time.as_ref().unwrap() }}</p>
{% endif %}
</div>
{% endfor %}
{% endif %}