Add item classes
This commit is contained in:
parent
ae50056252
commit
775bc6ba9e
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -595,6 +595,18 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diesel-derive-enum"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81c5131a2895ef64741dad1d483f358c2a229a3a2d1b256778cdc5e146db64d4"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.4.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel_derives"
|
name = "diesel_derives"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
@ -646,7 +658,7 @@ checksum = "0892a17df262a24294c382f0d5997571006e7a4348b4327557c4ff1cd4a8bccc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"either",
|
"either",
|
||||||
"heck",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
@ -803,6 +815,12 @@ version = "0.14.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -915,6 +933,7 @@ dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_actix",
|
"askama_actix",
|
||||||
"diesel",
|
"diesel",
|
||||||
|
"diesel-derive-enum",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
|
|
|
@ -15,6 +15,7 @@ actix-web = "4.8.0"
|
||||||
askama = { version = "0.12.1", features = ["with-actix-web"] }
|
askama = { version = "0.12.1", features = ["with-actix-web"] }
|
||||||
askama_actix = "0.14.0"
|
askama_actix = "0.14.0"
|
||||||
diesel = { version = "2.2.1", features = ["postgres", "r2d2", "uuid"] }
|
diesel = { version = "2.2.1", features = ["postgres", "r2d2", "uuid"] }
|
||||||
|
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||||
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
|
12
migrations/2024-07-05-104056_create_item_class/down.sql
Normal file
12
migrations/2024-07-05-104056_create_item_class/down.sql
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
ALTER TABLE items
|
||||||
|
DROP class;
|
||||||
|
|
||||||
|
DROP TABLE item_classes;
|
||||||
|
|
||||||
|
DROP TYPE item_class_type;
|
||||||
|
|
||||||
|
DROP FUNCTION check_item_class_parent;
|
47
migrations/2024-07-05-104056_create_item_class/up.sql
Normal file
47
migrations/2024-07-05-104056_create_item_class/up.sql
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
-- generic is a category of products (like CR2032 3 V battery)
|
||||||
|
-- or a product where details do not matter (like USB-A to USB-Micro-B cable 1 m)
|
||||||
|
-- specific is a concrete model of product (like Panasonic DL2032)
|
||||||
|
CREATE TYPE item_class_type AS ENUM ('generic', 'specific');
|
||||||
|
|
||||||
|
CREATE TABLE item_classes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
type item_class_type NOT NULL DEFAULT 'generic',
|
||||||
|
parent UUID REFERENCES item_classes(id),
|
||||||
|
CONSTRAINT parent_only_for_specific CHECK (type = 'generic' AND parent IS NULL OR type = 'specific' AND parent IS NOT NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO item_classes (id, name) values ('afd0e9bd-c4df-425e-8af4-6b5b0326a4ae', 'Default Item Class');
|
||||||
|
|
||||||
|
ALTER TABLE items
|
||||||
|
ADD class UUID NOT NULL REFERENCES item_classes(id) DEFAULT 'afd0e9bd-c4df-425e-8af4-6b5b0326a4ae';
|
||||||
|
ALTER TABLE items
|
||||||
|
ALTER class DROP DEFAULT;
|
||||||
|
|
||||||
|
DELETE FROM item_classes
|
||||||
|
WHERE (id = 'afd0e9bd-c4df-425e-8af4-6b5b0326a4ae'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM items WHERE class = 'afd0e9bd-c4df-425e-8af4-6b5b0326a4ae' LIMIT 1));
|
||||||
|
|
||||||
|
CREATE FUNCTION check_item_class_parent()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.parent IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT type FROM item_classes WHERE id = NEW.parent) = 'generic' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE EXCEPTION 'Specific item classes may only have a generic parent (to avoid recursion)';
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER prevent_item_class_recursion
|
||||||
|
BEFORE INSERT OR UPDATE ON item_classes
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION check_item_class_parent();
|
83
src/api/v1/item_class.rs
Normal file
83
src/api/v1/item_class.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use actix_web::{error, get, post, put, web, HttpResponse, Responder};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::*;
|
||||||
|
use crate::DbPool;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(add)
|
||||||
|
.service(list)
|
||||||
|
.service(show)
|
||||||
|
.service(items)
|
||||||
|
.service(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/item-class")]
|
||||||
|
async fn add(
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
new_item_class: web::Json<NewItemClass>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let item_class = web::block(move || {
|
||||||
|
manage::item_class::add(&mut pool.get().unwrap(), new_item_class.into_inner())
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(item_class))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-class")]
|
||||||
|
async fn list(pool: web::Data<DbPool>) -> actix_web::Result<impl Responder> {
|
||||||
|
let item_classes = web::block(move || manage::item_class::get_all(&mut pool.get().unwrap()))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(item_classes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-class/{id}")]
|
||||||
|
async fn show(pool: web::Data<DbPool>, path: web::Path<Uuid>) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item_class = web::block(move || manage::item_class::get(&mut pool.get().unwrap(), id))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(item_class))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-class/{id}/items")]
|
||||||
|
async fn items(
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let items = web::block(move || manage::item_class::items(&mut pool.get().unwrap(), id))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(items))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item-class/{id}")]
|
||||||
|
async fn update(
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
new_item_class: web::Json<NewItemClass>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item_class = web::block(move || {
|
||||||
|
manage::item_class::update(&mut pool.get().unwrap(), id, new_item_class.into_inner())
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(item_class))
|
||||||
|
}
|
|
@ -3,9 +3,10 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
mod item;
|
mod item;
|
||||||
|
mod item_class;
|
||||||
|
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.configure(item::config);
|
cfg.configure(item::config).configure(item_class::config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
||||||
use askama_actix::Template;
|
use askama_actix::Template;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -24,6 +26,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
struct ItemDetails {
|
struct ItemDetails {
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
item: Item,
|
item: Item,
|
||||||
|
item_class: ItemClass,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/item/{id}")]
|
#[get("/item/{id}")]
|
||||||
|
@ -34,11 +37,22 @@ async fn show_item(
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
let item = web::block(move || manage::item::get(&mut pool.get().unwrap(), id))
|
let mut conn = pool.get().unwrap();
|
||||||
|
|
||||||
|
let item = web::block(move || manage::item::get(&mut conn, id))
|
||||||
.await?
|
.await?
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Ok(ItemDetails { req, item })
|
let item_class =
|
||||||
|
web::block(move || manage::item_class::get(&mut pool.get().unwrap(), item.class))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ItemDetails {
|
||||||
|
req,
|
||||||
|
item,
|
||||||
|
item_class,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
@ -46,6 +60,7 @@ async fn show_item(
|
||||||
struct ItemList {
|
struct ItemList {
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
items: Vec<Item>,
|
items: Vec<Item>,
|
||||||
|
item_classes: HashMap<Uuid, ItemClass>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/items")]
|
#[get("/items")]
|
||||||
|
@ -53,11 +68,22 @@ async fn list_items(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
pool: web::Data<DbPool>,
|
pool: web::Data<DbPool>,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let items = web::block(move || manage::item::get_all(&mut pool.get().unwrap()))
|
let mut conn = pool.get().unwrap();
|
||||||
|
|
||||||
|
let items = web::block(move || manage::item::get_all(&mut conn))
|
||||||
.await?
|
.await?
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Ok(ItemList { req, items })
|
let item_classes =
|
||||||
|
web::block(move || manage::item_class::get_all_as_map(&mut pool.get().unwrap()))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ItemList {
|
||||||
|
req,
|
||||||
|
items,
|
||||||
|
item_classes,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
140
src/frontend/item_class.rs
Normal file
140
src/frontend/item_class.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
||||||
|
use askama_actix::Template;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::*;
|
||||||
|
use crate::DbPool;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(show_item_class)
|
||||||
|
.service(list_item_classes)
|
||||||
|
.service(add_item_class)
|
||||||
|
.service(add_item_class_post)
|
||||||
|
.service(edit_item_class)
|
||||||
|
.service(edit_item_class_post);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "item_class_details.html")]
|
||||||
|
struct ItemClassDetails {
|
||||||
|
req: HttpRequest,
|
||||||
|
item_class: ItemClass,
|
||||||
|
parent: Option<ItemClass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-class/{id}")]
|
||||||
|
async fn show_item_class(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
// FIXME hack
|
||||||
|
let pool_ = pool.clone();
|
||||||
|
|
||||||
|
let item_class = web::block(move || manage::item_class::get(&mut pool.get().unwrap(), id))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let parent = web::block(move || {
|
||||||
|
item_class.parent.map_or(Ok(None), |id| {
|
||||||
|
manage::item_class::get(&mut pool_.get().unwrap(), id).map(Some)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ItemClassDetails {
|
||||||
|
req,
|
||||||
|
item_class,
|
||||||
|
parent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "item_class_list.html")]
|
||||||
|
struct ItemClassList {
|
||||||
|
req: HttpRequest,
|
||||||
|
item_classes: Vec<ItemClass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-classes")]
|
||||||
|
async fn list_item_classes(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let item_classes = web::block(move || manage::item_class::get_all(&mut pool.get().unwrap()))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ItemClassList { req, item_classes })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "item_class_add.html")]
|
||||||
|
struct ItemClassAddForm {
|
||||||
|
req: HttpRequest,
|
||||||
|
data: Option<NewItemClass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-classes/add")]
|
||||||
|
async fn add_item_class(req: HttpRequest) -> actix_web::Result<impl Responder> {
|
||||||
|
Ok(ItemClassAddForm { req, data: None })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item-classes/add")]
|
||||||
|
async fn add_item_class_post(
|
||||||
|
data: web::Form<NewItemClass>,
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let item =
|
||||||
|
web::block(move || manage::item_class::add(&mut pool.get().unwrap(), data.into_inner()))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
Ok(web::Redirect::to("/item-class/".to_owned() + &item.id.to_string()).see_other())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "item_class_edit.html")]
|
||||||
|
struct ItemClassEditForm {
|
||||||
|
req: HttpRequest,
|
||||||
|
item_class: ItemClass,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-class/{id}/edit")]
|
||||||
|
async fn edit_item_class(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item_class = web::block(move || manage::item_class::get(&mut pool.get().unwrap(), id))
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ItemClassEditForm { req, item_class })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item-class/{id}/edit")]
|
||||||
|
async fn edit_item_class_post(
|
||||||
|
pool: web::Data<DbPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
data: web::Form<NewItemClass>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item_class = web::block(move || {
|
||||||
|
manage::item_class::update(&mut pool.get().unwrap(), id, data.into_inner())
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(web::Redirect::to("/item-class/".to_owned() + &item_class.id.to_string()).see_other())
|
||||||
|
}
|
|
@ -3,12 +3,15 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
mod item;
|
mod item;
|
||||||
|
mod item_class;
|
||||||
|
|
||||||
use actix_web::{get, web, HttpRequest, Responder};
|
use actix_web::{get, web, HttpRequest, Responder};
|
||||||
use askama_actix::Template;
|
use askama_actix::Template;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(index).configure(item::config);
|
cfg.service(index)
|
||||||
|
.configure(item::config)
|
||||||
|
.configure(item_class::config);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
|
61
src/manage/item_class.rs
Normal file
61
src/manage/item_class.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use diesel::pg::PgConnection;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{models::*, schema};
|
||||||
|
|
||||||
|
pub fn add(
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
new_item_class: NewItemClass,
|
||||||
|
) -> Result<ItemClass, diesel::result::Error> {
|
||||||
|
diesel::insert_into(schema::item_classes::table)
|
||||||
|
.values(new_item_class)
|
||||||
|
.returning(ItemClass::as_returning())
|
||||||
|
.get_result(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(conn: &mut PgConnection, id: Uuid) -> Result<ItemClass, diesel::result::Error> {
|
||||||
|
schema::item_classes::table
|
||||||
|
.filter(schema::item_classes::id.eq(id))
|
||||||
|
.select(ItemClass::as_select())
|
||||||
|
.first(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all(conn: &mut PgConnection) -> Result<Vec<ItemClass>, diesel::result::Error> {
|
||||||
|
schema::item_classes::table
|
||||||
|
.select(ItemClass::as_select())
|
||||||
|
.load(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_as_map(
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
) -> Result<HashMap<Uuid, ItemClass>, diesel::result::Error> {
|
||||||
|
Ok(get_all(conn)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|ic| (ic.id, ic))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
id: Uuid,
|
||||||
|
modified_item_class: NewItemClass,
|
||||||
|
) -> Result<ItemClass, diesel::result::Error> {
|
||||||
|
diesel::update(schema::item_classes::table.filter(schema::item_classes::id.eq(id)))
|
||||||
|
.set(modified_item_class)
|
||||||
|
.returning(ItemClass::as_returning())
|
||||||
|
.get_result(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items(conn: &mut PgConnection, id: Uuid) -> Result<Vec<Item>, diesel::result::Error> {
|
||||||
|
schema::items::table
|
||||||
|
.filter(schema::items::class.eq(id))
|
||||||
|
.select(Item::as_select())
|
||||||
|
.load(conn)
|
||||||
|
}
|
|
@ -3,3 +3,4 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
pub mod item;
|
pub mod item;
|
||||||
|
pub mod item_class;
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
use diesel_derive_enum::DbEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -15,6 +18,7 @@ pub struct Item {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub parent: Option<Uuid>,
|
pub parent: Option<Uuid>,
|
||||||
|
pub class: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Insertable, Deserialize, AsChangeset)]
|
#[derive(Debug, Insertable, Deserialize, AsChangeset)]
|
||||||
|
@ -24,4 +28,49 @@ pub struct NewItem {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub parent: Option<Uuid>,
|
pub parent: Option<Uuid>,
|
||||||
|
pub class: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, DbEnum, PartialEq, Deserialize, Serialize)]
|
||||||
|
#[ExistingTypePath = "sql_types::ItemClassType"]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ItemClassType {
|
||||||
|
Generic,
|
||||||
|
Specific,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemClassType {
|
||||||
|
pub const VARIANTS: [ItemClassType; 2] = [Self::Generic, Self::Specific];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ItemClassType {
|
||||||
|
// FIXME: currently, the HTML form relies on this matching the serde serializsation.
|
||||||
|
// This should not be the case.
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Generic => write!(f, "generic"),
|
||||||
|
Self::Specific => write!(f, "specific"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Queryable, Selectable, Insertable, Serialize)]
|
||||||
|
#[diesel(table_name = item_classes)]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct ItemClass {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub r#type: ItemClassType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Insertable, Deserialize, AsChangeset)]
|
||||||
|
#[diesel(table_name = item_classes)]
|
||||||
|
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||||
|
pub struct NewItemClass {
|
||||||
|
pub name: String,
|
||||||
|
pub r#type: ItemClassType,
|
||||||
|
#[serde(default)]
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,18 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
pub mod sql_types {
|
||||||
|
#[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)]
|
||||||
|
#[diesel(postgres_type(name = "item_class_type"))]
|
||||||
|
pub struct ItemClassType;
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
items (id) {
|
items (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
parent -> Nullable<Uuid>,
|
parent -> Nullable<Uuid>,
|
||||||
|
class -> Uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,3 +23,19 @@ diesel::table! {
|
||||||
parents -> Array<Uuid>,
|
parents -> Array<Uuid>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use super::sql_types::ItemClassType;
|
||||||
|
|
||||||
|
item_classes (id) {
|
||||||
|
id -> Uuid,
|
||||||
|
name -> Varchar,
|
||||||
|
r#type -> ItemClassType,
|
||||||
|
parent -> Nullable<Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::joinable!(items -> item_classes (class));
|
||||||
|
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(item_classes, items);
|
||||||
|
|
|
@ -31,6 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link{% if req.path() == "/items" %} active{% endif %}" href="/items">Items</a>
|
<a class="nav-link{% if req.path() == "/items" %} active{% endif %}" href="/items">Items</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link{% if req.path() == "/item-classes" %} active{% endif %}" href="/item-classes">Item Classes</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input type="text" class="form-control" id="name" name="name" required{% if let Some(data) = data %} value="{{ data.name }}"{% endif %}>
|
<input type="text" class="form-control" id="name" name="name" required{% if let Some(data) = data %} value="{{ data.name }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="class" class="form-label">Class</label>
|
||||||
|
<input type="text" class="form-control" id="class" name="class" required{% if let Some(data) = data %} value="{{ data.class }}"{% endif %}>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="parent" class="form-label">Parent</label>
|
<label for="parent" class="form-label">Parent</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|
50
templates/item_class_add.html
Normal file
50
templates/item_class_add.html
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{#
|
||||||
|
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% block page_title %}Add Item Class{% endblock %} – {{ branding }}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required{% if let Some(data) = data %} value="{{ data.name }}"{% endif %}>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="type" class="form-label">Type</label>
|
||||||
|
<select class="form-select" id="type" name="type" required{% if let Some(data) = data %} value="{{ data.type }}"{% endif %}>
|
||||||
|
{% for variant in ItemClassType::VARIANTS %}
|
||||||
|
<option>{{ variant }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="parent" class="form-label">Parent</label>
|
||||||
|
<input type="text" class="form-control" id="parent" name="parent" disabled{% if let Some(data) = data %}{% if let Some(parent) = data.parent %} value="{{ parent }}"{% endif %}{% endif %}>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
document.getElementById("type").addEventListener("change", e => {
|
||||||
|
console.log(e)
|
||||||
|
let parentInput = document.getElementById("parent")
|
||||||
|
switch (e.target.value) {
|
||||||
|
case "generic":
|
||||||
|
parentInput.disabled = true
|
||||||
|
parentInput.value = ""
|
||||||
|
break
|
||||||
|
case "specific":
|
||||||
|
parentInput.disabled = false
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error("invalid type!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
51
templates/item_class_details.html
Normal file
51
templates/item_class_details.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{#
|
||||||
|
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% block page_title %}{{ item_class.name }}{% endblock %} – Item Class Details – {{ branding }}{% endblock %}
|
||||||
|
{% block page_actions %}
|
||||||
|
<a class="btn btn-warning" href="/item-class/{{ item_class.id }}/edit">Edit</a>
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
UUID
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{ item_class.id }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{ item_class.name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{{ item_class.type }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if let Some(parent) = parent -%}
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Parent
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<a href="/item-class/{{ parent.id }}">{{ parent.name }} ({{ parent.id }})</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{%- endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
52
templates/item_class_edit.html
Normal file
52
templates/item_class_edit.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{#
|
||||||
|
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% block page_title %}{{ item_class.name }}{% endblock %} – Edit Item Class – {{ branding }}{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="uuid" class="form-label">UUID</label>
|
||||||
|
<input type="text" class="form-control" id="uuid" disabled required value="{{ item_class.id }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required value="{{ item_class.name }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="type" class="form-label">Type</label>
|
||||||
|
<select class="form-select" id="type" name="type" required value="{{ item_class.type }}">
|
||||||
|
{% for variant in ItemClassType::VARIANTS %}
|
||||||
|
<option>{{ variant }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="parent" class="form-label">Parent</label>
|
||||||
|
<input type="text" class="form-control" id="parent" name="parent"{% if let Some(parent) = item_class.parent %} value="{{ parent }}"{% else %} disabled{% endif %}>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Edit</button>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
document.getElementById("type").addEventListener("change", e => {
|
||||||
|
console.log(e)
|
||||||
|
let parentInput = document.getElementById("parent")
|
||||||
|
switch (e.target.value) {
|
||||||
|
case "generic":
|
||||||
|
parentInput.disabled = true
|
||||||
|
parentInput.value = ""
|
||||||
|
break
|
||||||
|
case "specific":
|
||||||
|
parentInput.disabled = false
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error("invalid type!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
31
templates/item_class_list.html
Normal file
31
templates/item_class_list.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{#
|
||||||
|
SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% block page_title %}Item Class List{% endblock %} – {{ branding }}{% endblock %}
|
||||||
|
{% block page_actions %}
|
||||||
|
<a class="btn btn-primary" href="/item-classes/add">Add</a>
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>UUID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item_class in item_classes -%}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/item-class/{{ item_class.id }}">{{ item_class.id }}</a></td>
|
||||||
|
<td>{{ item_class.name }}</td>
|
||||||
|
<td>{{ item_class.type }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor -%}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -28,6 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Class
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<a href="/item-class/{{ item.class }}">{{ item_class.name }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
Parent
|
Parent
|
||||||
|
|
|
@ -16,6 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input type="text" class="form-control" id="name" name="name" required value="{{ item.name }}">
|
<input type="text" class="form-control" id="name" name="name" required value="{{ item.name }}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="class" class="form-label">Class</label>
|
||||||
|
<input type="text" class="form-control" id="class" name="class" required value="{{ item.class }}">
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="parent" class="form-label">Parent</label>
|
<label for="parent" class="form-label">Parent</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|
|
@ -15,13 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
<tr>
|
<tr>
|
||||||
<th>UUID</th>
|
<th>UUID</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Class</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in items -%}
|
{% for item in items -%}
|
||||||
|
{% let class = item_classes.get(item.class).unwrap() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/item/{{ item.id }}">{{ item.id }}</a></td>
|
<td><a href="/item/{{ item.id }}">{{ item.id }}</a></td>
|
||||||
<td>{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
|
<td><a href="/item-class/{{ class.id }}">{{ class.name }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
Loading…
Reference in a new issue