Compare commits
10 Commits
f25e508bbd
...
45cf6dda10
Author | SHA1 | Date | |
---|---|---|---|
45cf6dda10 | |||
428f46b853 | |||
bdaf8ff20e | |||
512b061c7a | |||
2abeeb20df | |||
10e6ba80a2 | |||
e5df98a515 | |||
93574c3ac5 | |||
9893c37f80 | |||
f35b343768 |
@ -16,3 +16,4 @@ SMTP_PORT="1025"
|
||||
# SMTP_LOGIN=""
|
||||
# SMTP_PASSWORD=""
|
||||
SMTP_TLSTYPE="none"
|
||||
RUST_LOG="info,brass_web=trace,brass_db=trace"
|
||||
|
70
Cargo.lock
generated
70
Cargo.lock
generated
@ -775,6 +775,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
"brass-config",
|
||||
"chrono",
|
||||
"clap",
|
||||
"sqlx",
|
||||
]
|
||||
@ -787,6 +788,19 @@ dependencies = [
|
||||
"dotenvy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brass-db"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fake",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brass-macros"
|
||||
version = "0.1.0"
|
||||
@ -809,13 +823,13 @@ dependencies = [
|
||||
"argon2",
|
||||
"askama",
|
||||
"brass-config",
|
||||
"brass-db",
|
||||
"brass-macros",
|
||||
"built",
|
||||
"change-detection",
|
||||
"chrono",
|
||||
"fake",
|
||||
"futures-util",
|
||||
"garde",
|
||||
"insta",
|
||||
"lettre",
|
||||
"maud",
|
||||
@ -890,15 +904,6 @@ dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.22"
|
||||
@ -1007,20 +1012,6 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@ -1643,31 +1634,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garde"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a989bd2fd12136080f7825ff410d9239ce84a2a639487fc9d924ee42e2fb84f"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"garde_derive",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garde_derive"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f7f0545bbbba0a37d4d445890fa5759814e0716f02417b39f6fab292193df68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@ -3383,12 +3349,6 @@ dependencies = [
|
||||
"path-slash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
|
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = [ "cli", "config", "macros", "web", ]
|
||||
members = [ "cli", "config", "db", "macros", "web", ]
|
||||
resolver = "2"
|
||||
default-members = ["web"]
|
||||
|
||||
|
@ -15,3 +15,4 @@ brass-config = { path = "../config" }
|
||||
async-std = { version = "1.13.0", features = ["attributes"] }
|
||||
sqlx = { version = "0.8.2", features = ["runtime-async-std", "postgres"] }
|
||||
anyhow = "1.0.94"
|
||||
chrono = "0.4.41"
|
||||
|
@ -1,6 +1,9 @@
|
||||
use anyhow::Context;
|
||||
use chrono::Local;
|
||||
use sqlx::migrate::Migrate;
|
||||
use sqlx::{migrate::Migrator, Executor};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
@ -28,9 +31,14 @@ enum Command {
|
||||
Reset,
|
||||
#[command(about = "Run all pending migrations on database")]
|
||||
Migrate,
|
||||
#[command(about = "Create a new migration")]
|
||||
NewMigration { title: String },
|
||||
#[command(about = "Prepare sqlx query metadata for offline compile-time verification")]
|
||||
Prepare,
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
#[allow(unused)]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
let config = load_config(&cli.environment).expect("Could not load config!");
|
||||
@ -42,7 +50,6 @@ async fn main() {
|
||||
create_db(&db_config)
|
||||
.await
|
||||
.expect("Failed creating database.");
|
||||
|
||||
migrate_db(&db_config)
|
||||
.await
|
||||
.expect("Failed migrating database.");
|
||||
@ -51,20 +58,24 @@ async fn main() {
|
||||
drop_db(&db_config)
|
||||
.await
|
||||
.expect("Failed dropping database.");
|
||||
|
||||
create_db(&db_config)
|
||||
.await
|
||||
.expect("Failed creating database.");
|
||||
|
||||
migrate_db(&db_config)
|
||||
.await
|
||||
.expect("Failed migrating database.");
|
||||
},
|
||||
}
|
||||
Command::Migrate => {
|
||||
migrate_db(&db_config)
|
||||
.await
|
||||
.expect("Failed migrating database.");
|
||||
}
|
||||
Command::NewMigration { title } => {
|
||||
create_new_migration(&title)
|
||||
.await
|
||||
.expect("Failed creating new migration.");
|
||||
}
|
||||
Command::Prepare => prepare().await.expect("Failed preparing query metadata."),
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,13 +122,7 @@ async fn migrate_db(db_config: &PgConnectOptions) -> anyhow::Result<()> {
|
||||
.await
|
||||
.context("Connection to database failed!")?;
|
||||
|
||||
let migrations_path = PathBuf::from(
|
||||
std::env::var("CARGO_MANIFEST_DIR").expect("This command needs to be invoked using cargo"),
|
||||
)
|
||||
.join("..")
|
||||
.join("migrations")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let migrations_path = db_package_root()?.join("migrations");
|
||||
|
||||
let migrator = Migrator::new(Path::new(&migrations_path))
|
||||
.await
|
||||
@ -148,3 +153,56 @@ async fn migrate_db(db_config: &PgConnectOptions) -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_new_migration(title: &str) -> anyhow::Result<()> {
|
||||
let now = Local::now();
|
||||
let timestamp = now.format("%Y%m%d%H%M%S");
|
||||
let file_name = format!("{timestamp}_{title}.sql");
|
||||
let path = db_package_root()?.join("migrations").join(&file_name);
|
||||
|
||||
let mut file = File::create(&path).context(format!(r#"Could not create file "{:?}""#, path))?;
|
||||
file.write_all("".as_bytes())
|
||||
.context(format!(r#"Could not write file "{:?}""#, path))?;
|
||||
|
||||
println!("Created migration {file_name}.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prepare() -> anyhow::Result<()> {
|
||||
let cargo = std::env::var("CARGO")
|
||||
.map_err(|_| anyhow::anyhow!("Please invoke me using Cargo, e.g.: `cargo db <ARGS>`"))
|
||||
.expect("Existence of CARGO env var is asserted by calling `ensure_sqlx_cli_installed`");
|
||||
|
||||
let mut sqlx_prepare_command = {
|
||||
let mut cmd = std::process::Command::new(&cargo);
|
||||
|
||||
cmd.args(["sqlx", "prepare", "--", "--all-targets", "--all-features"]);
|
||||
|
||||
let cmd_cwd = db_package_root().context("Error finding the root of the db package!")?;
|
||||
cmd.current_dir(cmd_cwd);
|
||||
|
||||
cmd
|
||||
};
|
||||
|
||||
let o = sqlx_prepare_command
|
||||
.output()
|
||||
.context("Could not run {cargo} sqlx prepare!")?;
|
||||
|
||||
if !o.status.success() {
|
||||
let error = anyhow::anyhow!(String::from_utf8_lossy(&o.stdout).to_string()).context("Error generating query metadata. Are you sure the database is running and all migrations are applied?");
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
println!("Query data written to db/.sqlx directory; please check this into version control.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn db_package_root() -> Result<PathBuf, anyhow::Error> {
|
||||
Ok(PathBuf::from(
|
||||
std::env::var("CARGO_MANIFEST_DIR").expect("This command needs to be invoked using cargo"),
|
||||
)
|
||||
.join("..")
|
||||
.join("db")
|
||||
.canonicalize()?)
|
||||
}
|
||||
|
19
db/Cargo.toml
Normal file
19
db/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "brass-db"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0"
|
||||
authors = ["Max Hohlfeld <maxhohlfeld@posteo.de>"]
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
sqlx = { version = "^0.8", features = ["runtime-async-std-rustls", "postgres", "chrono"] }
|
||||
chrono = { version = "0.4.33", features = ["serde", "now"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
rand = { version = "0.9", features = ["os_rng"] }
|
||||
regex = "1.11.1"
|
||||
tracing = "0.1.41"
|
||||
fake = { version = "4", features = ["chrono", "derive"], optional = true}
|
||||
|
||||
[features]
|
||||
test-helpers = ["dep:fake"]
|
31
db/src/lib.rs
Normal file
31
db/src/lib.rs
Normal file
@ -0,0 +1,31 @@
|
||||
pub mod models;
|
||||
mod support;
|
||||
pub mod validation;
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
|
||||
use chrono::NaiveTime;
|
||||
|
||||
pub use support::{NoneToken, Token};
|
||||
|
||||
const START_OF_DAY: NaiveTime = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||
const END_OF_DAY: NaiveTime = NaiveTime::from_hms_opt(23, 59, 59).unwrap();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnsupportedEnumValue {
|
||||
pub value: u8,
|
||||
pub enum_name: &'static str,
|
||||
}
|
||||
|
||||
impl Display for UnsupportedEnumValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"unsupported enum value '{}' given for enum '{}'",
|
||||
self.value, self.enum_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for UnsupportedEnumValue {}
|
@ -1,8 +1,9 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::{query, PgPool};
|
||||
use sqlx::{PgPool, query};
|
||||
|
||||
use super::{assignment_changeset::AssignmentChangeset, Function, Result};
|
||||
use super::{AssignmentChangeset, Function, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Assignment {
|
||||
pub event_id: i32,
|
||||
pub availability_id: i32,
|
186
db/src/models/assignment_changeset.rs
Normal file
186
db/src/models/assignment_changeset.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::PgPool;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::validation::{
|
||||
AsyncValidate, AsyncValidateError, start_date_time_lies_before_end_date_time,
|
||||
};
|
||||
|
||||
use super::{Assignment, Availability, Event, Function, Role, User};
|
||||
|
||||
pub struct AssignmentChangeset {
|
||||
pub function: Function,
|
||||
pub time: (NaiveDateTime, NaiveDateTime),
|
||||
}
|
||||
|
||||
pub struct AssignmentContext<'a> {
|
||||
pub pool: &'a PgPool,
|
||||
pub user: &'a User,
|
||||
pub event_id: i32,
|
||||
pub availability_id: i32,
|
||||
}
|
||||
|
||||
impl<'a> AsyncValidate<'a> for AssignmentChangeset {
|
||||
type Context = AssignmentContext<'a>;
|
||||
|
||||
async fn validate_with_context(
|
||||
&self,
|
||||
context: &'a Self::Context,
|
||||
) -> Result<(), crate::validation::AsyncValidateError> {
|
||||
let Some(availability) =
|
||||
Availability::read_by_id_including_user(context.pool, context.availability_id).await?
|
||||
else {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Angegebener Verfügbarkeit des Nutzers existiert nicht.",
|
||||
));
|
||||
};
|
||||
|
||||
let Some(event) =
|
||||
Event::read_by_id_including_location(context.pool, context.event_id).await?
|
||||
else {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Angegebene Veranstaltung existiert nicht.",
|
||||
));
|
||||
};
|
||||
|
||||
user_is_admin_or_area_manager_of_event_area(context.user, &event)?;
|
||||
availability_user_inside_event_area(&availability, &event)?;
|
||||
available_time_fits(&self.time, &availability)?;
|
||||
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
||||
availability_not_already_assigned(&self.time, &availability, &event, context.pool).await?;
|
||||
user_of_availability_has_function(&self.function, &availability)?;
|
||||
event_has_free_slot_for_function(&self.function, &availability, &event, context.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn availability_user_inside_event_area(
|
||||
availability: &Availability,
|
||||
event: &Event,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let user = availability.user.as_ref().unwrap();
|
||||
let location = event.location.as_ref().unwrap();
|
||||
|
||||
if user.area_id != location.area_id {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Nutzer der Verfügbarkeit ist nicht im gleichen Bereich wie der Ort der Veranstaltung.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn available_time_fits(
|
||||
value: &(NaiveDateTime, NaiveDateTime),
|
||||
availability: &Availability,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if value.0 < availability.start || value.1 > availability.end {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Die verfügbar gemachte Zeit passt nicht zu der zugewiesenen Zeit für die Veranstaltung.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn user_of_availability_has_function(
|
||||
value: &Function,
|
||||
availability: &Availability,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let user_function = &availability.user.as_ref().unwrap().function;
|
||||
|
||||
if !user_function.contains(value) {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Nutzer der Verfügbarkeit besitzt nicht die benötigte Funktion um für diese Position zugewiesen zu werden.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn event_has_free_slot_for_function(
|
||||
value: &Function,
|
||||
availability: &Availability,
|
||||
event: &Event,
|
||||
pool: &PgPool,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
debug!(?event, "event parameter");
|
||||
let assignments_for_event: Vec<Assignment> = Assignment::read_all_by_event(pool, event.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|a| a.availability_id != availability.id)
|
||||
.collect();
|
||||
|
||||
debug!(?assignments_for_event, "existing assignments for event");
|
||||
|
||||
let assignments_with_function = assignments_for_event
|
||||
.iter()
|
||||
.filter(|a| a.function == *value)
|
||||
.count();
|
||||
|
||||
debug!(
|
||||
assignments_with_function,
|
||||
"amount of existing assignments for function"
|
||||
);
|
||||
|
||||
if match *value {
|
||||
Function::Posten => assignments_with_function >= event.amount_of_posten as usize,
|
||||
Function::Fuehrungsassistent => {
|
||||
event.voluntary_fuehrungsassistent && assignments_with_function >= 1
|
||||
}
|
||||
Function::Wachhabender => event.voluntary_wachhabender && assignments_with_function >= 1,
|
||||
} {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Veranstaltung hat bereits genug Zuweisungen für diese Funktion.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn availability_not_already_assigned(
|
||||
time: &(NaiveDateTime, NaiveDateTime),
|
||||
availability: &Availability,
|
||||
event: &Event,
|
||||
pool: &PgPool,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let list: Vec<Assignment> = Assignment::read_all_by_availability(pool, availability.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|a| a.event_id != event.id)
|
||||
.collect();
|
||||
|
||||
let has_start_time_during_assignment = |a: &Assignment| a.start >= time.0 && a.start <= time.1;
|
||||
let has_end_time_during_assignment = |a: &Assignment| a.end >= time.0 && a.end <= time.1;
|
||||
|
||||
if list
|
||||
.iter()
|
||||
.any(|a| has_start_time_during_assignment(a) || has_end_time_during_assignment(a))
|
||||
{
|
||||
return Err(AsyncValidateError::new(
|
||||
"Die Verfügbarkeit des Nutzers wurde bereits zu einer anderen Veranstaltung zugewiesen.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: maybe merge with event changeset
|
||||
fn user_is_admin_or_area_manager_of_event_area(
|
||||
user: &User,
|
||||
event: &Event,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let user_is_admin = user.role == Role::Admin;
|
||||
let user_is_area_manager_event_area =
|
||||
user.role == Role::AreaManager && user.area_id == event.location.as_ref().unwrap().area_id;
|
||||
|
||||
if !user_is_admin && !user_is_area_manager_event_area {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Du verfügst nicht über die Berechtigung, Zuweisungen zu Veranstaltungen vorzunehmen.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use sqlx::{query, PgPool};
|
||||
use sqlx::{PgPool, query};
|
||||
|
||||
use super::{Area, AvailabilityChangeset, Result, Role, User, UserFunction};
|
||||
|
@ -1,14 +1,10 @@
|
||||
use chrono::{Days, NaiveDateTime};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
utils::validation::{
|
||||
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError,
|
||||
},
|
||||
END_OF_DAY, START_OF_DAY,
|
||||
};
|
||||
|
||||
use super::Availability;
|
||||
use crate::{validation::{
|
||||
start_date_time_lies_before_end_date_time, AsyncValidate, AsyncValidateError
|
||||
}, END_OF_DAY, START_OF_DAY};
|
||||
|
||||
pub struct AvailabilityChangeset {
|
||||
pub time: (NaiveDateTime, NaiveDateTime),
|
@ -1,7 +1,7 @@
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use sqlx::{query, PgPool};
|
||||
use sqlx::{PgPool, query};
|
||||
|
||||
use super::{event_changeset::EventChangeset, Clothing, Location, Result};
|
||||
use super::{Clothing, EventChangeset, Location, Result};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Event {
|
248
db/src/models/event_changeset.rs
Normal file
248
db/src/models/event_changeset.rs
Normal file
@ -0,0 +1,248 @@
|
||||
use chrono::Days;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
#[cfg(feature = "test-helpers")]
|
||||
use fake::{Fake, Faker};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::END_OF_DAY;
|
||||
use crate::START_OF_DAY;
|
||||
use crate::models::Assignment;
|
||||
use crate::models::Availability;
|
||||
use crate::models::Event;
|
||||
use crate::models::Function;
|
||||
use crate::models::Location;
|
||||
use crate::models::Role;
|
||||
use crate::models::User;
|
||||
use crate::validation::AsyncValidate;
|
||||
use crate::validation::AsyncValidateError;
|
||||
use crate::validation::start_date_time_lies_before_end_date_time;
|
||||
|
||||
pub struct EventChangeset {
|
||||
pub time: (NaiveDateTime, NaiveDateTime),
|
||||
pub name: String,
|
||||
pub location_id: i32,
|
||||
pub voluntary_wachhabender: bool,
|
||||
pub voluntary_fuehrungsassistent: bool,
|
||||
pub amount_of_posten: i16,
|
||||
pub clothing: i32,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
pub struct EventContext<'a> {
|
||||
pub pool: &'a PgPool,
|
||||
pub event: Option<i32>,
|
||||
pub user: &'a User,
|
||||
}
|
||||
|
||||
impl<'a> AsyncValidate<'a> for EventChangeset {
|
||||
type Context = EventContext<'a>;
|
||||
|
||||
async fn validate_with_context(
|
||||
&self,
|
||||
context: &'a Self::Context,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let Some(location) = Location::read_by_id(context.pool, self.location_id).await? else {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Der angegebene Veranstaltungsort existiert nicht.",
|
||||
));
|
||||
};
|
||||
|
||||
user_is_admin_or_area_manager_of_event_location(context.user, &location)?;
|
||||
|
||||
start_date_time_lies_before_end_date_time(&self.time.0, &self.time.1)?;
|
||||
|
||||
let mut minimum_amount_of_posten = 0_i16;
|
||||
if let Some(id) = context.event {
|
||||
let event = Event::read_by_id_including_location(context.pool, id)
|
||||
.await?
|
||||
.unwrap();
|
||||
let assignments_for_event =
|
||||
Assignment::read_all_by_event(context.pool, event.id).await?;
|
||||
minimum_amount_of_posten = assignments_for_event
|
||||
.iter()
|
||||
.filter(|a| a.function == Function::Posten)
|
||||
.count() as i16;
|
||||
|
||||
time_can_be_extended_if_edit(&self.time, &event, &assignments_for_event, context.pool)
|
||||
.await?;
|
||||
date_unchanged_if_edit(&self.time, &event.start.date())?;
|
||||
can_unset_wachhabender(&self.voluntary_wachhabender, &assignments_for_event)?;
|
||||
can_unset_fuehrungsassistent(
|
||||
&self.voluntary_fuehrungsassistent,
|
||||
&assignments_for_event,
|
||||
)?;
|
||||
if location.area_id != event.location.unwrap().area_id {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Veranstaltungsort kann nicht zu einem Ort außerhalb des initialen Bereichs geändert werden.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !(minimum_amount_of_posten..=100).contains(&self.amount_of_posten) {
|
||||
return Err(AsyncValidateError::new(
|
||||
"Die Anzahl der Posten darf nicht kleiner als die Anzahl der bereits geplanten Posten und maximal 100 sein.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn user_is_admin_or_area_manager_of_event_location(
|
||||
user: &User,
|
||||
location: &Location,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if user.role != Role::Admin
|
||||
&& !(user.role == Role::AreaManager && user.area_id == location.area_id)
|
||||
{
|
||||
return Err(AsyncValidateError::new(
|
||||
"Du verfügst nicht über die Berechtigung, diese Veranstaltung zu erstellen bzw. zu bearbeiten.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn date_unchanged_if_edit(
|
||||
time: &(NaiveDateTime, NaiveDateTime),
|
||||
date_in_db: &NaiveDate,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if time.0.date() != *date_in_db {
|
||||
return Err(AsyncValidateError::new("event date can't be changed"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn time_can_be_extended_if_edit(
|
||||
time: &(NaiveDateTime, NaiveDateTime),
|
||||
event: &Event,
|
||||
assignments_for_event: &Vec<Assignment>,
|
||||
pool: &PgPool,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let start = event.start.date();
|
||||
let end = event.start.date().checked_add_days(Days::new(1)).unwrap();
|
||||
let mut common_time = (start.and_time(START_OF_DAY), end.and_time(END_OF_DAY));
|
||||
|
||||
for assignment in assignments_for_event {
|
||||
let availability = Availability::read_by_id(pool, assignment.availability_id)
|
||||
.await?
|
||||
.unwrap();
|
||||
let all_assignments =
|
||||
Assignment::read_all_by_availability(pool, assignment.availability_id).await?;
|
||||
|
||||
if all_assignments.len() == 1 {
|
||||
if availability.start > common_time.0 {
|
||||
common_time.0 = availability.start;
|
||||
}
|
||||
|
||||
if availability.end < common_time.1 {
|
||||
common_time.1 = availability.end;
|
||||
}
|
||||
} else {
|
||||
let mut slots = vec![(availability.start, availability.end)];
|
||||
for a in all_assignments
|
||||
.iter()
|
||||
.filter(|x| x.event_id != assignment.event_id)
|
||||
{
|
||||
let (fit, rest) = slots
|
||||
.into_iter()
|
||||
.partition(|s| s.0 >= a.start && s.1 <= a.end);
|
||||
slots = rest;
|
||||
let fit = fit.first().unwrap();
|
||||
|
||||
if fit.0 != a.start {
|
||||
slots.push((fit.0, a.start));
|
||||
}
|
||||
|
||||
if fit.1 != a.end {
|
||||
slots.push((a.end, fit.1));
|
||||
}
|
||||
}
|
||||
|
||||
let slot = slots
|
||||
.into_iter()
|
||||
.find(|s| s.0 >= assignment.start && s.1 <= assignment.end)
|
||||
.unwrap();
|
||||
|
||||
if slot.0 > common_time.0 {
|
||||
common_time.0 = slot.0;
|
||||
}
|
||||
|
||||
if slot.1 < common_time.1 {
|
||||
common_time.1 = slot.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let old_start_time = common_time.0;
|
||||
let new_start_time = time.0;
|
||||
let old_end_time = common_time.1;
|
||||
let new_end_time = time.1;
|
||||
|
||||
if new_start_time < old_start_time {
|
||||
return Err(AsyncValidateError::new(
|
||||
"starttime lies outside of available time for assigned people",
|
||||
));
|
||||
}
|
||||
|
||||
if new_end_time > old_end_time {
|
||||
return Err(AsyncValidateError::new(
|
||||
"endtime lies ouside of available time for assigned people",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn can_unset_fuehrungsassistent(
|
||||
fuehrungsassistent_required: &bool,
|
||||
assignments_for_event: &Vec<Assignment>,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if !*fuehrungsassistent_required
|
||||
&& assignments_for_event
|
||||
.iter()
|
||||
.any(|a| a.function == Function::Fuehrungsassistent)
|
||||
{
|
||||
return Err(AsyncValidateError::new(
|
||||
"fuehrungsassistent can't be set to not by ff, because a person is already assigned",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn can_unset_wachhabender(
|
||||
voluntary_wachhabender: &bool,
|
||||
assignments_for_event: &Vec<Assignment>,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
if !*voluntary_wachhabender
|
||||
&& assignments_for_event
|
||||
.iter()
|
||||
.any(|a| a.function == Function::Wachhabender)
|
||||
{
|
||||
return Err(AsyncValidateError::new(
|
||||
"wachhabender can't be set to not by ff, because a person is already assigned",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-helpers")]
|
||||
impl EventChangeset {
|
||||
pub fn create_for_test(start: NaiveDateTime, end: NaiveDateTime) -> EventChangeset {
|
||||
let changeset = EventChangeset {
|
||||
time: (start, end),
|
||||
name: Faker.fake(),
|
||||
location_id: 1,
|
||||
voluntary_wachhabender: true,
|
||||
voluntary_fuehrungsassistent: true,
|
||||
amount_of_posten: 5,
|
||||
clothing: 1,
|
||||
note: None,
|
||||
};
|
||||
|
||||
changeset
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use sqlx::{
|
||||
PgPool,
|
||||
postgres::{PgHasArrayType, PgTypeInfo},
|
||||
query, PgPool,
|
||||
query,
|
||||
};
|
||||
|
||||
use crate::utils::ApplicationError;
|
||||
|
||||
use super::Function;
|
||||
use super::{Function, Result};
|
||||
|
||||
pub struct ExportEventRow {
|
||||
pub start_timestamp: NaiveDateTime,
|
||||
@ -38,7 +37,7 @@ impl ExportEventRow {
|
||||
pool: &PgPool,
|
||||
time: (NaiveDate, NaiveDate),
|
||||
area: i32,
|
||||
) -> Result<Vec<ExportEventRow>, ApplicationError> {
|
||||
) -> Result<Vec<ExportEventRow>> {
|
||||
let rows = query!(
|
||||
"select
|
||||
event.starttimestamp,
|
@ -1,8 +1,9 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::utils::ApplicationError;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::UnsupportedEnumValue;
|
||||
|
||||
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, PartialOrd, Ord)]
|
||||
#[sqlx(type_name = "function", rename_all = "lowercase")]
|
||||
pub enum Function {
|
||||
@ -22,16 +23,16 @@ impl Display for Function {
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Function {
|
||||
type Error = ApplicationError;
|
||||
type Error = UnsupportedEnumValue;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
1 => Ok(Function::Posten),
|
||||
5 => Ok(Function::Fuehrungsassistent),
|
||||
10 => Ok(Function::Wachhabender),
|
||||
_ => Err(ApplicationError::UnsupportedEnumValue {
|
||||
value: value.to_string(),
|
||||
enum_name: String::from("Function"),
|
||||
_ => Err(UnsupportedEnumValue {
|
||||
value,
|
||||
enum_name: "Function",
|
||||
}),
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
use sqlx::{query, PgPool};
|
||||
use sqlx::{PgPool, query};
|
||||
|
||||
use super::Area;
|
||||
|
||||
use super::Result;
|
||||
use super::{Area, Result};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Location {
|
@ -25,7 +25,7 @@ pub use assignment_changeset::{AssignmentChangeset, AssignmentContext};
|
||||
pub use availability::Availability;
|
||||
pub use availability_assignment_state::AvailabilityAssignmentState;
|
||||
pub use availability_changeset::{
|
||||
find_free_date_time_slots, AvailabilityChangeset, AvailabilityContext,
|
||||
AvailabilityChangeset, AvailabilityContext, find_free_date_time_slots,
|
||||
};
|
||||
pub use clothing::Clothing;
|
||||
pub use event::Event;
|
||||
@ -33,7 +33,7 @@ pub use event_changeset::{EventChangeset, EventContext};
|
||||
pub use export_event_row::{ExportEventRow, SimpleAssignment};
|
||||
pub use function::Function;
|
||||
pub use location::Location;
|
||||
pub use password_reset::{NoneToken, PasswordReset, Token};
|
||||
pub use password_reset::PasswordReset;
|
||||
pub use registration::Registration;
|
||||
pub use role::Role;
|
||||
pub use user::User;
|
||||
@ -42,17 +42,4 @@ pub use user_funtion::UserFunction;
|
||||
pub use vehicle::Vehicle;
|
||||
pub use vehicle_assignment::VehicleAssignment;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
type Result<T> = std::result::Result<T, sqlx::Error>;
|
||||
|
||||
fn start_date_time_lies_before_end_date_time<T>(
|
||||
value: &(NaiveDateTime, NaiveDateTime),
|
||||
_context: &T,
|
||||
) -> garde::Result {
|
||||
if value.0 >= value.1 {
|
||||
return Err(garde::Error::new("endtime can't lie before starttime"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,19 +1,9 @@
|
||||
use chrono::TimeDelta;
|
||||
use sqlx::{query_as, PgPool};
|
||||
use sqlx::{PgPool, query_as};
|
||||
|
||||
use crate::support::{Token, generate_token_and_expiration};
|
||||
|
||||
use super::Result;
|
||||
use crate::utils::token_generation::generate_token_and_expiration;
|
||||
|
||||
pub trait Token {
|
||||
async fn delete(&self, pool: &PgPool) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct NoneToken {}
|
||||
impl Token for NoneToken {
|
||||
async fn delete(&self, _pool: &PgPool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PasswordReset {
|
@ -1,7 +1,5 @@
|
||||
use chrono::TimeDelta;
|
||||
use sqlx::{query_as, PgPool};
|
||||
|
||||
use crate::utils::token_generation::generate_token_and_expiration;
|
||||
use sqlx::{PgPool, query_as};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Registration {
|
||||
@ -9,7 +7,9 @@ pub struct Registration {
|
||||
pub userid: i32,
|
||||
}
|
||||
|
||||
use super::{password_reset::Token, Result};
|
||||
use crate::support::{Token, generate_token_and_expiration};
|
||||
|
||||
use super::Result;
|
||||
|
||||
impl Registration {
|
||||
pub async fn insert_new_for_user(pool: &PgPool, user_id: i32) -> Result<Registration> {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user