Compare commits
No commits in common. "master" and "1.0.1" have entirely different histories.
@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
@ -37,7 +37,6 @@ tracing = "0.1.41"
|
|||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
tracing-panic = "0.1.2"
|
tracing-panic = "0.1.2"
|
||||||
rust_xlsxwriter = "0.87.0"
|
rust_xlsxwriter = "0.87.0"
|
||||||
regex = "1.11.1"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
built = "0.7.4"
|
built = "0.7.4"
|
||||||
|
@ -3,7 +3,7 @@ use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::filters;
|
use crate::filters;
|
||||||
use crate::models::{Role, User};
|
use crate::models::{Availability, AvailabilityChangeset, 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,3 +35,16 @@ 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,10 +1,11 @@
|
|||||||
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::AvailabilityForm,
|
endpoints::availability::{find_adjacend_availability, AvailabilityForm},
|
||||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||||
utils::{self, validation::AsyncValidate, ApplicationError},
|
utils::{self, ApplicationError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/availability/new")]
|
#[actix_web::post("/availability/new")]
|
||||||
@ -13,33 +14,32 @@ 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(&context).await {
|
if let Err(e) = changeset.validate_with(&context) {
|
||||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(a) =
|
if let Some(a) = find_adjacend_availability(&changeset, None, &existing_availabilities) {
|
||||||
Availability::find_adjacent_by_time_for_user(pool.get_ref(), &start, &end, user.id, None)
|
let (changeset_start, changeset_end) = changeset.time;
|
||||||
.await?
|
|
||||||
{
|
if a.end == changeset_start {
|
||||||
if a.end == start {
|
|
||||||
changeset.time.0 = a.start;
|
changeset.time.0 = a.start;
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.start == end {
|
if a.start == changeset_end {
|
||||||
changeset.time.1 = a.end;
|
changeset.time.1 = a.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
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::AvailabilityForm, IdPath},
|
endpoints::{
|
||||||
|
availability::{find_adjacend_availability, AvailabilityForm},
|
||||||
|
IdPath,
|
||||||
|
},
|
||||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||||
utils::{self, validation::AsyncValidate, ApplicationError},
|
utils::{self, ApplicationError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/availability/edit/{id}")]
|
#[actix_web::post("/availability/edit/{id}")]
|
||||||
@ -22,38 +26,39 @@ pub async fn post(
|
|||||||
return Err(ApplicationError::Unauthorized);
|
return Err(ApplicationError::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = form.startdate.and_time(form.starttime);
|
let existing_availabilities: Vec<Availability> =
|
||||||
let end = form.enddate.and_time(form.endtime);
|
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 {
|
let context = AvailabilityContext {
|
||||||
pool: pool.get_ref(),
|
existing_availabilities: existing_availabilities.clone(),
|
||||||
user_id: user.id,
|
|
||||||
availability_to_get_edited: Some(availability.id),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let start = form.startdate.and_time(form.starttime);
|
||||||
|
let end = form.enddate.and_time(form.endtime);
|
||||||
|
|
||||||
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(&context).await {
|
if let Err(e) = changeset.validate_with(&context) {
|
||||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(a) = Availability::find_adjacent_by_time_for_user(
|
if let Some(a) =
|
||||||
pool.get_ref(),
|
find_adjacend_availability(&changeset, Some(availability.id), &existing_availabilities)
|
||||||
&start,
|
|
||||||
&end,
|
|
||||||
user.id,
|
|
||||||
Some(availability.id),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
{
|
{
|
||||||
if a.end == start {
|
let (changeset_start, changeset_end) = changeset.time;
|
||||||
|
|
||||||
|
if a.end == changeset_start {
|
||||||
changeset.time.0 = a.start;
|
changeset.time.0 = a.start;
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.start == end {
|
if a.start == changeset_end {
|
||||||
changeset.time.1 = a.end;
|
changeset.time.1 = a.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
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::{user::NewOrEditUserForm, IdPath},
|
endpoints::{user::NewOrEditUserForm, IdPath},
|
||||||
models::{Function, Role, User, UserChangeset},
|
models::{Area, Function, Role, User, UserChangeset},
|
||||||
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
|
utils::ApplicationError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/users/edit/{id}")]
|
#[actix_web::post("/users/edit/{id}")]
|
||||||
@ -33,6 +34,9 @@ pub async fn post_edit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let area_id = form.area.unwrap_or(user_in_db.area_id);
|
let area_id = form.area.unwrap_or(user_in_db.area_id);
|
||||||
|
if Area::read_by_id(pool.get_ref(), area_id).await?.is_none() {
|
||||||
|
return Err(ApplicationError::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
let mut functions = Vec::with_capacity(3);
|
let mut functions = Vec::with_capacity(3);
|
||||||
|
|
||||||
@ -63,8 +67,7 @@ pub async fn post_edit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = DbContext::new(pool.get_ref());
|
if let Err(e) = changeset.validate() {
|
||||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
|
||||||
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -211,6 +214,6 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||||
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
|
assert_eq!(StatusCode::BAD_REQUEST, response.status());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
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::user::NewOrEditUserForm,
|
endpoints::user::NewOrEditUserForm,
|
||||||
mail::Mailer,
|
mail::Mailer,
|
||||||
models::{Function, Registration, Role, User, UserChangeset},
|
models::{Function, Registration, Role, User, UserChangeset},
|
||||||
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
|
utils::ApplicationError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/users/new")]
|
#[actix_web::post("/users/new")]
|
||||||
@ -57,8 +58,7 @@ pub async fn post_new(
|
|||||||
.body("email: an user already exists with the same email"));
|
.body("email: an user already exists with the same email"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = DbContext::new(pool.get_ref());
|
if let Err(e) = changeset.validate() {
|
||||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
|
||||||
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -341,49 +341,6 @@ 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,63 +1,38 @@
|
|||||||
use chrono::{Days, NaiveDateTime};
|
use chrono::{Days, NaiveDateTime};
|
||||||
use sqlx::PgPool;
|
use garde::Validate;
|
||||||
|
|
||||||
use crate::{
|
use crate::{END_OF_DAY, START_OF_DAY};
|
||||||
utils::validation::{
|
|
||||||
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError,
|
|
||||||
},
|
|
||||||
END_OF_DAY, START_OF_DAY,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Availability;
|
use super::{start_date_time_lies_before_end_date_time, 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<'a> {
|
pub struct AvailabilityContext {
|
||||||
pub pool: &'a PgPool,
|
pub existing_availabilities: Vec<Availability>,
|
||||||
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),
|
||||||
existing_availabilities: &Vec<Availability>,
|
context: &AvailabilityContext,
|
||||||
) -> Result<(), AsyncValidateError> {
|
) -> garde::Result {
|
||||||
if existing_availabilities.is_empty() {
|
if context.existing_availabilities.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let free_slots = find_free_date_time_slots(existing_availabilities);
|
let free_slots = find_free_date_time_slots(&context.existing_availabilities);
|
||||||
|
|
||||||
if free_slots.is_empty() {
|
if free_slots.is_empty() {
|
||||||
return Err(AsyncValidateError::new(
|
return Err(garde::Error::new(
|
||||||
"cant create a availability as every time slot is already filled",
|
"cant create a availability as every time slot is already filled",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -66,7 +41,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(AsyncValidateError::new(
|
return Err(garde::Error::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",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,24 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use fake::{faker::internet::en::SafeEmail, faker::name::en::Name, Dummy};
|
use fake::{faker::internet::en::SafeEmail, faker::name::en::Name, Dummy};
|
||||||
|
|
||||||
use sqlx::PgPool;
|
use garde::Validate;
|
||||||
|
|
||||||
use crate::utils::validation::{email_is_valid, AsyncValidate, AsyncValidateError, DbContext};
|
use super::{Function, Role};
|
||||||
|
|
||||||
use super::{Area, Function, Role};
|
#[derive(Debug, Validate)]
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(test, derive(Dummy))]
|
#[cfg_attr(test, derive(Dummy))]
|
||||||
|
#[garde(allow_unvalidated)]
|
||||||
pub struct UserChangeset {
|
pub struct UserChangeset {
|
||||||
#[cfg_attr(test, dummy(faker = "Name()"))]
|
#[cfg_attr(test, dummy(faker = "Name()"))]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[garde(email)]
|
||||||
#[cfg_attr(test, dummy(faker = "SafeEmail()"))]
|
#[cfg_attr(test, dummy(faker = "SafeEmail()"))]
|
||||||
pub email: String,
|
pub email: String,
|
||||||
#[cfg_attr(test, dummy(expr = "Role::Staff"))]
|
#[cfg_attr(test, dummy(expr = "Role::Staff"))]
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
#[cfg_attr(test, dummy(expr = "vec![Function::Posten]"))]
|
#[cfg_attr(test, dummy(expr = "vec![Function::Posten]"))]
|
||||||
pub functions: Vec<Function>,
|
pub functions: Vec<Function>,
|
||||||
|
/// check before: must exist and user can create other user for this area
|
||||||
#[cfg_attr(test, dummy(expr = "1"))]
|
#[cfg_attr(test, dummy(expr = "1"))]
|
||||||
pub area_id: i32,
|
pub area_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
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(context.pool, self.area_id).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn area_exists(pool: &PgPool, id: i32) -> Result<(), AsyncValidateError> {
|
|
||||||
if Area::read_by_id(pool, id).await?.is_none() {
|
|
||||||
return Err(AsyncValidateError::new(
|
|
||||||
"Angegebener Bereich für Nutzer existiert nicht!",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
mod app_customization;
|
|
||||||
mod application_error;
|
mod application_error;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
mod date_time_format;
|
mod date_time_format;
|
||||||
@ -6,16 +5,16 @@ pub mod event_planning_template;
|
|||||||
pub mod manage_commands;
|
pub mod manage_commands;
|
||||||
pub mod password_change;
|
pub mod password_change;
|
||||||
mod template_response_trait;
|
mod template_response_trait;
|
||||||
|
mod app_customization;
|
||||||
pub mod token_generation;
|
pub mod token_generation;
|
||||||
pub mod validation;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_helper;
|
pub mod test_helper;
|
||||||
|
|
||||||
pub use app_customization::Customization;
|
|
||||||
pub use application_error::ApplicationError;
|
pub use application_error::ApplicationError;
|
||||||
pub use date_time_format::DateTimeFormat;
|
pub use date_time_format::DateTimeFormat;
|
||||||
pub use template_response_trait::TemplateResponse;
|
pub use template_response_trait::TemplateResponse;
|
||||||
|
pub use app_customization::Customization;
|
||||||
|
|
||||||
use chrono::{NaiveDate, Utc};
|
use chrono::{NaiveDate, Utc};
|
||||||
|
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
// great inspiration taken from https://github.com/jprochazk/garde/blob/main/garde/src/rules/email.rs
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use super::AsyncValidateError;
|
|
||||||
|
|
||||||
pub fn email_is_valid(email: &str) -> Result<(), AsyncValidateError> {
|
|
||||||
if email.is_empty() {
|
|
||||||
return Err(AsyncValidateError::new("E-Mail ist leer!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (user, domain) = email
|
|
||||||
.split_once('@')
|
|
||||||
.ok_or(AsyncValidateError::new("E-Mail enthält kein '@'!"))?;
|
|
||||||
|
|
||||||
if user.len() > 64 {
|
|
||||||
return Err(AsyncValidateError::new("Nutzerteil der E-Mail zu lang!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_re = Regex::new(r"(?i-u)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap();
|
|
||||||
|
|
||||||
if !user_re.is_match(user) {
|
|
||||||
return Err(AsyncValidateError::new(
|
|
||||||
"Nutzerteil der E-Mail enthält unerlaubte Zeichen.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if domain.len() > 255 {
|
|
||||||
return Err(AsyncValidateError::new(
|
|
||||||
"Domainteil der E-Mail ist zu lang.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let domain_re = Regex::new(
|
|
||||||
r"(?i-u)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if !domain_re.is_match(domain) {
|
|
||||||
return Err(AsyncValidateError::new(
|
|
||||||
"Domainteil der E-Mail enthält unerlaubte Zeichen!",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn email_validation_works_correctly() {
|
|
||||||
assert!(email_is_valid("abc@example.com").is_ok());
|
|
||||||
assert!(email_is_valid("admin.new@example-domain.de").is_ok());
|
|
||||||
assert!(email_is_valid("admin!new@sub.web.www.example-domain.de").is_ok());
|
|
||||||
|
|
||||||
assert!(email_is_valid("admin.domain.de").is_err());
|
|
||||||
assert!(email_is_valid("admin@web@domain.de").is_err());
|
|
||||||
assert!(email_is_valid("@domain.de").is_err());
|
|
||||||
assert!(email_is_valid("user@").is_err());
|
|
||||||
assert!(email_is_valid("").is_err());
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AsyncValidateError {
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for AsyncValidateError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for AsyncValidateError {}
|
|
||||||
|
|
||||||
impl AsyncValidateError {
|
|
||||||
pub fn new(message: &str) -> Self {
|
|
||||||
AsyncValidateError {
|
|
||||||
message: message.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<sqlx::Error> for AsyncValidateError {
|
|
||||||
fn from(value: sqlx::Error) -> Self {
|
|
||||||
error!(error = %value, "database error while validation input");
|
|
||||||
AsyncValidateError::new("Datenbankfehler beim Validieren!")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
use super::AsyncValidateError;
|
|
||||||
|
|
||||||
pub trait AsyncValidate<'a> {
|
|
||||||
type Context: 'a;
|
|
||||||
|
|
||||||
async fn validate_with_context(&self, context: &'a Self::Context) -> Result<(), AsyncValidateError>;
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
|||||||
date|fmt_date(DayMonthYear) }}</h1>
|
date|fmt_date(DayMonthYear) }}</h1>
|
||||||
|
|
||||||
<input type="hidden" name="startdate" value="{{ date }}">
|
<input type="hidden" name="startdate" value="{{ date }}">
|
||||||
<input type="hidden" name="enddate" value="{{ enddate.as_ref().unwrap_or(date) }}" id="enddate">
|
<input type="hidden" name="enddate" value="{{ date }}" id="enddate">
|
||||||
|
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label">
|
<div class="field-label">
|
||||||
@ -35,7 +35,6 @@
|
|||||||
then if (value of the previous <input/>) is greater than (value of me)
|
then if (value of the previous <input/>) is greater than (value of me)
|
||||||
then set the value of #enddate to "{{ datetomorrow }}"
|
then set the value of #enddate to "{{ datetomorrow }}"
|
||||||
then put "{{ datetomorrow|fmt_date(DayMonth) }}" into #ed
|
then put "{{ datetomorrow|fmt_date(DayMonth) }}" into #ed
|
||||||
then set checked of #radionextday to true
|
|
||||||
end' />
|
end' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,7 +55,7 @@
|
|||||||
am selben Tag
|
am selben Tag
|
||||||
</label>
|
</label>
|
||||||
<label class="radio ml-3">
|
<label class="radio ml-3">
|
||||||
<input type="radio" id="radionextday" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
|
<input type="radio" name="isovernight" {{ is_overnight|cond_show("checked")|safe }}
|
||||||
_='on click set the value of #enddate to "{{ datetomorrow }}"
|
_='on click set the value of #enddate to "{{ datetomorrow }}"
|
||||||
then put "{{ datetomorrow|fmt_date(DayMonth) }}" into #ed'>
|
then put "{{ datetomorrow|fmt_date(DayMonth) }}" into #ed'>
|
||||||
am Tag darauf
|
am Tag darauf
|
||||||
@ -81,7 +80,7 @@
|
|||||||
<use href="/static/feather-sprite.svg#info" />
|
<use href="/static/feather-sprite.svg#info" />
|
||||||
</svg>
|
</svg>
|
||||||
{% let start_time = start.unwrap_or(NaiveTime::from_hms_opt(10, 0, 0).unwrap()) %}
|
{% let start_time = start.unwrap_or(NaiveTime::from_hms_opt(10, 0, 0).unwrap()) %}
|
||||||
{% let end_time = end.unwrap_or(NaiveTime::from_hms_opt(20, 0, 0).unwrap()) %}
|
{% let end_time = start.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
|
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>
|
bis <span id="ed">{{ enddate.as_ref().unwrap_or(date)|fmt_date(DayMonth) }}</span>
|
||||||
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span> Uhr
|
<span id="et">{{ end_time|fmt_time(HourMinute) }}</span> Uhr
|
||||||
|
Loading…
x
Reference in New Issue
Block a user