Compare commits

...

8 commits

Author SHA1 Message Date
Simon Bruder b9022ed125
Update dependencies
All checks were successful
/ build (push) Successful in 1m3s
2024-08-28 15:29:39 +02:00
Simon Bruder dfa233dd3c
Remove duplicate clippy in devShell
It is already included in rust-bin.
2024-08-28 15:29:39 +02:00
Simon Bruder 659a6cb776
Add licensing information page 2024-08-28 15:29:38 +02:00
Simon Bruder bbca62fa8b
Fix event add modal handler registration 2024-08-28 15:29:37 +02:00
Simon Bruder 89ce5b7d40
Ensure datalist hint is cleared 2024-08-28 15:29:36 +02:00
Simon Bruder 6007e4b96a
Show item class parents with breadcrumbs 2024-08-28 15:29:36 +02:00
Simon Bruder 61f356269c
Generalize parents_breadcrumb 2024-08-28 15:29:35 +02:00
Simon Bruder 413a02cdaa
Show item classes as tree 2024-08-28 15:29:34 +02:00
19 changed files with 1044 additions and 141 deletions

View file

@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "WITH RECURSIVE item_class_children AS (\n SELECT\n item_classes.id,\n array_remove(array_agg(children.id), NULL) AS \"children\"\n FROM item_classes\n LEFT JOIN item_classes AS \"children\"\n ON item_classes.id = children.parent\n GROUP BY item_classes.id\n ),\n cte AS (\n SELECT\n item_classes.id,\n item_classes.name,\n item_class_children.children,\n 0 AS \"reverse_level\"\n FROM item_classes\n JOIN item_class_children\n ON item_classes.id = item_class_children.id\n WHERE item_class_children.children = '{}'\n\n UNION\n\n SELECT\n item_classes.id,\n item_classes.name,\n item_class_children.children,\n cte.reverse_level + 1\n FROM item_classes\n JOIN item_class_children\n ON item_classes.id = item_class_children.id\n JOIN cte\n ON cte.id = ANY (item_class_children.children)\n )\n SELECT\n id AS \"id!\",\n name AS \"name!\",\n children AS \"children!\"\n FROM cte\n GROUP BY id, name, children\n ORDER BY max(reverse_level)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "children!",
"type_info": "UuidArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
null,
null
]
},
"hash": "689a5177bdbc4ac788579a47fe033eb6b9639a356fe7ec543691e385ffef51e7"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT item_classes.id, item_classes.name\n FROM item_classes\n JOIN unnest((SELECT parents FROM item_class_tree WHERE id = $1))\n WITH ORDINALITY AS parents(id, n)\n ON item_classes.id = parents.id\n ORDER BY parents.n",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "85e1b1bae08c7edda9671214e6eeece6556236c25253a45c8ddc834751f72694"
}

338
Cargo.lock generated
View file

