diff --git a/migrations/20250515174927_clothing_table.sql b/migrations/20250515174927_clothing_table.sql new file mode 100644 index 00000000..cb1868f7 --- /dev/null +++ b/migrations/20250515174927_clothing_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE clothing +( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); + +INSERT INTO clothing (name) VALUES ('Tuchuniform'); + +ALTER TABLE event + ALTER COLUMN clothing SET DATA TYPE INTEGER USING 1, + ADD FOREIGN KEY (clothing) REFERENCES clothing (id) ON DELETE CASCADE; diff --git a/web/src/endpoints/clothing/get_overview.rs b/web/src/endpoints/clothing/get_overview.rs new file mode 100644 index 00000000..58de97f5 --- /dev/null +++ b/web/src/endpoints/clothing/get_overview.rs @@ -0,0 +1,86 @@ +use actix_web::{web, Responder}; +use askama::Template; +use sqlx::PgPool; + +use crate::{ + models::{Clothing, Role, User}, + utils::{ApplicationError, TemplateResponse}, +}; + +#[derive(Template)] +#[cfg_attr(not(test), template(path = "clothing/overview.html"))] +#[cfg_attr( + test, + template(path = "clothing/overview.html", block = "content"), + allow(dead_code) +)] +pub struct ClothingOverviewTemplate { + user: User, + clothings: Vec, +} + +#[actix_web::get("/clothing")] +pub async fn get( + user: web::ReqData, + pool: web::Data, +) -> Result { + if user.role != Role::Admin { + return Err(ApplicationError::Unauthorized); + } + + let clothings = Clothing::read_all(pool.get_ref()).await?; + + let template = ClothingOverviewTemplate { + user: user.into_inner(), + clothings, + }; + + Ok(template.to_response()?) +} + +#[cfg(test)] +mod tests { + use crate::{ + models::{Clothing, Role}, + utils::test_helper::{ + assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode, + }, + }; + use brass_macros::db_test; + + #[db_test] + async fn user_cant_view_overview(context: &DbTestContext) { + let app = context.app().await; + + let config = RequestConfig::new("/clothing"); + + let response = test_get(&context.db_pool, &app, &config).await; + assert_eq!(StatusCode::UNAUTHORIZED, response.status()); + } + + #[db_test] + async fn area_manager_cant_view_overview(context: &DbTestContext) { + let app = context.app().await; + + let config = RequestConfig::new("/clothing").with_role(Role::AreaManager); + + let response = test_get(&context.db_pool, &app, &config).await; + assert_eq!(StatusCode::UNAUTHORIZED, response.status()); + } + + #[db_test] + async fn produces_template_fine_when_user_is_admin(context: &DbTestContext) { + let app = context.app().await; + Clothing::create(&context.db_pool, "Schutzkleidung Form 1") + .await + .unwrap(); + + let config = RequestConfig::new("/clothing").with_role(Role::Admin); + + let response = test_get(&context.db_pool, &app, &config).await; + assert_eq!(StatusCode::OK, response.status()); + + let body = read_body(response).await; + assert_snapshot!(body); + } +} diff --git a/web/src/endpoints/clothing/mod.rs b/web/src/endpoints/clothing/mod.rs new file mode 100644 index 00000000..c85fc4b5 --- /dev/null +++ b/web/src/endpoints/clothing/mod.rs @@ -0,0 +1 @@ +pub mod get_overview; diff --git a/web/src/endpoints/events/get_edit.rs b/web/src/endpoints/events/get_edit.rs index 6f25f0f7..3f8ced30 100644 --- a/web/src/endpoints/events/get_edit.rs +++ b/web/src/endpoints/events/get_edit.rs @@ -59,7 +59,7 @@ pub async fn get( voluntary_wachhabender: event.voluntary_wachhabender, voluntary_fuehrungsassistent: event.voluntary_fuehrungsassistent, amount_of_posten: Some(event.amount_of_posten), - clothing: Some(event.clothing), + clothing: Some(event.clothing.id), canceled: event.canceled, note: event.note, }; @@ -107,7 +107,7 @@ async fn produces_template(context: &DbTestContext) { voluntary_fuehrungsassistent: false, voluntary_wachhabender: false, amount_of_posten: 2, - clothing: "Tuchuniform".to_string(), + clothing: 1, note: None, }; diff --git a/web/src/endpoints/events/mod.rs b/web/src/endpoints/events/mod.rs index aa39db3a..96a8a292 100644 --- a/web/src/endpoints/events/mod.rs +++ b/web/src/endpoints/events/mod.rs @@ -30,7 +30,7 @@ pub struct NewOrEditEventTemplate { voluntary_wachhabender: bool, voluntary_fuehrungsassistent: bool, amount_of_posten: Option, - clothing: Option, + clothing: Option, canceled: bool, note: Option, amount_of_planned_posten: usize, @@ -49,7 +49,7 @@ pub struct NewOrEditEventForm { voluntarywachhabender: bool, voluntaryfuehrungsassistent: bool, amount: i16, - clothing: String, + clothing: i32, note: Option, } diff --git a/web/src/endpoints/events/post_edit.rs b/web/src/endpoints/events/post_edit.rs index 0a6a7866..59b7815e 100644 --- a/web/src/endpoints/events/post_edit.rs +++ b/web/src/endpoints/events/post_edit.rs @@ -45,7 +45,7 @@ pub async fn post( let changeset = EventChangeset { amount_of_posten: form.amount, - clothing: form.clothing.clone(), + clothing: form.clothing, location_id: form.location, time: (form.date.and_time(form.start), form.end), name: form.name.clone(), diff --git a/web/src/endpoints/events/post_new.rs b/web/src/endpoints/events/post_new.rs index 40a57bd6..a90e04ef 100644 --- a/web/src/endpoints/events/post_new.rs +++ b/web/src/endpoints/events/post_new.rs @@ -28,7 +28,7 @@ pub async fn post( let changeset = EventChangeset { amount_of_posten: form.amount, - clothing: form.clothing.clone(), + clothing: form.clothing, location_id: form.location, time: (form.date.and_time(form.start), form.end), name: form.name.clone(), diff --git a/web/src/endpoints/mod.rs b/web/src/endpoints/mod.rs index c1d60bb1..06128d35 100644 --- a/web/src/endpoints/mod.rs +++ b/web/src/endpoints/mod.rs @@ -5,11 +5,12 @@ use serde::Deserialize; mod area; mod assignment; mod availability; +mod clothing; mod events; mod export; mod imprint; mod location; -pub mod user; +pub mod user; // TODO: why pub? mod vehicle; mod vehicle_assignment; diff --git a/web/src/models/clothing.rs b/web/src/models/clothing.rs new file mode 100644 index 00000000..cc29e042 --- /dev/null +++ b/web/src/models/clothing.rs @@ -0,0 +1,62 @@ +use sqlx::{query, PgPool}; + +use super::Result; + +#[derive(Debug, Clone)] +pub struct Clothing { + pub id: i32, + pub name: String, +} + +impl Clothing { + pub async fn create(pool: &PgPool, name: &str) -> Result<()> { + query!("INSERT INTO clothing (name) VALUES ($1);", name) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn read_all(pool: &PgPool) -> Result> { + let records = query!("SELECT * FROM clothing;").fetch_all(pool).await?; + + let clothing_options = records + .into_iter() + .map(|v| Clothing { + id: v.id, + name: v.name, + }) + .collect(); + + Ok(clothing_options) + } + + pub async fn read(pool: &PgPool, id: i32) -> Result> { + let record = query!("SELECT * FROM clothing WHERE id = $1;", id) + .fetch_optional(pool) + .await?; + + let vehicle = record.map(|v| Clothing { + id: v.id, + name: v.name, + }); + + Ok(vehicle) + } + + pub async fn update(pool: &PgPool, id: i32, name: &str) -> Result<()> { + query!("UPDATE clothing SET name = $1 WHERE id = $2;", name, id) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn delete(pool: &PgPool, id: i32) -> Result<()> { + query!("DELETE FROM clothing WHERE id = $1;", id) + .execute(pool) + .await?; + + Ok(()) + } +} diff --git a/web/src/models/event.rs b/web/src/models/event.rs index fe9583d8..831755b3 100644 --- a/web/src/models/event.rs +++ b/web/src/models/event.rs @@ -1,7 +1,7 @@ use chrono::{NaiveDate, NaiveDateTime}; use sqlx::{query, PgPool}; -use super::{event_changeset::EventChangeset, Location, Result}; +use super::{event_changeset::EventChangeset, Clothing, Location, Result}; #[derive(Clone, Debug)] pub struct Event { @@ -14,7 +14,7 @@ pub struct Event { pub voluntary_wachhabender: bool, pub voluntary_fuehrungsassistent: bool, pub amount_of_posten: i16, - pub clothing: String, + pub clothing: Clothing, pub canceled: bool, pub note: Option, } @@ -51,9 +51,12 @@ impl Event { event.note, location.id, location.name AS locationName, - location.areaId AS locationAreaId + location.areaId AS locationAreaId, + clothing.id AS clothingId, + clothing.name AS clothingName FROM event JOIN location ON event.locationId = location.id + JOIN clothing ON event.clothing = clothing.id WHERE starttimestamp::date = $1 AND location.areaId = $2; "#, @@ -80,7 +83,10 @@ impl Event { voluntary_wachhabender: record.voluntarywachhabender, voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent, amount_of_posten: record.amountofposten, - clothing: record.clothing.to_string(), + clothing: Clothing { + id: record.clothingid, + name: record.clothingname, + }, canceled: record.canceled, note: record.note, }) @@ -106,9 +112,12 @@ impl Event { event.note, location.id, location.name AS locationName, - location.areaId AS locationAreaId + location.areaId AS locationAreaId, + clothing.id AS clothingId, + clothing.name AS clothingName FROM event JOIN location ON event.locationId = location.id + JOIN clothing ON event.clothing = clothing.id WHERE event.id = $1; "#, id @@ -131,7 +140,10 @@ impl Event { voluntary_wachhabender: record.voluntarywachhabender, voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent, amount_of_posten: record.amountofposten, - clothing: record.clothing.to_string(), + clothing: Clothing { + id: record.clothingid, + name: record.clothingname, + }, canceled: record.canceled, note: record.note, }); diff --git a/web/src/models/event_changeset.rs b/web/src/models/event_changeset.rs index 042b5b69..f8c06fc9 100644 --- a/web/src/models/event_changeset.rs +++ b/web/src/models/event_changeset.rs @@ -25,7 +25,7 @@ pub struct EventChangeset { pub voluntary_fuehrungsassistent: bool, #[garde(range(min = ctx.as_ref().map(|c: &EventContext| c.amount_of_assigned_posten).unwrap_or(0), max = 100))] pub amount_of_posten: i16, - pub clothing: String, + pub clothing: i32, pub note: Option, } @@ -39,7 +39,7 @@ impl EventChangeset { voluntary_wachhabender: true, voluntary_fuehrungsassistent: true, amount_of_posten: 5, - clothing: "Tuchuniform".to_string(), + clothing: 1, note: None, }; diff --git a/web/src/models/mod.rs b/web/src/models/mod.rs index b86490d7..acf360d9 100644 --- a/web/src/models/mod.rs +++ b/web/src/models/mod.rs @@ -4,6 +4,7 @@ mod assignment_changeset; mod availability; mod availability_assignment_state; mod availability_changeset; +mod clothing; mod event; mod event_changeset; mod function; @@ -25,6 +26,7 @@ pub use availability_assignment_state::AvailabilityAssignmentState; pub use availability_changeset::{ find_free_date_time_slots, AvailabilityChangeset, AvailabilityContext, }; +pub use clothing::Clothing; pub use event::Event; pub use event_changeset::{EventChangeset, EventContext}; pub use function::Function; diff --git a/web/templates/clothing/clothing_entry_edit.html b/web/templates/clothing/clothing_entry_edit.html new file mode 100644 index 00000000..a5dcfad6 --- /dev/null +++ b/web/templates/clothing/clothing_entry_edit.html @@ -0,0 +1,15 @@ +
  • + +
    + + +
    +
  • diff --git a/web/templates/clothing/clothing_entry_read.html b/web/templates/clothing/clothing_entry_read.html new file mode 100644 index 00000000..0055c9b3 --- /dev/null +++ b/web/templates/clothing/clothing_entry_read.html @@ -0,0 +1,15 @@ +
  • + {{ c.name }}" +
    + + +
    +
  • diff --git a/web/templates/clothing/overview.html b/web/templates/clothing/overview.html new file mode 100644 index 00000000..247f9b07 --- /dev/null +++ b/web/templates/clothing/overview.html @@ -0,0 +1,20 @@ +{% extends "nav.html" %} + +{% block content %} +
    +
    +

    + Anzugsordnungen +

    +

    zur Auswahl bei der Erstellung von Events

    + +
    +
      + {% for c in clothings %} + {% include "clothing_entry_read.html" %} + {% endfor %} +
    +
    +
    + +{% endblock %} diff --git a/web/templates/events/plan.html b/web/templates/events/plan.html index 67926498..2f929a3d 100644 --- a/web/templates/events/plan.html +++ b/web/templates/events/plan.html @@ -39,7 +39,7 @@
    -

    Anzugsordnung: {{ event.clothing }}

    +

    Anzugsordnung: {{ event.clothing.name }}

    diff --git a/web/templates/index.html b/web/templates/index.html index 4a1e866b..ef3dedcb 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -127,7 +127,7 @@
    -

    Anzugsordnung: {{ event.clothing }}

    +

    Anzugsordnung: {{ event.clothing.name }}

    {% if let Some(note) = event.note %} diff --git a/web/templates/nav.html b/web/templates/nav.html index e86d640f..68b9606c 100644 --- a/web/templates/nav.html +++ b/web/templates/nav.html @@ -8,7 +8,7 @@ Fahrzeuge + + {% if user.role == Role::Admin %} + + Anzugsordnung + + {% endif %} {% endif %}