From d1e067407bf0a8d572c8d511ca0fd3d9fc086a2b Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Thu, 3 Jul 2025 21:16:43 +0200 Subject: [PATCH] feat: validation of new availability --- db/src/models/availability_changeset.rs | 59 +++++++++++++------ db/src/validation/mod.rs | 4 +- web/src/endpoints/availability/post_new.rs | 11 +++- web/src/endpoints/availability/post_update.rs | 11 +++- web/templates/availability/new_or_edit.html | 16 +++-- web/templates/user/new_or_edit.html | 3 +- 6 files changed, 73 insertions(+), 31 deletions(-) diff --git a/db/src/models/availability_changeset.rs b/db/src/models/availability_changeset.rs index 1d155544..b5bf027f 100644 --- a/db/src/models/availability_changeset.rs +++ b/db/src/models/availability_changeset.rs @@ -2,9 +2,11 @@ use chrono::{Days, NaiveDateTime}; use sqlx::PgPool; use super::Availability; -use crate::{validation::{ - start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError -}, END_OF_DAY, START_OF_DAY}; +use crate::{ + END_OF_DAY, START_OF_DAY, + models::Assignment, + validation::{AsyncValidate, AsyncValidateError, start_date_time_lies_before_end_date_time}, +}; pub struct AvailabilityChangeset { pub time: (NaiveDateTime, NaiveDateTime), @@ -14,7 +16,7 @@ pub struct AvailabilityChangeset { pub struct AvailabilityContext<'a> { pub pool: &'a PgPool, pub user_id: i32, - pub availability_to_get_edited: Option, + pub availability: Option, } impl<'a> AsyncValidate<'a> for AvailabilityChangeset { @@ -28,48 +30,69 @@ impl<'a> AsyncValidate<'a> for AvailabilityChangeset { Availability::read_by_user_and_date(context.pool, context.user_id, &self.time.0.date()) .await?; - if let Some(existing) = context.availability_to_get_edited { + start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?; + + if let Some(availability) = context.availability { existing_availabilities = existing_availabilities .into_iter() - .filter(|a| a.id != existing) + .filter(|a| a.id != availability) .collect(); + + time_is_not_already_assigned(&self.time, availability, context.pool).await?; } - time_is_not_already_made_available(&self.time, &existing_availabilities)?; - start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?; + if !existing_availabilities.is_empty() { + time_is_not_already_made_available(&self.time, &existing_availabilities)?; + } Ok(()) } } fn time_is_not_already_made_available( - value: &(NaiveDateTime, NaiveDateTime), + (start, end): &(NaiveDateTime, NaiveDateTime), existing_availabilities: &Vec, ) -> Result<(), AsyncValidateError> { - if existing_availabilities.is_empty() { - return Ok(()); - } - let free_slots = find_free_date_time_slots(existing_availabilities); if free_slots.is_empty() { return Err(AsyncValidateError::new( - "cant create a availability as every time slot is already filled", + "Verfügbarkeit kann nicht erstellt werden, da bereits alle Zeiträume verfügbar gemacht wurden.", )); } - let free_block_found_for_start = free_slots.iter().any(|s| s.0 <= value.0 && s.1 >= value.0); - let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= value.1 && s.1 >= value.1); + let free_block_found_for_start = free_slots.iter().any(|s| s.0 <= *start && s.1 >= *start); + let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= *end && s.1 >= *end); + let is_already_present_as_is = existing_availabilities + .iter() + .any(|a| a.start == *start && a.end == a.end); - if !free_block_found_for_start || !free_block_found_for_end { + if !free_block_found_for_start || !free_block_found_for_end || is_already_present_as_is { return Err(AsyncValidateError::new( - "cant create availability as there exists already a availability with the desired time", + "Verfügbarkeit kann nicht erstellt werden, da eine vorhandene Verfügbarkeit überschnitten würde.", )); } Ok(()) } +async fn time_is_not_already_assigned( + (start, end): &(NaiveDateTime, NaiveDateTime), + availability: i32, + pool: &PgPool, +) -> Result<(), AsyncValidateError> { + let existing_assignments = Assignment::read_all_by_availability(pool, availability).await?; + for a in existing_assignments { + if a.start < *start || a.end > *end { + return Err(AsyncValidateError::new( + "Verfügbarkeitszeit kann nicht verkleinert werden, da bereits eine Planung für diese Zeit existiert.", + )); + } + } + + Ok(()) +} + pub fn find_free_date_time_slots( availabilities: &[Availability], ) -> Vec<(NaiveDateTime, NaiveDateTime)> { diff --git a/db/src/validation/mod.rs b/db/src/validation/mod.rs index 5090cffc..7e8e755b 100644 --- a/db/src/validation/mod.rs +++ b/db/src/validation/mod.rs @@ -5,8 +5,8 @@ mod validation_trait; use chrono::NaiveDateTime; pub use email::email_is_valid; pub use error::AsyncValidateError; -pub use validation_trait::AsyncValidate; use sqlx::PgPool; +pub use validation_trait::AsyncValidate; pub struct DbContext<'a> { pub pool: &'a PgPool, @@ -24,7 +24,7 @@ pub fn start_date_time_lies_before_end_date_time( ) -> Result<(), AsyncValidateError> { if start >= end { return Err(AsyncValidateError::new( - "endtime can't lie before starttime", + "Ende kann nicht vor dem Beginn liegen.", )); } diff --git a/web/src/endpoints/availability/post_new.rs b/web/src/endpoints/availability/post_new.rs index e4506fb8..7e0602da 100644 --- a/web/src/endpoints/availability/post_new.rs +++ b/web/src/endpoints/availability/post_new.rs @@ -1,4 +1,5 @@ use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use maud::html; use sqlx::PgPool; use crate::{ @@ -22,7 +23,7 @@ pub async fn post( let context = AvailabilityContext { pool: pool.get_ref(), user_id: user.id, - availability_to_get_edited: None, + availability: None, }; let mut changeset = AvailabilityChangeset { @@ -31,7 +32,13 @@ pub async fn post( }; if let Err(e) = changeset.validate_with_context(&context).await { - return Ok(HttpResponse::BadRequest().body(e.to_string())); + let error_message = html! { + svg class="icon is-small" { + use href="/static/feather-sprite.svg#alert-triangle" {} + } + " " (e) + }; + return Ok(HttpResponse::UnprocessableEntity().body(error_message.into_string())); }; if let Some(a) = diff --git a/web/src/endpoints/availability/post_update.rs b/web/src/endpoints/availability/post_update.rs index f1e5a02e..b7f4d315 100644 --- a/web/src/endpoints/availability/post_update.rs +++ b/web/src/endpoints/availability/post_update.rs @@ -1,4 +1,5 @@ use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; +use maud::html; use sqlx::PgPool; use crate::{ @@ -31,7 +32,7 @@ pub async fn post( let context = AvailabilityContext { pool: pool.get_ref(), user_id: user.id, - availability_to_get_edited: Some(availability.id), + availability: Some(availability.id), }; let mut changeset = AvailabilityChangeset { @@ -40,7 +41,13 @@ pub async fn post( }; if let Err(e) = changeset.validate_with_context(&context).await { - return Ok(HttpResponse::BadRequest().body(e.to_string())); + let error_message = html! { + svg class="icon is-small" { + use href="/static/feather-sprite.svg#alert-triangle" {} + } + " " (e) + }; + return Ok(HttpResponse::UnprocessableEntity().body(error_message.into_string())); }; if let Some(a) = Availability::find_adjacent_by_time_for_user( diff --git a/web/templates/availability/new_or_edit.html b/web/templates/availability/new_or_edit.html index d4f7d963..a32bbd7e 100644 --- a/web/templates/availability/new_or_edit.html +++ b/web/templates/availability/new_or_edit.html @@ -4,7 +4,8 @@
{% let is_edit = id.is_some() %} -
+

{% if is_edit %}Bearbeite{% else %}Neue{% endif %} Vefügbarkeit für {{ date|fmt_date(WeekdayDayMonthYear) }}

@@ -18,7 +19,7 @@
+ _="on change put the value of me into #st then put '' into #error"> {% if slot_suggestions.len() > 0 %}

noch mögliche Zeiträume:

@@ -32,7 +33,7 @@
) is greater than (value of me) then set the value of #enddate to "{{ datetomorrow }}" then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed @@ -53,13 +54,15 @@
@@ -90,6 +93,7 @@ {{ end_time|fmt_time(HourMinute) }} Uhr

+

@@ -99,7 +103,7 @@
-