refactor: assignment validation

This commit is contained in:
Max Hohlfeld 2025-06-30 11:28:01 +02:00
parent e5df98a515
commit 10e6ba80a2
2 changed files with 127 additions and 92 deletions

View File

@ -1,44 +1,82 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use garde::Validate; use sqlx::PgPool;
use crate::models::start_date_time_lies_before_end_date_time; use crate::validation::{
AsyncValidate, AsyncValidateError, start_date_time_lies_before_end_date_time,
use super::{
Assignment, Availability, Event, Function, UserFunction,
}; };
#[derive(Validate)] use super::{Assignment, Availability, Event, Function, Role, User};
#[garde(allow_unvalidated)]
#[garde(context(AssignmentContext as ctx))]
/// check before: event and availability and must exist and user of availability is in event location
pub struct AssignmentChangeset { pub struct AssignmentChangeset {
#[garde(
custom(user_of_availability_has_function),
custom(event_has_free_slot_for_function)
)]
pub function: Function, pub function: Function,
#[garde(
custom(available_time_fits),
custom(start_date_time_lies_before_end_date_time),
custom(availability_not_already_assigned)
)]
pub time: (NaiveDateTime, NaiveDateTime), pub time: (NaiveDateTime, NaiveDateTime),
} }
pub struct AssignmentContext { pub struct AssignmentContext<'a> {
pub event: Event, pub pool: &'a PgPool,
pub availability: Availability, pub user: &'a User,
pub user_function: UserFunction, pub event_id: i32,
pub assignments_for_event: Vec<Assignment>, pub availability_id: i32,
pub assignments_for_availability: Vec<Assignment>, }
impl<'a> AsyncValidate<'a> for AssignmentChangeset {
type Context = AssignmentContext<'a>;
async fn validate_with_context(
&self,
context: &'a Self::Context,
) -> Result<(), crate::validation::AsyncValidateError> {
let Some(availability) =
Availability::read_by_id_including_user(context.pool, context.availability_id).await?
else {
return Err(AsyncValidateError::new(
"Angegebener Verfügbarkeit des Nutzers existiert nicht!",
));
};
let Some(event) =
Event::read_by_id_including_location(context.pool, context.event_id).await?
else {
return Err(AsyncValidateError::new(
"Angegebenes Event existiert nicht!",
));
};
user_is_admin_or_area_manager_of_event_area(context.user, &event)?;
availability_user_inside_event_area(&availability, &event)?;
available_time_fits(&self.time, &availability)?;
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
availability_not_already_assigned(&self.time, &availability, &event, context.pool).await?;
user_of_availability_has_function(&self.function, &availability)?;
event_has_free_slot_for_function(&self.function, &availability, &event, context.pool)
.await?;
Ok(())
}
}
fn availability_user_inside_event_area(
availability: &Availability,
event: &Event,
) -> Result<(), AsyncValidateError> {
let user = availability.user.as_ref().unwrap();
let location = event.location.as_ref().unwrap();
if user.area_id != location.area_id {
return Err(AsyncValidateError::new(
"Nutzer der Verfügbarkeit ist nicht im gleichen Bereich wie der Ort der Veranstaltung!",
));
}
Ok(())
} }
fn available_time_fits( fn available_time_fits(
value: &(NaiveDateTime, NaiveDateTime), value: &(NaiveDateTime, NaiveDateTime),
context: &AssignmentContext, availability: &Availability,
) -> garde::Result { ) -> Result<(), AsyncValidateError> {
if value.0 < context.availability.start || value.1 > context.availability.end { if value.0 < availability.start || value.1 > availability.end {
return Err(garde::Error::new( return Err(AsyncValidateError::new(
"time not made available can't be assigned", "time not made available can't be assigned",
)); ));
} }
@ -48,10 +86,12 @@ fn available_time_fits(
fn user_of_availability_has_function( fn user_of_availability_has_function(
value: &Function, value: &Function,
context: &AssignmentContext, availability: &Availability,
) -> garde::Result { ) -> Result<(), AsyncValidateError> {
if !context.user_function.contains(value) { let user_function = &availability.user.as_ref().unwrap().function;
return Err(garde::Error::new(
if !user_function.contains(value) {
return Err(AsyncValidateError::new(
"user has not the required function for this assignment", "user has not the required function for this assignment",
)); ));
} }
@ -59,26 +99,31 @@ fn user_of_availability_has_function(
Ok(()) Ok(())
} }
fn event_has_free_slot_for_function( async fn event_has_free_slot_for_function(
value: &Function, value: &Function,
context: &AssignmentContext, availability: &Availability,
) -> garde::Result { event: &Event,
let list: Vec<&Assignment> = context pool: &PgPool,
.assignments_for_event ) -> Result<(), AsyncValidateError> {
.iter() let assignments_for_event: Vec<Assignment> = Assignment::read_all_by_event(pool, event.id)
.filter(|a| a.availability_id != context.availability.id && a.event_id != context.event.id) .await?
.into_iter()
.filter(|a| a.availability_id != availability.id && a.event_id != event.id)
.collect(); .collect();
let a = list let assignments_with_function = assignments_for_event
.iter() .iter()
.filter(|a| a.function == Function::Posten) .filter(|a| a.function == *value)
.count(); .count();
if match *value { if match *value {
Function::Posten => a >= context.event.amount_of_posten as usize, Function::Posten => assignments_with_function >= event.amount_of_posten as usize,
Function::Fuehrungsassistent => context.event.voluntary_fuehrungsassistent && a >= 1, Function::Fuehrungsassistent => {
Function::Wachhabender => context.event.voluntary_wachhabender && a >= 1, event.voluntary_fuehrungsassistent && assignments_with_function >= 1
}
Function::Wachhabender => event.voluntary_wachhabender && assignments_with_function >= 1,
} { } {
return Err(garde::Error::new( return Err(AsyncValidateError::new(
"event already has enough assignments for this function", "event already has enough assignments for this function",
)); ));
} }
@ -86,28 +131,44 @@ fn event_has_free_slot_for_function(
Ok(()) Ok(())
} }
fn availability_not_already_assigned( async fn availability_not_already_assigned(
value: &(NaiveDateTime, NaiveDateTime), time: &(NaiveDateTime, NaiveDateTime),
context: &AssignmentContext, availability: &Availability,
) -> garde::Result { event: &Event,
let list: Vec<&Assignment> = context pool: &PgPool,
.assignments_for_availability ) -> Result<(), AsyncValidateError> {
.iter() let list: Vec<Assignment> = Assignment::read_all_by_availability(pool, availability.id)
.filter(|a| a.availability_id != context.availability.id && a.event_id != context.event.id) .await?
.into_iter()
.filter(|a| a.availability_id != availability.id && a.event_id != event.id)
.collect(); .collect();
let has_start_time_during_assignment = let has_start_time_during_assignment = |a: &Assignment| a.start >= time.0 && a.start <= time.1;
|a: &Assignment| a.start >= value.0 && a.start <= value.1; let has_end_time_during_assignment = |a: &Assignment| a.end >= time.0 && a.end <= time.1;
let has_end_time_during_assignment = |a: &Assignment| a.end >= value.0 && a.end <= value.1;
if list if list
.iter() .iter()
.any(|a| has_start_time_during_assignment(a) || has_end_time_during_assignment(a)) .any(|a| has_start_time_during_assignment(a) || has_end_time_during_assignment(a))
{ {
return Err(garde::Error::new( return Err(AsyncValidateError::new(
"availability is already assigned for that time", "availability is already assigned for that time",
)); ));
} }
Ok(()) Ok(())
} }
fn user_is_admin_or_area_manager_of_event_area(
user: &User,
event: &Event,
) -> Result<(), AsyncValidateError> {
let user_is_admin = user.role == Role::Admin;
let user_is_area_manager_event_area =
user.role == Role::AreaManager && user.area_id == event.location.as_ref().unwrap().area_id;
if !user_is_admin || !user_is_area_manager_event_area {
return Err(AsyncValidateError::new(""));
}
Ok(())
}

View File

@ -1,5 +1,4 @@
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use garde::Validate;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
@ -13,8 +12,9 @@ use crate::{
}, },
}; };
use brass_db::models::{ use brass_db::{
Assignment, AssignmentChangeset, AssignmentContext, Availability, Event, Function, Role, User, models::{Assignment, AssignmentChangeset, AssignmentContext, Event, Function, User},
validation::AsyncValidate,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -35,28 +35,6 @@ pub async fn post(
return Ok(HttpResponse::NotFound().finish()); return Ok(HttpResponse::NotFound().finish());
}; };
let user_is_admin_or_area_manager_of_event_area = user.role == Role::Admin
|| (user.role == Role::AreaManager
&& user.area_id == event.location.as_ref().unwrap().area_id);
if !user_is_admin_or_area_manager_of_event_area {
return Err(ApplicationError::Unauthorized);
}
let Some(availability) =
Availability::read_by_id_including_user(pool.get_ref(), query.availability).await?
else {
return Ok(HttpResponse::NotFound().finish());
};
let availability_user_not_in_event_location_area =
availability.user.as_ref().unwrap().area_id != event.location.as_ref().unwrap().area_id;
if availability_user_not_in_event_location_area {
return Ok(HttpResponse::BadRequest()
.body("availability user is not in the same area as event location"));
}
let function = Function::try_from(query.function)?; let function = Function::try_from(query.function)?;
let changeset = AssignmentChangeset { let changeset = AssignmentChangeset {
@ -64,22 +42,18 @@ pub async fn post(
time: (event.start, event.end), time: (event.start, event.end),
}; };
let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?;
let assignments_for_availability =
Assignment::read_all_by_availability(pool.get_ref(), availability.id).await?;
let context = AssignmentContext { let context = AssignmentContext {
event: event.clone(), user: &user.into_inner(),
availability: availability.clone(), event_id: event.id,
user_function: availability.user.as_ref().unwrap().function.clone(), availability_id: query.availability,
assignments_for_event, pool: pool.get_ref(),
assignments_for_availability,
}; };
if let Err(e) = changeset.validate_with(&context) { if let Err(e) = changeset.validate_with_context(&context).await {
return Ok(HttpResponse::BadRequest().body(e.to_string())); return Ok(HttpResponse::BadRequest().body(e.to_string()));
}; };
Assignment::create(pool.get_ref(), event.id, availability.id, changeset).await?; Assignment::create(pool.get_ref(), event.id, query.availability, changeset).await?;
let availabilities = generate_availability_assignment_list(pool.get_ref(), &event).await?; let availabilities = generate_availability_assignment_list(pool.get_ref(), &event).await?;