@ -21,9 +21,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.8.0" version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -111,16 +111,16 @@ dependencies = [
[[package]] [[package]]
name = "actix-server" name = "actix-server"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894"
dependencies = [ dependencies = [
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"futures-core", "futures-core",
"futures-util", "futures-util",
"mio 0.8.11", "mio",
"socket2", "socket2",
"tokio", "tokio",
"tracing", "tracing",
@ -165,9 +165,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web" name = "actix-web"
version = "4.8.0" version = "4.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
@ -187,6 +187,7 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"impl-more",
"itoa", "itoa",
"language-tags", "language-tags",
"log", "log",
@ -213,7 +214,7 @@ dependencies = [
"actix-router", "actix-router",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -231,6 +232,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "aead" name = "aead"
version = "0.5.2" version = "0.5.2"
@ -366,9 +373,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.4" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "atoi" name = "atoi"
@ -395,7 +402,7 @@ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.7.4",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@ -500,9 +507,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.1" version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]] [[package]]
name = "bytestring" name = "bytestring"
@ -514,13 +521,46 @@ dependencies = [
] ]
[[package]] [[package]]
name = "cc" name = "camino"
version = "1.1.6" version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "cc"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
"shlex",
] ]
[[package]] [[package]]
@ -541,9 +581,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.11" version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -551,9 +591,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.11" version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -563,14 +603,14 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.11" version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -617,9 +657,9 @@ dependencies = [
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.12" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -683,6 +723,12 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "current_platform"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74858bcfe44b22016cb49337d7b6f04618c58e5dbfdef61b06b8c434324a0bc"
[[package]] [[package]]
name = "datamatrix" name = "datamatrix"
version = "0.3.1" version = "0.3.1"
@ -724,9 +770,15 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustc_version", "rustc_version",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -754,6 +806,45 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "embed-licensing"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10940ff2389474cf2468887b1e93afe16911db1db2a35f9d26bbdf8e95b3362f"
dependencies = [
"embed-licensing-core",
"embed-licensing-macros",
"spdx",
]
[[package]]
name = "embed-licensing-core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93af86b1ab9c24dcbf99a3e2bc7d65fd7bfef5238240ba8436e85caf752cc66f"
dependencies = [
"cargo-platform",
"cargo_metadata",
"current_platform",
"proc-macro2",
"quote",
"serde_json",
"spdx",
"syn 2.0.75",
"thiserror",
]
[[package]]
name = "embed-licensing-macros"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d215383571a79d8af239986e959b016073b5f16e8586d274ba694e97db027dd0"
dependencies = [
"embed-licensing-core",
"quote",
"syn 2.0.75",
]
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.34" version = "0.8.34"
@ -780,7 +871,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -863,12 +954,12 @@ checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.30" version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide 0.8.0",
] ]
[[package]] [[package]]
@ -949,7 +1040,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -1150,10 +1241,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "indexmap" name = "impl-more"
version = "2.2.6" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d"
[[package]]
name = "indexmap"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -1200,9 +1297,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.69" version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1234,6 +1331,7 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"clap", "clap",
"datamatrix", "datamatrix",
"embed-licensing",
"enum-iterator", "enum-iterator",
"env_logger 0.11.5", "env_logger 0.11.5",
"futures-util", "futures-util",
@ -1242,6 +1340,7 @@ dependencies = [
"maud", "maud",
"mime", "mime",
"mime_guess", "mime_guess",
"pretty_assertions",
"printpdf", "printpdf",
"quickcheck", "quickcheck",
"quickcheck_macros", "quickcheck_macros",
@ -1249,6 +1348,7 @@ dependencies = [
"serde", "serde",
"serde_urlencoded", "serde_urlencoded",
"serde_variant", "serde_variant",
"spdx",
"sqlx", "sqlx",
"thiserror", "thiserror",
"time", "time",
@ -1257,9 +1357,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.155" version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]] [[package]]
name = "libm" name = "libm"
@ -1361,7 +1461,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -1418,25 +1518,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "mio" name = "miniz_oxide"
version = "0.8.11" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [ dependencies = [
"libc", "adler2",
"log",
"wasi",
"windows-sys 0.48.0",
] ]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
"log",
"wasi", "wasi",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -1506,9 +1604,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.2" version = "0.36.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -1646,9 +1744,22 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]] [[package]]
name = "printpdf" name = "printpdf"
@ -1775,9 +1886,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.5" version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1850,7 +1961,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn 2.0.72", "syn 2.0.75",
"walkdir", "walkdir",
] ]
@ -1918,34 +2029,38 @@ name = "semver"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.204" version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.204" version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.120" version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr",
"ryu", "ryu",
"serde", "serde",
] ]
@ -1993,6 +2108,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.2" version = "1.4.2"
@ -2037,6 +2158,15 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "spdx"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc"
dependencies = [
"smallvec",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@ -2304,9 +2434,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.72" version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2315,14 +2445,15 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.10.1" version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"once_cell",
"rustix", "rustix",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2342,7 +2473,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -2393,14 +2524,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.39.2" version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"libc", "libc",
"mio 1.0.1", "mio",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
@ -2452,7 +2583,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -2508,9 +2639,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-properties" name = "unicode-properties"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
@ -2603,34 +2734,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.92" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.92" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.92" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -2638,22 +2770,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.92" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.92" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]] [[package]]
name = "weezl" name = "weezl"
@ -2673,11 +2805,11 @@ dependencies = [
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2698,6 +2830,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.48.5" version = "0.48.5"
@ -2819,12 +2960,19 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [ dependencies = [
"byteorder",
"zerocopy-derive", "zerocopy-derive",
] ]
@ -2836,7 +2984,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.72", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -2856,18 +3004,18 @@ dependencies = [
[[package]] [[package]]
name = "zstd-safe" name = "zstd-safe"
version = "7.2.0" version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [ dependencies = [
"zstd-sys", "zstd-sys",
] ]
[[package]] [[package]]
name = "zstd-sys" name = "zstd-sys"
version = "2.0.12+zstd.1.5.6" version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",

View file

@ -8,6 +8,7 @@ version = "0.0.0"
authors = ["Simon Bruder <simon@sbruder.de>"] authors = ["Simon Bruder <simon@sbruder.de>"]
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
repository = "https://git.sbruder.de/simon/li7y"
[dependencies] [dependencies]
actix-identity = "0.7.1" actix-identity = "0.7.1"
@ -17,6 +18,7 @@ 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"] } clap = { version = "4.5.10", features = ["derive", "env"] }
datamatrix = "0.3.1" datamatrix = "0.3.1"
embed-licensing = { version = "0.3.1", features = ["current_platform"] }
enum-iterator = "2.1.0" enum-iterator = "2.1.0"
env_logger = "0.11.3" env_logger = "0.11.3"
futures-util = "0.3.30" futures-util = "0.3.30"
@ -30,6 +32,7 @@ rust-embed = { version = "8.5.0", features = ["actix"] }
serde = { version = "1.0.203", features = ["serde_derive"] } serde = { version = "1.0.203", features = ["serde_derive"] }
serde_urlencoded = "0.7.1" serde_urlencoded = "0.7.1"
serde_variant = "0.1.3" serde_variant = "0.1.3"
spdx = { version = "0.10.6", features = ["text"] }
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 = ["parsing", "serde"] }
@ -37,6 +40,7 @@ uuid = { version = "1.9.0", features = ["serde", "v4"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3.8.0" actix-http = "3.8.0"
pretty_assertions = "1.4.0"
quickcheck = "1.0.3" quickcheck = "1.0.3"
quickcheck_macros = "1.0.0" quickcheck_macros = "1.0.0"

View file

@ -10,6 +10,7 @@ allow = [
"Apache-2.0", "Apache-2.0",
"BSD-3-Clause", "BSD-3-Clause",
"MIT", "MIT",
"MPL-2.0",
"Unicode-DFS-2016", "Unicode-DFS-2016",
] ]
confidence-threshold = 0.95 confidence-threshold = 0.95

View file

@ -146,7 +146,6 @@
cargo-deny cargo-deny
cargo-tarpaulin cargo-tarpaulin
cargo-watch cargo-watch
clippy
graphviz graphviz
postgresql postgresql
reuse reuse

View file

@ -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 sqlx::query; use sqlx::query;
use uuid::Uuid; use uuid::Uuid;
@ -13,6 +15,12 @@ pub struct ItemClassListEntry {
pub parent: Option<ItemClassPreview>, pub parent: Option<ItemClassPreview>,
} }
pub struct ItemClassTreeElement {
pub id: Uuid,
pub name: String,
pub children: Vec<ItemClassTreeElement>,
}
impl ItemClassRepository { impl ItemClassRepository {
pub async fn list(&self) -> sqlx::Result<Vec<ItemClassListEntry>> { pub async fn list(&self) -> sqlx::Result<Vec<ItemClassListEntry>> {
query!( query!(
@ -33,4 +41,77 @@ impl ItemClassRepository {
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await
} }
pub async fn tree(&self) -> sqlx::Result<Vec<ItemClassTreeElement>> {
let mut mappings: HashMap<Uuid, ItemClassTreeElement> = HashMap::new();
for row in query!(
r##"WITH RECURSIVE item_class_children AS (
SELECT
item_classes.id,
array_remove(array_agg(children.id), NULL) AS "children"
FROM item_classes
LEFT JOIN item_classes AS "children"
ON item_classes.id = children.parent
GROUP BY item_classes.id
),
cte AS (
SELECT
item_classes.id,
item_classes.name,
item_class_children.children,
0 AS "reverse_level"
FROM item_classes
JOIN item_class_children
ON item_classes.id = item_class_children.id
WHERE item_class_children.children = '{}'
UNION
SELECT
item_classes.id,
item_classes.name,
item_class_children.children,
cte.reverse_level + 1
FROM item_classes
JOIN item_class_children
ON item_classes.id = item_class_children.id
JOIN cte
ON cte.id = ANY (item_class_children.children)
)
SELECT
id AS "id!",
name AS "name!",
children AS "children!"
FROM cte
GROUP BY id, name, children
ORDER BY max(reverse_level)"##
)
.fetch_all(&self.pool)
.await?
{
let mut children = if row.children.is_empty() {
Vec::new()
} else {
row.children
.iter()
.map(|id| mappings.remove(id).unwrap())
.collect()
};
children.sort_by(|this, other| this.name.cmp(&other.name));
mappings.insert(
row.id,
ItemClassTreeElement {
id: row.id,
name: row.name,
children,
},
);
}
let mut item_classes = mappings.into_values().collect::<Vec<_>>();
item_classes.sort_by(|this, other| this.name.cmp(&other.name));
Ok(item_classes)
}
} }

View file

@ -9,11 +9,12 @@ mod edit;
mod list; mod list;
mod show; mod show;
use sqlx::PgPool; use sqlx::{query, PgPool};
use uuid::Uuid; use uuid::Uuid;
pub use add::{ItemClassAddForm, ItemClassAddFormPrefilled}; pub use add::{ItemClassAddForm, ItemClassAddFormPrefilled};
pub use edit::ItemClassEditForm; pub use edit::ItemClassEditForm;
pub use list::ItemClassTreeElement;
#[derive(Clone)] #[derive(Clone)]
pub struct ItemClassRepository { pub struct ItemClassRepository {
@ -36,3 +37,20 @@ impl ItemClassPreview {
Self { id, name } Self { id, name }
} }
} }
impl ItemClassRepository {
pub async fn parents(&self, id: Uuid) -> sqlx::Result<Vec<ItemClassPreview>> {
query!(
r#"SELECT item_classes.id, item_classes.name
FROM item_classes
JOIN unnest((SELECT parents FROM item_class_tree WHERE id = $1))
WITH ORDINALITY AS parents(id, n)
ON item_classes.id = parents.id
ORDER BY parents.n"#,
id
)
.map(|row| ItemClassPreview::new(row.id, row.name))
.fetch_all(&self.pool)
.await
}
}

View file

@ -4,7 +4,7 @@
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, Render};
use crate::database::{items::ItemPreview, ItemRepository}; use crate::database::{items::ItemPreview, ItemRepository};
use crate::frontend::templates::{ use crate::frontend::templates::{
@ -61,7 +61,13 @@ async fn get(
td { (ItemPreview::new(item.id, item.name.clone().terse())) } td { (ItemPreview::new(item.id, item.name.clone().terse())) }
td { (item.state) } 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.iter().map(|parent| parent as &dyn Render).collect(),
false
))
}
} }
} }
} }

