Compare commits
No commits in common. "45cf6dda103a34ec7ae9da238bb835a2a24608ad" and "f25e508bbdd5eeddc3df3d9bb9259a30d028ec98" have entirely different histories.
45cf6dda10
...
f25e508bbd
@ -16,4 +16,3 @@ SMTP_PORT="1025"
|
|||||||
# SMTP_LOGIN=""
|
# SMTP_LOGIN=""
|
||||||
# SMTP_PASSWORD=""
|
# SMTP_PASSWORD=""
|
||||||
SMTP_TLSTYPE="none"
|
SMTP_TLSTYPE="none"
|
||||||
RUST_LOG="info,brass_web=trace,brass_db=trace"
|
|
||||||
|
70
Cargo.lock
generated
70
Cargo.lock
generated
@ -775,7 +775,6 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"async-std",
|
"async-std",
|
||||||
"brass-config",
|
"brass-config",
|
||||||
"chrono",
|
|
||||||
"clap",
|
"clap",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
]
|
]
|
||||||
@ -788,19 +787,6 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "brass-db"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"fake",
|
|
||||||
"rand 0.9.1",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"sqlx",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brass-macros"
|
name = "brass-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -823,13 +809,13 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"askama",
|
"askama",
|
||||||
"brass-config",
|
"brass-config",
|
||||||
"brass-db",
|
|
||||||
"brass-macros",
|
"brass-macros",
|
||||||
"built",
|
"built",
|
||||||
"change-detection",
|
"change-detection",
|
||||||
"chrono",
|
"chrono",
|
||||||
"fake",
|
"fake",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"garde",
|
||||||
"insta",
|
"insta",
|
||||||
"lettre",
|
"lettre",
|
||||||
"maud",
|
"maud",
|
||||||
@ -904,6 +890,15 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.22"
|
version = "1.2.22"
|
||||||
@ -1012,6 +1007,20 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
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]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -1634,6 +1643,31 @@ dependencies = [
|
|||||||
"slab",
|
"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]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@ -3349,6 +3383,12 @@ dependencies = [
|
|||||||
"path-slash",
|
"path-slash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [ "cli", "config", "db", "macros", "web", ]
|
members = [ "cli", "config", "macros", "web", ]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
default-members = ["web"]
|
default-members = ["web"]
|
||||||
|
|
||||||
|
@ -15,4 +15,3 @@ brass-config = { path = "../config" }
|
|||||||
async-std = { version = "1.13.0", features = ["attributes"] }
|
async-std = { version = "1.13.0", features = ["attributes"] }
|
||||||
sqlx = { version = "0.8.2", features = ["runtime-async-std", "postgres"] }
|
sqlx = { version = "0.8.2", features = ["runtime-async-std", "postgres"] }
|
||||||
anyhow = "1.0.94"
|
anyhow = "1.0.94"
|
||||||
chrono = "0.4.41"
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use chrono::Local;
|
|
||||||
use sqlx::migrate::Migrate;
|
use sqlx::migrate::Migrate;
|
||||||
use sqlx::{migrate::Migrator, Executor};
|
use sqlx::{migrate::Migrator, Executor};
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -31,14 +28,9 @@ enum Command {
|
|||||||
Reset,
|
Reset,
|
||||||
#[command(about = "Run all pending migrations on database")]
|
#[command(about = "Run all pending migrations on database")]
|
||||||
Migrate,
|
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]
|
#[async_std::main]
|
||||||
#[allow(unused)]
|
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = load_config(&cli.environment).expect("Could not load config!");
|
let config = load_config(&cli.environment).expect("Could not load config!");
|
||||||
@ -50,6 +42,7 @@ async fn main() {
|
|||||||
create_db(&db_config)
|
create_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed creating database.");
|
.expect("Failed creating database.");
|
||||||
|
|
||||||
migrate_db(&db_config)
|
migrate_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed migrating database.");
|
.expect("Failed migrating database.");
|
||||||
@ -58,24 +51,20 @@ async fn main() {
|
|||||||
drop_db(&db_config)
|
drop_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed dropping database.");
|
.expect("Failed dropping database.");
|
||||||
|
|
||||||
create_db(&db_config)
|
create_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed creating database.");
|
.expect("Failed creating database.");
|
||||||
|
|
||||||
migrate_db(&db_config)
|
migrate_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed migrating database.");
|
.expect("Failed migrating database.");
|
||||||
}
|
},
|
||||||
Command::Migrate => {
|
Command::Migrate => {
|
||||||
migrate_db(&db_config)
|
migrate_db(&db_config)
|
||||||
.await
|
.await
|
||||||
.expect("Failed migrating database.");
|
.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."),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +111,13 @@ async fn migrate_db(db_config: &PgConnectOptions) -> anyhow::Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("Connection to database failed!")?;
|
.context("Connection to database failed!")?;
|
||||||
|
|
||||||
let migrations_path = db_package_root()?.join("migrations");
|
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 migrator = Migrator::new(Path::new(&migrations_path))
|
let migrator = Migrator::new(Path::new(&migrations_path))
|
||||||
.await
|
.await
|
||||||
@ -153,56 +148,3 @@ async fn migrate_db(db_config: &PgConnectOptions) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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()?)
|
|
||||||
}
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
[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"]
|
|
@ -1,31 +0,0 @@
|
|||||||
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,186 +0,0 @@
|
|||||||
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,248 +0,0 @@
|
|||||||
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,46 +0,0 @@
|
|||||||
#[cfg(feature = "test-helpers")]
|
|
||||||
use fake::{Dummy, faker::internet::en::SafeEmail, faker::name::en::Name};
|
|
||||||
|
|
||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
use super::{Area, Function, Role};
|
|
||||||
use crate::validation::{AsyncValidate, AsyncValidateError, DbContext, email_is_valid};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[cfg_attr(feature = "test-helpers", derive(Dummy))]
|
|
||||||
pub struct UserChangeset {
|
|
||||||
#[cfg_attr(feature = "test-helpers", dummy(faker = "Name()"))]
|
|
||||||
pub name: String,
|
|
||||||
#[cfg_attr(feature = "test-helpers", dummy(faker = "SafeEmail()"))]
|
|
||||||
pub email: String,
|
|
||||||
#[cfg_attr(feature = "test-helpers", dummy(expr = "Role::Staff"))]
|
|
||||||
pub role: Role,
|
|
||||||
#[cfg_attr(feature = "test-helpers", dummy(expr = "vec![Function::Posten]"))]
|
|
||||||
pub functions: Vec<Function>,
|
|
||||||
#[cfg_attr(feature = "test-helpers", dummy(expr = "1"))]
|
|
||||||
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,5 +0,0 @@
|
|||||||
mod token_generation;
|
|
||||||
mod token_trait;
|
|
||||||
|
|
||||||
pub use token_generation::generate_token_and_expiration;
|
|
||||||
pub use token_trait::{Token, NoneToken};
|
|
@ -1,18 +0,0 @@
|
|||||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
|
||||||
use rand::{Rng, distr::Alphanumeric, rng};
|
|
||||||
|
|
||||||
pub fn generate_token_and_expiration(
|
|
||||||
token_length_bytes: usize,
|
|
||||||
validity: TimeDelta,
|
|
||||||
) -> (String, NaiveDateTime) {
|
|
||||||
let value = std::iter::repeat(())
|
|
||||||
.map(|()| rng().sample(Alphanumeric))
|
|
||||||
.take(token_length_bytes)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let token = String::from_utf8(value).unwrap();
|
|
||||||
|
|
||||||
let expires = Utc::now().naive_utc() + validity;
|
|
||||||
|
|
||||||
(token, expires)
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
use sqlx::PgPool;
|
|
||||||
|
|
||||||
pub trait Token {
|
|
||||||
fn delete(&self, pool: &PgPool) -> impl Future<Output = Result<(), sqlx::Error>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NoneToken {}
|
|
||||||
impl Token for NoneToken {
|
|
||||||
async fn delete(&self, _pool: &PgPool) -> Result<(), sqlx::Error> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
use super::AsyncValidateError;
|
|
||||||
|
|
||||||
pub trait AsyncValidate<'a> {
|
|
||||||
type Context: 'a;
|
|
||||||
|
|
||||||
fn validate_with_context(
|
|
||||||
&self,
|
|
||||||
context: &'a Self::Context,
|
|
||||||
) -> impl Future<Output = Result<(), AsyncValidateError>>;
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
imports_granularity = "Crate"
|
|
@ -28,15 +28,16 @@ zxcvbn = "3.1.0"
|
|||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
brass-macros = { path = "../macros" }
|
brass-macros = { path = "../macros" }
|
||||||
brass-config = { path = "../config" }
|
brass-config = { path = "../config" }
|
||||||
brass-db = { path = "../db" }
|
|
||||||
actix-http = "3.9.0"
|
actix-http = "3.9.0"
|
||||||
askama = "0.13.0"
|
askama = "0.13.0"
|
||||||
|
garde = { version = "0.22.0", features = ["derive", "email"] }
|
||||||
maud = "0.27.0"
|
maud = "0.27.0"
|
||||||
tracing-actix-web = "0.7.18"
|
tracing-actix-web = "0.7.18"
|
||||||
tracing = "0.1.41"
|
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"
|
||||||
@ -46,5 +47,4 @@ change-detection = "1.2.0"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { version = "1.41.1", features = ["yaml", "filters"] }
|
insta = { version = "1.41.1", features = ["yaml", "filters"] }
|
||||||
fake = { version = "4", features = ["chrono", "derive"]}
|
fake = { version = "4", features = ["chrono", "derive"]}
|
||||||
brass-db = { path = "../db", features = ["test-helpers"] }
|
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
---
|
|
||||||
source: web/src/endpoints/assignment/post_new.rs
|
|
||||||
expression: body
|
|
||||||
snapshot_kind: text
|
|
||||||
---
|
|
||||||
<table class="table is-fullwidth">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Funktion</th>
|
|
||||||
<th>Zeitraum</th>
|
|
||||||
<th>Kommentar</th>
|
|
||||||
<th>Planung</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td>Max Mustermann</td>
|
|
||||||
<td>
|
|
||||||
<div class="tags"><span class="tag is-primary is-light">Posten</span></div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
10:00 bis 10.01.2025 20:00
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="dropdown">
|
|
||||||
<div class="dropdown-trigger">
|
|
||||||
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
|
|
||||||
|
|
||||||
<span>als Posten geplant</span>
|
|
||||||
<svg class="icon">
|
|
||||||
<use href="/static/feather-sprite.svg#edit-2" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
|
||||||
<div class="dropdown-content" hx-target="closest table" hx-swap="outerHTML">
|
|
||||||
|
|
||||||
<a class="dropdown-item"
|
|
||||||
hx-post="/assignments/new?event=1&availability=1&function=1" disabled>
|
|
||||||
als Posten planen</a>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<hr class="dropdown-divider" />
|
|
||||||
<a class="dropdown-item"
|
|
||||||
hx-delete="/assignments/delete?event=1&availability=1"
|
|
||||||
class="button is-small">entplanen</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
@ -1,8 +1,11 @@
|
|||||||
use actix_web::{web, HttpResponse, Responder};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use brass_db::models::{Area, Role, User};
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
use crate::{
|
||||||
|
endpoints::IdPath,
|
||||||
|
models::{Area, Role, User},
|
||||||
|
utils::ApplicationError,
|
||||||
|
};
|
||||||
|
|
||||||
#[actix_web::delete("/area/delete/{id}")]
|
#[actix_web::delete("/area/delete/{id}")]
|
||||||
pub async fn delete(
|
pub async fn delete(
|
||||||
@ -25,8 +28,10 @@ pub async fn delete(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::utils::test_helper::{test_delete, DbTestContext, RequestConfig, StatusCode};
|
use crate::{
|
||||||
use brass_db::models::{Area, Function, Location, Role};
|
models::{Area, Function, Location, Role},
|
||||||
|
utils::test_helper::{test_delete, DbTestContext, RequestConfig, StatusCode},
|
||||||
|
};
|
||||||
use brass_macros::db_test;
|
use brass_macros::db_test;
|
||||||
|
|
||||||
#[db_test]
|
#[db_test]
|
||||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
endpoints::{area::NewOrEditAreaTemplate, IdPath},
|
endpoints::{area::NewOrEditAreaTemplate, IdPath},
|
||||||
|
models::{Area, Role, User},
|
||||||
utils::{ApplicationError, TemplateResponse},
|
utils::{ApplicationError, TemplateResponse},
|
||||||
};
|
};
|
||||||
use brass_db::models::{Area, Role, User};
|
|
||||||
|
|
||||||
#[actix_web::get("/area/edit/{id}")]
|
#[actix_web::get("/area/edit/{id}")]
|
||||||
async fn get(
|
async fn get(
|
||||||
@ -32,18 +32,23 @@ async fn get(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use actix_http::StatusCode;
|
use actix_http::StatusCode;
|
||||||
use brass_db::models::Role;
|
|
||||||
use brass_macros::db_test;
|
use brass_macros::db_test;
|
||||||
|
|
||||||
use crate::utils::test_helper::{
|
use crate::{
|
||||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig,
|
models::{Function, Role},
|
||||||
|
utils::test_helper::{assert_snapshot, read_body, test_get, DbTestContext, RequestConfig},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[db_test]
|
#[db_test]
|
||||||
async fn produces_template_when_area_exists_and_user_is_admin(context: &DbTestContext) {
|
async fn produces_template_when_area_exists_and_user_is_admin(context: &DbTestContext) {
|
||||||
let app = context.app().await;
|
let app = context.app().await;
|
||||||
|
|
||||||
let config = RequestConfig::new("/area/edit/1").with_role(Role::Admin);
|
let config = RequestConfig {
|
||||||
|
uri: "/area/edit/1".to_string(),
|
||||||
|
role: Role::Admin,
|
||||||
|
function: vec![Function::Posten],
|
||||||
|
user_area: 1,
|
||||||
|
};
|
||||||
let response = test_get(&context.db_pool, app, &config).await;
|
let response = test_get(&context.db_pool, app, &config).await;
|
||||||
|
|
||||||
assert_eq!(StatusCode::OK, response.status());
|
assert_eq!(StatusCode::OK, response.status());
|
||||||
@ -56,7 +61,12 @@ mod tests {
|
|||||||
async fn returns_unauthorized_when_user_is_not_admin(context: &DbTestContext) {
|
async fn returns_unauthorized_when_user_is_not_admin(context: &DbTestContext) {
|
||||||
let app = context.app().await;
|
let app = context.app().await;
|
||||||
|
|
||||||
let config = RequestConfig::new("/area/edit/1").with_role(Role::AreaManager);
|
let config = RequestConfig {
|
||||||
|
uri: "/area/edit/1".to_string(),
|
||||||
|
role: Role::AreaManager,
|
||||||
|
function: vec![Function::Posten],
|
||||||
|
user_area: 1,
|
||||||
|
};
|
||||||
let response = test_get(&context.db_pool, app, &config).await;
|
let response = test_get(&context.db_pool, app, &config).await;
|
||||||
|
|
||||||
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
||||||
@ -66,7 +76,12 @@ mod tests {
|
|||||||
async fn returns_not_found_when_area_does_not_exist(context: &DbTestContext) {
|
async fn returns_not_found_when_area_does_not_exist(context: &DbTestContext) {
|
||||||
let app = context.app().await;
|
let app = context.app().await;
|
||||||
|
|
||||||
let config = RequestConfig::new("/area/edit/2").with_role(Role::Admin);
|
let config = RequestConfig {
|
||||||
|
uri: "/area/edit/2".to_string(),
|
||||||
|
role: Role::Admin,
|
||||||
|
function: vec![Function::Posten],
|
||||||
|
user_area: 1,
|
||||||
|
};
|
||||||
let response = test_get(&context.db_pool, app, &config).await;
|
let response = test_get(&context.db_pool, app, &config).await;
|
||||||
|
|
||||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
endpoints::area::NewOrEditAreaTemplate,
|
endpoints::area::NewOrEditAreaTemplate,
|
||||||
|
models::{Role, User},
|
||||||
utils::{ApplicationError, TemplateResponse},
|
utils::{ApplicationError, TemplateResponse},
|
||||||
};
|
};
|
||||||
use actix_web::{web, Responder};
|
use actix_web::{web, Responder};
|
||||||
use brass_db::models::{Role, User};
|
|
||||||
|
|
||||||
#[actix_web::get("/area/new")]
|
#[actix_web::get("/area/new")]
|
||||||
async fn get(user: web::ReqData<User>) -> Result<impl Responder, ApplicationError> {
|
async fn get(user: web::ReqData<User>) -> Result<impl Responder, ApplicationError> {
|
||||||
@ -21,13 +21,13 @@ async fn get(user: web::ReqData<User>) -> Result<impl Responder, ApplicationErro
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use brass_macros::db_test;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
models::{Function, Role},
|
||||||
utils::test_helper::{
|
utils::test_helper::{
|
||||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use brass_db::models::{Function, Role};
|
|
||||||
use brass_macros::db_test;
|
|
||||||
|
|
||||||
#[db_test]
|
#[db_test]
|
||||||
async fn produces_template_when_user_is_admin(context: &DbTestContext) {
|
async fn produces_template_when_user_is_admin(context: &DbTestContext) {
|
||||||
|
@ -7,7 +7,7 @@ pub mod delete;
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use brass_db::models::{Area, Role, User};
|
use crate::models::{Area, Role, User};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[cfg_attr(not(test), template(path = "area/new_or_edit.html"))]
|
#[cfg_attr(not(test), template(path = "area/new_or_edit.html"))]
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
use crate::{
|
||||||
use brass_db::models::{Area, Role, User};
|
endpoints::IdPath,
|
||||||
|
models::{Area, Role, User},
|
||||||
|
utils::ApplicationError,
|
||||||
|
};
|
||||||
|
|
||||||
use super::AreaForm;
|
use super::AreaForm;
|
||||||
|
|
||||||
@ -32,11 +35,11 @@ pub async fn post(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use actix_http::StatusCode;
|
use actix_http::StatusCode;
|
||||||
use brass_db::models::{Area, Function, Role};
|
|
||||||
use brass_macros::db_test;
|
use brass_macros::db_test;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
endpoints::area::AreaForm,
|
endpoints::area::AreaForm,
|
||||||
|
models::{Area, Function, Role},
|
||||||
utils::test_helper::{test_post, DbTestContext, RequestConfig},
|
utils::test_helper::{test_post, DbTestContext, RequestConfig},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,7 +58,7 @@ mod tests {
|
|||||||
name: "Neuer Name".to_string(),
|
name: "Neuer Name".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||||
|
|
||||||
assert_eq!(StatusCode::FOUND, response.status());
|
assert_eq!(StatusCode::FOUND, response.status());
|
||||||
|
|
||||||
@ -82,7 +85,7 @@ mod tests {
|
|||||||
name: "Neuer Name".to_string(),
|
name: "Neuer Name".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||||
|
|
||||||
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
||||||
}
|
}
|
||||||
@ -102,7 +105,7 @@ mod tests {
|
|||||||
name: "Neuer Name".to_string(),
|
name: "Neuer Name".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||||
|
|
||||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||||
}
|
}
|
||||||
|
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