feat: WIP add clothing table
This commit is contained in:
parent
c9b075216a
commit
5b5e312152
11
migrations/20250515174927_clothing_table.sql
Normal file
11
migrations/20250515174927_clothing_table.sql
Normal file
@ -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;
|
86
web/src/endpoints/clothing/get_overview.rs
Normal file
86
web/src/endpoints/clothing/get_overview.rs
Normal file
@ -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<Clothing>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::get("/clothing")]
|
||||||
|
pub async fn get(
|
||||||
|
user: web::ReqData<User>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
) -> Result<impl Responder, ApplicationError> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
1
web/src/endpoints/clothing/mod.rs
Normal file
1
web/src/endpoints/clothing/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod get_overview;
|
@ -59,7 +59,7 @@ pub async fn get(
|
|||||||
voluntary_wachhabender: event.voluntary_wachhabender,
|
voluntary_wachhabender: event.voluntary_wachhabender,
|
||||||
voluntary_fuehrungsassistent: event.voluntary_fuehrungsassistent,
|
voluntary_fuehrungsassistent: event.voluntary_fuehrungsassistent,
|
||||||
amount_of_posten: Some(event.amount_of_posten),
|
amount_of_posten: Some(event.amount_of_posten),
|
||||||
clothing: Some(event.clothing),
|
clothing: Some(event.clothing.id),
|
||||||
canceled: event.canceled,
|
canceled: event.canceled,
|
||||||
note: event.note,
|
note: event.note,
|
||||||
};
|
};
|
||||||
@ -107,7 +107,7 @@ async fn produces_template(context: &DbTestContext) {
|
|||||||
voluntary_fuehrungsassistent: false,
|
voluntary_fuehrungsassistent: false,
|
||||||
voluntary_wachhabender: false,
|
voluntary_wachhabender: false,
|
||||||
amount_of_posten: 2,
|
amount_of_posten: 2,
|
||||||
clothing: "Tuchuniform".to_string(),
|
clothing: 1,
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ pub struct NewOrEditEventTemplate {
|
|||||||
voluntary_wachhabender: bool,
|
voluntary_wachhabender: bool,
|
||||||
voluntary_fuehrungsassistent: bool,
|
voluntary_fuehrungsassistent: bool,
|
||||||
amount_of_posten: Option<i16>,
|
amount_of_posten: Option<i16>,
|
||||||
clothing: Option<String>,
|
clothing: Option<i32>,
|
||||||
canceled: bool,
|
canceled: bool,
|
||||||
note: Option<String>,
|
note: Option<String>,
|
||||||
amount_of_planned_posten: usize,
|
amount_of_planned_posten: usize,
|
||||||
@ -49,7 +49,7 @@ pub struct NewOrEditEventForm {
|
|||||||
voluntarywachhabender: bool,
|
voluntarywachhabender: bool,
|
||||||
voluntaryfuehrungsassistent: bool,
|
voluntaryfuehrungsassistent: bool,
|
||||||
amount: i16,
|
amount: i16,
|
||||||
clothing: String,
|
clothing: i32,
|
||||||
note: Option<String>,
|
note: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ pub async fn post(
|
|||||||
|
|
||||||
let changeset = EventChangeset {
|
let changeset = EventChangeset {
|
||||||
amount_of_posten: form.amount,
|
amount_of_posten: form.amount,
|
||||||
clothing: form.clothing.clone(),
|
clothing: form.clothing,
|
||||||
location_id: form.location,
|
location_id: form.location,
|
||||||
time: (form.date.and_time(form.start), form.end),
|
time: (form.date.and_time(form.start), form.end),
|
||||||
name: form.name.clone(),
|
name: form.name.clone(),
|
||||||
|
@ -28,7 +28,7 @@ pub async fn post(
|
|||||||
|
|
||||||
let changeset = EventChangeset {
|
let changeset = EventChangeset {
|
||||||
amount_of_posten: form.amount,
|
amount_of_posten: form.amount,
|
||||||
clothing: form.clothing.clone(),
|
clothing: form.clothing,
|
||||||
location_id: form.location,
|
location_id: form.location,
|
||||||
time: (form.date.and_time(form.start), form.end),
|
time: (form.date.and_time(form.start), form.end),
|
||||||
name: form.name.clone(),
|
name: form.name.clone(),
|
||||||
|
@ -5,11 +5,12 @@ use serde::Deserialize;
|
|||||||
mod area;
|
mod area;
|
||||||
mod assignment;
|
mod assignment;
|
||||||
mod availability;
|
mod availability;
|
||||||
|
mod clothing;
|
||||||
mod events;
|
mod events;
|
||||||
mod export;
|
mod export;
|
||||||
mod imprint;
|
mod imprint;
|
||||||
mod location;
|
mod location;
|
||||||
pub mod user;
|
pub mod user; // TODO: why pub?
|
||||||
mod vehicle;
|
mod vehicle;
|
||||||
mod vehicle_assignment;
|
mod vehicle_assignment;
|
||||||
|
|
||||||
|
62
web/src/models/clothing.rs
Normal file
62
web/src/models/clothing.rs
Normal file
@ -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<Vec<Clothing>> {
|
||||||
|
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<Option<Clothing>> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use sqlx::{query, PgPool};
|
use sqlx::{query, PgPool};
|
||||||
|
|
||||||
use super::{event_changeset::EventChangeset, Location, Result};
|
use super::{event_changeset::EventChangeset, Clothing, Location, Result};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
@ -14,7 +14,7 @@ pub struct Event {
|
|||||||
pub voluntary_wachhabender: bool,
|
pub voluntary_wachhabender: bool,
|
||||||
pub voluntary_fuehrungsassistent: bool,
|
pub voluntary_fuehrungsassistent: bool,
|
||||||
pub amount_of_posten: i16,
|
pub amount_of_posten: i16,
|
||||||
pub clothing: String,
|
pub clothing: Clothing,
|
||||||
pub canceled: bool,
|
pub canceled: bool,
|
||||||
pub note: Option<String>,
|
pub note: Option<String>,
|
||||||
}
|
}
|
||||||
@ -51,9 +51,12 @@ impl Event {
|
|||||||
event.note,
|
event.note,
|
||||||
location.id,
|
location.id,
|
||||||
location.name AS locationName,
|
location.name AS locationName,
|
||||||
location.areaId AS locationAreaId
|
location.areaId AS locationAreaId,
|
||||||
|
clothing.id AS clothingId,
|
||||||
|
clothing.name AS clothingName
|
||||||
FROM event
|
FROM event
|
||||||
JOIN location ON event.locationId = location.id
|
JOIN location ON event.locationId = location.id
|
||||||
|
JOIN clothing ON event.clothing = clothing.id
|
||||||
WHERE starttimestamp::date = $1
|
WHERE starttimestamp::date = $1
|
||||||
AND location.areaId = $2;
|
AND location.areaId = $2;
|
||||||
"#,
|
"#,
|
||||||
@ -80,7 +83,10 @@ impl Event {
|
|||||||
voluntary_wachhabender: record.voluntarywachhabender,
|
voluntary_wachhabender: record.voluntarywachhabender,
|
||||||
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
|
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
|
||||||
amount_of_posten: record.amountofposten,
|
amount_of_posten: record.amountofposten,
|
||||||
clothing: record.clothing.to_string(),
|
clothing: Clothing {
|
||||||
|
id: record.clothingid,
|
||||||
|
name: record.clothingname,
|
||||||
|
},
|
||||||
canceled: record.canceled,
|
canceled: record.canceled,
|
||||||
note: record.note,
|
note: record.note,
|
||||||
})
|
})
|
||||||
@ -106,9 +112,12 @@ impl Event {
|
|||||||
event.note,
|
event.note,
|
||||||
location.id,
|
location.id,
|
||||||
location.name AS locationName,
|
location.name AS locationName,
|
||||||
location.areaId AS locationAreaId
|
location.areaId AS locationAreaId,
|
||||||
|
clothing.id AS clothingId,
|
||||||
|
clothing.name AS clothingName
|
||||||
FROM event
|
FROM event
|
||||||
JOIN location ON event.locationId = location.id
|
JOIN location ON event.locationId = location.id
|
||||||
|
JOIN clothing ON event.clothing = clothing.id
|
||||||
WHERE event.id = $1;
|
WHERE event.id = $1;
|
||||||
"#,
|
"#,
|
||||||
id
|
id
|
||||||
@ -131,7 +140,10 @@ impl Event {
|
|||||||
voluntary_wachhabender: record.voluntarywachhabender,
|
voluntary_wachhabender: record.voluntarywachhabender,
|
||||||
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
|
voluntary_fuehrungsassistent: record.voluntaryfuehrungsassistent,
|
||||||
amount_of_posten: record.amountofposten,
|
amount_of_posten: record.amountofposten,
|
||||||
clothing: record.clothing.to_string(),
|
clothing: Clothing {
|
||||||
|
id: record.clothingid,
|
||||||
|
name: record.clothingname,
|
||||||
|
},
|
||||||
canceled: record.canceled,
|
canceled: record.canceled,
|
||||||
note: record.note,
|
note: record.note,
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,7 @@ pub struct EventChangeset {
|
|||||||
pub voluntary_fuehrungsassistent: bool,
|
pub voluntary_fuehrungsassistent: bool,
|
||||||
#[garde(range(min = ctx.as_ref().map(|c: &EventContext| c.amount_of_assigned_posten).unwrap_or(0), max = 100))]
|
#[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 amount_of_posten: i16,
|
||||||
pub clothing: String,
|
pub clothing: i32,
|
||||||
pub note: Option<String>,
|
pub note: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ impl EventChangeset {
|
|||||||
voluntary_wachhabender: true,
|
voluntary_wachhabender: true,
|
||||||
voluntary_fuehrungsassistent: true,
|
voluntary_fuehrungsassistent: true,
|
||||||
amount_of_posten: 5,
|
amount_of_posten: 5,
|
||||||
clothing: "Tuchuniform".to_string(),
|
clothing: 1,
|
||||||
note: None,
|
note: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ mod assignment_changeset;
|
|||||||
mod availability;
|
mod availability;
|
||||||
mod availability_assignment_state;
|
mod availability_assignment_state;
|
||||||
mod availability_changeset;
|
mod availability_changeset;
|
||||||
|
mod clothing;
|
||||||
mod event;
|
mod event;
|
||||||
mod event_changeset;
|
mod event_changeset;
|
||||||
mod function;
|
mod function;
|
||||||
@ -25,6 +26,7 @@ pub use availability_assignment_state::AvailabilityAssignmentState;
|
|||||||
pub use availability_changeset::{
|
pub use availability_changeset::{
|
||||||
find_free_date_time_slots, AvailabilityChangeset, AvailabilityContext,
|
find_free_date_time_slots, AvailabilityChangeset, AvailabilityContext,
|
||||||
};
|
};
|
||||||
|
pub use clothing::Clothing;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
pub use event_changeset::{EventChangeset, EventContext};
|
pub use event_changeset::{EventChangeset, EventContext};
|
||||||
pub use function::Function;
|
pub use function::Function;
|
||||||
|
15
web/templates/clothing/clothing_entry_edit.html
Normal file
15
web/templates/clothing/clothing_entry_edit.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<li>
|
||||||
|
<input class="input" type="text" value="{{ c.name }}" />
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button">
|
||||||
|
<svg class="icon">
|
||||||
|
<use href="/static/feather-sprite.svg#check-circle" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="button">
|
||||||
|
<svg class="icon">
|
||||||
|
<use href="/static/feather-sprite.svg#x-circle" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
15
web/templates/clothing/clothing_entry_read.html
Normal file
15
web/templates/clothing/clothing_entry_read.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<li>
|
||||||
|
{{ c.name }}"
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button">
|
||||||
|
<svg class="icon">
|
||||||
|
<use href="/static/feather-sprite.svg#edit" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="button">
|
||||||
|
<svg class="icon">
|
||||||
|
<use href="/static/feather-sprite.svg#x-circle" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
20
web/templates/clothing/overview.html
Normal file
20
web/templates/clothing/overview.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "nav.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h3 class="title is-3">
|
||||||
|
Anzugsordnungen
|
||||||
|
</h3>
|
||||||
|
<p class="content">zur Auswahl bei der Erstellung von Events</p>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<ul>
|
||||||
|
{% for c in clothings %}
|
||||||
|
{% include "clothing_entry_read.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cell is-col-span-2">
|
<div class="cell is-col-span-2">
|
||||||
<p><b>Anzugsordnung:</b> {{ event.clothing }}</p>
|
<p><b>Anzugsordnung:</b> {{ event.clothing.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cell is-col-span-2">
|
<div class="cell is-col-span-2">
|
||||||
|
@ -127,7 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cell is-col-span-2">
|
<div class="cell is-col-span-2">
|
||||||
<p><b>Anzugsordnung:</b> {{ event.clothing }}</p>
|
<p><b>Anzugsordnung:</b> {{ event.clothing.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if let Some(note) = event.note %}
|
{% if let Some(note) = event.note %}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false"
|
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false"
|
||||||
hx-on:click="[this, document.getElementById('navMenu')].forEach(e => e.classList.toggle('is-active'));">
|
hx-on:click="[this, document.getElementById('navMenu')].forEach(e => e.classList.toggle('is-active'));">
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
@ -51,6 +51,12 @@
|
|||||||
<a href="/vehicles" class="navbar-item">
|
<a href="/vehicles" class="navbar-item">
|
||||||
Fahrzeuge
|
Fahrzeuge
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{% if user.role == Role::Admin %}
|
||||||
|
<a href="/clothing" class="navbar-item">
|
||||||
|
Anzugsordnung
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user