Compare commits
6 Commits
master
...
feature/db
Author | SHA1 | Date | |
---|---|---|---|
2abeeb20df | |||
10e6ba80a2 | |||
e5df98a515 | |||
93574c3ac5 | |||
9893c37f80 | |||
f35b343768 |
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -775,6 +775,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
"brass-config",
|
||||
"chrono",
|
||||
"clap",
|
||||
"sqlx",
|
||||
]
|
||||
@ -787,6 +788,20 @@ dependencies = [
|
||||
"dotenvy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brass-db"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fake",
|
||||
"garde",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brass-macros"
|
||||
version = "0.1.0"
|
||||
@ -809,6 +824,7 @@ dependencies = [
|
||||
"argon2",
|
||||
"askama",
|
||||
"brass-config",
|
||||
"brass-db",
|
||||
"brass-macros",
|
||||
"built",
|
||||
"change-detection",
|
||||
|
@ -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,10 @@
|
||||
use anyhow::Context;
|
||||
use chrono::{Local, NaiveDateTime, Utc};
|
||||
use sqlx::migrate::Migrate;
|
||||
use sqlx::{migrate::Migrator, Executor};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::SystemTime;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
@ -28,6 +32,8 @@ enum Command {
|
||||
Reset,
|
||||
#[command(about = "Run all pending migrations on database")]
|
||||
Migrate,
|
||||
#[command(about = "Create a new migration")]
|
||||
NewMigration { title: String },
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
@ -59,12 +65,17 @@ async fn main() {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,27 @@ 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(())
|
||||
}
|
||||
|
||||
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()?)
|
||||
}
|
||||
|
20
db/Cargo.toml
Normal file
20
db/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[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"] }
|
||||
garde = { version = "0.22.0", features = ["derive", "email"] } # refactor out
|
||||
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,7 +1,7 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::{query, PgPool};
|
||||
use sqlx::{PgPool, query};
|
||||
|
||||
use super::{assignment_changeset::AssignmentChangeset, Function, Result};
|
||||
use super::{AssignmentChangeset, Function, Result};
|
||||
|
||||
pub struct Assignment {
|
||||
pub event_id: i32,
|
174
db/src/models/assignment_changeset.rs
Normal file
174
db/src/models/assignment_changeset.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use sqlx::PgPool;
|
||||
|
||||
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(
|
||||
"Angegebenes Event 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(
|
||||
"time not made available can't be assigned",
|
||||
));
|
||||
}
|
||||
|
||||
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(
|
||||
"user has not the required function for this assignment",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn event_has_free_slot_for_function(
|
||||
value: &Function,
|
||||
availability: &Availability,
|
||||
event: &Event,
|
||||
pool: &PgPool,
|
||||
) -> Result<(), AsyncValidateError> {
|
||||
let assignments_for_event: Vec<Assignment> = Assignment::read_all_by_event(pool, event.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|a| a.availability_id != availability.id && a.event_id != event.id)
|
||||
.collect();
|
||||
|
||||
let assignments_with_function = assignments_for_event
|
||||
.iter()
|
||||
.filter(|a| a.function == *value)
|
||||
.count();
|
||||
|
||||
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(
|
||||
"event already has enough assignments for this function",
|
||||
));
|
||||
}
|
||||
|
||||
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.availability_id != availability.id && 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(
|
||||
"availability is already assigned for that time",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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("TODO: admin or areamanager"));
|
||||
}
|
||||
|
||||
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 {
|
@ -1,6 +1,6 @@
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-helpers")]
|
||||
use fake::{Fake, Faker};
|
||||
use garde::Validate;
|
||||
|
||||
@ -29,7 +29,7 @@ pub struct EventChangeset {
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-helpers")]
|
||||
impl EventChangeset {
|
||||
pub fn create_for_test(start: NaiveDateTime, end: NaiveDateTime) -> EventChangeset {
|
||||
let changeset = EventChangeset {
|
||||
@ -55,8 +55,14 @@ pub struct EventContext {
|
||||
pub amount_of_assigned_posten: i16,
|
||||
}
|
||||
|
||||
fn date_unchanged_if_edit(value: &(NaiveDateTime, NaiveDateTime), context: &Option<EventContext>) -> garde::Result {
|
||||
if context.as_ref().is_some_and(|c| c.date_in_db != value.0.date() ) {
|
||||
fn date_unchanged_if_edit(
|
||||
value: &(NaiveDateTime, NaiveDateTime),
|
||||
context: &Option<EventContext>,
|
||||
) -> garde::Result {
|
||||
if context
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.date_in_db != value.0.date())
|
||||
{
|
||||
return Err(garde::Error::new("event date can't be changed"));
|
||||
}
|
||||
|
@ -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;
|
@ -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> {
|
@ -1,4 +1,4 @@
|
||||
use crate::utils::ApplicationError;
|
||||
use crate::UnsupportedEnumValue;
|
||||
|
||||
#[derive(sqlx::Type, Debug, Clone, Copy, PartialEq)]
|
||||
#[sqlx(type_name = "role", rename_all = "lowercase")]
|
||||
@ -9,16 +9,16 @@ pub enum Role {
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Role {
|
||||
type Error = ApplicationError;
|
||||
type Error = UnsupportedEnumValue;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
1 => Ok(Role::Staff),
|
||||
10 => Ok(Role::AreaManager),
|
||||
100 => Ok(Role::Admin),
|
||||
_ => Err(ApplicationError::UnsupportedEnumValue {
|
||||
value: value.to_string(),
|
||||
enum_name: String::from("Role"),
|
||||
_ => Err(UnsupportedEnumValue {
|
||||
value,
|
||||
enum_name: "Role",
|
||||
}),
|
||||
}
|
||||
}
|
@ -107,7 +107,7 @@ impl User {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn read_for_login(pool: &PgPool, email: &str) -> anyhow::Result<User> {
|
||||
pub async fn read_for_login(pool: &PgPool, email: &str) -> Result<User> {
|
||||
let record = sqlx::query!(
|
||||
r#"
|
||||
SELECT id,
|
||||
@ -155,7 +155,7 @@ impl User {
|
||||
Ok(record.and_then(|r| Some(r.id)))
|
||||
}
|
||||
|
||||
pub async fn read_all(pool: &PgPool) -> anyhow::Result<Vec<User>> {
|
||||
pub async fn read_all(pool: &PgPool) -> Result<Vec<User>> {
|
||||
let records = sqlx::query!(
|
||||
r#"
|
||||
SELECT id,
|
||||
@ -343,7 +343,7 @@ impl User {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_login_timestamp(pool: &PgPool, id: i32) -> anyhow::Result<()> {
|
||||
pub async fn update_login_timestamp(pool: &PgPool, id: i32) -> Result<()> {
|
||||
sqlx::query!("UPDATE user_ SET lastLogin = NOW() WHERE id = $1;", id)
|
||||
.execute(pool)
|
||||
.await?;
|
46
db/src/models/user_changeset.rs
Normal file
46
db/src/models/user_changeset.rs
Normal file
@ -0,0 +1,46 @@
|
||||
#[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(())
|
||||
}
|
5
db/src/support/mod.rs
Normal file
5
db/src/support/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod token_generation;
|
||||
mod token_trait;
|
||||
|
||||
pub use token_generation::generate_token_and_expiration;
|
||||
pub use token_trait::{Token, NoneToken};
|
18
db/src/support/token_generation.rs
Normal file
18
db/src/support/token_generation.rs
Normal file
@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
12
db/src/support/token_trait.rs
Normal file
12
db/src/support/token_trait.rs
Normal file
@ -0,0 +1,12 @@
|
||||
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,11 +1,11 @@
|
||||
mod email;
|
||||
mod error;
|
||||
mod r#trait;
|
||||
mod validation_trait;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
pub use email::email_is_valid;
|
||||
pub use error::AsyncValidateError;
|
||||
pub use r#trait::AsyncValidate;
|
||||
pub use validation_trait::AsyncValidate;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub struct DbContext<'a> {
|
10
db/src/validation/validation_trait.rs
Normal file
10
db/src/validation/validation_trait.rs
Normal file
@ -0,0 +1,10 @@
|
||||
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
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@ -0,0 +1 @@
|
||||
imports_granularity = "Crate"
|
@ -28,6 +28,7 @@ zxcvbn = "3.1.0"
|
||||
thiserror = "2"
|
||||
brass-macros = { path = "../macros" }
|
||||
brass-config = { path = "../config" }
|
||||
brass-db = { path = "../db" }
|
||||
actix-http = "3.9.0"
|
||||
askama = "0.13.0"
|
||||
garde = { version = "0.22.0", features = ["derive", "email"] }
|
||||
@ -37,7 +38,6 @@ tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracing-panic = "0.1.2"
|
||||
rust_xlsxwriter = "0.87.0"
|
||||
regex = "1.11.1"
|
||||
|
||||
[build-dependencies]
|
||||
built = "0.7.4"
|
||||
@ -47,4 +47,5 @@ change-detection = "1.2.0"
|
||||
[dev-dependencies]
|
||||
insta = { version = "1.41.1", features = ["yaml", "filters"] }
|
||||
fake = { version = "4", features = ["chrono", "derive"]}
|
||||
brass-db = { path = "../db", features = ["test-helpers"] }
|
||||
regex = "1.11.1"
|
||||
|
@ -0,0 +1,67 @@
|
||||
---
|
||||
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,11 +1,8 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Area, Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
||||
|
||||
#[actix_web::delete("/area/delete/{id}")]
|
||||
pub async fn delete(
|
||||
@ -28,10 +25,8 @@ pub async fn delete(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
models::{Area, Function, Location, Role},
|
||||
utils::test_helper::{test_delete, DbTestContext, RequestConfig, StatusCode},
|
||||
};
|
||||
use crate::utils::test_helper::{test_delete, DbTestContext, RequestConfig, StatusCode};
|
||||
use brass_db::models::{Area, Function, Location, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
#[db_test]
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{area::NewOrEditAreaTemplate, IdPath},
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[actix_web::get("/area/edit/{id}")]
|
||||
async fn get(
|
||||
@ -32,23 +32,18 @@ async fn get(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_http::StatusCode;
|
||||
use brass_db::models::Role;
|
||||
use brass_macros::db_test;
|
||||
|
||||
use crate::{
|
||||
models::{Function, Role},
|
||||
utils::test_helper::{assert_snapshot, read_body, test_get, DbTestContext, RequestConfig},
|
||||
use crate::utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig,
|
||||
};
|
||||
|
||||
#[db_test]
|
||||
async fn produces_template_when_area_exists_and_user_is_admin(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let config = RequestConfig {
|
||||
uri: "/area/edit/1".to_string(),
|
||||
role: Role::Admin,
|
||||
function: vec![Function::Posten],
|
||||
user_area: 1,
|
||||
};
|
||||
let config = RequestConfig::new("/area/edit/1").with_role(Role::Admin);
|
||||
let response = test_get(&context.db_pool, app, &config).await;
|
||||
|
||||
assert_eq!(StatusCode::OK, response.status());
|
||||
@ -61,12 +56,7 @@ mod tests {
|
||||
async fn returns_unauthorized_when_user_is_not_admin(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let config = RequestConfig {
|
||||
uri: "/area/edit/1".to_string(),
|
||||
role: Role::AreaManager,
|
||||
function: vec![Function::Posten],
|
||||
user_area: 1,
|
||||
};
|
||||
let config = RequestConfig::new("/area/edit/1").with_role(Role::AreaManager);
|
||||
let response = test_get(&context.db_pool, app, &config).await;
|
||||
|
||||
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
||||
@ -76,12 +66,7 @@ mod tests {
|
||||
async fn returns_not_found_when_area_does_not_exist(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let config = RequestConfig {
|
||||
uri: "/area/edit/2".to_string(),
|
||||
role: Role::Admin,
|
||||
function: vec![Function::Posten],
|
||||
user_area: 1,
|
||||
};
|
||||
let config = RequestConfig::new("/area/edit/2").with_role(Role::Admin);
|
||||
let response = test_get(&context.db_pool, app, &config).await;
|
||||
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
endpoints::area::NewOrEditAreaTemplate,
|
||||
models::{Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use actix_web::{web, Responder};
|
||||
use brass_db::models::{Role, User};
|
||||
|
||||
#[actix_web::get("/area/new")]
|
||||
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)]
|
||||
mod tests {
|
||||
use brass_macros::db_test;
|
||||
use crate::{
|
||||
models::{Function, Role},
|
||||
utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||
},
|
||||
};
|
||||
use brass_db::models::{Function, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
#[db_test]
|
||||
async fn produces_template_when_user_is_admin(context: &DbTestContext) {
|
||||
|
@ -7,7 +7,7 @@ pub mod delete;
|
||||
use askama::Template;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::models::{Area, Role, User};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[derive(Template)]
|
||||
#[cfg_attr(not(test), template(path = "area/new_or_edit.html"))]
|
||||
|
@ -1,11 +1,8 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Area, Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
use super::AreaForm;
|
||||
|
||||
@ -35,11 +32,11 @@ pub async fn post(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_http::StatusCode;
|
||||
use brass_db::models::{Area, Function, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
use crate::{
|
||||
endpoints::area::AreaForm,
|
||||
models::{Area, Function, Role},
|
||||
utils::test_helper::{test_post, DbTestContext, RequestConfig},
|
||||
};
|
||||
|
||||
@ -58,7 +55,7 @@ mod tests {
|
||||
name: "Neuer Name".to_string(),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
||||
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
@ -85,7 +82,7 @@ mod tests {
|
||||
name: "Neuer Name".to_string(),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
||||
|
||||
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
||||
}
|
||||
@ -105,7 +102,7 @@ mod tests {
|
||||
name: "Neuer Name".to_string(),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
||||
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::area::AreaForm,
|
||||
models::{Area, Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use crate::{endpoints::area::AreaForm, utils::ApplicationError};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[actix_web::post("/area/new")]
|
||||
pub async fn post(
|
||||
@ -28,11 +25,11 @@ pub async fn post(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_http::StatusCode;
|
||||
use brass_db::models::{Area, Function, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
use crate::{
|
||||
endpoints::area::AreaForm,
|
||||
models::{Area, Function, Role},
|
||||
utils::test_helper::{test_post, DbTestContext, RequestConfig},
|
||||
};
|
||||
|
||||
@ -51,7 +48,7 @@ mod tests {
|
||||
name: "Neuer Name".to_string(),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
||||
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
@ -78,7 +75,7 @@ mod tests {
|
||||
name: "Neuer Name".to_string(),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, request).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(request)).await;
|
||||
|
||||
assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::assignment::PlanEventPersonalTablePartialTemplate,
|
||||
models::{Assignment, Event, Role, User},
|
||||
utils::{
|
||||
event_planning_template::{
|
||||
generate_availability_assignment_list, generate_status_whether_staff_is_required,
|
||||
@ -12,6 +11,7 @@ use crate::{
|
||||
ApplicationError, TemplateResponse,
|
||||
},
|
||||
};
|
||||
use brass_db::models::{Assignment, Event, Role, User};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AssignmentDeleteQuery {
|
||||
@ -71,12 +71,10 @@ mod tests {
|
||||
use fake::{faker::chrono::en::Date, Fake, Faker};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
Assignment, AssignmentChangeset, Availability, AvailabilityChangeset, Event,
|
||||
EventChangeset, Function, Location, Role, User,
|
||||
},
|
||||
utils::test_helper::{test_delete, DbTestContext, RequestConfig},
|
||||
use crate::utils::test_helper::{test_delete, DbTestContext, RequestConfig};
|
||||
use brass_db::models::{
|
||||
Assignment, AssignmentChangeset, Availability, AvailabilityChangeset, Event,
|
||||
EventChangeset, Function, Location, Role, User,
|
||||
};
|
||||
|
||||
async fn arrange(pool: &PgPool) -> anyhow::Result<()> {
|
||||
|
@ -1,10 +1,8 @@
|
||||
use askama::Template;
|
||||
|
||||
use crate::{
|
||||
filters,
|
||||
models::{Availability, AvailabilityAssignmentState, Event},
|
||||
};
|
||||
use crate::filters;
|
||||
|
||||
use brass_db::models::{Availability, AvailabilityAssignmentState, Event};
|
||||
pub mod delete;
|
||||
pub mod post_new;
|
||||
|
||||
|
@ -1,14 +1,9 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use garde::Validate;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::assignment::PlanEventPersonalTablePartialTemplate,
|
||||
models::{
|
||||
Assignment, AssignmentChangeset, AssignmentContext, Availability, Event, Function, Role,
|
||||
User,
|
||||
},
|
||||
utils::{
|
||||
event_planning_template::{
|
||||
generate_availability_assignment_list, generate_status_whether_staff_is_required,
|
||||
@ -17,6 +12,11 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use brass_db::{
|
||||
models::{Assignment, AssignmentChangeset, AssignmentContext, Event, Function, User},
|
||||
validation::AsyncValidate,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AssignmentQuery {
|
||||
availability: i32,
|
||||
@ -35,28 +35,6 @@ pub async fn post(
|
||||
return Ok(HttpResponse::NotFound().finish());
|
||||
};
|
||||
|
||||
let user_is_admin_or_area_manager_of_event_area = user.role == Role::Admin
|
||||
|| (user.role == Role::AreaManager
|
||||
&& user.area_id == event.location.as_ref().unwrap().area_id);
|
||||
|
||||
if !user_is_admin_or_area_manager_of_event_area {
|
||||
return Err(ApplicationError::Unauthorized);
|
||||
}
|
||||
|
||||
let Some(availability) =
|
||||
Availability::read_by_id_including_user(pool.get_ref(), query.availability).await?
|
||||
else {
|
||||
return Ok(HttpResponse::NotFound().finish());
|
||||
};
|
||||
|
||||
let availability_user_not_in_event_location_area =
|
||||
availability.user.as_ref().unwrap().area_id != event.location.as_ref().unwrap().area_id;
|
||||
|
||||
if availability_user_not_in_event_location_area {
|
||||
return Ok(HttpResponse::BadRequest()
|
||||
.body("availability user is not in the same area as event location"));
|
||||
}
|
||||
|
||||
let function = Function::try_from(query.function)?;
|
||||
|
||||
let changeset = AssignmentChangeset {
|
||||
@ -64,22 +42,18 @@ pub async fn post(
|
||||
time: (event.start, event.end),
|
||||
};
|
||||
|
||||
let assignments_for_event = Assignment::read_all_by_event(pool.get_ref(), event.id).await?;
|
||||
let assignments_for_availability =
|
||||
Assignment::read_all_by_availability(pool.get_ref(), availability.id).await?;
|
||||
let context = AssignmentContext {
|
||||
event: event.clone(),
|
||||
availability: availability.clone(),
|
||||
user_function: availability.user.as_ref().unwrap().function.clone(),
|
||||
assignments_for_event,
|
||||
assignments_for_availability,
|
||||
user: &user.into_inner(),
|
||||
event_id: event.id,
|
||||
availability_id: query.availability,
|
||||
pool: pool.get_ref(),
|
||||
};
|
||||
|
||||
if let Err(e) = changeset.validate_with(&context) {
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
if let Err(e) = changeset.validate_with_context(&context).await {
|
||||
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
||||
};
|
||||
|
||||
Assignment::create(pool.get_ref(), event.id, availability.id, changeset).await?;
|
||||
Assignment::create(pool.get_ref(), event.id, query.availability, changeset).await?;
|
||||
|
||||
let availabilities = generate_availability_assignment_list(pool.get_ref(), &event).await?;
|
||||
|
||||
@ -99,3 +73,181 @@ pub async fn post(
|
||||
|
||||
Ok(template.to_response()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_http::StatusCode;
|
||||
use brass_db::models::{
|
||||
Area, Availability, AvailabilityChangeset, Event, EventChangeset, Location, Role, User,
|
||||
UserChangeset,
|
||||
};
|
||||
use brass_macros::db_test;
|
||||
use chrono::NaiveDateTime;
|
||||
use fake::{Fake, Faker};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::utils::test_helper::{
|
||||
assert_snapshot, test_post, DbTestContext, NaiveDateTimeExt, RequestConfig,
|
||||
ServiceResponseExt,
|
||||
};
|
||||
|
||||
async fn arrange(pool: &PgPool) {
|
||||
Location::create(pool, &Faker.fake::<String>(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut user_changeset: UserChangeset = Faker.fake();
|
||||
user_changeset.name = String::from("Max Mustermann");
|
||||
|
||||
User::create(pool, user_changeset).await.unwrap();
|
||||
}
|
||||
|
||||
async fn arrange_event(pool: &PgPool, start: NaiveDateTime, end: NaiveDateTime, location: i32) {
|
||||
let mut changeset: EventChangeset = EventChangeset::create_for_test(start, end);
|
||||
changeset.location_id = location;
|
||||
|
||||
Event::create(pool, changeset).await.unwrap();
|
||||
}
|
||||
|
||||
async fn arrange_availability(pool: &PgPool, start: NaiveDateTime, end: NaiveDateTime) {
|
||||
Availability::create(
|
||||
pool,
|
||||
1,
|
||||
AvailabilityChangeset {
|
||||
time: (start, end),
|
||||
comment: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn response_produces_updated_template(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
|
||||
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
|
||||
|
||||
arrange(&context.db_pool).await;
|
||||
arrange_event(&context.db_pool, start, end, 1).await;
|
||||
arrange_availability(&context.db_pool, start, end).await;
|
||||
|
||||
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
|
||||
.with_role(Role::Admin);
|
||||
|
||||
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
|
||||
let (status, body) = response.into_status_and_body().await;
|
||||
|
||||
assert_eq!(StatusCode::OK, status, "{body}");
|
||||
|
||||
assert_snapshot!(body);
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_availability_does_not_exist(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
|
||||
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
|
||||
arrange(&context.db_pool).await;
|
||||
arrange_event(&context.db_pool, start, end, 1).await;
|
||||
|
||||
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
|
||||
.with_role(Role::Admin);
|
||||
|
||||
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
|
||||
|
||||
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_event_does_not_exist(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
|
||||
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
|
||||
arrange(&context.db_pool).await;
|
||||
arrange_availability(&context.db_pool, start, end).await;
|
||||
|
||||
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
|
||||
.with_role(Role::Admin);
|
||||
|
||||
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
|
||||
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_area_manager_is_different_area_from_event(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
|
||||
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
|
||||
|
||||
arrange(&context.db_pool).await;
|
||||
arrange_event(&context.db_pool, start, end, 1).await;
|
||||
arrange_availability(&context.db_pool, start, end).await;
|
||||
Area::create(&context.db_pool, &Faker.fake::<String>())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
|
||||
.with_role(Role::AreaManager)
|
||||
.with_user_area(2);
|
||||
|
||||
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
|
||||
|
||||
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_availability_user_not_in_event_area(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
|
||||
let start = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 10, 0, 0).unwrap();
|
||||
let end = NaiveDateTime::from_ymd_and_hms(2025, 01, 10, 20, 0, 0).unwrap();
|
||||
|
||||
arrange(&context.db_pool).await;
|
||||
Area::create(&context.db_pool, &Faker.fake::<String>())
|
||||
.await
|
||||
.unwrap();
|
||||
Location::create(&context.db_pool, &Faker.fake::<String>(), 2)
|
||||
.await
|
||||
.unwrap();
|
||||
arrange_event(&context.db_pool, start, end, 2).await;
|
||||
arrange_availability(&context.db_pool, start, end).await;
|
||||
|
||||
let config = RequestConfig::new("/assignments/new?availability=1&function=1&event=1")
|
||||
.with_role(Role::Admin);
|
||||
|
||||
let response = test_post::<_, _, String>(&context.db_pool, app, &config, None).await;
|
||||
|
||||
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_assignment_time_doesnt_fit_into_availability_time(context: &DbTestContext) {
|
||||
assert!(false)
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_end_time_lies_before_start_time(context: &DbTestContext) {
|
||||
assert!(false)
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_availability_time_already_assigned(context: &DbTestContext) {
|
||||
assert!(false)
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_availability_user_does_not_have_function(context: &DbTestContext) {
|
||||
assert!(false)
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
fn fails_when_event_already_has_enough_assignments_for_function(context: &DbTestContext) {
|
||||
assert!(false)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Availability, User}, utils::ApplicationError,
|
||||
};
|
||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
||||
use brass_db::models::{Availability, User};
|
||||
|
||||
#[actix_web::delete("/availability/delete/{id}")]
|
||||
pub async fn delete(
|
||||
|
@ -3,9 +3,11 @@ use chrono::{Days, NaiveDate, NaiveTime};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::endpoints::availability::NewOrEditAvailabilityTemplate;
|
||||
use crate::models::{find_free_date_time_slots, Availability, User};
|
||||
use crate::utils::{ApplicationError, TemplateResponse};
|
||||
use crate::{
|
||||
endpoints::availability::NewOrEditAvailabilityTemplate,
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{find_free_date_time_slots, Availability, User};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AvailabilityNewQuery {
|
||||
|
@ -4,15 +4,17 @@ use chrono::{NaiveDate, Utc};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::filters;
|
||||
use crate::models::{
|
||||
find_free_date_time_slots, Area, Assignment, Availability, Event, Function, Role, User, Vehicle,
|
||||
use crate::{
|
||||
filters,
|
||||
utils::{
|
||||
event_planning_template::generate_vehicles_assigned_and_available,
|
||||
ApplicationError,
|
||||
DateTimeFormat::{DayMonthYear, DayMonthYearHourMinute, HourMinute},
|
||||
TemplateResponse,
|
||||
},
|
||||
};
|
||||
use crate::utils::{
|
||||
event_planning_template::generate_vehicles_assigned_and_available,
|
||||
ApplicationError,
|
||||
DateTimeFormat::{DayMonthYear, DayMonthYearHourMinute, HourMinute},
|
||||
TemplateResponse,
|
||||
use brass_db::models::{
|
||||
find_free_date_time_slots, Area, Assignment, Availability, Event, Function, Role, User, Vehicle,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -4,9 +4,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{availability::NewOrEditAvailabilityTemplate, IdPath},
|
||||
models::{find_free_date_time_slots, Availability, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{find_free_date_time_slots, Availability, User};
|
||||
|
||||
#[actix_web::get("/availability/edit/{id}")]
|
||||
pub async fn get(
|
||||
|
@ -2,9 +2,11 @@ use askama::Template;
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::filters;
|
||||
use crate::models::{Role, User};
|
||||
use crate::utils::DateTimeFormat::{DayMonth, DayMonthYear, DayMonthYearHourMinute, HourMinute};
|
||||
use crate::{
|
||||
filters,
|
||||
utils::DateTimeFormat::{DayMonth, DayMonthYear, DayMonthYearHourMinute, HourMinute},
|
||||
};
|
||||
use brass_db::models::{Role, User};
|
||||
|
||||
pub mod delete;
|
||||
pub mod get_new;
|
||||
|
@ -3,8 +3,11 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::availability::AvailabilityForm,
|
||||
utils::{self, ApplicationError},
|
||||
};
|
||||
use brass_db::{
|
||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||
utils::{self, validation::AsyncValidate, ApplicationError},
|
||||
validation::AsyncValidate,
|
||||
};
|
||||
|
||||
#[actix_web::post("/availability/new")]
|
||||
|
@ -3,8 +3,11 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{availability::AvailabilityForm, IdPath},
|
||||
utils::{self, ApplicationError},
|
||||
};
|
||||
use brass_db::{
|
||||
models::{Availability, AvailabilityChangeset, AvailabilityContext, User},
|
||||
utils::{self, validation::AsyncValidate, ApplicationError},
|
||||
validation::AsyncValidate,
|
||||
};
|
||||
|
||||
#[actix_web::post("/availability/edit/{id}")]
|
||||
|
@ -1,11 +1,8 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Clothing, Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
||||
use brass_db::models::{Clothing, Role, User};
|
||||
|
||||
#[actix_web::delete("/clothing/{id}")]
|
||||
pub async fn delete(
|
||||
@ -28,13 +25,8 @@ pub async fn delete(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
models::{Clothing, Role},
|
||||
utils::test_helper::{
|
||||
test_delete, DbTestContext, RequestConfig,
|
||||
StatusCode,
|
||||
},
|
||||
};
|
||||
use crate::utils::test_helper::{test_delete, DbTestContext, RequestConfig, StatusCode};
|
||||
use brass_db::models::{Clothing, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
#[db_test]
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{clothing::EditClothingPartialTemplate, IdPath},
|
||||
models::{Clothing, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Clothing, Role, User};
|
||||
|
||||
#[actix_web::get("/clothing/edit/{id}")]
|
||||
pub async fn get(
|
||||
@ -31,12 +31,10 @@ pub async fn get(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
models::{Clothing, Role},
|
||||
utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||
},
|
||||
use crate::utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||
};
|
||||
use brass_db::models::{Clothing, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
#[db_test]
|
||||
|
@ -2,9 +2,9 @@ use actix_web::{web, Responder};
|
||||
|
||||
use crate::{
|
||||
endpoints::clothing::EditClothingPartialTemplate,
|
||||
models::{Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Role, User};
|
||||
|
||||
#[actix_web::get("/clothing/new")]
|
||||
pub async fn get(user: web::ReqData<User>) -> Result<impl Responder, ApplicationError> {
|
||||
|
@ -2,10 +2,8 @@ use actix_web::{web, Responder};
|
||||
use askama::Template;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{Clothing, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use crate::utils::{ApplicationError, TemplateResponse};
|
||||
use brass_db::models::{Clothing, Role, User};
|
||||
|
||||
#[derive(Template)]
|
||||
#[cfg_attr(not(test), template(path = "clothing/overview.html"))]
|
||||
@ -40,12 +38,10 @@ pub async fn get(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
models::{Clothing, Role},
|
||||
utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||
},
|
||||
use crate::utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||
};
|
||||
use brass_db::models::{Clothing, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
#[db_test]
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{clothing::ReadClothingPartialTemplate, IdPath},
|
||||
models::{Clothing, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Clothing, Role, User};
|
||||
|
||||
#[actix_web::get("/clothing/{id}")]
|
||||
pub async fn get(
|
||||
@ -28,12 +28,10 @@ pub async fn get(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
models::{Clothing, Role},
|
||||
utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||
},
|
||||
use crate::utils::test_helper::{
|
||||
assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode,
|
||||
};
|
||||
use brass_db::models::{Clothing, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
#[db_test]
|
||||
|
@ -2,8 +2,8 @@ use askama::Template;
|
||||
use garde::Validate;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::models::Clothing;
|
||||
use crate::filters;
|
||||
use brass_db::models::Clothing;
|
||||
|
||||
pub mod delete;
|
||||
pub mod get_edit;
|
||||
@ -28,6 +28,6 @@ struct ReadClothingPartialTemplate {
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
struct NewOrEditClothingForm {
|
||||
#[garde(length(min=3))]
|
||||
#[garde(length(min = 3))]
|
||||
name: String,
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ use crate::{
|
||||
clothing::{NewOrEditClothingForm, ReadClothingPartialTemplate},
|
||||
IdPath,
|
||||
},
|
||||
models::{Clothing, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Clothing, Role, User};
|
||||
|
||||
#[actix_web::post("/clothing/{id}")]
|
||||
pub async fn post(
|
||||
|
@ -4,9 +4,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::clothing::{NewOrEditClothingForm, ReadClothingPartialTemplate},
|
||||
models::{Clothing, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Clothing, Role, User};
|
||||
|
||||
#[actix_web::post("/clothing")]
|
||||
pub async fn post(
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Assignment, Event, Role, User},
|
||||
utils::{self, ApplicationError},
|
||||
};
|
||||
use brass_db::models::{Assignment, Event, Role, User};
|
||||
|
||||
#[actix_web::delete("/events/{id}")]
|
||||
pub async fn delete(
|
||||
|
@ -11,9 +11,9 @@ use chrono::{NaiveDate, NaiveTime};
|
||||
|
||||
use crate::{
|
||||
endpoints::{events::NewOrEditEventTemplate, IdPath},
|
||||
models::{Assignment, Clothing, Event, Function, Location, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Assignment, Clothing, Event, Function, Location, Role, User};
|
||||
|
||||
#[actix_web::get("/events/{id}/edit")]
|
||||
pub async fn get(
|
||||
@ -94,13 +94,14 @@ async fn produces_template(context: &DbTestContext) {
|
||||
//)
|
||||
//.await
|
||||
//.unwrap();
|
||||
//// TODO: refactor
|
||||
Location::create(&context.db_pool, "Hauptbahnhof", 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let date = NaiveDate::parse_from_str("2025-01-01", "%F").unwrap();
|
||||
|
||||
let changeset = crate::models::EventChangeset {
|
||||
let changeset = brass_db::models::EventChangeset {
|
||||
time: (
|
||||
date.and_time(NaiveTime::parse_from_str("08:00", "%R").unwrap()),
|
||||
date.and_time(NaiveTime::parse_from_str("10:00", "%R").unwrap()),
|
||||
@ -120,7 +121,7 @@ async fn produces_template(context: &DbTestContext) {
|
||||
let config = RequestConfig {
|
||||
uri: "/events/1/edit".to_string(),
|
||||
role: Role::Admin,
|
||||
function: vec![crate::models::Function::Posten],
|
||||
function: vec![brass_db::models::Function::Posten],
|
||||
user_area: 1,
|
||||
};
|
||||
let response = test_get(&context.db_pool, app, &config).await;
|
||||
|
@ -4,9 +4,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{events::NewOrEditEventTemplate, NaiveDateQuery},
|
||||
models::{Clothing, Location, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Clothing, Location, Role, User};
|
||||
|
||||
#[actix_web::get("/events/new")]
|
||||
pub async fn get(
|
||||
|
@ -5,7 +5,6 @@ use sqlx::PgPool;
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
filters,
|
||||
models::{Availability, AvailabilityAssignmentState, Event, Role, User, Vehicle},
|
||||
utils::{
|
||||
event_planning_template::{
|
||||
generate_availability_assignment_list, generate_status_whether_staff_is_required,
|
||||
@ -14,6 +13,7 @@ use crate::{
|
||||
ApplicationError, TemplateResponse,
|
||||
},
|
||||
};
|
||||
use brass_db::models::{Availability, AvailabilityAssignmentState, Event, Role, User, Vehicle};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "events/plan.html")]
|
||||
|
@ -3,9 +3,8 @@ use chrono::{Days, NaiveDateTime};
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::filters;
|
||||
use crate::models::{Clothing, Location, Role, User};
|
||||
use crate::utils::DateTimeFormat::{DayMonthYear, HourMinute, YearMonthDayTHourMinute};
|
||||
use crate::{filters, utils::DateTimeFormat::{DayMonthYear, HourMinute, YearMonthDayTHourMinute}};
|
||||
use brass_db::models::{Clothing, Location, Role, User};
|
||||
|
||||
pub mod delete;
|
||||
pub mod get_edit;
|
||||
|
@ -5,13 +5,13 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{events::NewOrEditEventForm, IdPath},
|
||||
models::{
|
||||
Assignment, AssignmentChangeset, Availability, Event, EventChangeset, EventContext,
|
||||
Function, Location, Role, User,
|
||||
},
|
||||
utils::{self, ApplicationError},
|
||||
END_OF_DAY, START_OF_DAY,
|
||||
};
|
||||
use brass_db::models::{
|
||||
Assignment, AssignmentChangeset, Availability, Event, EventChangeset, EventContext, Function,
|
||||
Location, Role, User,
|
||||
};
|
||||
|
||||
#[actix_web::post("/events/{id}/edit")]
|
||||
pub async fn post(
|
||||
|
@ -4,9 +4,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::events::NewOrEditEventForm,
|
||||
models::{Event, EventChangeset, Location, Role, User},
|
||||
utils::{self, ApplicationError},
|
||||
};
|
||||
use brass_db::models::{Event, EventChangeset, Location, Role, User};
|
||||
|
||||
#[actix_web::post("/events/new")]
|
||||
pub async fn post(
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Event, Role, User},
|
||||
utils::{self, ApplicationError},
|
||||
};
|
||||
use brass_db::models::{Event, Role, User};
|
||||
|
||||
#[actix_web::put("/events/{id}/cancel")]
|
||||
pub async fn put_cancel(
|
||||
|
@ -1,12 +1,10 @@
|
||||
use actix_web::{web, Responder};
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use askama::Template;
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use crate::utils::{ApplicationError, TemplateResponse};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "export/availability.html")]
|
||||
|
@ -7,10 +7,8 @@ use quick_xml::se::Serializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{Area, Availability, Role, User, UserFunction},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use crate::utils::ApplicationError;
|
||||
use brass_db::models::{Area, Availability, Role, User, UserFunction};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExportQuery {
|
||||
|
@ -3,10 +3,8 @@ use askama::Template;
|
||||
use chrono::{Datelike, Months, NaiveDate, Utc};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use crate::utils::{ApplicationError, TemplateResponse};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "export/events.html")]
|
||||
|
@ -1,15 +1,12 @@
|
||||
use crate::models::{ExportEventRow, Function, SimpleAssignment};
|
||||
use actix_http::header::CONTENT_DISPOSITION;
|
||||
use actix_web::{http::header::ContentDisposition, web, HttpResponse, Responder};
|
||||
use brass_db::models::{ExportEventRow, Function, Role, SimpleAssignment, User};
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
use rust_xlsxwriter::workbook::Workbook;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{Role, User},
|
||||
utils::{ApplicationError, DateTimeFormat},
|
||||
};
|
||||
use crate::utils::{ApplicationError, DateTimeFormat};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExportQuery {
|
||||
|
@ -1,11 +1,8 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Location, Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
||||
use brass_db::models::{Location, Role, User};
|
||||
|
||||
#[actix_web::delete("/locations/delete/{id}")]
|
||||
pub async fn delete(
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{location::LocationTemplate, IdPath},
|
||||
models::{Area, Location, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Area, Location, Role, User};
|
||||
|
||||
#[actix_web::get("/locations/edit/{id}")]
|
||||
pub async fn get(
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::location::LocationTemplate,
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[actix_web::get("/locations/new")]
|
||||
pub async fn get(
|
||||
|
@ -2,10 +2,8 @@ use actix_web::{web, Responder};
|
||||
use askama::Template;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{Area, Location, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use crate::utils::{ApplicationError, TemplateResponse};
|
||||
use brass_db::models::{Area, Location, Role, User};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "location/overview.html")]
|
||||
|
@ -1,15 +1,15 @@
|
||||
use askama::Template;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::{Area, Location, Role, User};
|
||||
use crate::filters;
|
||||
use brass_db::models::{Area, Location, Role, User};
|
||||
|
||||
pub mod delete;
|
||||
pub mod get_edit;
|
||||
pub mod get_new;
|
||||
pub mod get_overview;
|
||||
pub mod post_new;
|
||||
pub mod get_edit;
|
||||
pub mod post_edit;
|
||||
pub mod delete;
|
||||
pub mod post_new;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "location/new_or_edit.html")]
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{location::LocationForm, IdPath},
|
||||
models::{Location, Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use brass_db::models::{Location, Role, User};
|
||||
|
||||
#[actix_web::post("/locations/edit/{id}")]
|
||||
pub async fn post(
|
||||
|
@ -1,15 +1,8 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use brass_macros::db_test;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::location::LocationForm,
|
||||
models::{Location, Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::utils::test_helper::{test_post, DbTestContext, RequestConfig, StatusCode};
|
||||
use crate::{endpoints::location::LocationForm, utils::ApplicationError};
|
||||
use brass_db::models::{Location, Role, User};
|
||||
|
||||
#[actix_web::post("/locations/new")]
|
||||
pub async fn post(
|
||||
@ -35,58 +28,68 @@ pub async fn post(
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
async fn works_when_user_is_admin(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
let config = RequestConfig {
|
||||
uri: "/locations/new".to_string(),
|
||||
role: Role::Admin,
|
||||
function: vec![crate::models::Function::Posten],
|
||||
user_area: 1,
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
endpoints::location::LocationForm,
|
||||
utils::test_helper::{test_post, DbTestContext, RequestConfig, StatusCode},
|
||||
};
|
||||
use brass_db::models::{Function, Location, Role};
|
||||
use brass_macros::db_test;
|
||||
|
||||
let form = LocationForm {
|
||||
name: "Hauptbahnhof".to_string(),
|
||||
area: Some(1),
|
||||
};
|
||||
#[db_test]
|
||||
async fn works_when_user_is_admin(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
let config = RequestConfig {
|
||||
uri: "/locations/new".to_string(),
|
||||
role: Role::Admin,
|
||||
function: vec![Function::Posten],
|
||||
user_area: 1,
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
let form = LocationForm {
|
||||
name: "Hauptbahnhof".to_string(),
|
||||
area: Some(1),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
"Hauptbahnhof".to_string(),
|
||||
Location::read_by_id(&context.db_pool, 1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.name
|
||||
);
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
async fn uses_area_id_of_area_manager(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
let config = RequestConfig {
|
||||
uri: "/locations/new".to_string(),
|
||||
role: Role::AreaManager,
|
||||
function: vec![crate::models::Function::Posten],
|
||||
user_area: 1,
|
||||
};
|
||||
|
||||
let form = LocationForm {
|
||||
name: "Hauptbahnhof".to_string(),
|
||||
area: None,
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
assert_eq!(
|
||||
"Hauptbahnhof".to_string(),
|
||||
Location::read_by_id(&context.db_pool, 1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.name
|
||||
);
|
||||
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
assert_eq!(
|
||||
"Hauptbahnhof".to_string(),
|
||||
Location::read_by_id(&context.db_pool, 1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.name
|
||||
);
|
||||
}
|
||||
|
||||
#[db_test]
|
||||
async fn uses_area_id_of_area_manager(context: &DbTestContext) {
|
||||
let app = context.app().await;
|
||||
let config = RequestConfig {
|
||||
uri: "/locations/new".to_string(),
|
||||
role: Role::AreaManager,
|
||||
function: vec![Function::Posten],
|
||||
user_area: 1,
|
||||
};
|
||||
|
||||
let form = LocationForm {
|
||||
name: "Hauptbahnhof".to_string(),
|
||||
area: None,
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
assert_eq!(
|
||||
"Hauptbahnhof".to_string(),
|
||||
Location::read_by_id(&context.db_pool, 1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::IdPath,
|
||||
models::{Role, User},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
use crate::{endpoints::IdPath, utils::ApplicationError};
|
||||
use brass_db::models::{Role, User};
|
||||
|
||||
#[actix_web::delete("/users/{id}")]
|
||||
pub async fn delete(
|
||||
|
@ -1,10 +1,9 @@
|
||||
use actix_web::{web, Responder};
|
||||
use askama::Template;
|
||||
|
||||
use crate::{
|
||||
models::User,
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use crate::utils::{ApplicationError, TemplateResponse};
|
||||
|
||||
use brass_db::models::User;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user/profile_change_password.html")]
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{user::NewOrEditUserTemplate, IdPath},
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[actix_web::get("/users/edit/{id}")]
|
||||
pub async fn get_edit(
|
||||
|
@ -3,9 +3,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::user::NewOrEditUserTemplate,
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[actix_web::get("/users/new")]
|
||||
pub async fn get_new(
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
filters,
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
use actix_web::{web, Responder};
|
||||
use askama::Template;
|
||||
@ -14,7 +14,7 @@ pub struct UsersTemplate {
|
||||
user: User,
|
||||
area: Option<Area>,
|
||||
users: Vec<User>,
|
||||
is_oob: bool
|
||||
is_oob: bool,
|
||||
}
|
||||
|
||||
#[actix_web::get("/users")]
|
||||
@ -44,7 +44,7 @@ pub async fn get_overview(
|
||||
user: user.into_inner(),
|
||||
area,
|
||||
users,
|
||||
is_oob: false
|
||||
is_oob: false,
|
||||
};
|
||||
|
||||
Ok(template.to_response()?)
|
||||
|
@ -4,9 +4,9 @@ use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
filters,
|
||||
models::{Area, Role, User},
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use brass_db::models::{Area, Role, User};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user/profile.html")]
|
||||
|
@ -3,10 +3,8 @@ use actix_web::{get, http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::Registration,
|
||||
utils::{ApplicationError, TemplateResponse},
|
||||
};
|
||||
use crate::utils::{ApplicationError, TemplateResponse};
|
||||
use brass_db::models::Registration;
|
||||
|
||||
use super::ResetPasswordTemplate;
|
||||
|
||||
|
@ -4,10 +4,8 @@ use askama::Template;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::PasswordReset,
|
||||
utils::{ApplicationError, Customization, TemplateResponse},
|
||||
};
|
||||
use crate::utils::{ApplicationError, Customization, TemplateResponse};
|
||||
use brass_db::models::PasswordReset;
|
||||
|
||||
use super::ResetPasswordTemplate;
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
use crate::{
|
||||
filters,
|
||||
models::{Area, Role, User},
|
||||
};
|
||||
use crate::filters;
|
||||
use askama::Template;
|
||||
use brass_db::models::{Area, Role, User};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod delete;
|
||||
@ -22,8 +20,8 @@ pub mod post_new;
|
||||
pub mod post_register;
|
||||
pub mod post_resend_registration;
|
||||
pub mod post_reset;
|
||||
pub mod put_receive_notifications;
|
||||
pub mod put_lock;
|
||||
pub mod put_receive_notifications;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "user/new_or_edit.html")]
|
||||
|
@ -3,10 +3,8 @@ use maud::html;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::{NoneToken, User},
|
||||
utils::{password_change::PasswordChangeBuilder, ApplicationError},
|
||||
};
|
||||
use crate::utils::{password_change::PasswordChangeBuilder, ApplicationError};
|
||||
use brass_db::{models::User, NoneToken};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChangePasswordForm {
|
||||
|
@ -1,10 +1,13 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use brass_db::{
|
||||
models::{Function, Role, User, UserChangeset},
|
||||
validation::{AsyncValidate, DbContext},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::{user::NewOrEditUserForm, IdPath},
|
||||
models::{Function, Role, User, UserChangeset},
|
||||
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
|
||||
utils::ApplicationError,
|
||||
};
|
||||
|
||||
#[actix_web::post("/users/edit/{id}")]
|
||||
@ -78,7 +81,8 @@ pub async fn post_edit(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{endpoints::user::NewOrEditUserForm, models::*, utils::test_helper::*};
|
||||
use crate::{endpoints::user::NewOrEditUserForm, utils::test_helper::*};
|
||||
use brass_db::models::*;
|
||||
use brass_macros::db_test;
|
||||
use fake::{
|
||||
faker::{internet::en::SafeEmail, name::en::Name},
|
||||
@ -112,7 +116,7 @@ mod tests {
|
||||
area: Some(2),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
let updated_user = User::read_by_id(&context.db_pool, 1)
|
||||
@ -147,7 +151,7 @@ mod tests {
|
||||
area: Some(1),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
|
||||
assert_eq!(StatusCode::BAD_REQUEST, response.status());
|
||||
}
|
||||
|
||||
@ -172,7 +176,7 @@ mod tests {
|
||||
area: Some(2),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
|
||||
assert_eq!(StatusCode::FOUND, response.status());
|
||||
|
||||
let updated_user = User::read_by_id(&context.db_pool, 1)
|
||||
@ -210,7 +214,7 @@ mod tests {
|
||||
area: Some(2),
|
||||
};
|
||||
|
||||
let response = test_post(&context.db_pool, app, &config, form).await;
|
||||
let response = test_post(&context.db_pool, app, &config, Some(form)).await;
|
||||
assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status());
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse, Responder};
|
||||
use brass_db::models::User;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{models::User, utils::auth::hash_plain_password_with_salt};
|
||||
use crate::utils::auth::hash_plain_password_with_salt;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct LoginForm {
|
||||
|
@ -1,11 +1,10 @@
|
||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
endpoints::user::NewOrEditUserForm,
|
||||
mail::Mailer,
|
||||
use crate::{endpoints::user::NewOrEditUserForm, mail::Mailer, utils::ApplicationError};
|
||||
use brass_db::{
|
||||
models::{Function, Registration, Role, User, UserChangeset},
|
||||
utils::{validation::{AsyncValidate, DbContext}, ApplicationError},
|
||||
validation::{AsyncValidate, DbContext},
|
||||
};
|
||||
|
||||
#[actix_web::post("/users/new")]
|
||||
|
@ -3,10 +3,8 @@ use maud::html;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
models::Registration,
|
||||
utils::{password_change::PasswordChangeBuilder, ApplicationError},
|
||||
};
|
||||
use crate::utils::{password_change::PasswordChangeBuilder, ApplicationError};
|
||||
use brass_db::models::Registration;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RegisterForm {
|
||||
|
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