feat: validation with garde

This commit is contained in:
Max Hohlfeld 2025-01-15 00:11:15 +01:00
parent 9060905483
commit 290c7bcc43
17 changed files with 840 additions and 112 deletions

View File

@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE assignment SET function = $1, startTime = $2, endTime = $3 WHERE eventId = $4 AND availabillityId = $5;",
"describe": {
"columns": [],
"parameters": {
"Left": [
{
"Custom": {
"name": "function",
"kind": {
"Enum": [
"posten",
"fuehrungsassistent",
"wachhabender"
]
}
}
},
"Time",
"Time",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "2a7e94e6d4fcaa3afc2755ee236d116547656c3418174463aacadc69d57cbf63"
}

View File

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT * FROM location WHERE id= $1);",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
null
]
},
"hash": "2d516ada804b4e3306e2a444602b303cf579a1c2d9c445d68f7b17319dc43007"
}

View File

@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE event SET date = $1, startTime = $2, endTime = $3, name = $4, locationId = $5, voluntaryWachhabender = $6, voluntaryFuehrungsassistent = $7, amountOfPosten = $8, clothing = $9, note = $10 WHERE id = $11;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Date",
"Time",
"Time",
"Text",
"Int4",
"Bool",
"Bool",
"Int2",
"Text",
"Text",
"Int4"
]
},
"nullable": []
},
"hash": "5e66fa920534af67e80c538be4fa5dbae6ac53427e697be5c71d36a16ea01154"
}

227
Cargo.lock generated
View File