View file

@ -4,7 +4,7 @@
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, Render};
use serde_variant::to_variant_name; use serde_variant::to_variant_name;
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
@ -121,7 +121,13 @@ async fn get(
} }
tr { tr {
th { "Parents" } th { "Parents" }
td { (templates::helpers::parents_breadcrumb(item.name, &parents, true)) } td {
(templates::helpers::parents_breadcrumb(
&item.name,
parents.iter().map(|parent| parent as &dyn Render).collect(),
true
))
}
} }
tr { tr {
th { "Original Packaging" } th { "Original Packaging" }

View file

@ -6,7 +6,6 @@ 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 crate::database::item_classes::ItemClassPreview;
use crate::database::ItemClassRepository; use crate::database::ItemClassRepository;
use crate::frontend::templates::{ use crate::frontend::templates::{
self, self,
@ -24,7 +23,7 @@ async fn get(
user: Identity, user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let item_classes = item_class_repo let item_classes = item_class_repo
.list() .tree()
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
@ -47,26 +46,9 @@ async fn get(
..Default::default() ..Default::default()
}, },
html! { html! {
table .table { ul {
thead {
tr {
th { "Name" }
th { "Parents" }
}
}
tbody {
@for item_class in item_classes { @for item_class in item_classes {
tr { (item_class)
td { (ItemClassPreview::new(item_class.id, item_class.name)) }
td {
@if let Some(parent) = item_class.parent {
(parent)
} @else {
"-"
}
}
}
}
} }
} }
}, },

View file

@ -4,7 +4,7 @@
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, Render};
use uuid::Uuid; use uuid::Uuid;
use crate::database::ItemClassRepository; use crate::database::ItemClassRepository;
@ -31,6 +31,11 @@ async fn get(
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
let parents = item_class_repo
.parents(id)
.await
.map_err(error::ErrorInternalServerError)?;
let children = item_class_repo let children = item_class_repo
.children(id) .children(id)
.await .await
@ -96,10 +101,14 @@ async fn get(
th { "Name" } th { "Name" }
td { (item_class.name) } td { (item_class.name) }
} }
@if let Some(parent) = item_class.parent {
tr { tr {
th { "Parent" } th { "Parents" }
td { (parent) } td {
(templates::helpers::parents_breadcrumb(
&item_class.name,
parents.iter().map(|parent| parent as &dyn Render).collect(),
true
))
} }
} }
tr { tr {

488
src/frontend/licensing.rs Normal file
View file

@ -0,0 +1,488 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::{BTreeSet, VecDeque};
use std::sync::OnceLock;
use actix_identity::Identity;
use actix_web::{get, web, Responder};
use embed_licensing::{CrateLicense, Licensing};
use maud::{html, Markup, Render};
use spdx::expression::ExprNode;
use spdx::LicenseReq;
use super::templates;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get);
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Ord)]
enum SpdxExpression {
Req(LicenseReq),
And(BTreeSet<SpdxExpression>),
Or(BTreeSet<SpdxExpression>),
}
fn render_separated_list(list: impl IntoIterator<Item: Render>, separator: &str) -> Markup {
let mut iter = list.into_iter();
html! {
"("
(iter.next().unwrap())
@for item in iter {
small .font-monospace { " " (separator) " " }
(item)
}
")"
}
}
// TODO: remove the outermost parentheses
impl Render for SpdxExpression {
fn render(&self) -> Markup {
match self {
Self::Req(req) => html! {
@let license = req.license.id().expect("only SPDX license identifiers supported");
a .font-monospace href={ "#license-" (license.name) } { (license.name) }
@if let Some(exception) = req.exception {
small .font-monospace { " WITH " }
a .font-monospace href={ "#exception-" (exception.name) } { (exception.name) }
}
},
Self::And(items) => render_separated_list(items, "AND"),
Self::Or(items) => render_separated_list(items, "OR"),
}
}
}
impl From<&spdx::Expression> for SpdxExpression {
fn from(value: &spdx::Expression) -> Self {
let mut stack = VecDeque::new();
let mut expr = None;
for node in value.iter() {
match node {
ExprNode::Op(op) => {
let last = expr.unwrap_or_else(|| stack.pop_back().unwrap());
expr = Some(match op {
spdx::expression::Operator::Or => {
SpdxExpression::Or(BTreeSet::from([last, stack.pop_back().unwrap()]))
}
spdx::expression::Operator::And => {
SpdxExpression::And(BTreeSet::from([last, stack.pop_back().unwrap()]))
}
});
}
ExprNode::Req(req) => {
stack.push_back(SpdxExpression::Req(req.req.clone()));
}
}
}
// special case for single req
if expr.is_none() && stack.len() == 1 {
return stack.pop_back().unwrap();
}
expr.expect("empty expression not possible").simplify()
}
}
macro_rules! spdx_expression_simplify_impl {
( $variant:ident, $items:expr ) => {{
let mut changed = false;
for item in $items.clone().into_iter() {
// if is split to avoid referencing a moved value
if let Self::$variant(_) = item {
$items.remove(&item);
changed = true;
}
if let Self::$variant(mut inner_items) = item {
$items.append(&mut inner_items);
}
}
if changed {
Self::$variant($items).simplify()
} else {
Self::$variant($items.into_iter().map(|it| it.simplify()).collect())
}
}};
}
impl SpdxExpression {
fn simplify(self) -> Self {
match self {
Self::Req(_) => self,
Self::And(mut items) => spdx_expression_simplify_impl!(And, items),
Self::Or(mut items) => spdx_expression_simplify_impl!(Or, items),
}
}
}
trait Package {
fn name(&self) -> &str;
fn version(&self) -> Option<&str>;
fn authors(&self) -> &[String];
fn website(&self) -> &str;
fn license(&self) -> &CrateLicense;
}
impl Render for dyn Package {
fn render(&self) -> Markup {
html! {
div .col-3 {
div .card {
div .card-body {
h4 .card-title {
a href=(self.website()) { (self.name()) }
@if let Some(version) = self.version() {
" " (version)
}
}
}
ul .list-group .list-group-flush {
@if self.authors().is_empty() {
li .list-group-item { em { "no authors specified in Cargo manifest" } }
}
@let author_limit = 3;
@let authors = self.authors().iter().take(author_limit);
@let more_authors = self.authors().iter().skip(author_limit);
@for author in authors {
li .list-group-item { (author) }
}
@if self.authors().len() > author_limit {
@for author in more_authors {
li .list-group-item.d-none.author-hidden { (author) }
}
li
.list-group-item.text-center.bg-secondary-subtle.author-expand
style="cursor: pointer; user-select: none;"
data-expanded="false"
{ "" }
}
}
div .card-body.align-content-end {
@match self.license() {
CrateLicense::SpdxExpression(expr) => {
(SpdxExpression::from(expr))
},
CrateLicense::Other(content) => {
@let modal_id = format!("license-modal-{}-{}", self.name(), self.version().unwrap_or(""));
button
.btn.btn-primary
type="button"
data-bs-toggle="modal"
data-bs-target={ "#" (modal_id) }
{ "custom license" }
div .modal.modal-lg #(modal_id) {
div .modal-dialog {
div .modal-content {
div.modal-header {
h1 .modal-title.fs-5 {
"custom license of "
(self.name())
@if let Some(version) = self.version() {
" " (version)
}
}
button .btn-close type="button" data-bs-dismiss="modal";
}
div .modal-body {
pre style="text-wrap: wrap" { (content) }
}
}
}
}
},
}
}
}
}
}
}
}
impl Package for embed_licensing::Crate {
fn name(&self) -> &str {
&self.name
}
fn version(&self) -> Option<&str> {
Some(&self.version)
}
fn authors(&self) -> &[String] {
&self.authors
}
fn website(&self) -> &str {
&self.website
}
fn license(&self) -> &CrateLicense {
&self.license
}
}
struct OtherPackage {
name: String,
website: String,
authors: Vec<String>,
license: CrateLicense,
}
impl Package for OtherPackage {
fn name(&self) -> &str {
&self.name
}
fn version(&self) -> Option<&str> {
None
}
fn authors(&self) -> &[String] {
&self.authors
}
fn website(&self) -> &str {
&self.website
}
fn license(&self) -> &CrateLicense {
&self.license
}
}
fn other_packages() -> &'static [OtherPackage] {
static OTHER_PACKAGES: OnceLock<Vec<OtherPackage>> = OnceLock::new();
OTHER_PACKAGES.get_or_init(|| {
vec![OtherPackage {
name: "Bootstrap".to_string(),
website: "https://getbootstrap.com/".to_string(),
authors: vec!["The Bootstrap Authors".to_string()],
license: CrateLicense::SpdxExpression(spdx::Expression::parse("MIT").unwrap()),
}]
})
}
fn licensing() -> &'static Licensing {
static LICENSING: OnceLock<Licensing> = OnceLock::new();
LICENSING.get_or_init(|| {
let mut licensing = embed_licensing::collect!(platform(current));
for other_package in other_packages() {
if let CrateLicense::SpdxExpression(expr) = &other_package.license {
for node in expr.iter() {
if let spdx::expression::ExprNode::Req(spdx::expression::ExpressionReq {
req,
..
}) = node
{
let license = req
.license
.id()
.expect("only SPDX license identifiers supported");
if !licensing.licenses.contains(&license) {
licensing.licenses.push(license)
}
if let Some(exception) = req.exception {
if !licensing.exceptions.contains(&exception) {
licensing.exceptions.push(exception)
}
}
}
}
}
}
licensing.licenses.sort_unstable();
licensing.exceptions.sort_unstable();
licensing
})
}
#[get("/licensing")]
async fn get(user: Option<Identity>) -> impl Responder {
let licensing = licensing();
templates::base(
templates::TemplateConfig {
path: "/licensing",
title: Some("Licensing"),
page_title: Some(Box::new("Licensing")),
user,
..Default::default()
},
html! {
h3 .mt-4 { "Rust Packages" }
div .row.g-2 {
@for package in &licensing.packages {
(package as &dyn Package)
}
}
h3 .mt-4 { "Other Packages" }
div .row.g-2 {
@for package in other_packages() {
(package as &dyn Package)
}
}
h3 .mt-4 { "Licenses" }
@for license in &licensing.licenses {
h4 #{ "license-" (license.name) } .mt-3 { (license.full_name) " (" span .font-monospace { (license.name) } ")" }
pre style="text-wrap: wrap" { (license.text()) }
}
h3 .mt-4 { "Exceptions" }
@for exception in &licensing.exceptions {
h4 #{ "exception-" (exception.name) } .mt-3.font-monospace { (exception.name) }
pre style="text-wrap: wrap" { (exception.text()) }
}
},
)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use pretty_assertions::assert_eq;
use spdx::{exception_id, license_id, LicenseItem, LicenseReq};
use super::SpdxExpression;
fn req(id: &str, or_later: bool, exception: Option<&str>) -> SpdxExpression {
SpdxExpression::Req(LicenseReq {
license: LicenseItem::Spdx {
id: license_id(id).unwrap(),
or_later,
},
exception: exception.map(exception_id).map(Option::unwrap),
})
}
#[test]
fn test_spdx_expression_from_simple() {
assert_eq!(
SpdxExpression::from(&spdx::Expression::parse("MIT").unwrap()),
req("MIT", false, None)
);
}
#[test]
fn test_spdx_expression_from_complex() {
assert_eq!(
SpdxExpression::from(
&spdx::Expression::parse("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT OR (CC0-1.0 AND Unlicense)")
.unwrap()
),
SpdxExpression::Or(BTreeSet::from([
req("Apache-2.0", false, Some("LLVM-exception")),
req("Apache-2.0", false, None),
req("MIT", false, None),
SpdxExpression::And(BTreeSet::from([
req("CC0-1.0", false, None),
req("Unlicense", false, None),
]))
]))
);
}
#[test]
fn test_spdx_expression_simplify_or() {
assert_eq!(
SpdxExpression::Or(BTreeSet::from([
SpdxExpression::Or(BTreeSet::from([])),
SpdxExpression::Or(BTreeSet::from([
req("Apache-2.0", false, Some("LLVM-exception")),
req("MIT", false, None),
SpdxExpression::Or(BTreeSet::from([
req("MIT", false, None),
req("Unlicense", false, None),
])),
])),
req("LGPL-2.1", true, None),
]))
.simplify(),
SpdxExpression::Or(BTreeSet::from([
req("Apache-2.0", false, Some("LLVM-exception")),
req("LGPL-2.1", true, None),
req("MIT", false, None),
req("Unlicense", false, None),
]))
);
}
#[test]
fn test_spdx_expression_simplify_and() {
assert_eq!(
SpdxExpression::And(BTreeSet::from([
SpdxExpression::And(BTreeSet::from([])),
SpdxExpression::And(BTreeSet::from([
req("MIT", false, None),
req("Apache-2.0", false, Some("LLVM-exception")),
SpdxExpression::And(BTreeSet::from([
req("MIT", false, None),
req("Unlicense", false, None),
])),
])),
req("LGPL-2.1", true, None),
]))
.simplify(),
SpdxExpression::And(BTreeSet::from([
req("Apache-2.0", false, Some("LLVM-exception")),
req("LGPL-2.1", true, None),
req("MIT", false, None),
req("Unlicense", false, None),
]))
);
}
#[test]
fn test_spdx_expression_simplify_mixed() {
assert_eq!(
SpdxExpression::Or(BTreeSet::from([
SpdxExpression::And(BTreeSet::from([
req("MIT", false, None),
req("Apache-2.0", false, Some("LLVM-exception")),
])),
SpdxExpression::Or(BTreeSet::from([
req("MIT", false, None),
req("Unlicense", false, None),
])),
SpdxExpression::And(BTreeSet::from([
req("CC0-1.0", false, None),
req("GPL-3.0", false, None),
])),
req("LGPL-2.1", true, None),
]))
.simplify(),
SpdxExpression::Or(BTreeSet::from([
req("MIT", false, None),
req("Unlicense", false, None),
req("LGPL-2.1", true, None),
SpdxExpression::And(BTreeSet::from([
req("MIT", false, None),
req("Apache-2.0", false, Some("LLVM-exception")),
])),
SpdxExpression::And(BTreeSet::from([
req("CC0-1.0", false, None),
req("GPL-3.0", false, None),
])),
]))
);
}
}

