feat: validate availabilit changeset
This commit is contained in:
parent
b2969b988d
commit
b65b4c7a00
49
.sqlx/query-2288f64f63f07e7dd947c036e5c2be4c563788b3b988b721bd12797fd19a7a95.json
generated
Normal file
49
.sqlx/query-2288f64f63f07e7dd947c036e5c2be4c563788b3b988b721bd12797fd19a7a95.json
generated
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT\n availability.id,\n availability.userId,\n availability.startTimestamp,\n availability.endTimestamp,\n availability.comment\n FROM availability\n WHERE availability.userId = $1\n AND (availability.endtimestamp = $2\n OR availability.starttimestamp = $3)\n AND (availability.id <> $4 OR $4 IS NULL);\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "userid",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "starttimestamp",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "endtimestamp",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "comment",
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int4",
|
||||||
|
"Timestamptz",
|
||||||
|
"Timestamptz",
|
||||||
|
"Int4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "2288f64f63f07e7dd947c036e5c2be4c563788b3b988b721bd12797fd19a7a95"
|
||||||
|
}
|
@ -3,7 +3,7 @@ use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::filters;
|
use crate::filters;
|
||||||
use crate::models::{Availability, AvailabilityChangeset, Role, User};
|
use crate::models::{Role, User};
|
||||||
use crate::utils::DateTimeFormat::{DayMonth, DayMonthYear, DayMonthYearHourMinute, HourMinute};
|
use crate::utils::DateTimeFormat::{DayMonth, DayMonthYear, DayMonthYearHourMinute, HourMinute};
|
||||||
|
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
@ -35,16 +35,3 @@ pub struct AvailabilityForm {
|
|||||||
pub endtime: NaiveTime,
|
pub endtime: NaiveTime,
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_adjacend_availability<'a>(
|
|
||||||
changeset: &AvailabilityChangeset,
|
|
||||||
availability_id_to_be_updated: Option<i32>,
|
|
||||||
existing_availabilities: &'a [Availability],
|
|
||||||
) -> Option<&'a Availability> {
|
|
||||||
let existing_availability = existing_availabilities
|
|
||||||
.iter()
|
|
||||||
.filter(|a| availability_id_to_be_updated.is_none_or(|id| a.id != id))
|
|
||||||
.find(|a| a.start == changeset.time.1 || a.end == changeset.time.0);
|
|
||||||
|
|
||||||
return existing_availability;
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
use garde::Validate;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
endpoints::availability::{find_adjacend_availability, AvailabilityForm},
|
endpoints::availability::AvailabilityForm,
|
||||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||||
utils::{self, ApplicationError},
|
utils::{self, validation::AsyncValidate, ApplicationError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/availability/new")]
|
#[actix_web::post("/availability/new")]
|
||||||
@ -14,32 +13,33 @@ pub async fn post(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
form: web::Form<AvailabilityForm>,
|
form: web::Form<AvailabilityForm>,
|
||||||
) -> Result<impl Responder, ApplicationError> {
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
let existing_availabilities =
|
|
||||||
Availability::read_by_user_and_date(pool.get_ref(), user.id, &form.startdate).await?;
|
|
||||||
let context = AvailabilityContext {
|
|
||||||
existing_availabilities: existing_availabilities.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let start = form.startdate.and_time(form.starttime);
|
let start = form.startdate.and_time(form.starttime);
|
||||||
let end = form.enddate.and_time(form.endtime);
|
let end = form.enddate.and_time(form.endtime);
|
||||||
|
|
||||||
|
let context = AvailabilityContext {
|
||||||
|
pool: pool.get_ref(),
|
||||||
|
user_id: user.id,
|
||||||
|
availability_to_get_edited: None,
|
||||||
|
};
|
||||||
|
|
||||||
let mut changeset = AvailabilityChangeset {
|
let mut changeset = AvailabilityChangeset {
|
||||||
time: (start, end),
|
time: (start, end),
|
||||||
comment: form.comment.clone(),
|
comment: form.comment.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
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()));
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(a) = find_adjacend_availability(&changeset, None, &existing_availabilities) {
|
if let Some(a) =
|
||||||
let (changeset_start, changeset_end) = changeset.time;
|
Availability::find_adjacent_by_time_for_user(pool.get_ref(), &start, &end, user.id, None)
|
||||||
|
.await?
|
||||||
if a.end == changeset_start {
|
{
|
||||||
|
if a.end == start {
|
||||||
changeset.time.0 = a.start;
|
changeset.time.0 = a.start;
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.start == changeset_end {
|
if a.start == end {
|
||||||
changeset.time.1 = a.end;
|
changeset.time.1 = a.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
use garde::Validate;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
endpoints::{
|
endpoints::{availability::AvailabilityForm, IdPath},
|
||||||
availability::{find_adjacend_availability, AvailabilityForm},
|
|
||||||
IdPath,
|
|
||||||
},
|
|
||||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||||
utils::{self, ApplicationError},
|
utils::{self, validation::AsyncValidate, ApplicationError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/availability/edit/{id}")]
|
#[actix_web::post("/availability/edit/{id}")]
|
||||||
@ -26,39 +22,38 @@ pub async fn post(
|
|||||||
return Err(ApplicationError::Unauthorized);
|
return Err(ApplicationError::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let existing_availabilities: Vec<Availability> =
|
|
||||||
Availability::read_by_user_and_date(pool.get_ref(), user.id, &availability.start.date())
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|a| a.id != availability.id)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let context = AvailabilityContext {
|
|
||||||
existing_availabilities: existing_availabilities.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let start = form.startdate.and_time(form.starttime);
|
let start = form.startdate.and_time(form.starttime);
|
||||||
let end = form.enddate.and_time(form.endtime);
|
let end = form.enddate.and_time(form.endtime);
|
||||||
|
|
||||||
|
let context = AvailabilityContext {
|
||||||
|
pool: pool.get_ref(),
|
||||||
|
user_id: user.id,
|
||||||
|
availability_to_get_edited: Some(availability.id),
|
||||||
|
};
|
||||||
|
|
||||||
let mut changeset = AvailabilityChangeset {
|
let mut changeset = AvailabilityChangeset {
|
||||||
time: (start, end),
|
time: (start, end),
|
||||||
comment: form.comment.clone(),
|
comment: form.comment.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
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()));
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(a) =
|
if let Some(a) = Availability::find_adjacent_by_time_for_user(
|
||||||
find_adjacend_availability(&changeset, Some(availability.id), &existing_availabilities)
|
pool.get_ref(),
|
||||||
|
&start,
|
||||||
|
&end,
|
||||||
|
user.id,
|
||||||
|
Some(availability.id),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
{
|
{
|
||||||
let (changeset_start, changeset_end) = changeset.time;
|
if a.end == start {
|
||||||
|
|
||||||
if a.end == changeset_start {
|
|
||||||
changeset.time.0 = a.start;
|
changeset.time.0 = a.start;
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.start == changeset_end {
|
if a.start == end {
|
||||||
changeset.time.1 = a.end;
|
changeset.time.1 = a.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +341,49 @@ impl Availability {
|
|||||||
Ok(availabilities)
|
Ok(availabilities)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_adjacent_by_time_for_user(
|
||||||
|
pool: &PgPool,
|
||||||
|
start: &NaiveDateTime,
|
||||||
|
end: &NaiveDateTime,
|
||||||
|
user: i32,
|
||||||
|
availability_to_ignore: Option<i32>,
|
||||||
|
) -> Result<Option<Availability>> {
|
||||||
|
let records = query!(
|
||||||
|
r##"
|
||||||
|
SELECT
|
||||||
|
availability.id,
|
||||||
|
availability.userId,
|
||||||
|
availability.startTimestamp,
|
||||||
|
availability.endTimestamp,
|
||||||
|
availability.comment
|
||||||
|
FROM availability
|
||||||
|
WHERE availability.userId = $1
|
||||||
|
AND (availability.endtimestamp = $2
|
||||||
|
OR availability.starttimestamp = $3)
|
||||||
|
AND (availability.id <> $4 OR $4 IS NULL);
|
||||||
|
"##,
|
||||||
|
user,
|
||||||
|
start.and_utc(),
|
||||||
|
end.and_utc(),
|
||||||
|
availability_to_ignore
|
||||||
|
)
|
||||||
|
.fetch_all(pool) // possible to find up to two availabilities (upper and lower), for now we only pick one and extend it
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let adjacent_avaialability = records.first().and_then(|r| {
|
||||||
|
Some(Availability {
|
||||||
|
id: r.id,
|
||||||
|
user_id: r.userid,
|
||||||
|
user: None,
|
||||||
|
start: r.starttimestamp.naive_utc(),
|
||||||
|
end: r.endtimestamp.naive_utc(),
|
||||||
|
comment: r.comment.clone(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(adjacent_avaialability)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update(pool: &PgPool, id: i32, changeset: AvailabilityChangeset) -> Result<()> {
|
pub async fn update(pool: &PgPool, id: i32, changeset: AvailabilityChangeset) -> Result<()> {
|
||||||
query!(
|
query!(
|
||||||
"UPDATE availability SET startTimestamp = $1, endTimestamp = $2, comment = $3 WHERE id = $4",
|
"UPDATE availability SET startTimestamp = $1, endTimestamp = $2, comment = $3 WHERE id = $4",
|
||||||
|
@ -1,38 +1,63 @@
|
|||||||
use chrono::{Days, NaiveDateTime};
|
use chrono::{Days, NaiveDateTime};
|
||||||
use garde::Validate;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{END_OF_DAY, START_OF_DAY};
|
use crate::{
|
||||||
|
utils::validation::{
|
||||||
|
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError,
|
||||||
|
},
|
||||||
|
END_OF_DAY, START_OF_DAY,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{start_date_time_lies_before_end_date_time, Availability};
|
use super::Availability;
|
||||||
|
|
||||||
#[derive(Validate)]
|
|
||||||
#[garde(allow_unvalidated)]
|
|
||||||
#[garde(context(AvailabilityContext))]
|
|
||||||
pub struct AvailabilityChangeset {
|
pub struct AvailabilityChangeset {
|
||||||
#[garde(
|
|
||||||
custom(time_is_not_already_made_available),
|
|
||||||
custom(start_date_time_lies_before_end_date_time)
|
|
||||||
)]
|
|
||||||
pub time: (NaiveDateTime, NaiveDateTime),
|
pub time: (NaiveDateTime, NaiveDateTime),
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AvailabilityContext {
|
pub struct AvailabilityContext<'a> {
|
||||||
pub existing_availabilities: Vec<Availability>,
|
pub pool: &'a PgPool,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub availability_to_get_edited: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AsyncValidate<'a> for AvailabilityChangeset {
|
||||||
|
type Context = AvailabilityContext<'a>;
|
||||||
|
|
||||||
|
async fn validate_with_context(
|
||||||
|
&self,
|
||||||
|
context: &'a Self::Context,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
let mut existing_availabilities =
|
||||||
|
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 {
|
||||||
|
existing_availabilities = existing_availabilities
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| a.id != existing)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
time_is_not_already_made_available(&self.time, &existing_availabilities)?;
|
||||||
|
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn time_is_not_already_made_available(
|
fn time_is_not_already_made_available(
|
||||||
value: &(NaiveDateTime, NaiveDateTime),
|
value: &(NaiveDateTime, NaiveDateTime),
|
||||||
context: &AvailabilityContext,
|
existing_availabilities: &Vec<Availability>,
|
||||||
) -> garde::Result {
|
) -> Result<(), AsyncValidateError> {
|
||||||
if context.existing_availabilities.is_empty() {
|
if existing_availabilities.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let free_slots = find_free_date_time_slots(&context.existing_availabilities);
|
let free_slots = find_free_date_time_slots(existing_availabilities);
|
||||||
|
|
||||||
if free_slots.is_empty() {
|
if free_slots.is_empty() {
|
||||||
return Err(garde::Error::new(
|
return Err(AsyncValidateError::new(
|
||||||
"cant create a availability as every time slot is already filled",
|
"cant create a availability as every time slot is already filled",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -41,7 +66,7 @@ fn time_is_not_already_made_available(
|
|||||||
let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= value.1 && s.1 >= value.1);
|
let free_block_found_for_end = free_slots.iter().any(|s| s.0 <= value.1 && s.1 >= value.1);
|
||||||
|
|
||||||
if !free_block_found_for_start || !free_block_found_for_end {
|
if !free_block_found_for_start || !free_block_found_for_end {
|
||||||
return Err(garde::Error::new(
|
return Err(AsyncValidateError::new(
|
||||||
"cant create availability as there exists already a availability with the desired time",
|
"cant create availability as there exists already a availability with the desired time",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -23,14 +23,14 @@ pub struct UserChangeset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl <'a>AsyncValidate<'a> for UserChangeset {
|
impl <'a>AsyncValidate<'a> for UserChangeset {
|
||||||
|
type Context = DbContext<'a>;
|
||||||
|
|
||||||
async fn validate_with_context(&self, context: &'a Self::Context) -> Result<(), AsyncValidateError> {
|
async fn validate_with_context(&self, context: &'a Self::Context) -> Result<(), AsyncValidateError> {
|
||||||
email_is_valid(&self.email)?;
|
email_is_valid(&self.email)?;
|
||||||
area_exists(context.pool, self.area_id).await?;
|
area_exists(context.pool, self.area_id).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
type Context = DbContext<'a>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn area_exists(pool: &PgPool, id: i32) -> Result<(), AsyncValidateError> {
|
async fn area_exists(pool: &PgPool, id: i32) -> Result<(), AsyncValidateError> {
|
||||||
|
@ -2,6 +2,7 @@ mod email;
|
|||||||
mod error;
|
mod error;
|
||||||
mod r#trait;
|
mod r#trait;
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
pub use email::email_is_valid;
|
pub use email::email_is_valid;
|
||||||
pub use error::AsyncValidateError;
|
pub use error::AsyncValidateError;
|
||||||
pub use r#trait::AsyncValidate;
|
pub use r#trait::AsyncValidate;
|
||||||
@ -16,3 +17,16 @@ impl<'a> DbContext<'a> {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn start_date_time_lies_before_end_date_time(
|
||||||
|
start: &NaiveDateTime,
|
||||||
|
end: &NaiveDateTime,
|
||||||
|
) -> Result<(), AsyncValidateError> {
|
||||||
|
if start >= end {
|
||||||
|
return Err(AsyncValidateError::new(
|
||||||
|
"endtime can't lie before starttime",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user