@ -655,6 +655,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.3" version = "0.5.3"
@ -764,10 +773,11 @@ dependencies = [
"dotenv", "dotenv",
"fake", "fake",
"futures-util", "futures-util",
"garde",
"insta", "insta",
"lettre", "lettre",
"pico-args", "pico-args",
"quick-xml", "quick-xml 0.37.2",
"rand", "rand",
"regex", "regex",
"rinja", "rinja",
@ -775,7 +785,7 @@ dependencies = [
"serde_json", "serde_json",
"sqlx", "sqlx",
"static-files", "static-files",
"thiserror", "thiserror 2.0.9",
"zxcvbn", "zxcvbn",
] ]
@ -833,6 +843,25 @@ dependencies = [
"bytes", "bytes",
] ]
[[package]]
name = "card-validate"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655fa70596e2a38372c0c0c4449ec0166ad9cc43d91558bbecc1a6f38bf9eb91"
dependencies = [
"lazy_static",
"regex",
]
[[package]]
name = "castaway"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.7" version = "1.2.7"
@ -941,6 +970,21 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "compact_str"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"serde",
"static_assertions",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -1350,7 +1394,7 @@ checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [ dependencies = [
"bit-set", "bit-set",
"regex-automata", "regex-automata",
"regex-syntax", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@ -1537,6 +1581,37 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "garde"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dbf10452e3dbf51033a5035a05762b2653c43bf84d46e96f15bc93beedd426d"
dependencies = [
"card-validate",
"compact_str",
"garde_derive",
"idna",
"once_cell",
"phonenumber",
"regex",
"serde",
"smallvec",
"unicode-segmentation",
"url",
]
[[package]]
name = "garde_derive"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccfdbc9c39fad7991686e229c55cf71565eafe73dcb2cf38ddf1d4aa3ca7e176"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.95",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@ -1972,6 +2047,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@ -2137,6 +2221,15 @@ dependencies = [
"value-bag", "value-bag",
] ]
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@ -2291,6 +2384,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "oncemutex"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.1" version = "0.3.1"
@ -2417,6 +2516,27 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phonenumber"
version = "0.3.6+8.13.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11756237b57b8cc5e97dc8b1e70ea436324d30e7075de63b14fd15073a8f692a"
dependencies = [
"bincode",
"either",
"fnv",
"itertools 0.12.1",
"lazy_static",
"nom",
"quick-xml 0.31.0",
"regex",
"regex-cache",
"serde",
"serde_derive",
"strum",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "pico-args" name = "pico-args"
version = "0.5.0" version = "0.5.0"
@ -2549,6 +2669,15 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.37.2" version = "0.37.2"
@ -2622,7 +2751,7 @@ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata", "regex-automata",
"regex-syntax", "regex-syntax 0.8.5",
] ]
[[package]] [[package]]
@ -2633,7 +2762,19 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax", "regex-syntax 0.8.5",
]
[[package]]
name = "regex-cache"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7b62d69743b8b94f353b6b7c3deb4c5582828328bcb8d5fedf214373808793"
dependencies = [
"lru-cache",
"oncemutex",
"regex",
"regex-syntax 0.6.29",
] ]
[[package]] [[package]]
@ -2642,6 +2783,12 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.5"
@ -2812,6 +2959,12 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -3060,7 +3213,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"smallvec", "smallvec",
"thiserror", "thiserror 2.0.9",
"tracing", "tracing",
"url", "url",
"webpki-roots", "webpki-roots",
@ -3143,7 +3296,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror 2.0.9",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -3181,7 +3334,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror 2.0.9",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -3240,6 +3393,12 @@ dependencies = [
"path-slash", "path-slash",
] ]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.5" version = "0.1.5"
@ -3257,6 +3416,28 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.95",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -3310,13 +3491,33 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.9" version = "2.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 2.0.9",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
] ]
[[package]] [[package]]
@ -3487,6 +3688,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@ -4008,7 +4215,7 @@ dependencies = [
"chrono", "chrono",
"derive_builder", "derive_builder",
"fancy-regex", "fancy-regex",
"itertools", "itertools 0.13.0",
"lazy_static", "lazy_static",
"regex", "regex",
"time", "time",

View File

@ -33,6 +33,7 @@ brass-macros = { path = "../macros" }
brass-config = { path = "../config" } brass-config = { path = "../config" }
actix-http = "3.9.0" actix-http = "3.9.0"
rinja = "0.3.5" rinja = "0.3.5"
garde = { version = "0.21.0", features = ["full"] }
[build-dependencies] [build-dependencies]
built = "0.7.4" built = "0.7.4"

View File

@ -1,11 +1,15 @@
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use garde::Validate;
use rinja::Template; use rinja::Template;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use crate::{ use crate::{
endpoints::assignment::PlanEventPersonalTablePartialTemplate, endpoints::assignment::PlanEventPersonalTablePartialTemplate,
models::{Assignment, Availabillity, Event, Function, Role, User}, models::{
Assignment, AssignmentChangeset, AssignmentContext, Availabillity, Event, Function, Role,
User,
},
utils::{ utils::{
event_planning_template::{ event_planning_template::{
generate_availabillity_assignment_list, generate_status_whether_staff_is_required, generate_availabillity_assignment_list, generate_status_whether_staff_is_required,
@ -40,55 +44,43 @@ pub async fn post(
return Err(ApplicationError::Unauthorized); return Err(ApplicationError::Unauthorized);
} }
let Some(availability) = let Some(availabillity) =
Availabillity::read_by_id_including_user(pool.get_ref(), query.availabillity).await? Availabillity::read_by_id_including_user(pool.get_ref(), query.availabillity).await?
else { else {
return Ok(HttpResponse::NotFound().finish()); return Ok(HttpResponse::NotFound().finish());
}; };
let availability_user_not_in_event_location_area = let availability_user_not_in_event_location_area =
availability.user.as_ref().unwrap().area_id != event.location.as_ref().unwrap().area_id; availabillity.user.as_ref().unwrap().area_id != event.location.as_ref().unwrap().area_id;
let existing_assignments_for_availabillity = if availability_user_not_in_event_location_area {
Assignment::read_all_by_availabillity(pool.get_ref(), availability.id).await?; return Ok(HttpResponse::BadRequest()
let has_start_time_during_event = .body("availability user is not in the same area as event location"));
|a: &Assignment| a.start_time >= event.start_time && a.start_time <= event.end_time;
let has_end_time_during_event =
|a: &Assignment| a.end_time >= event.start_time && a.end_time <= event.end_time;
let availability_already_assigned = existing_assignments_for_availabillity
.iter()
.any(|a| has_start_time_during_event(a) || has_end_time_during_event(a));
let function = Function::try_from(query.function)?;
let user_not_qualified_for_assigned_function =
availability.user.as_ref().unwrap().function < function;
let a = Assignment::count_by_event_and_function(pool.get_ref(), event.id, function).await?;
let event_already_has_enough_assignments_for_function = match function {
Function::Posten => a >= event.amount_of_posten as i64,
Function::Fuehrungsassistent => event.voluntary_fuehrungsassistent && a >= 1,
Function::Wachhabender => event.voluntary_wachhabender && a >= 1,
};
if availability_user_not_in_event_location_area
|| availability_already_assigned
|| user_not_qualified_for_assigned_function
|| event_already_has_enough_assignments_for_function
{
// TODO: Fehlermeldung verbessern
return Ok(HttpResponse::BadRequest().body(format!("{availability_user_not_in_event_location_area} {availability_already_assigned} {user_not_qualified_for_assigned_function} {event_already_has_enough_assignments_for_function}")));
} }
Assignment::create( let function = Function::try_from(query.function)?;
pool.get_ref(),
event.id, let changeset = AssignmentChangeset {
availability.id,
function, function,
event.start_time, time: (event.start_time, event.end_time),
event.end_time, };
)
.await?; let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?;
let assignments_for_availabillity =
Assignment::read_all_by_availabillity(pool.get_ref(), availabillity.id).await?;
let context = AssignmentContext {
event: event.clone(),
availabillity: availabillity.clone(),
user_function: availabillity.user.as_ref().unwrap().function,
assignments_for_event,
assignments_for_availabillity,
};
if let Err(e) = changeset.validate_with(&context) {
return Ok(HttpResponse::BadRequest().body(e.to_string()));
};
Assignment::create(pool.get_ref(), event.id, availabillity.id, changeset).await?;
let availabillities = generate_availabillity_assignment_list(pool.get_ref(), &event).await?; let availabillities = generate_availabillity_assignment_list(pool.get_ref(), &event).await?;

View File

@ -86,21 +86,24 @@ async fn produces_template(context: &DbTestContext) {
.await .await
.unwrap(); .unwrap();
Event::create( let changeset = crate::models::EventChangeset {
&context.db_pool, date: NaiveDate::parse_from_str("2025-01-01", "%F").unwrap(),
&NaiveDate::parse_from_str("2025-01-01", "%F").unwrap(), time: (
&NaiveTime::parse_from_str("08:00", "%R").unwrap(), NaiveTime::parse_from_str("08:00", "%R").unwrap(),
&NaiveTime::parse_from_str("10:00", "%R").unwrap(), NaiveTime::parse_from_str("10:00", "%R").unwrap(),
&"Vorstellung".to_string(), ),
1, name: "Vorstellung".to_string(),
false, location_id: 1,
false, voluntary_fuehrungsassistent: false,
2, voluntary_wachhabender: false,
&"Tuchuniform".to_string(), amount_of_posten: 2,
None, clothing: "Tuchuniform".to_string(),
) note: None,
.await };
.unwrap();
Event::create(&context.db_pool, changeset)
.await
.unwrap();
let app = context.app().await; let app = context.app().await;
let config = RequestConfig { let config = RequestConfig {

View File

@ -8,6 +8,7 @@ pub mod get_edit;
pub mod get_new; pub mod get_new;
pub mod get_plan; pub mod get_plan;
pub mod post_new; pub mod post_new;
pub mod post_edit;
#[derive(Template)] #[derive(Template)]
#[cfg_attr(not(test), template(path = "events/new_or_edit.html"))] #[cfg_attr(not(test), template(path = "events/new_or_edit.html"))]

View File

@ -0,0 +1,187 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use chrono::{NaiveDate, NaiveTime};
use garde::Validate;
use serde::Deserialize;
use sqlx::PgPool;
use crate::{
endpoints::IdPath,
models::{
Assignment, AssignmentChangeset, Availabillity, Event, EventChangeset, EventContext,
Function, Location, Role, User,
},
utils::{self, ApplicationError},
};
#[derive(Deserialize)]
pub struct EditEventForm {
name: String,
date: NaiveDate,
from: NaiveTime,
till: NaiveTime,
location: i32,
voluntarywachhabender: Option<bool>,
voluntaryfuehrungsassistent: Option<bool>,
amount: i16,
clothing: String,
note: Option<String>,
}
#[actix_web::post("/events/{id}/edit")]
pub async fn post(
user: web::ReqData<User>,
pool: web::Data<PgPool>,
form: web::Form<EditEventForm>,
path: web::Path<IdPath>,
) -> Result<impl Responder, ApplicationError> {
if user.role != Role::Admin && user.role != Role::AreaManager {
return Err(ApplicationError::Unauthorized);
}
let Some(event) = Event::read_by_id_including_location(pool.get_ref(), path.id).await? else {
return Ok(HttpResponse::NotFound().finish());
};
if event.location_id != form.location {
let Some(location) = Location::read_by_id(pool.get_ref(), form.location).await? else {
return Ok(HttpResponse::BadRequest().body("Location does not exist"));
};
if user.role != Role::Admin && user.area_id != location.area_id {
return Ok(HttpResponse::BadRequest().body("Can't use location outside of your area"));
}
if event.location.as_ref().unwrap().area_id != location.area_id {
return Ok(HttpResponse::BadRequest()
.body("Can't change to a location outside of previous location area"));
}
}
let changeset = EventChangeset {
date: form.date,
amount_of_posten: form.amount,
clothing: form.clothing.clone(),
location_id: form.location,
time: (form.from, form.till),
name: form.name.clone(),
note: form
.note
.clone()
.and_then(|n| if n.len() != 0 { Some(n) } else { None }),
voluntary_wachhabender: form.voluntarywachhabender.unwrap_or(false),
voluntary_fuehrungsassistent: form.voluntaryfuehrungsassistent.unwrap_or(false),
};
let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?;
let mut common_time = (
NaiveTime::parse_from_str("00:00", "%R").unwrap(),
NaiveTime::parse_from_str("23:59", "%R").unwrap(),
);
for assignment in &assignments_for_event {
let availability = Availabillity::read_by_id(pool.get_ref(), assignment.availabillity_id)
.await?
.unwrap();
let all_assignments =
Assignment::read_all_by_availabillity(pool.get_ref(), assignment.availabillity_id)
.await?;
if all_assignments.len() == 1 {
if availability.start_time.is_some() && availability.end_time.is_some() {
let start = availability.start_time.unwrap();
let end = availability.end_time.unwrap();
if start > common_time.0 {
common_time.0 = start;
}
if end < common_time.1 {
common_time.1 = end;
}
}
} else {
let mut slots = if availability.start_time.is_some() && availability.end_time.is_some()
{
vec![(
availability.start_time.as_ref().unwrap().clone(),
availability.end_time.as_ref().unwrap().clone(),
)]
} else {
vec![(
NaiveTime::parse_from_str("00:00", "%R").unwrap(),
NaiveTime::parse_from_str("23:59", "%R").unwrap(),
)]
};
for a in all_assignments
.iter()
.filter(|x| x.event_id != assignment.event_id)
{
let (fit, rest) = slots
.into_iter()
.partition(|s| s.0 >= a.start_time && s.1 <= a.end_time);
slots = rest;
let fit = fit.first().unwrap();
if fit.0 != a.start_time {
slots.push((fit.0, a.start_time));
}
if fit.1 != a.end_time {
slots.push((a.end_time, fit.1));
}
}
let slot = slots
.into_iter()
.find(|s| s.0 >= assignment.start_time && s.1 <= assignment.end_time)
.unwrap();
if slot.0 > common_time.0 {
common_time.0 = slot.0;
}
if slot.1 < common_time.1 {
common_time.1 = slot.1;
}
}
}
let context = Some(EventContext {
date_in_db: event.date,
common_min_max_available_time: common_time,
// safe as max amount of posten can be only 100
amount_of_assigned_posten: assignments_for_event
.iter()
.filter(|a| a.function == Function::Posten)
.count() as i16,
wachhabender_assigned: assignments_for_event
.iter()
.any(|a| a.function == Function::Wachhabender),
fuehrungsassistent_assigned: assignments_for_event
.iter()
.any(|a| a.function == Function::Fuehrungsassistent),
});
if let Err(e) = changeset.validate_with(&context) {
return Ok(HttpResponse::BadRequest().body(e.to_string()));
};
if event.start_time != changeset.time.0 || event.end_time != changeset.time.1 {
for a in assignments_for_event {
let c = AssignmentChangeset {
function: a.function,
time: changeset.time.clone(),
};
Assignment::update(pool.get_ref(), a.event_id, a.availabillity_id, c).await?;
}
}
Event::update(pool.get_ref(), event.id, changeset).await?;
let url = utils::get_return_url_for_date(&form.date);
println!("redirecto to {url}");
Ok(HttpResponse::Found()
.insert_header((LOCATION, url.clone()))
.insert_header(("HX-LOCATION", url))
.finish())
}

View File

@ -1,10 +1,11 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use chrono::{NaiveDate, NaiveTime}; use chrono::{NaiveDate, NaiveTime};
use garde::Validate;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use crate::{ use crate::{
models::{Event, Role, User}, models::{Event, EventChangeset, Location, Role, User},
utils::{self, ApplicationError}, utils::{self, ApplicationError},
}; };
@ -32,24 +33,34 @@ pub async fn post(
return Err(ApplicationError::Unauthorized); return Err(ApplicationError::Unauthorized);
} }
println!("{:?}", form.note); let Some(location) = Location::read_by_id(pool.get_ref(), form.location).await? else {
return Ok(HttpResponse::BadRequest().body("Location does not exist"));
};
Event::create( if user.role != Role::Admin && user.area_id != location.area_id {
pool.get_ref(), return Ok(HttpResponse::BadRequest().body("Can't use location outside of your area"));
&form.date, }
&form.from,
&form.till, let changeset = EventChangeset {
&form.name, date: form.date,
form.location, amount_of_posten: form.amount,
form.voluntarywachhabender.unwrap_or(false), clothing: form.clothing.clone(),
form.voluntaryfuehrungsassistent.unwrap_or(false), location_id: form.location,
form.amount, time: (form.from, form.till),
&form.clothing, name: form.name.clone(),
form.note note: form
.as_ref() .note
.clone()
.and_then(|n| if n.len() != 0 { Some(n) } else { None }), .and_then(|n| if n.len() != 0 { Some(n) } else { None }),
) voluntary_wachhabender: form.voluntarywachhabender.unwrap_or(false),
.await?; voluntary_fuehrungsassistent: form.voluntaryfuehrungsassistent.unwrap_or(false),
};
if let Err(e) = changeset.validate_with(&None) {
return Ok(HttpResponse::BadRequest().body(e.to_string()));
};
Event::create(pool.get_ref(), changeset).await?;
let url = utils::get_return_url_for_date(&form.date); let url = utils::get_return_url_for_date(&form.date);
println!("redirecto to {url}"); println!("redirecto to {url}");

View File

@ -7,9 +7,9 @@ mod assignment;
mod availability; mod availability;
mod events; mod events;
mod export; mod export;
mod imprint;
mod location; mod location;
pub mod user; pub mod user;
mod imprint;
mod vehicle; mod vehicle;
mod vehicle_assignment; mod vehicle_assignment;
@ -60,6 +60,7 @@ pub fn init(cfg: &mut ServiceConfig) {
cfg.service(events::post_new::post); cfg.service(events::post_new::post);
cfg.service(events::get_plan::get); cfg.service(events::get_plan::get);
cfg.service(events::get_edit::get); cfg.service(events::get_edit::get);
cfg.service(events::post_edit::post);
cfg.service(assignment::post_new::post); cfg.service(assignment::post_new::post);
cfg.service(assignment::delete::delete); cfg.service(assignment::delete::delete);

View File

@ -1,7 +1,7 @@
use chrono::NaiveTime; use chrono::NaiveTime;
use sqlx::{query, PgPool}; use sqlx::{query, PgPool};
use super::{Function, Result}; use super::{assignment_changeset::AssignmentChangeset, Function, Result};
pub struct Assignment { pub struct Assignment {
pub event_id: i32, pub event_id: i32,
@ -16,9 +16,7 @@ impl Assignment {
pool: &PgPool, pool: &PgPool,
event_id: i32, event_id: i32,
availabillity_id: i32, availabillity_id: i32,
function: Function, changeset: AssignmentChangeset
start_time: NaiveTime,
end_time: NaiveTime,
) -> Result<()> { ) -> Result<()> {
query!( query!(
r##" r##"
@ -27,9 +25,9 @@ impl Assignment {
"##, "##,
event_id, event_id,
availabillity_id, availabillity_id,
function as Function, changeset.function as Function,
start_time, changeset.time.0,
end_time changeset.time.1
) )
.execute(pool) .execute(pool)
.await?; .await?;
@ -154,6 +152,17 @@ impl Assignment {
Ok(assignemnet) Ok(assignemnet)
} }
pub async fn update(
pool: &PgPool,
event_id: i32,
availabillity_id: i32,
changeset: AssignmentChangeset,
) -> Result<()> {
query!("UPDATE assignment SET function = $1, startTime = $2, endTime = $3 WHERE eventId = $4 AND availabillityId = $5;", changeset.function as Function, changeset.time.0, changeset.time.1, event_id, availabillity_id).execute(pool).await?;
Ok(())
}
pub async fn delete(pool: &PgPool, event_id: i32, availabillity_id: i32) -> Result<()> { pub async fn delete(pool: &PgPool, event_id: i32, availabillity_id: i32) -> Result<()> {
query!("DELETE FROM assignment WHERE assignment.eventId = $1 AND assignment.availabillityId = $2;", query!("DELETE FROM assignment WHERE assignment.eventId = $1 AND assignment.availabillityId = $2;",
event_id, event_id,

View File

@ -0,0 +1,119 @@
use chrono::NaiveTime;
use garde::Validate;
use super::{start_time_lies_before_end_time, Assignment, Availabillity, Event, Function};
#[derive(Validate)]
#[garde(allow_unvalidated)]
#[garde(context(AssignmentContext as ctx))]
/// check before: event and availabillity and must exist and user of availabillity is in event location
pub struct AssignmentChangeset {
#[garde(
custom(user_of_availability_has_function),
custom(event_has_free_slot_for_function)
)]
pub function: Function,
#[garde(
custom(available_time_fits),
custom(start_time_lies_before_end_time),
custom(availabillity_not_already_assigned)
)]
pub time: (NaiveTime, NaiveTime),
}
pub struct AssignmentContext {
pub event: Event,
pub availabillity: Availabillity,
pub user_function: Function,
pub assignments_for_event: Vec<Assignment>,
pub assignments_for_availabillity: Vec<Assignment>,
}
fn available_time_fits(
value: &(NaiveTime, NaiveTime),
context: &AssignmentContext,
) -> garde::Result {
if context.availabillity.start_time.is_some() && context.availabillity.end_time.is_some() {
let start = context.availabillity.start_time.as_ref().unwrap();
let end = context.availabillity.end_time.as_ref().unwrap();
if value.0 < *start || value.1 > *end {
return Err(garde::Error::new(
"time not made available can't be assigned",
));
}
}
Ok(())
}
fn user_of_availability_has_function(
value: &Function,
context: &AssignmentContext,
) -> garde::Result {
if *value > context.user_function {
return Err(garde::Error::new(
"user has not the required function for this assignment",
));
}
Ok(())
}
fn event_has_free_slot_for_function(
_value: &Function,
context: &AssignmentContext,
) -> garde::Result {
let list: Vec<&Assignment> = context
.assignments_for_event
.iter()
.filter(|a| {
a.availabillity_id != context.availabillity.id && a.event_id != context.event.id
})
.collect();
let a = list
.iter()
.filter(|a| a.function == Function::Posten)
.count();
if match context.user_function {
Function::Posten => a >= context.event.amount_of_posten as usize,
Function::Fuehrungsassistent => context.event.voluntary_fuehrungsassistent && a >= 1,
Function::Wachhabender => context.event.voluntary_wachhabender && a >= 1,
} {
return Err(garde::Error::new(
"event already has enough assignments for this function",
));
}
Ok(())
}
fn availabillity_not_already_assigned(
value: &(NaiveTime, NaiveTime),
context: &AssignmentContext,
) -> garde::Result {
let list: Vec<&Assignment> = context
.assignments_for_availabillity
.iter()
.filter(|a| {
a.availabillity_id != context.availabillity.id && a.event_id != context.event.id
})
.collect();
let has_start_time_during_assignment =
|a: &Assignment| a.start_time >= value.0 && a.start_time <= value.1;
let has_end_time_during_assignment =
|a: &Assignment| a.end_time >= value.0 && a.end_time <= value.1;
if list
.iter()
.any(|a| has_start_time_during_assignment(a) || has_end_time_during_assignment(a))
{
return Err(garde::Error::new(
"availabillity is already assigned for that time",
));
}
Ok(())
}

View File

@ -1,9 +1,9 @@
use chrono::{NaiveDate, NaiveTime}; use chrono::{NaiveDate, NaiveTime};
use sqlx::{query, PgPool}; use sqlx::{query, PgPool};
use super::{Location, Result}; use super::{event_changeset::EventChangeset, Location, Result};
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct Event { pub struct Event {
pub id: i32, pub id: i32,
pub date: NaiveDate, pub date: NaiveDate,
@ -21,24 +21,12 @@ pub struct Event {
} }
impl Event { impl Event {
pub async fn create( pub async fn create(pool: &PgPool, changeset: EventChangeset) -> Result<()> {
pool: &PgPool,
date: &NaiveDate,
start_time: &NaiveTime,
end_time: &NaiveTime,
name: &String,
location_id: i32,
voluntary_wachhabender: bool,
voluntary_fuehrungsassistent: bool,
amount_of_posten: i16,
clothing: &String,
note: Option<&String>,
) -> Result<()> {
query!(r#" query!(r#"
INSERT INTO event (date, startTime, endTime, name, locationId, voluntaryWachhabender, voluntaryFuehrungsassistent, amountOfPosten, clothing, note) INSERT INTO event (date, startTime, endTime, name, locationId, voluntaryWachhabender, voluntaryFuehrungsassistent, amountOfPosten, clothing, note)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
"#, "#,
date, start_time, end_time, name, location_id, voluntary_wachhabender, voluntary_fuehrungsassistent, amount_of_posten, clothing, note).execute(pool).await?; changeset.date, changeset.time.0, changeset.time.1,changeset.name, changeset.location_id, changeset.voluntary_wachhabender, changeset.voluntary_fuehrungsassistent, changeset.amount_of_posten, changeset.clothing, changeset.note).execute(pool).await?;
Ok(()) Ok(())
} }
@ -157,4 +145,17 @@ impl Event {
Ok(event) Ok(event)
} }
pub async fn update(pool: &PgPool, id: i32, changeset: EventChangeset) -> Result<()> {
query!(r#"
UPDATE event SET date = $1, startTime = $2, endTime = $3, name = $4, locationId = $5, voluntaryWachhabender = $6, voluntaryFuehrungsassistent = $7, amountOfPosten = $8, clothing = $9, note = $10 WHERE id = $11;
"#,
changeset.date,
changeset.time.0,
changeset.time.1,changeset.name, changeset.location_id, changeset.voluntary_wachhabender, changeset.voluntary_fuehrungsassistent, changeset.amount_of_posten, changeset.clothing, changeset.note, id)
.execute(pool)
.await?;
Ok(())
}
} }

View File

@ -0,0 +1,97 @@
use chrono::NaiveDate;
use chrono::NaiveTime;
use garde::Validate;
use super::start_time_lies_before_end_time;
#[derive(Validate)]
#[garde(allow_unvalidated)]
#[garde(context(Option<EventContext> as ctx))]
pub struct EventChangeset {
#[garde(custom(date_unchanged_if_edit))]
pub date: NaiveDate,
#[garde(
custom(start_time_lies_before_end_time),
custom(time_can_be_extended_if_edit)
)]
pub time: (NaiveTime, NaiveTime),
pub name: String,
/// check before: must exist and user can create event for this location
pub location_id: i32,
#[garde(custom(can_unset_wachhabender))]
pub voluntary_wachhabender: bool,
#[garde(custom(can_unset_fuehrungsassistent))]
pub voluntary_fuehrungsassistent: bool,
#[garde(range(min = ctx.as_ref().and_then(|c: &EventContext| Some(c.amount_of_assigned_posten)).unwrap_or(1), max = 100))]
pub amount_of_posten: i16,
pub clothing: String,
pub note: Option<String>,
}
pub struct EventContext {
pub date_in_db: NaiveDate,
pub common_min_max_available_time: (NaiveTime, NaiveTime),
pub wachhabender_assigned: bool,
pub fuehrungsassistent_assigned: bool,
pub amount_of_assigned_posten: i16,
}
fn date_unchanged_if_edit(value: &NaiveDate, context: &Option<EventContext>) -> garde::Result {
if context.as_ref().is_some_and(|c| c.date_in_db != *value) {
return Err(garde::Error::new("event date can't be changed"));
}
Ok(())
}
fn time_can_be_extended_if_edit(
value: &(NaiveTime, NaiveTime),
context: &Option<EventContext>,
) -> garde::Result {
if let Some(context) = context {
let old_start_time = context.common_min_max_available_time.0;
let new_start_time = value.0;
let old_end_time = context.common_min_max_available_time.1;
let new_end_time = value.1;
if new_start_time < old_start_time {
return Err(garde::Error::new(
"starttime lies outside of available time for assigned people",
));
}
if new_end_time > old_end_time {
return Err(garde::Error::new(
"endtime lies ouside of available time for assigned people",
));
}
}
Ok(())
}
fn can_unset_fuehrungsassistent(value: &bool, context: &Option<EventContext>) -> garde::Result {
if context
.as_ref()
.is_some_and(|c| !*value && c.fuehrungsassistent_assigned)
{
return Err(garde::Error::new(
"fuehrungsassistent can't be set to not by ff, because a person is already assigned",
));
}
Ok(())
}
fn can_unset_wachhabender(value: &bool, context: &Option<EventContext>) -> garde::Result {
if context
.as_ref()
.is_some_and(|c| !*value && c.wachhabender_assigned)
{
return Err(garde::Error::new(
"wachhabender can't be set to not by ff, because a person is already assigned",
));
}
Ok(())
}

View File

@ -4,7 +4,7 @@ use super::Area;
use super::Result; use super::Result;
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct Location { pub struct Location {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -138,4 +138,12 @@ impl Location {
Ok(()) Ok(())
} }
pub async fn exists(pool: &PgPool, id: i32) -> Result<bool> {
let b = query!("SELECT EXISTS(SELECT * FROM location WHERE id= $1);", id)
.fetch_one(pool)
.await?;
Ok(b.exists.is_some_and(|e| e))
}
} }

View File

@ -1,27 +1,43 @@
mod area; mod area;
mod assignement; mod assignement;
mod assignment_changeset;
mod availabillity; mod availabillity;
mod event; mod event;
mod event_changeset;
mod function; mod function;
mod location; mod location;
mod password_reset;
mod registration;
mod role; mod role;
mod user; mod user;
mod vehicle; mod vehicle;
mod password_reset;
mod registration;
mod vehicle_assignement; mod vehicle_assignement;
pub use area::Area; pub use area::Area;
pub use assignement::Assignment;
pub use assignment_changeset::{AssignmentChangeset, AssignmentContext};
pub use availabillity::{Availabillity, AvailabillityAssignmentState}; pub use availabillity::{Availabillity, AvailabillityAssignmentState};
use chrono::NaiveTime;
pub use event::Event; pub use event::Event;
pub use event_changeset::{EventChangeset, EventContext};
pub use function::Function; pub use function::Function;
pub use location::Location; pub use location::Location;
pub use password_reset::{NoneToken, PasswordReset, Token};
pub use registration::Registration;
pub use role::Role; pub use role::Role;
pub use user::User; pub use user::User;
pub use assignement::Assignment;
pub use password_reset::{PasswordReset, Token, NoneToken};
pub use registration::Registration;
pub use vehicle::Vehicle; pub use vehicle::Vehicle;
pub use vehicle_assignement::VehicleAssignement; pub use vehicle_assignement::VehicleAssignement;
type Result<T> = std::result::Result<T, sqlx::Error>; type Result<T> = std::result::Result<T, sqlx::Error>;
fn start_time_lies_before_end_time<T>(
value: &(NaiveTime, NaiveTime),
_context: &T,
) -> garde::Result {
if value.0 >= value.1 {
return Err(garde::Error::new("endtime can't lie before starttime"));
}
Ok(())
}