View file

@ -7,6 +7,7 @@ mod item;
mod item_class; mod item_class;
mod jump; mod jump;
mod labels; mod labels;
mod licensing;
pub mod templates; pub mod templates;
use actix_identity::Identity; use actix_identity::Identity;
@ -19,7 +20,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.configure(item::config) .configure(item::config)
.configure(item_class::config) .configure(item_class::config)
.configure(jump::config) .configure(jump::config)
.configure(labels::config); .configure(labels::config)
.configure(licensing::config);
} }
#[get("/")] #[get("/")]

View file

@ -7,7 +7,6 @@ use std::fmt;
use maud::{html, Markup, PreEscaped, Render}; use maud::{html, Markup, PreEscaped, Render};
use uuid::Uuid; use uuid::Uuid;
use crate::database::items::{ItemName, ItemPreview};
use crate::label::LabelPreset; use crate::label::LabelPreset;
pub enum Css<'a> { pub enum Css<'a> {
@ -170,7 +169,7 @@ impl PageActionGroup {
} }
} }
pub fn parents_breadcrumb(name: ItemName, parents: &[ItemPreview], full: bool) -> Markup { pub fn parents_breadcrumb(name: &dyn Render, parents: Vec<&dyn Render>, full: bool) -> Markup {
const LIMIT: usize = 3; const LIMIT: usize = 3;
html! { html! {
@ -178,10 +177,10 @@ pub fn parents_breadcrumb(name: ItemName, parents: &[ItemPreview], full: bool) -
@if !full && parents.len() > LIMIT { @if !full && parents.len() > LIMIT {
li .breadcrumb-item { "" } li .breadcrumb-item { "" }
} }
@let parents: Box<dyn Iterator<Item = &ItemPreview>> = if full { @let parents: Box<dyn Iterator<Item = &dyn Render>> = if full {
Box::new(parents.iter()) Box::new(parents.into_iter())
} else { } else {
Box::new(parents.iter().rev().take(LIMIT).rev()) Box::new(parents.into_iter().rev().take(LIMIT).rev())
}; };
@for parent in parents { @for parent in parents {
li .breadcrumb-item { li .breadcrumb-item {

View file

@ -59,7 +59,13 @@ fn footer() -> Markup {
html! { html! {
footer .mt-auto.py-3.bg-body-tertiary.text-body-tertiary { footer .mt-auto.py-3.bg-body-tertiary.text-body-tertiary {
div .container { div .container {
p .mb-0 { "li7y is free software, released under the terms of the AGPL v3" } p .mb-0 {
"li7y is free software, released under the terms of the AGPL v3 ("
a href="https://git.sbruder.de/simon/li7y" { "source code" }
", "
a href="/licensing" { "licensing of all dependencies" }
")"
}
} }
} }
} }

View file

@ -7,7 +7,11 @@ use std::fmt::{self, Display};
use maud::{html, Markup, Render}; use maud::{html, Markup, Render};
use crate::database::items::ItemPreview; use crate::database::items::ItemPreview;
use crate::database::{item_classes::ItemClassPreview, item_events::ItemEvent, items::ItemName}; use crate::database::{
item_classes::{ItemClassPreview, ItemClassTreeElement},
item_events::ItemEvent,
items::ItemName,
};
impl Render for ItemClassPreview { impl Render for ItemClassPreview {
fn render(&self) -> Markup { fn render(&self) -> Markup {
@ -68,3 +72,20 @@ impl Render for ItemPreview {
} }
} }
} }
impl Render for ItemClassTreeElement {
fn render(&self) -> Markup {
html! {
li {
(ItemClassPreview::new(self.id, self.name.clone()))
@if !self.children.is_empty() {
ul {
@for child in &self.children {
(child)
}
}
}
}
}
}
}

View file

@ -16,9 +16,8 @@
const datalistHint = (input, hint) => { const datalistHint = (input, hint) => {
const selected = input.list.querySelector(`option[value="${input.value}"]`); const selected = input.list.querySelector(`option[value="${input.value}"]`);
if (selected === null) hint.innerHTML = ""
hint.innerText = "" if (selected !== null) {
else {
const linkPrefix = input.list.dataset.linkPrefix; const linkPrefix = input.list.dataset.linkPrefix;
if (linkPrefix === undefined) { if (linkPrefix === undefined) {
hint.innerHTML = selected.innerHTML hint.innerHTML = selected.innerHTML
@ -86,7 +85,32 @@
}) })
} }
document.getElementById("add-event-modal").addEventListener("show.bs.modal", e => { const addEventModal = document.getElementById("add-event-modal")
if (addEventModal !== null) {
addEventModal.addEventListener("show.bs.modal", e => {
document.getElementById("event").value = e.relatedTarget.dataset.eventType document.getElementById("event").value = e.relatedTarget.dataset.eventType
}) })
}
Array.from(document.getElementsByClassName("author-expand")).forEach(expanderEl => {
expanderEl.addEventListener("click", _ => {
// it implicitly converts true/false to "true"/"false" in the dataset,
// but does not convert "true"/"false" from the datast into true/false
expanderEl.dataset.expanded = expanderEl.dataset.expanded !== "true"
if (expanderEl.dataset.expanded === "true") {
expanderEl.innerText = "⮝"
} else {
expanderEl.innerText = "⮟"
}
Array.from(expanderEl.parentElement.getElementsByClassName("author-hidden")).forEach(authorEl => {
if (expanderEl.dataset.expanded === "true") {
authorEl.classList.remove("d-none")
} else {
authorEl.classList.add("d-none")
}
})
})
})
})() })()

49
tests/licensing.rs Normal file
View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::{body::MessageBody, test};
use sqlx::PgPool;
#[allow(dead_code)]
mod common;
#[sqlx::test(fixtures("default"))]
async fn is_linked(pool: PgPool) {
let srv = test::init_service(li7y::app(&common::config(), &pool)).await;
let req = test::TestRequest::get().uri("/login").to_request();
let res = test::call_service(&srv, req).await;
assert!(res.status().is_success());
let body = String::from_utf8(res.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
assert!(body.contains(r#"<a href="/licensing">"#));
}
#[sqlx::test(fixtures("default"))]
async fn contains_basic_information(pool: PgPool) {
let srv = test::init_service(li7y::app(&common::config(), &pool)).await;
let req = test::TestRequest::get().uri("/licensing").to_request();
let res = test::call_service(&srv, req).await;
assert!(res.status().is_success());
let body = String::from_utf8(res.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
// crate names
assert!(body.contains(">actix-web<"));
assert!(body.contains(">sqlx-postgres<"));
// author names (only me and entries without email address)
assert!(body.contains(">Simon Bruder &lt;simon@sbruder.de&gt;<"));
assert!(body.contains(">RustCrypto Developers<"));
// license links (only partial, as I dont want to check class names)
assert!(body.contains(r##"href="#license-Apache-2.0">Apache-2.0<"##));
assert!(body.contains(r##"href="#license-MIT">MIT<"##));
}