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_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,
|
||||
};
|
||||
|
||||
|
@ -30,7 +30,7 @@ pub struct NewOrEditEventTemplate {
|
||||
voluntary_wachhabender: bool,
|
||||
voluntary_fuehrungsassistent: bool,
|
||||
amount_of_posten: Option<i16>,
|
||||
clothing: Option<String>,
|
||||
clothing: Option<i32>,
|
||||
canceled: bool,
|
||||
note: Option<String>,
|
||||
amount_of_planned_posten: usize,
|
||||
@ -49,7 +49,7 @@ pub struct NewOrEditEventForm {
|
||||
voluntarywachhabender: bool,
|
||||
voluntaryfuehrungsassistent: bool,
|
||||
amount: i16,
|
||||
clothing: String,
|
||||
clothing: i32,
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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(),
|
||||
|
@ -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;
|
||||
|
||||
|
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 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<String>,
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
|
@ -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<String>,
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ impl EventChangeset {
|
||||
voluntary_wachhabender: true,
|
||||
voluntary_fuehrungsassistent: true,
|
||||
amount_of_posten: 5,
|
||||
clothing: "Tuchuniform".to_string(),
|
||||
clothing: 1,
|
||||
note: None,
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
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 class="cell is-col-span-2">
|
||||
<p><b>Anzugsordnung:</b> {{ event.clothing }}</p>
|
||||
<p><b>Anzugsordnung:</b> {{ event.clothing.name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="cell is-col-span-2">
|
||||
|
@ -127,7 +127,7 @@
|
||||
</div>
|
||||
|
||||
<div class="cell is-col-span-2">
|
||||
<p><b>Anzugsordnung:</b> {{ event.clothing }}</p>
|
||||
<p><b>Anzugsordnung:</b> {{ event.clothing.name }}</p>
|
||||
</div>
|
||||
|
||||
{% if let Some(note) = event.note %}
|
||||
|
@ -8,7 +8,7 @@
|
||||
</a>
|
||||
|
||||
<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>
|
||||
@ -51,6 +51,12 @@
|
||||
<a href="/vehicles" class="navbar-item">
|
||||
Fahrzeuge
|
||||
</a>
|
||||
|
||||
{% if user.role == Role::Admin %}
|
||||
<a href="/clothing" class="navbar-item">
|
||||
Anzugsordnung
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user