Compare commits
No commits in common. "b8184c459ba04ba11118d4d78be20805cae98e1a" and "35230b6c37e1bdc404390999ab32db7924c2ca8c" have entirely different histories.
b8184c459b
...
35230b6c37
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.original_packaging = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "class_name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "02438c29c249ce2b446d15a41d07c40ce73946e62744a486cbbac256c4a4cf55"
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "INSERT INTO item_events (item, date, event, description)\n VALUES ($1, $2, $3, $4)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid",
|
|
||||||
"Date",
|
|
||||||
{
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_event",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"acquire",
|
|
||||||
"borrow",
|
|
||||||
"buy",
|
|
||||||
"dispose",
|
|
||||||
"gift",
|
|
||||||
"loan",
|
|
||||||
"lose",
|
|
||||||
"recieve_gift",
|
|
||||||
"return_borrowed",
|
|
||||||
"return_loaned",
|
|
||||||
"sell",
|
|
||||||
"use"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Varchar"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "11f1edc0dce92fbd127159dbebd72b16de479d071f03433b480087bef99f5b1c"
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "SELECT\n event AS \"event: ItemEvent\",\n next AS \"next: ItemState\"\n FROM item_events_transitions\n WHERE state = $1",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "event: ItemEvent",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_event",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"acquire",
|
|
||||||
"borrow",
|
|
||||||
"buy",
|
|
||||||
"dispose",
|
|
||||||
"gift",
|
|
||||||
"loan",
|
|
||||||
"lose",
|
|
||||||
"recieve_gift",
|
|
||||||
"return_borrowed",
|
|
||||||
"return_loaned",
|
|
||||||
"sell",
|
|
||||||
"use"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "next: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
{
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "1b860018f6cd1b18d28a87ba9da1f96fd7c8021c5e2ea652e6f6fe11a823c32c"
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n WITH RECURSIVE cte AS (\n SELECT\n id,\n ARRAY[]::UUID[] AS parents,\n ARRAY[]::VARCHAR[] AS parent_names,\n ARRAY[]::VARCHAR[] AS parent_class_names\n FROM items\n WHERE parent IS NULL\n\n UNION\n\n SELECT\n items.id,\n cte.parents || items.parent,\n cte.parent_names || parent.name,\n cte.parent_class_names || parent_class.name\n FROM cte\n JOIN items\n ON items.parent = cte.id\n JOIN items AS \"parent\"\n ON parent.id = cte.id\n JOIN item_classes AS \"parent_class\"\n ON parent.class = parent_class.id\n )\n SELECT\n cte.id AS \"id!\",\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n cte.parents AS \"parents!\",\n cte.parent_names AS \"parent_names!: Vec<Option<String>>\",\n cte.parent_class_names AS \"parent_class_names!\",\n item_states.state AS \"state!: ItemState\"\n FROM cte\n JOIN items\n ON cte.id = items.id\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n ORDER BY items.created_at\n ",
|
"query": "\n WITH RECURSIVE cte AS (\n SELECT\n id,\n ARRAY[]::UUID[] AS parents,\n ARRAY[]::VARCHAR[] AS parent_names,\n ARRAY[]::VARCHAR[] AS parent_class_names\n FROM items\n WHERE parent IS NULL\n\n UNION\n\n SELECT\n items.id,\n cte.parents || items.parent,\n cte.parent_names || parent.name,\n cte.parent_class_names || parent_class.name\n FROM cte\n JOIN items\n ON items.parent = cte.id\n JOIN items AS \"parent\"\n ON parent.id = cte.id\n JOIN item_classes AS \"parent_class\"\n ON parent.class = parent_class.id\n )\n SELECT\n cte.id AS \"id!\",\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n cte.parents AS \"parents!\",\n cte.parent_names AS \"parent_names!: Vec<Option<String>>\",\n cte.parent_class_names AS \"parent_class_names!\"\n FROM cte\n JOIN items\n ON cte.id = items.id\n JOIN item_classes\n ON items.class = item_classes.id\n ORDER BY items.created_at\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
@ -37,23 +37,6 @@
|
||||||
"ordinal": 6,
|
"ordinal": 6,
|
||||||
"name": "parent_class_names!",
|
"name": "parent_class_names!",
|
||||||
"type_info": "VarcharArray"
|
"type_info": "VarcharArray"
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 7,
|
|
||||||
"name": "state!: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
@ -66,9 +49,8 @@
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null
|
||||||
true
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "3fe94e76159c7911db688854710271e351ca34273dfdb21a7499f588715a91ee"
|
"hash": "3dedb7b184103c1d418f7b94e26c75aea0c7d22e009299d1b87443e350578171"
|
||||||
}
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.class = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "class_name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "482df01f0509cc8ec18ffe1caea8f65f11c170b67424e35697540ae12e577227"
|
||||||
|
}
|
|
@ -1,106 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "SELECT\n items.id,\n items.short_id,\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n items.original_packaging,\n op.name AS \"original_packaging_name?\",\n op_class.name AS \"original_packaging_class_name?\",\n op_state.state AS \"original_packaging_state: ItemState\",\n items.description,\n item_states.state AS \"state!: ItemState\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n LEFT JOIN items AS \"op\"\n ON items.original_packaging = op.id\n LEFT JOIN item_classes AS \"op_class\"\n ON op.class = op_class.id\n LEFT JOIN item_states AS \"op_state\"\n ON op.id = op_state.item\n WHERE items.id = $1",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "short_id",
|
|
||||||
"type_info": "Int4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "class",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 4,
|
|
||||||
"name": "class_name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 5,
|
|
||||||
"name": "original_packaging",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 6,
|
|
||||||
"name": "original_packaging_name?",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 7,
|
|
||||||
"name": "original_packaging_class_name?",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 8,
|
|
||||||
"name": "original_packaging_state: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 9,
|
|
||||||
"name": "description",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 10,
|
|
||||||
"name": "state!: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "8683674fed595842f0369f2dafbc11c7f9f031931a5a4f8b43f3ffa1b8b6d10b"
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "SELECT\n items.id,\n items.name,\n item_classes.name AS \"class_name\",\n item_states.state AS \"state!: ItemState\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n WHERE items.parent = $1",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "class_name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "state!: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "915945504790c0295800c3e5dae1d38dba1c47e9be6cbf120313263336b8ca21"
|
|
||||||
}
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT\n items.id,\n items.short_id,\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n items.original_packaging,\n op.name AS \"original_packaging_name?\",\n op_class.name AS \"original_packaging_class_name?\",\n items.description\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n LEFT JOIN items AS \"op\"\n ON items.original_packaging = op.id\n LEFT JOIN item_classes AS \"op_class\"\n ON op.class = op_class.id\n WHERE items.id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "short_id",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "class",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "class_name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "original_packaging",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "original_packaging_name?",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "original_packaging_class_name?",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "description",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9c1a1e9c33539f6ec4800c9e8fc8ce3b65f49c34ad848321dc1b151c21cb0043"
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.parent = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "class_name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9e1704d0b60906061460e2c972fccf5d87b053857c161651360e5a8fdd80e397"
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "SELECT\n items.id,\n items.name,\n item_classes.name AS \"class_name\",\n item_states.state AS \"state!: ItemState\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n WHERE items.class = $1",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "class_name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "state!: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "a0ac133f1dce54853319b776ff6e7d3f186dc380e091858f85950719d73eee67"
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "SELECT\n items.id,\n items.name,\n item_classes.name AS \"class_name\",\n item_states.state AS \"state!: ItemState\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n WHERE items.original_packaging = $1",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "class_name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "state!: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "cad9b7d82b518e4a88b62fa5ea335a2451af8fafeab425868d7196ed9f078874"
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "DELETE FROM item_events WHERE id = $1 RETURNING item",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "item",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int4"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "ce9d4ef7aff0cfbbace291d10e459771013e090463d7b858a26df1295e31184a"
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "SELECT id, date, event AS \"event: ItemEvent\", description FROM item_events WHERE item = $1",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type_info": "Int4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "date",
|
|
||||||
"type_info": "Date"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "event: ItemEvent",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_event",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"acquire",
|
|
||||||
"borrow",
|
|
||||||
"buy",
|
|
||||||
"dispose",
|
|
||||||
"gift",
|
|
||||||
"loan",
|
|
||||||
"lose",
|
|
||||||
"recieve_gift",
|
|
||||||
"return_borrowed",
|
|
||||||
"return_loaned",
|
|
||||||
"sell",
|
|
||||||
"use"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "description",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "f700f5e4657cfb3d572e9f11c23d408c8f31c8d75737398429e256a167c91d68"
|
|
||||||
}
|
|
55
Cargo.lock
generated
55
Cargo.lock
generated
|
@ -540,46 +540,6 @@ dependencies = [
|
||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap"
|
|
||||||
version = "4.5.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142"
|
|
||||||
dependencies = [
|
|
||||||
"clap_builder",
|
|
||||||
"clap_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_builder"
|
|
||||||
version = "4.5.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac"
|
|
||||||
dependencies = [
|
|
||||||
"anstream",
|
|
||||||
"anstyle",
|
|
||||||
"clap_lex",
|
|
||||||
"strsim",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_derive"
|
|
||||||
version = "4.5.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
|
||||||
dependencies = [
|
|
||||||
"heck 0.5.0",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.68",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_lex"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -1056,12 +1016,6 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heck"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
@ -1216,7 +1170,6 @@ dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"barcoders",
|
"barcoders",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"clap",
|
|
||||||
"datamatrix",
|
"datamatrix",
|
||||||
"enum-iterator",
|
"enum-iterator",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
@ -2089,7 +2042,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
"heck 0.4.1",
|
"heck",
|
||||||
"hex",
|
"hex",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -2227,12 +2180,6 @@ dependencies = [
|
||||||
"unicode-properties",
|
"unicode-properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
|
@ -15,7 +15,6 @@ actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
||||||
actix-web = { version = "4.8.0", features = ["cookies"] }
|
actix-web = { version = "4.8.0", features = ["cookies"] }
|
||||||
barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
|
barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
clap = { version = "4.5.10", features = ["derive", "env"] }
|
|
||||||
datamatrix = "0.3.1"
|
datamatrix = "0.3.1"
|
||||||
enum-iterator = "2.1.0"
|
enum-iterator = "2.1.0"
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
|
@ -32,7 +31,7 @@ serde_urlencoded = "0.7.1"
|
||||||
serde_variant = "0.1.3"
|
serde_variant = "0.1.3"
|
||||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid", "time"] }
|
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid", "time"] }
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
time = { version = "0.3.36", features = ["parsing", "serde"] }
|
time = { version = "0.3.36", features = ["serde"] }
|
||||||
uuid = { version = "1.9.0", features = ["serde", "v4"] }
|
uuid = { version = "1.9.0", features = ["serde", "v4"] }
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
|
|
|
@ -105,7 +105,6 @@
|
||||||
cargo-deny
|
cargo-deny
|
||||||
cargo-watch
|
cargo-watch
|
||||||
clippy
|
clippy
|
||||||
graphviz
|
|
||||||
postgresql.lib
|
postgresql.lib
|
||||||
postgresql
|
postgresql
|
||||||
reuse
|
reuse
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
digraph item_status {
|
|
||||||
rankdir=LR;
|
|
||||||
node [shape=point]; q0;
|
|
||||||
node [shape=circle]; owned loaned borrowed inactive;
|
|
||||||
q0 -> inactive;
|
|
||||||
// s/\(.*\) -> \(.*\) \[label="\(.*\)"\];/('\1', '\3', '\2'),/
|
|
||||||
inactive -> owned [label="buy"];
|
|
||||||
inactive -> owned [label="recieve_gift"];
|
|
||||||
inactive -> owned [label="acquire"]; // generic, also used as fallback for older items
|
|
||||||
inactive -> borrowed [label="borrow"];
|
|
||||||
owned -> inactive [label="sell"];
|
|
||||||
owned -> inactive [label="gift"];
|
|
||||||
owned -> inactive [label="lose"];
|
|
||||||
owned -> inactive [label="use"];
|
|
||||||
owned -> inactive [label="dispose"];
|
|
||||||
owned -> loaned [label="loan"];
|
|
||||||
loaned -> owned [label="return_loaned"];
|
|
||||||
borrowed -> inactive [label="return_borrowed"];
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
DROP TRIGGER add_default_item_event ON items;
|
|
||||||
DROP FUNCTION add_default_item_event;
|
|
||||||
|
|
||||||
DROP VIEW item_states;
|
|
||||||
|
|
||||||
DROP TRIGGER check_item_events_delete ON item_events;
|
|
||||||
DROP FUNCTION check_item_events_delete;
|
|
||||||
DROP TRIGGER check_item_events_fsm ON item_events;
|
|
||||||
DROP FUNCTION check_item_events_fsm;
|
|
||||||
DROP TABLE item_events;
|
|
||||||
|
|
||||||
DROP AGGREGATE item_events_fsm(item_event);
|
|
||||||
DROP FUNCTION item_events_transition;
|
|
||||||
DROP TABLE item_events_transitions;
|
|
||||||
DROP TYPE item_state;
|
|
||||||
DROP TYPE item_event;
|
|
|
@ -1,153 +0,0 @@
|
||||||
-- SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
-- This datbase design is inspired by the following two blog posts by Felix Geisendörfer and Raphael Medaer:
|
|
||||||
--
|
|
||||||
-- https://felixge.de/2017/07/27/implementing-state-machines-in-postgresql/
|
|
||||||
-- https://raphael.medaer.me/2019/06/12/pgfsm.html
|
|
||||||
--
|
|
||||||
-- I chose a design that makes a compromise between both designs.
|
|
||||||
-- To simplify modifying transitions, it uses a table for storing the transitions,
|
|
||||||
-- but it does not version it, as the design should not radically change.
|
|
||||||
|
|
||||||
CREATE TYPE item_state AS ENUM (
|
|
||||||
'borrowed',
|
|
||||||
'inactive',
|
|
||||||
'loaned',
|
|
||||||
'owned'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TYPE item_event AS ENUM (
|
|
||||||
'acquire',
|
|
||||||
'borrow',
|
|
||||||
'buy',
|
|
||||||
'dispose',
|
|
||||||
'gift',
|
|
||||||
'loan',
|
|
||||||
'lose',
|
|
||||||
'recieve_gift',
|
|
||||||
'return_borrowed',
|
|
||||||
'return_loaned',
|
|
||||||
'sell',
|
|
||||||
'use'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE item_events_transitions (
|
|
||||||
state item_state,
|
|
||||||
event item_event,
|
|
||||||
next item_state,
|
|
||||||
PRIMARY KEY (state, event, next)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO item_events_transitions VALUES
|
|
||||||
('inactive', 'buy', 'owned'),
|
|
||||||
('inactive', 'recieve_gift', 'owned'),
|
|
||||||
('inactive', 'acquire', 'owned'),
|
|
||||||
('inactive', 'borrow', 'borrowed'),
|
|
||||||
('owned', 'sell', 'inactive'),
|
|
||||||
('owned', 'gift', 'inactive'),
|
|
||||||
('owned', 'lose', 'inactive'),
|
|
||||||
('owned', 'use', 'inactive'),
|
|
||||||
('owned', 'dispose', 'inactive'),
|
|
||||||
('owned', 'loan', 'loaned'),
|
|
||||||
('loaned', 'return_loaned', 'owned'),
|
|
||||||
('borrowed', 'return_borrowed', 'inactive');
|
|
||||||
|
|
||||||
CREATE FUNCTION item_events_transition(_state item_state, _event item_event)
|
|
||||||
RETURNS item_state AS $$
|
|
||||||
SELECT next
|
|
||||||
FROM item_events_transitions
|
|
||||||
WHERE state = _state AND event = _event;
|
|
||||||
$$ LANGUAGE sql STRICT;
|
|
||||||
|
|
||||||
CREATE AGGREGATE item_events_fsm(item_event) (
|
|
||||||
SFUNC = item_events_transition,
|
|
||||||
STYPE = item_state,
|
|
||||||
INITCOND = 'inactive'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE item_events (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
item UUID NOT NULL REFERENCES items(id),
|
|
||||||
date DATE NOT NULL DEFAULT now(),
|
|
||||||
event item_event NOT NULL,
|
|
||||||
description VARCHAR NOT NULL DEFAULT ''
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE FUNCTION check_item_events_fsm()
|
|
||||||
RETURNS trigger AS $$
|
|
||||||
BEGIN
|
|
||||||
IF (SELECT item_events_fsm(event ORDER BY id) FROM (
|
|
||||||
SELECT id, event FROM item_events WHERE item = NEW.item
|
|
||||||
UNION ALL
|
|
||||||
SELECT NEW.id, NEW.event
|
|
||||||
) events) IS NULL THEN
|
|
||||||
RAISE EXCEPTION 'Event not possible from current state';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN NEW;
|
|
||||||
END
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
CREATE TRIGGER check_item_events_fsm
|
|
||||||
BEFORE INSERT ON item_events
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE PROCEDURE check_item_events_fsm();
|
|
||||||
|
|
||||||
CREATE FUNCTION check_item_events_delete()
|
|
||||||
RETURNS trigger AS $$
|
|
||||||
BEGIN
|
|
||||||
IF (SELECT OLD.id <> max(id) FROM item_events WHERE item = OLD.item) THEN
|
|
||||||
RAISE EXCEPTION 'Only the last event of an item can be deleted';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN OLD;
|
|
||||||
END
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
CREATE TRIGGER check_item_events_delete
|
|
||||||
BEFORE DELETE ON item_events
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE PROCEDURE check_item_events_delete();
|
|
||||||
|
|
||||||
CREATE VIEW item_states AS
|
|
||||||
-- probably not the best query, but it works
|
|
||||||
SELECT
|
|
||||||
items.id AS "item",
|
|
||||||
state,
|
|
||||||
event AS "last_event",
|
|
||||||
date AS "last_event_date",
|
|
||||||
item_events.description AS "last_event_description"
|
|
||||||
FROM item_events
|
|
||||||
-- items without eny event must be included
|
|
||||||
RIGHT JOIN items
|
|
||||||
ON item_events.item = items.id
|
|
||||||
JOIN (
|
|
||||||
SELECT
|
|
||||||
item_events_fsm(event ORDER BY item_events.id) AS "state",
|
|
||||||
max(item_events.id) AS "id",
|
|
||||||
items.id AS "item"
|
|
||||||
FROM item_events
|
|
||||||
-- see above
|
|
||||||
RIGHT JOIN items
|
|
||||||
ON item_events.item = items.id
|
|
||||||
GROUP BY item_events.item, items.id
|
|
||||||
) last_event
|
|
||||||
ON items.id = last_event.item AND (item_events.id = last_event.id OR last_event.id IS NULL);
|
|
||||||
|
|
||||||
CREATE FUNCTION add_default_item_event()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO item_events (item, event, description)
|
|
||||||
VALUES (NEW.id, 'acquire', 'automatically added on item insert');
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
CREATE TRIGGER add_default_item_event
|
|
||||||
AFTER INSERT ON items
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION add_default_item_event();
|
|
||||||
|
|
||||||
INSERT INTO item_events (item, event, description)
|
|
||||||
SELECT id, 'acquire', 'automatically added on migration' FROM items;
|
|
|
@ -1,34 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
use std::net::{IpAddr, Ipv6Addr};
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
/// A lightweight inventory management system
|
|
||||||
#[derive(Clone, Parser, Debug)]
|
|
||||||
#[command(version, about)]
|
|
||||||
pub struct Config {
|
|
||||||
/// Database URL of PostgreSQL database
|
|
||||||
#[arg(long, env)]
|
|
||||||
pub database_url: String,
|
|
||||||
|
|
||||||
/// Secret key for encrypting session cookie
|
|
||||||
///
|
|
||||||
/// Can be generated with head -c 64 /dev/urandom | base64 -w0
|
|
||||||
#[arg(long, env)]
|
|
||||||
pub secret_key: Option<String>,
|
|
||||||
|
|
||||||
/// Address for HTTP server to listen on
|
|
||||||
#[arg(long, env, default_value_t = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))]
|
|
||||||
pub listen_address: std::net::IpAddr,
|
|
||||||
|
|
||||||
/// Port for HTTP server to listen on
|
|
||||||
#[arg(long, env, default_value_t = 8080)]
|
|
||||||
pub listen_port: u16,
|
|
||||||
|
|
||||||
/// Superuser password
|
|
||||||
#[arg(long, env)]
|
|
||||||
pub superuser_password: String,
|
|
||||||
}
|
|
|
@ -59,10 +59,12 @@ async fn login(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
form: web::Form<LoginForm>,
|
form: web::Form<LoginForm>,
|
||||||
query: web::Query<LoginQuery>,
|
query: web::Query<LoginQuery>,
|
||||||
config: web::Data<crate::Config>,
|
|
||||||
) -> Result<impl Responder, error::Error> {
|
) -> Result<impl Responder, error::Error> {
|
||||||
// Very basic authentication for now (only password, hardcoded in configuration)
|
// Very basic authentication for now (only password, hardcoded in environment variable)
|
||||||
if form.password == config.superuser_password {
|
if form.password
|
||||||
|
== std::env::var("SUPERUSER_PASSWORD")
|
||||||
|
.map_err(|_| error::ErrorInternalServerError("login disabled (no password set)"))?
|
||||||
|
{
|
||||||
Identity::login(&req.extensions(), "superuser".into())
|
Identity::login(&req.extensions(), "superuser".into())
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
Ok(
|
Ok(
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
use actix_identity::Identity;
|
|
||||||
use actix_web::{error, post, web, Responder};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::{query, query_scalar, PgPool};
|
|
||||||
use time::Date;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::frontend::templates::helpers::ItemEvent;
|
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
|
||||||
cfg.service(delete).service(add);
|
|
||||||
}
|
|
||||||
|
|
||||||
// not the best HTTP method, but there is no (non-JS) way of sending a DELETE request
|
|
||||||
#[post("/items/event/{id}/delete")]
|
|
||||||
async fn delete(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<i32>,
|
|
||||||
_user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let item_id = query_scalar!("DELETE FROM item_events WHERE id = $1 RETURNING item", id)
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(web::Redirect::to(format!("/item/{}", item_id)).see_other())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct NewEvent {
|
|
||||||
date: Date,
|
|
||||||
event: ItemEvent,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/item/{id}/events/add")]
|
|
||||||
async fn add(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
data: web::Form<NewEvent>,
|
|
||||||
_user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let data = data.into_inner();
|
|
||||||
|
|
||||||
query!(
|
|
||||||
"INSERT INTO item_events (item, date, event, description)
|
|
||||||
VALUES ($1, $2, $3, $4)",
|
|
||||||
id,
|
|
||||||
data.date,
|
|
||||||
data.event as ItemEvent,
|
|
||||||
data.description
|
|
||||||
)
|
|
||||||
.execute(pool.as_ref())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(web::Redirect::to(format!("/item/{}", id)).see_other())
|
|
||||||
}
|
|
|
@ -10,9 +10,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::frontend::templates::{
|
use crate::frontend::templates::{
|
||||||
self,
|
self,
|
||||||
helpers::{
|
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod},
|
||||||
Colour, ItemName, ItemPreview, ItemState, PageAction, PageActionGroup, PageActionMethod,
|
|
||||||
},
|
|
||||||
TemplateConfig,
|
TemplateConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,7 +24,6 @@ struct ItemListEntry {
|
||||||
class: Uuid,
|
class: Uuid,
|
||||||
class_name: String,
|
class_name: String,
|
||||||
parents: Vec<ItemPreview>,
|
parents: Vec<ItemPreview>,
|
||||||
state: ItemState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/items")]
|
#[get("/items")]
|
||||||
|
@ -64,15 +61,12 @@ async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
|
||||||
item_classes.name AS "class_name",
|
item_classes.name AS "class_name",
|
||||||
cte.parents AS "parents!",
|
cte.parents AS "parents!",
|
||||||
cte.parent_names AS "parent_names!: Vec<Option<String>>",
|
cte.parent_names AS "parent_names!: Vec<Option<String>>",
|
||||||
cte.parent_class_names AS "parent_class_names!",
|
cte.parent_class_names AS "parent_class_names!"
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM cte
|
FROM cte
|
||||||
JOIN items
|
JOIN items
|
||||||
ON cte.id = items.id
|
ON cte.id = items.id
|
||||||
JOIN item_classes
|
JOIN item_classes
|
||||||
ON items.class = item_classes.id
|
ON items.class = item_classes.id
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
ORDER BY items.created_at
|
ORDER BY items.created_at
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
|
@ -84,7 +78,6 @@ async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
|
||||||
parents: itertools::izip!(row.parents, row.parent_names, row.parent_class_names)
|
parents: itertools::izip!(row.parents, row.parent_names, row.parent_class_names)
|
||||||
.map(|(id, name, class_name)| ItemPreview::from_parts(id, name.as_ref(), &class_name))
|
.map(|(id, name, class_name)| ItemPreview::from_parts(id, name.as_ref(), &class_name))
|
||||||
.collect(),
|
.collect(),
|
||||||
state: row.state,
|
|
||||||
})
|
})
|
||||||
.fetch_all(pool.as_ref())
|
.fetch_all(pool.as_ref())
|
||||||
.await
|
.await
|
||||||
|
@ -113,7 +106,6 @@ async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
|
||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
th { "Name" }
|
th { "Name" }
|
||||||
th { "State" }
|
|
||||||
th { "Class" }
|
th { "Class" }
|
||||||
th { "Parents" }
|
th { "Parents" }
|
||||||
}
|
}
|
||||||
|
@ -122,7 +114,6 @@ async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
|
||||||
@for item in items {
|
@for item in items {
|
||||||
tr {
|
tr {
|
||||||
td { (ItemPreview::new(item.id, item.name.clone().terse())) }
|
td { (ItemPreview::new(item.id, item.name.clone().terse())) }
|
||||||
td { (item.state) }
|
|
||||||
td { a href={ "/item-class/" (item.class) } { (item.class_name) } }
|
td { a href={ "/item-class/" (item.class) } { (item.class_name) } }
|
||||||
td { (templates::helpers::parents_breadcrumb(item.name, &item.parents, false)) }
|
td { (templates::helpers::parents_breadcrumb(item.name, &item.parents, false)) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
mod add;
|
mod add;
|
||||||
mod delete;
|
mod delete;
|
||||||
mod edit;
|
mod edit;
|
||||||
mod event;
|
|
||||||
mod list;
|
mod list;
|
||||||
mod show;
|
mod show;
|
||||||
|
|
||||||
|
@ -15,7 +14,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.configure(add::config)
|
cfg.configure(add::config)
|
||||||
.configure(delete::config)
|
.configure(delete::config)
|
||||||
.configure(edit::config)
|
.configure(edit::config)
|
||||||
.configure(event::config)
|
|
||||||
.configure(list::config)
|
.configure(list::config)
|
||||||
.configure(show::config);
|
.configure(show::config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,17 +5,12 @@
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, web, Responder};
|
use actix_web::{error, get, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde_variant::to_variant_name;
|
use sqlx::{query, PgPool};
|
||||||
use sqlx::{query, query_as, PgPool};
|
|
||||||
use time::{Date, OffsetDateTime};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::frontend::templates::{
|
use crate::frontend::templates::{
|
||||||
self, forms,
|
self,
|
||||||
helpers::{
|
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod},
|
||||||
Colour, ItemEvent, ItemName, ItemPreview, ItemState, PageAction, PageActionGroup,
|
|
||||||
PageActionMethod,
|
|
||||||
},
|
|
||||||
TemplateConfig,
|
TemplateConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,14 +26,6 @@ struct ItemDetails {
|
||||||
class_name: String,
|
class_name: String,
|
||||||
original_packaging: Option<ItemPreview>,
|
original_packaging: Option<ItemPreview>,
|
||||||
description: String,
|
description: String,
|
||||||
state: ItemState,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ItemEventDetails {
|
|
||||||
id: i32,
|
|
||||||
date: Date,
|
|
||||||
event: ItemEvent,
|
|
||||||
description: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/item/{id}")]
|
#[get("/item/{id}")]
|
||||||
|
@ -59,20 +46,14 @@ async fn get(
|
||||||
items.original_packaging,
|
items.original_packaging,
|
||||||
op.name AS "original_packaging_name?",
|
op.name AS "original_packaging_name?",
|
||||||
op_class.name AS "original_packaging_class_name?",
|
op_class.name AS "original_packaging_class_name?",
|
||||||
op_state.state AS "original_packaging_state: ItemState",
|
items.description
|
||||||
items.description,
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
FROM items
|
||||||
JOIN item_classes
|
JOIN item_classes
|
||||||
ON items.class = item_classes.id
|
ON items.class = item_classes.id
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
LEFT JOIN items AS "op"
|
LEFT JOIN items AS "op"
|
||||||
ON items.original_packaging = op.id
|
ON items.original_packaging = op.id
|
||||||
LEFT JOIN item_classes AS "op_class"
|
LEFT JOIN item_classes AS "op_class"
|
||||||
ON op.class = op_class.id
|
ON op.class = op_class.id
|
||||||
LEFT JOIN item_states AS "op_state"
|
|
||||||
ON op.id = op_state.item
|
|
||||||
WHERE items.id = $1"#,
|
WHERE items.id = $1"#,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
|
@ -88,37 +69,13 @@ async fn get(
|
||||||
row.original_packaging_name.as_ref(),
|
row.original_packaging_name.as_ref(),
|
||||||
&row.original_packaging_class_name.unwrap(),
|
&row.original_packaging_class_name.unwrap(),
|
||||||
)
|
)
|
||||||
.with_state(row.original_packaging_state.unwrap())
|
|
||||||
}),
|
}),
|
||||||
description: row.description,
|
description: row.description,
|
||||||
state: row.state,
|
|
||||||
})
|
})
|
||||||
.fetch_one(pool.as_ref())
|
.fetch_one(pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let events = query_as!(
|
|
||||||
ItemEventDetails,
|
|
||||||
r#"SELECT id, date, event AS "event: ItemEvent", description FROM item_events WHERE item = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let possible_events = query!(
|
|
||||||
r#"SELECT
|
|
||||||
event AS "event: ItemEvent",
|
|
||||||
next AS "next: ItemState"
|
|
||||||
FROM item_events_transitions
|
|
||||||
WHERE state = $1"#,
|
|
||||||
item.state as ItemState
|
|
||||||
)
|
|
||||||
.map(|row| (row.event, row.next))
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let parents = query!(
|
let parents = query!(
|
||||||
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
||||||
FROM items
|
FROM items
|
||||||
|
@ -136,43 +93,27 @@ async fn get(
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let children = query!(
|
let children = query!(
|
||||||
r#"SELECT
|
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
||||||
items.id,
|
|
||||||
items.name,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
FROM items
|
||||||
JOIN item_classes
|
JOIN item_classes
|
||||||
ON items.class = item_classes.id
|
ON items.class = item_classes.id
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
WHERE items.parent = $1"#,
|
WHERE items.parent = $1"#,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
.map(|row| {
|
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
|
||||||
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name).with_state(row.state)
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
.fetch_all(pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let original_packaging_of = query!(
|
let original_packaging_of = query!(
|
||||||
r#"SELECT
|
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
||||||
items.id,
|
|
||||||
items.name,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
FROM items
|
||||||
JOIN item_classes
|
JOIN item_classes
|
||||||
ON items.class = item_classes.id
|
ON items.class = item_classes.id
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
WHERE items.original_packaging = $1"#,
|
WHERE items.original_packaging = $1"#,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
.map(|row| {
|
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
|
||||||
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name).with_state(row.state)
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
.fetch_all(pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
@ -185,7 +126,6 @@ async fn get(
|
||||||
path: &format!("/item/{}", item.id),
|
path: &format!("/item/{}", item.id),
|
||||||
title: Some(&title),
|
title: Some(&title),
|
||||||
page_title: Some(Box::new(item.name.clone())),
|
page_title: Some(Box::new(item.name.clone())),
|
||||||
page_title_extra: Some(Box::new(item.state)),
|
|
||||||
page_actions: vec![
|
page_actions: vec![
|
||||||
(PageActionGroup::Button {
|
(PageActionGroup::Button {
|
||||||
action: PageAction {
|
action: PageAction {
|
||||||
|
@ -217,8 +157,6 @@ async fn get(
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
html! {
|
html! {
|
||||||
div .row {
|
|
||||||
div .col-md-8 {
|
|
||||||
table .table {
|
table .table {
|
||||||
tr {
|
tr {
|
||||||
th { "UUID" }
|
th { "UUID" }
|
||||||
|
@ -255,86 +193,6 @@ async fn get(
|
||||||
td style="white-space: pre-wrap" { (item.description) }
|
td style="white-space: pre-wrap" { (item.description) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
div .col-md-4 {
|
|
||||||
div .card {
|
|
||||||
div .card-header {
|
|
||||||
"Events"
|
|
||||||
}
|
|
||||||
ul .list-group.list-group-flush {
|
|
||||||
@for (idx, event) in events.iter().enumerate() {
|
|
||||||
li .list-group-item {
|
|
||||||
div .d-flex.justify-content-between.align-items-start.mb-2[!event.description.is_empty()] {
|
|
||||||
strong { (event.event) }
|
|
||||||
span .badge.text-bg-secondary { (event.date) }
|
|
||||||
}
|
|
||||||
@if idx + 1 == events.len() {
|
|
||||||
form .float-end action={ "/items/event/" (event.id) "/delete" } method="POST" {
|
|
||||||
button .btn.text-bg-danger.btn-sm type="submit" { "Delete" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@if !event.description.is_empty() {
|
|
||||||
p .mb-0 { (event.description) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@if events.is_empty() {
|
|
||||||
li .list-group-item.text-secondary { "no events" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div .card-body {
|
|
||||||
div .d-flex.gap-1.flex-wrap {
|
|
||||||
@for (event, next) in &possible_events {
|
|
||||||
button
|
|
||||||
.btn.(next.colour().button())
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#add-event-modal"
|
|
||||||
data-event-type=(to_variant_name(event).unwrap())
|
|
||||||
{ (event) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div .modal #add-event-modal tabindex="-1" {
|
|
||||||
div .modal-dialog {
|
|
||||||
form .modal-content action={ "/item/" (id) "/events/add" } method="POST" {
|
|
||||||
div .modal-header {
|
|
||||||
h5 .modal-title { "Add event" }
|
|
||||||
button .btn-close type="button" data-bs-dismiss="modal";
|
|
||||||
}
|
|
||||||
div .modal-body {
|
|
||||||
div .mb-3 {
|
|
||||||
select .form-select name="event" id="event" {
|
|
||||||
@for (event, _) in &possible_events {
|
|
||||||
option value=(to_variant_name(event).unwrap()) { (event) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "description",
|
|
||||||
title: "Description",
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Date,
|
|
||||||
name: "date",
|
|
||||||
title: "Date",
|
|
||||||
value: Some(&OffsetDateTime::now_utc().date()),
|
|
||||||
required: true,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
div .modal-footer {
|
|
||||||
button .btn.btn-secondary type="button" data-bs-dismiss="modal" { "Cancel" }
|
|
||||||
button .btn.btn-primary type="submit" { "Add" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !children.is_empty() {
|
@if !children.is_empty() {
|
||||||
div .d-flex.justify-content-between.mt-4 {
|
div .d-flex.justify-content-between.mt-4 {
|
||||||
|
|
|
@ -11,8 +11,7 @@ use uuid::Uuid;
|
||||||
use crate::frontend::templates::{
|
use crate::frontend::templates::{
|
||||||
self,
|
self,
|
||||||
helpers::{
|
helpers::{
|
||||||
Colour, ItemClassPreview, ItemPreview, ItemState, PageAction, PageActionGroup,
|
Colour, ItemClassPreview, ItemPreview, PageAction, PageActionGroup, PageActionMethod,
|
||||||
PageActionMethod,
|
|
||||||
},
|
},
|
||||||
TemplateConfig,
|
TemplateConfig,
|
||||||
};
|
};
|
||||||
|
@ -71,22 +70,14 @@ async fn get(
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let items = query!(
|
let items = query!(
|
||||||
r#"SELECT
|
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
||||||
items.id,
|
|
||||||
items.name,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
FROM items
|
||||||
JOIN item_classes
|
JOIN item_classes
|
||||||
ON items.class = item_classes.id
|
ON items.class = item_classes.id
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
WHERE items.class = $1"#,
|
WHERE items.class = $1"#,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
.map(|row| {
|
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
|
||||||
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name).with_state(row.state)
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
.fetch_all(pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
|
@ -10,7 +10,6 @@ use super::datalist::Datalist;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum InputType {
|
pub enum InputType {
|
||||||
Date,
|
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
}
|
}
|
||||||
|
@ -18,7 +17,6 @@ pub enum InputType {
|
||||||
impl Display for InputType {
|
impl Display for InputType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Date => write!(f, "date"),
|
|
||||||
Self::Text => write!(f, "text"),
|
Self::Text => write!(f, "text"),
|
||||||
Self::Textarea => write!(f, "textarea"),
|
Self::Textarea => write!(f, "textarea"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ use std::fmt::{self, Display};
|
||||||
|
|
||||||
use crate::label::LabelPreset;
|
use crate::label::LabelPreset;
|
||||||
use maud::{html, Markup, PreEscaped, Render};
|
use maud::{html, Markup, PreEscaped, Render};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub enum Css<'a> {
|
pub enum Css<'a> {
|
||||||
|
@ -79,13 +78,9 @@ impl fmt::Display for Colour {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Colour {
|
impl Colour {
|
||||||
pub fn button(&self) -> String {
|
fn button(&self) -> String {
|
||||||
format!("btn-{self}")
|
format!("btn-{self}")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_bg(&self) -> String {
|
|
||||||
format!("text-bg-{self}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -139,39 +134,25 @@ impl Render for ItemName {
|
||||||
pub struct ItemPreview {
|
pub struct ItemPreview {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: ItemName,
|
pub name: ItemName,
|
||||||
pub state: Option<ItemState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ItemPreview {
|
impl ItemPreview {
|
||||||
pub fn new(id: Uuid, name: ItemName) -> Self {
|
pub fn new(id: Uuid, name: ItemName) -> Self {
|
||||||
Self {
|
Self { id, name }
|
||||||
id,
|
|
||||||
name,
|
|
||||||
state: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_parts(id: Uuid, item_name: Option<&String>, class_name: &String) -> Self {
|
pub fn from_parts(id: Uuid, item_name: Option<&String>, class_name: &String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
name: ItemName::new(item_name, class_name),
|
name: ItemName::new(item_name, class_name),
|
||||||
state: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_state(mut self, state: ItemState) -> Self {
|
|
||||||
self.state = Some(state);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ItemPreview {
|
impl Render for ItemPreview {
|
||||||
fn render(&self) -> Markup {
|
fn render(&self) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
a .me-1[self.state.is_some()] href={ "/item/" (self.id) } { (self.name) }
|
a href={ "/item/" (self.id) } { (self.name) }
|
||||||
@if let Some(ref state) = self.state {
|
|
||||||
(state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,77 +279,3 @@ pub fn parents_breadcrumb(name: ItemName, parents: &[ItemPreview], full: bool) -
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Is this module the right place for ItemState and ItemEvent?
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, sqlx::Type)]
|
|
||||||
#[sqlx(rename_all = "snake_case", type_name = "item_state")]
|
|
||||||
pub enum ItemState {
|
|
||||||
Borrowed,
|
|
||||||
Inactive,
|
|
||||||
Loaned,
|
|
||||||
Owned,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemState {
|
|
||||||
pub fn colour(&self) -> Colour {
|
|
||||||
match self {
|
|
||||||
ItemState::Borrowed => Colour::Warning,
|
|
||||||
ItemState::Inactive => Colour::Secondary,
|
|
||||||
ItemState::Loaned => Colour::Danger,
|
|
||||||
ItemState::Owned => Colour::Primary,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ItemState {
|
|
||||||
fn render(&self) -> Markup {
|
|
||||||
html! {
|
|
||||||
span .badge.(self.colour().text_bg()) {
|
|
||||||
@match self {
|
|
||||||
ItemState::Borrowed => "borrowed",
|
|
||||||
ItemState::Inactive => "inactive",
|
|
||||||
ItemState::Loaned => "loaned",
|
|
||||||
ItemState::Owned => "owned",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, sqlx::Type)]
|
|
||||||
#[sqlx(rename_all = "snake_case", type_name = "item_event")]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ItemEvent {
|
|
||||||
Acquire,
|
|
||||||
Borrow,
|
|
||||||
Buy,
|
|
||||||
Dispose,
|
|
||||||
Gift,
|
|
||||||
Loan,
|
|
||||||
Lose,
|
|
||||||
RecieveGift,
|
|
||||||
ReturnBorrowed,
|
|
||||||
ReturnLoaned,
|
|
||||||
Sell,
|
|
||||||
Use,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ItemEvent {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
ItemEvent::Acquire => write!(f, "acquire"),
|
|
||||||
ItemEvent::Borrow => write!(f, "borrow"),
|
|
||||||
ItemEvent::Buy => write!(f, "buy"),
|
|
||||||
ItemEvent::Dispose => write!(f, "dispose"),
|
|
||||||
ItemEvent::Gift => write!(f, "gift"),
|
|
||||||
ItemEvent::Loan => write!(f, "loan"),
|
|
||||||
ItemEvent::Lose => write!(f, "lose"),
|
|
||||||
ItemEvent::RecieveGift => write!(f, "recieve gift"),
|
|
||||||
ItemEvent::ReturnBorrowed => write!(f, "return borrowed"),
|
|
||||||
ItemEvent::ReturnLoaned => write!(f, "return loaned"),
|
|
||||||
ItemEvent::Sell => write!(f, "sell"),
|
|
||||||
ItemEvent::Use => write!(f, "use"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -68,7 +68,6 @@ pub struct TemplateConfig<'a> {
|
||||||
pub path: &'a str,
|
pub path: &'a str,
|
||||||
pub title: Option<&'a str>,
|
pub title: Option<&'a str>,
|
||||||
pub page_title: Option<Box<dyn Render + 'a>>,
|
pub page_title: Option<Box<dyn Render + 'a>>,
|
||||||
pub page_title_extra: Option<Box<dyn Render + 'a>>,
|
|
||||||
pub page_actions: Vec<PageActionGroup>,
|
pub page_actions: Vec<PageActionGroup>,
|
||||||
pub extra_css: Vec<Css<'a>>,
|
pub extra_css: Vec<Css<'a>>,
|
||||||
pub extra_js: Vec<Js<'a>>,
|
pub extra_js: Vec<Js<'a>>,
|
||||||
|
@ -82,7 +81,6 @@ impl Default for TemplateConfig<'_> {
|
||||||
path: "/",
|
path: "/",
|
||||||
title: None,
|
title: None,
|
||||||
page_title: None,
|
page_title: None,
|
||||||
page_title_extra: None,
|
|
||||||
page_actions: Vec::new(),
|
page_actions: Vec::new(),
|
||||||
extra_css: Vec::new(),
|
extra_css: Vec::new(),
|
||||||
extra_js: Vec::new(),
|
extra_js: Vec::new(),
|
||||||
|
@ -117,23 +115,18 @@ pub fn base(config: TemplateConfig, content: Markup) -> Markup {
|
||||||
(navbar(&config))
|
(navbar(&config))
|
||||||
|
|
||||||
main .container.my-4 {
|
main .container.my-4 {
|
||||||
|
div .d-flex.justify-content-between.mb-3 {
|
||||||
div {
|
div {
|
||||||
div .float-end.d-flex.align-items-start.h-100.gap-1 {
|
|
||||||
@for page_action in config.page_actions {
|
|
||||||
(page_action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div .d-flex.align-items-center.gap-1 {
|
|
||||||
@if let Some(ref page_title) = config.page_title {
|
@if let Some(ref page_title) = config.page_title {
|
||||||
h2 {
|
h2 {
|
||||||
(page_title)
|
(page_title)
|
||||||
|
|
||||||
@if let Some(ref page_title_extra) = config.page_title_extra {
|
|
||||||
" "
|
|
||||||
(page_title_extra)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div .d-flex.h-100.gap-1 {
|
||||||
|
@for page_action in config.page_actions {
|
||||||
|
(page_action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
mod config;
|
|
||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
|
||||||
pub use config::Config;
|
|
||||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -2,18 +2,17 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
||||||
use actix_web::middleware::ErrorHandlers;
|
use actix_web::middleware::ErrorHandlers;
|
||||||
use actix_web::{cookie::Key, http::StatusCode, web, App, HttpResponse, HttpServer};
|
use actix_web::{cookie::Key, http::StatusCode, web, App, HttpResponse, HttpServer};
|
||||||
use base64::prelude::{Engine as _, BASE64_STANDARD};
|
use base64::prelude::{Engine as _, BASE64_STANDARD};
|
||||||
use clap::Parser;
|
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
|
|
||||||
use li7y::Config;
|
|
||||||
|
|
||||||
#[derive(Embed)]
|
#[derive(Embed)]
|
||||||
#[folder = "static"]
|
#[folder = "static"]
|
||||||
struct Static;
|
struct Static;
|
||||||
|
@ -22,22 +21,22 @@ struct Static;
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
let config = Config::parse();
|
|
||||||
|
|
||||||
// generate a secret key with head -c 64 /dev/urandom | base64 -w0
|
// generate a secret key with head -c 64 /dev/urandom | base64 -w0
|
||||||
let secret_key = match config.secret_key {
|
let secret_key = match env::var("SECRET_KEY") {
|
||||||
Some(ref encoded) => Key::from(
|
Ok(encoded) => Key::from(
|
||||||
&BASE64_STANDARD
|
&BASE64_STANDARD
|
||||||
.decode(encoded)
|
.decode(encoded)
|
||||||
.expect("failed to decode base64 in SECRET_KEY"),
|
.expect("failed to decode base64 in SECRET_KEY"),
|
||||||
),
|
),
|
||||||
None => {
|
Err(_) => {
|
||||||
warn!("SECRET_KEY was not specified, using randomly generated key");
|
warn!("SECRET_KEY was not specified, using randomly generated key");
|
||||||
Key::generate()
|
Key::generate()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let pool: sqlx::PgPool = sqlx::Pool::<sqlx::postgres::Postgres>::connect(&config.database_url)
|
let pool: sqlx::PgPool = sqlx::Pool::<sqlx::postgres::Postgres>::connect(
|
||||||
|
&env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("failed to connect to database");
|
.expect("failed to connect to database");
|
||||||
|
|
||||||
|
@ -46,14 +45,15 @@ async fn main() -> std::io::Result<()> {
|
||||||
.await
|
.await
|
||||||
.expect("failed to run migrations");
|
.expect("failed to run migrations");
|
||||||
|
|
||||||
let address = config.listen_address;
|
let address = env::var("LISTEN_ADDRESS").unwrap_or("::1".to_string());
|
||||||
let port = config.listen_port;
|
let port = env::var("LISTEN_PORT").map_or(8080, |s| {
|
||||||
|
s.parse::<u16>().expect("failed to parse LISTEN_PORT")
|
||||||
|
});
|
||||||
|
|
||||||
info!("Starting on {address}:{port}");
|
info!("Starting on {address}:{port}");
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(config.clone()))
|
|
||||||
.app_data(web::Data::new(pool.clone()))
|
.app_data(web::Data::new(pool.clone()))
|
||||||
.service(web::scope("/static").route(
|
.service(web::scope("/static").route(
|
||||||
"/{_:.*}",
|
"/{_:.*}",
|
||||||
|
|
|
@ -85,8 +85,4 @@
|
||||||
.join(",")
|
.join(",")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("add-event-modal").addEventListener("show.bs.modal", e => {
|
|
||||||
document.getElementById("event").value = e.relatedTarget.dataset.eventType
|
|
||||||
})
|
|
||||||
})()
|
})()
|
||||||
|
|
Loading…
Reference in a new issue