feat: validation of new availability
This commit is contained in:
parent
b42540ac2f
commit
d1e067407b
@ -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<i32>,
|
||||
pub availability: Option<i32>,
|
||||
}
|
||||
|
||||
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<Availability>,
|
||||
) -> 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)> {
|
||||
|
@ -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.",
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -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) =
|
||||
|
@ -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(
|
||||
|
@ -4,7 +4,8 @@
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% let is_edit = id.is_some() %}
|
||||
<form method="post" action="/availability/{% if is_edit %}edit/{{ id.unwrap() }}{% else %}new{% endif %}">
|
||||
<form hx-post="/availability/{% if is_edit %}edit/{{ id.unwrap() }}{% else %}new{% endif %}" hx-target="body"
|
||||
hx-target-422="#error">
|
||||
<h1 class="title">{% if is_edit %}Bearbeite{% else %}Neue{% endif %} Vefügbarkeit für {{
|
||||
date|fmt_date(WeekdayDayMonthYear) }}</h1>
|
||||
|
||||
@ -18,7 +19,7 @@
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<input class="input" type="time" name="starttime" required {{ start|insert_time_value|safe }}
|
||||
_="on change put the value of me into #st">
|
||||
_="on change put the value of me into #st then put '' into #error">
|
||||
{% if slot_suggestions.len() > 0 %}
|
||||
<p class="help">noch mögliche Zeiträume:</p>
|
||||
<div class="tags help">
|
||||
@ -32,7 +33,7 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<input class="input" type="time" name="endtime" required {{ end|insert_time_value|safe }}
|
||||
_='on change put the value of me into #et
|
||||
_='on change put the value of me into #et then put "" into #error
|
||||
then if (value of the previous <input/>) 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 @@
|
||||
<label class="radio">
|
||||
<input type="radio" name="isovernight" {{ is_overnight|invert|ref|cond_show("checked")|safe }}
|
||||
_='on click set the value of #enddate to "{{ date }}"
|
||||
then put "{{ date|fmt_date(WeekdayDayMonth) }}" into #ed'>
|
||||
then put "{{ date|fmt_date(WeekdayDayMonth) }}" into #ed
|
||||
then put "" into #error'>
|
||||
am selben Tag
|
||||
</label>
|
||||
<label class="radio ml-3">
|
||||
<input type="radio" id="radionextday" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
|
||||
_='on click set the value of #enddate to "{{ datetomorrow }}"
|
||||
then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed'>
|
||||
then put "{{ datetomorrow|fmt_date(WeekdayDayMonth) }}" into #ed
|
||||
then put "" into #error'>
|
||||
am Tag darauf
|
||||
</label>
|
||||
</div>
|
||||
@ -90,6 +93,7 @@
|
||||
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span>
|
||||
Uhr
|
||||
</p>
|
||||
<p id="error" class="help is-danger"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -99,7 +103,7 @@
|
||||
<div class="field-body">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-success">
|
||||
<button class="button is-success" _="on click put '' into #error">
|
||||
<svg class="icon">
|
||||
<use href="/static/feather-sprite.svg#check-circle" />
|
||||
</svg>
|
||||
|
@ -3,7 +3,8 @@
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<form hx-post="/users/{% if let Some(id) = id %}edit/{{ id }}{% else %}new{% endif %}" hx-target-422="find p">
|
||||
<form hx-post="/users/{% if let Some(id) = id %}edit/{{ id }}{% else %}new{% endif %}" hx-target="body"
|
||||
hx-target-422="find p">
|
||||
<h1 class="title">
|
||||
{% if let Some(name) = name %}Nutzer '{{ name }}' bearbeiten{% else %}Neuen Nutzer anlegen{% endif %}
|
||||
</h1>
|
||||
|
Loading…
x
Reference in New Issue
Block a user