Compare commits

..

3 Commits

12 changed files with 216 additions and 86 deletions

View 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"
}

View File

@ -3,7 +3,7 @@ use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use serde::Deserialize;
use crate::filters;
use crate::models::{Availability, AvailabilityChangeset, Role, User};
use crate::models::{Role, User};
use crate::utils::DateTimeFormat::{DayMonth, DayMonthYear, DayMonthYearHourMinute, HourMinute};
pub mod delete;
@ -35,16 +35,3 @@ pub struct AvailabilityForm {
pub endtime: NaiveTime,
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;
}

View File

@ -1,11 +1,10 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use garde::Validate;
use sqlx::PgPool;
use crate::{
endpoints::availability::{find_adjacend_availability, AvailabilityForm},
endpoints::availability::AvailabilityForm,
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
utils::{self, ApplicationError},
utils::{self, validation::AsyncValidate, ApplicationError},
};
#[actix_web::post("/availability/new")]
@ -14,32 +13,33 @@ pub async fn post(
pool: web::Data<PgPool>,
form: web::Form<AvailabilityForm>,
) -> 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 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 {
time: (start, end),
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()));
};
if let Some(a) = find_adjacend_availability(&changeset, None, &existing_availabilities) {
let (changeset_start, changeset_end) = changeset.time;
if a.end == changeset_start {
if let Some(a) =
Availability::find_adjacent_by_time_for_user(pool.get_ref(), &start, &end, user.id, None)
.await?
{
if a.end == start {
changeset.time.0 = a.start;
}
if a.start == changeset_end {
if a.start == end {
changeset.time.1 = a.end;
}

View File

@ -1,14 +1,10 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use garde::Validate;
use sqlx::PgPool;
use crate::{
endpoints::{
availability::{find_adjacend_availability, AvailabilityForm},
IdPath,
},
endpoints::{availability::AvailabilityForm, IdPath},
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
utils::{self, ApplicationError},
utils::{self, validation::AsyncValidate, ApplicationError},
};
#[actix_web::post("/availability/edit/{id}")]
@ -26,39 +22,38 @@ pub async fn post(
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 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 {
time: (start, end),
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()));
};
if let Some(a) =
find_adjacend_availability(&changeset, Some(availability.id), &existing_availabilities)
if let Some(a) = Availability::find_adjacent_by_time_for_user(
pool.get_ref(),
&start,
&end,
user.id,
Some(availability.id),
)
.await?
{
let (changeset_start, changeset_end) = changeset.time;
if a.end == changeset_start {
if a.end == start {
changeset.time.0 = a.start;
}
if a.start == changeset_end {
if a.start == end {
changeset.time.1 = a.end;
}

View File

@ -4,7 +4,7 @@ use sqlx::PgPool;
use crate::{
endpoints::{user::NewOrEditUserForm, IdPath},
models::{Function, Role, User, UserChangeset},
utils::{validation::AsyncValidate, ApplicationError},
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
};
#[actix_web::post("/users/edit/{id}")]
@ -63,7 +63,8 @@ pub async fn post_edit(
}
}
if let Err(e) = changeset.validate_with_pool(pool.get_ref()).await {
let context = DbContext::new(pool.get_ref());
if let Err(e) = changeset.validate_with_context(&context).await {
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
};

View File

@ -5,7 +5,7 @@ use crate::{
endpoints::user::NewOrEditUserForm,
mail::Mailer,
models::{Function, Registration, Role, User, UserChangeset},
utils::{validation::AsyncValidate, ApplicationError},
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
};
#[actix_web::post("/users/new")]
@ -57,7 +57,8 @@ pub async fn post_new(
.body("email: an user already exists with the same email"));
}
if let Err(e) = changeset.validate_with_pool(pool.get_ref()).await {
let context = DbContext::new(pool.get_ref());
if let Err(e) = changeset.validate_with_context(&context).await {
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
};

View File

@ -341,6 +341,49 @@ impl Availability {
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<()> {
query!(
"UPDATE availability SET startTimestamp = $1, endTimestamp = $2, comment = $3 WHERE id = $4",

View File

@ -1,38 +1,63 @@
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 {
#[garde(
custom(time_is_not_already_made_available),
custom(start_date_time_lies_before_end_date_time)
)]
pub time: (NaiveDateTime, NaiveDateTime),
pub comment: Option<String>,
}
pub struct AvailabilityContext {
pub existing_availabilities: Vec<Availability>,
pub struct AvailabilityContext<'a> {
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(
value: &(NaiveDateTime, NaiveDateTime),
context: &AvailabilityContext,
) -> garde::Result {
if context.existing_availabilities.is_empty() {
existing_availabilities: &Vec<Availability>,
) -> Result<(), AsyncValidateError> {
if existing_availabilities.is_empty() {
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() {
return Err(garde::Error::new(
return Err(AsyncValidateError::new(
"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);
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",
));
}

View File

@ -3,7 +3,7 @@ use fake::{faker::internet::en::SafeEmail, faker::name::en::Name, Dummy};
use sqlx::PgPool;
use crate::utils::validation::{email_is_valid, AsyncValidate, AsyncValidateError};
use crate::utils::validation::{email_is_valid, AsyncValidate, AsyncValidateError, DbContext};
use super::{Area, Function, Role};
@ -22,10 +22,12 @@ pub struct UserChangeset {
pub area_id: i32,
}
impl AsyncValidate for UserChangeset {
async fn validate_with_pool(&self, pool: &sqlx::PgPool) -> Result<(), AsyncValidateError> {
impl <'a>AsyncValidate<'a> for UserChangeset {
type Context = DbContext<'a>;
async fn validate_with_context(&self, context: &'a Self::Context) -> Result<(), AsyncValidateError> {
email_is_valid(&self.email)?;
area_exists(pool, self.area_id).await?;
area_exists(context.pool, self.area_id).await?;
Ok(())
}

View File

@ -2,6 +2,31 @@ mod email;
mod error;
mod r#trait;
use chrono::NaiveDateTime;
pub use email::email_is_valid;
pub use error::AsyncValidateError;
pub use r#trait::AsyncValidate;
use sqlx::PgPool;
pub struct DbContext<'a> {
pub pool: &'a PgPool,
}
impl<'a> DbContext<'a> {
pub fn new(pool: &'a PgPool) -> Self {
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(())
}

View File

@ -1,7 +1,8 @@
use sqlx::PgPool;
use super::AsyncValidateError;
pub trait AsyncValidate {
async fn validate_with_pool(&self, pool: &PgPool) -> Result<(), AsyncValidateError>;
pub trait AsyncValidate<'a> {
type Context: 'a;
async fn validate_with_context(&self, context: &'a Self::Context) -> Result<(), AsyncValidateError>;
}

View File

@ -9,7 +9,7 @@
date|fmt_date(DayMonthYear) }}</h1>
<input type="hidden" name="startdate" value="{{ date }}">
<input type="hidden" name="enddate" value="{{ date }}" id="enddate">
<input type="hidden" name="enddate" value="{{ enddate.as_ref().unwrap_or(date) }}" id="enddate">
<div class="field is-horizontal">
<div class="field-label">
@ -35,6 +35,7 @@
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(DayMonth) }}" into #ed
then set checked of #radionextday to true
end' />
</div>
</div>
@ -55,7 +56,7 @@
am selben Tag
</label>
<label class="radio ml-3">
<input type="radio" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
<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(DayMonth) }}" into #ed'>
am Tag darauf
@ -80,7 +81,7 @@
<use href="/static/feather-sprite.svg#info" />
</svg>
{% let start_time = start.unwrap_or(NaiveTime::from_hms_opt(10, 0, 0).unwrap()) %}
{% let end_time = start.unwrap_or(NaiveTime::from_hms_opt(20, 0, 0).unwrap()) %}
{% let end_time = end.unwrap_or(NaiveTime::from_hms_opt(20, 0, 0).unwrap()) %}
verfügbar von {{ date|fmt_date(DayMonth) }} <span id="st">{{ start_time|fmt_time(HourMinute) }}</span> Uhr
bis <span id="ed">{{ enddate.as_ref().unwrap_or(date)|fmt_date(DayMonth) }}</span>
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span> Uhr