feat: set functions for user

This commit is contained in:
Max Hohlfeld 2025-03-23 18:10:20 +01:00
parent c830704fa4
commit 736b69d2c3
6 changed files with 93 additions and 54 deletions

View File

@ -8,6 +8,7 @@ use crate::{
}; };
use actix_web::HttpResponse; use actix_web::HttpResponse;
use rinja::Template; use rinja::Template;
use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use zxcvbn::{zxcvbn, Score}; use zxcvbn::{zxcvbn, Score};
@ -26,9 +27,9 @@ pub mod post_edit;
pub mod post_login; pub mod post_login;
pub mod post_new; pub mod post_new;
pub mod post_register; pub mod post_register;
pub mod post_resend_registration;
pub mod post_reset; pub mod post_reset;
pub mod post_toggle; pub mod post_toggle;
pub mod post_resend_registration;
#[derive(Template)] #[derive(Template)]
#[template(path = "user/new_or_edit.html")] #[template(path = "user/new_or_edit.html")]
@ -45,6 +46,21 @@ pub struct NewOrEditUserTemplate {
area_id: Option<i32>, area_id: Option<i32>,
} }
#[derive(Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
struct NewOrEditUserForm {
email: String,
name: String,
role: u8,
#[serde(rename(deserialize = "is-posten"))]
is_posten: Option<bool>,
#[serde(rename(deserialize = "is-wachhabender"))]
is_wachhabender: Option<bool>,
#[serde(rename(deserialize = "is-fuehrungsassistent"))]
is_fuehrungsassistent: Option<bool>,
area: Option<i32>,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "user/change_password.html")] #[template(path = "user/change_password.html")]
struct ResetPasswordTemplate<'a> { struct ResetPasswordTemplate<'a> {

View File

@ -1,30 +1,19 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use garde::Validate; use garde::Validate;
use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use crate::{ use crate::{
endpoints::IdPath, endpoints::{user::NewOrEditUserForm, IdPath},
models::{Area, Role, User, UserChangeset}, models::{Area, Function, Role, User, UserChangeset},
utils::ApplicationError, utils::ApplicationError,
}; };
#[derive(Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct EditUserForm {
email: String,
name: String,
role: u8,
function: u8,
area: Option<i32>,
}
#[actix_web::post("/users/edit/{id}")] #[actix_web::post("/users/edit/{id}")]
pub async fn post_edit( pub async fn post_edit(
user: web::ReqData<User>, user: web::ReqData<User>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<IdPath>, path: web::Path<IdPath>,
form: web::Form<EditUserForm>, form: web::Form<NewOrEditUserForm>,
) -> Result<impl Responder, ApplicationError> { ) -> Result<impl Responder, ApplicationError> {
if user.role != Role::AreaManager && user.role != Role::Admin { if user.role != Role::AreaManager && user.role != Role::Admin {
return Err(ApplicationError::Unauthorized); return Err(ApplicationError::Unauthorized);
@ -47,11 +36,25 @@ pub async fn post_edit(
return Err(ApplicationError::Unauthorized); return Err(ApplicationError::Unauthorized);
} }
let mut functions = Vec::with_capacity(3);
if form.is_posten.unwrap_or(false) {
functions.push(Function::Posten);
}
if form.is_wachhabender.unwrap_or(false) {
functions.push(Function::Wachhabender);
}
if form.is_fuehrungsassistent.unwrap_or(false) {
functions.push(Function::Fuehrungsassistent);
}
let changeset = UserChangeset { let changeset = UserChangeset {
name: form.name.clone(), name: form.name.clone(),
email: form.email.clone(), email: form.email.clone(),
role: form.role.try_into()?, role: form.role.try_into()?,
function: form.function.try_into()?, functions,
area_id, area_id,
}; };
@ -69,8 +72,7 @@ pub async fn post_edit(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use crate::{endpoints::user::NewOrEditUserForm, models::*, utils::test_helper::*};
use crate::{models::*, utils::test_helper::*};
use brass_macros::db_test; use brass_macros::db_test;
use fake::{ use fake::{
faker::{internet::en::SafeEmail, name::en::Name}, faker::{internet::en::SafeEmail, name::en::Name},
@ -94,11 +96,13 @@ mod tests {
let new_name: String = Name().fake(); let new_name: String = Name().fake();
let new_mail: String = SafeEmail().fake(); let new_mail: String = SafeEmail().fake();
let form = EditUserForm { let form = NewOrEditUserForm {
name: new_name.clone(), name: new_name.clone(),
email: new_mail.clone(), email: new_mail.clone(),
role: Role::AreaManager as u8, role: Role::AreaManager as u8,
function: Function::Fuehrungsassistent as u8, is_posten: None,
is_wachhabender: None,
is_fuehrungsassistent: Some(true),
area: Some(2), area: Some(2),
}; };
@ -127,11 +131,13 @@ mod tests {
user_area: 1, user_area: 1,
}; };
let form = EditUserForm { let form = NewOrEditUserForm {
name: "".to_string(), name: "".to_string(),
email: "".to_string(), email: "".to_string(),
role: Role::AreaManager as u8, role: Role::AreaManager as u8,
function: Function::Fuehrungsassistent as u8, is_posten: None,
is_wachhabender: None,
is_fuehrungsassistent: Some(true),
area: Some(1), area: Some(1),
}; };

View File

@ -1,28 +1,19 @@
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
use garde::Validate; use garde::Validate;
use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use crate::{ use crate::{
endpoints::user::NewOrEditUserForm,
mail::Mailer, mail::Mailer,
models::{Function, Registration, Role, User, UserChangeset}, models::{Function, Registration, Role, User, UserChangeset},
utils::ApplicationError, utils::ApplicationError,
}; };
#[derive(Deserialize)]
pub struct NewUserForm {
email: String,
name: String,
role: u8,
function: u8,
area: Option<i32>,
}
#[actix_web::post("/users/new")] #[actix_web::post("/users/new")]
pub async fn post_new( pub async fn post_new(
user: web::ReqData<User>, user: web::ReqData<User>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
form: web::Form<NewUserForm>, form: web::Form<NewOrEditUserForm>,
mailer: web::Data<Mailer>, mailer: web::Data<Mailer>,
) -> Result<impl Responder, ApplicationError> { ) -> Result<impl Responder, ApplicationError> {
if user.role != Role::AreaManager && user.role != Role::Admin { if user.role != Role::AreaManager && user.role != Role::Admin {
@ -36,29 +27,39 @@ pub async fn post_new(
} }
let role = Role::try_from(form.role)?; let role = Role::try_from(form.role)?;
let function = Function::try_from(form.function)?; let mut functions = Vec::with_capacity(3);
if form.is_posten.unwrap_or(false) {
functions.push(Function::Posten);
}
if form.is_wachhabender.unwrap_or(false) {
functions.push(Function::Wachhabender);
}
if form.is_fuehrungsassistent.unwrap_or(false) {
functions.push(Function::Fuehrungsassistent);
}
let changeset = UserChangeset { let changeset = UserChangeset {
name: form.name.clone(), name: form.name.clone(),
email: form.email.clone(), email: form.email.clone(),
role, role,
function, functions,
area_id area_id,
}; };
if let Err(e) = changeset.validate() { if let Err(e) = changeset.validate() {
return Ok(HttpResponse::BadRequest().body(e.to_string())); return Ok(HttpResponse::BadRequest().body(e.to_string()));
}; };
let id = User::create( let id = User::create(pool.get_ref(), changeset).await?;
pool.get_ref(),
changeset
)
.await?;
let registration = Registration::insert_new_for_user(pool.get_ref(), id).await?; let registration = Registration::insert_new_for_user(pool.get_ref(), id).await?;
let new_user = User::read_by_id(pool.get_ref(), id).await?.unwrap(); let new_user = User::read_by_id(pool.get_ref(), id).await?.unwrap();
mailer.send_registration_mail(&new_user, &registration.token).await?; mailer
.send_registration_mail(&new_user, &registration.token)
.await?;
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.insert_header((LOCATION, "/users")) .insert_header((LOCATION, "/users"))

View File

@ -30,7 +30,7 @@ impl User {
changeset.name, changeset.name,
changeset.email, changeset.email,
changeset.role as Role, changeset.role as Role,
changeset.function as Function, changeset.functions.as_slice() as &[Function],
changeset.area_id changeset.area_id
) )
.fetch_one(pool) .fetch_one(pool)
@ -285,7 +285,7 @@ impl User {
changeset.name, changeset.name,
changeset.email, changeset.email,
changeset.role as Role, changeset.role as Role,
changeset.function as Function, changeset.functions.as_slice() as &[Function],
changeset.area_id, changeset.area_id,
id id
) )

View File

@ -5,7 +5,7 @@ use garde::Validate;
use super::{Function, Role}; use super::{Function, Role};
#[derive(Validate)] #[derive(Debug, Validate)]
#[cfg_attr(test, derive(Dummy))] #[cfg_attr(test, derive(Dummy))]
#[garde(allow_unvalidated)] #[garde(allow_unvalidated)]
pub struct UserChangeset { pub struct UserChangeset {
@ -16,8 +16,8 @@ pub struct UserChangeset {
pub email: String, pub email: String,
#[cfg_attr(test, dummy(expr = "Role::Staff"))] #[cfg_attr(test, dummy(expr = "Role::Staff"))]
pub role: Role, pub role: Role,
#[cfg_attr(test, dummy(expr = "Function::Posten"))] #[cfg_attr(test, dummy(expr = "vec![Function::Posten]"))]
pub function: Function, pub functions: Vec<Function>,
/// check before: must exist and user can create other user for this area /// check before: must exist and user can create other user for this area
#[cfg_attr(test, dummy(expr = "1"))] #[cfg_attr(test, dummy(expr = "1"))]
pub area_id: i32, pub area_id: i32,

View File

@ -64,12 +64,28 @@
<label class="label">Funktion</label> <label class="label">Funktion</label>
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field is-narrow"> <div class="field is-grouped">
<div class="control"> <div class="control">
<div class="select is-fullwidth"> <label class="checkbox">
<select name="function"> <input id="is-posten" type="checkbox" name="is-posten" value="true" {{
</select> is_posten.unwrap_or(false)|ref|cond_show("checked") }} />
</div> Posten
</label>
</div>
<div class="control">
<label class="checkbox">
<input type="checkbox" name="is-wachhabender" value="true" {{
is_wachhabender.unwrap_or(false)|ref|cond_show("checked") }}
_="on change if me.checked then set checked of previous <input/> to true" />
Wachhabender
</label>
</div>
<div class="control">
<label class="checkbox">
<input type="checkbox" name="is-fuehrungsassistent" value="true" {{
is_fuehrungsassistent.unwrap_or(false)|ref|cond_show("checked") }} />
Führungsassistent
</label>
</div> </div>
</div> </div>
</div> </div>
@ -87,8 +103,8 @@
<select name="area"> <select name="area">
{% for area in areas.as_ref().unwrap() %} {% for area in areas.as_ref().unwrap() %}
<option value="{{ area.id }}" {{ area_id|is_some_and_eq(area.id)|ref|cond_show("selected") }}>{{ <option value="{{ area.id }}" {{ area_id|is_some_and_eq(area.id)|ref|cond_show("selected") }}>{{
area.name }}</option> area.name }}</input>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div>