Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
Simon Bruder | 76901b1ab8 | ||
Simon Bruder | 0f46fd8e25 | ||
Simon Bruder | 9207ff7ec8 | ||
Simon Bruder | cb25a4c29b | ||
Simon Bruder | b574bded1c | ||
Simon Bruder | 615f073995 |
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
151
Cargo.lock
generated
151
Cargo.lock
generated
|
@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -213,7 +213,7 @@ dependencies = [
|
|||
"actix-router",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -513,6 +513,38 @@ dependencies = [
|
|||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
|
||||
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.6"
|
||||
|
@ -570,7 +602,7 @@ dependencies = [
|
|||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -683,6 +715,12 @@ dependencies = [
|
|||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "current_platform"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a74858bcfe44b22016cb49337d7b6f04618c58e5dbfdef61b06b8c434324a0bc"
|
||||
|
||||
[[package]]
|
||||
name = "datamatrix"
|
||||
version = "0.3.1"
|
||||
|
@ -724,9 +762,15 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -754,6 +798,45 @@ dependencies = [
|
|||
"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.74",
|
||||
"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.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.34"
|
||||
|
@ -780,7 +863,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -949,7 +1032,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1234,6 +1317,7 @@ dependencies = [
|
|||
"base64 0.22.1",
|
||||
"clap",
|
||||
"datamatrix",
|
||||
"embed-licensing",
|
||||
"enum-iterator",
|
||||
"env_logger 0.11.5",
|
||||
"futures-util",
|
||||
|
@ -1242,6 +1326,7 @@ dependencies = [
|
|||
"maud",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"pretty_assertions",
|
||||
"printpdf",
|
||||
"quickcheck",
|
||||
"quickcheck_macros",
|
||||
|
@ -1249,6 +1334,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_urlencoded",
|
||||
"serde_variant",
|
||||
"spdx",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"time",
|
||||
|
@ -1361,7 +1447,7 @@ dependencies = [
|
|||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1650,6 +1736,16 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "printpdf"
|
||||
version = "0.7.0"
|
||||
|
@ -1850,7 +1946,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
@ -1918,6 +2014,9 @@ name = "semver"
|
|||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
|
@ -1936,16 +2035,17 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.120"
|
||||
version = "1.0.124"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
||||
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
@ -2037,6 +2137,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
|
@ -2304,9 +2413,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.72"
|
||||
version = "2.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2342,7 +2451,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2452,7 +2561,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2622,7 +2731,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
@ -2644,7 +2753,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
@ -2819,6 +2928,12 @@ version = "0.52.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
|
@ -2836,7 +2951,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -8,6 +8,7 @@ version = "0.0.0"
|
|||
authors = ["Simon Bruder <simon@sbruder.de>"]
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
repository = "https://git.sbruder.de/simon/li7y"
|
||||
|
||||
[dependencies]
|
||||
actix-identity = "0.7.1"
|
||||
|
@ -17,6 +18,7 @@ barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
|
|||
base64 = "0.22.1"
|
||||
clap = { version = "4.5.10", features = ["derive", "env"] }
|
||||
datamatrix = "0.3.1"
|
||||
embed-licensing = { version = "0.3.1", features = ["current_platform"] }
|
||||
enum-iterator = "2.1.0"
|
||||
env_logger = "0.11.3"
|
||||
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_urlencoded = "0.7.1"
|
||||
serde_variant = "0.1.3"
|
||||
spdx = { version = "0.10.6", features = ["text"] }
|
||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid", "time"] }
|
||||
thiserror = "1.0.61"
|
||||
time = { version = "0.3.36", features = ["parsing", "serde"] }
|
||||
|
@ -37,6 +40,7 @@ uuid = { version = "1.9.0", features = ["serde", "v4"] }
|
|||
|
||||
[dev-dependencies]
|
||||
actix-http = "3.8.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
quickcheck = "1.0.3"
|
||||
quickcheck_macros = "1.0.0"
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ allow = [
|
|||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
confidence-threshold = 0.95
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use sqlx::query;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -13,6 +15,12 @@ pub struct ItemClassListEntry {
|
|||
pub parent: Option<ItemClassPreview>,
|
||||
}
|
||||
|
||||
pub struct ItemClassTreeElement {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub children: Vec<ItemClassTreeElement>,
|
||||
}
|
||||
|
||||
impl ItemClassRepository {
|
||||
pub async fn list(&self) -> sqlx::Result<Vec<ItemClassListEntry>> {
|
||||
query!(
|
||||
|
@ -33,4 +41,77 @@ impl ItemClassRepository {
|
|||
.fetch_all(&self.pool)
|
||||
.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(dbg!(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,12 @@ mod edit;
|
|||
mod list;
|
||||
mod show;
|
||||
|
||||
use sqlx::PgPool;
|
||||
use sqlx::{query, PgPool};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use add::{ItemClassAddForm, ItemClassAddFormPrefilled};
|
||||
pub use edit::ItemClassEditForm;
|
||||
pub use list::ItemClassTreeElement;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ItemClassRepository {
|
||||
|
@ -36,3 +37,20 @@ impl ItemClassPreview {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{error, get, web, Responder};
|
||||
use maud::html;
|
||||
use maud::{html, Render};
|
||||
|
||||
use crate::database::{items::ItemPreview, ItemRepository};
|
||||
use crate::frontend::templates::{
|
||||
|
@ -61,7 +61,13 @@ async fn get(
|
|||
td { (ItemPreview::new(item.id, item.name.clone().terse())) }
|
||||
td { (item.state) }
|
||||
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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{error, get, web, Responder};
|
||||
use maud::html;
|
||||
use maud::{html, Render};
|
||||
use serde_variant::to_variant_name;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
@ -121,7 +121,13 @@ async fn get(
|
|||
}
|
||||
tr {
|
||||
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 {
|
||||
th { "Original Packaging" }
|
||||
|
|
|
@ -6,7 +6,6 @@ use actix_identity::Identity;
|
|||
use actix_web::{error, get, web, Responder};
|
||||
use maud::html;
|
||||
|
||||
use crate::database::item_classes::ItemClassPreview;
|
||||
use crate::database::ItemClassRepository;
|
||||
use crate::frontend::templates::{
|
||||
self,
|
||||
|
@ -24,7 +23,7 @@ async fn get(
|
|||
user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let item_classes = item_class_repo
|
||||
.list()
|
||||
.tree()
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
|
@ -47,26 +46,9 @@ async fn get(
|
|||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
table .table {
|
||||
thead {
|
||||
tr {
|
||||
th { "Name" }
|
||||
th { "Parents" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for item_class in item_classes {
|
||||
tr {
|
||||
td { (ItemClassPreview::new(item_class.id, item_class.name)) }
|
||||
td {
|
||||
@if let Some(parent) = item_class.parent {
|
||||
(parent)
|
||||
} @else {
|
||||
"-"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ul {
|
||||
@for item_class in item_classes {
|
||||
(item_class)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{error, get, web, Responder};
|
||||
use maud::html;
|
||||
use maud::{html, Render};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::ItemClassRepository;
|
||||
|
@ -31,6 +31,11 @@ async fn get(
|
|||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
let parents = item_class_repo
|
||||
.parents(id)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
let children = item_class_repo
|
||||
.children(id)
|
||||
.await
|
||||
|
@ -96,10 +101,14 @@ async fn get(
|
|||
th { "Name" }
|
||||
td { (item_class.name) }
|
||||
}
|
||||
@if let Some(parent) = item_class.parent {
|
||||
tr {
|
||||
th { "Parent" }
|
||||
td { (parent) }
|
||||
tr {
|
||||
th { "Parents" }
|
||||
td {
|
||||
(templates::helpers::parents_breadcrumb(
|
||||
&item_class.name,
|
||||
parents.iter().map(|parent| parent as &dyn Render).collect(),
|
||||
true
|
||||
))
|
||||
}
|
||||
}
|
||||
tr {
|
||||
|
|
488
src/frontend/licensing.rs
Normal file
488
src/frontend/licensing.rs
Normal 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),
|
||||
])),
|
||||
]))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ mod item;
|
|||
mod item_class;
|
||||
mod jump;
|
||||
mod labels;
|
||||
mod licensing;
|
||||
pub mod templates;
|
||||
|
||||
use actix_identity::Identity;
|
||||
|
@ -19,7 +20,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
.configure(item::config)
|
||||
.configure(item_class::config)
|
||||
.configure(jump::config)
|
||||
.configure(labels::config);
|
||||
.configure(labels::config)
|
||||
.configure(licensing::config);
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
|
|
|
@ -7,7 +7,6 @@ use std::fmt;
|
|||
use maud::{html, Markup, PreEscaped, Render};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::items::{ItemName, ItemPreview};
|
||||
use crate::label::LabelPreset;
|
||||
|
||||
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;
|
||||
|
||||
html! {
|
||||
|
@ -178,10 +177,10 @@ pub fn parents_breadcrumb(name: ItemName, parents: &[ItemPreview], full: bool) -
|
|||
@if !full && parents.len() > LIMIT {
|
||||
li .breadcrumb-item { "…" }
|
||||
}
|
||||
@let parents: Box<dyn Iterator<Item = &ItemPreview>> = if full {
|
||||
Box::new(parents.iter())
|
||||
@let parents: Box<dyn Iterator<Item = &dyn Render>> = if full {
|
||||
Box::new(parents.into_iter())
|
||||
} else {
|
||||
Box::new(parents.iter().rev().take(LIMIT).rev())
|
||||
Box::new(parents.into_iter().rev().take(LIMIT).rev())
|
||||
};
|
||||
@for parent in parents {
|
||||
li .breadcrumb-item {
|
||||
|
|
|
@ -59,7 +59,13 @@ fn footer() -> Markup {
|
|||
html! {
|
||||
footer .mt-auto.py-3.bg-body-tertiary.text-body-tertiary {
|
||||
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" }
|
||||
")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ use std::fmt::{self, Display};
|
|||
use maud::{html, Markup, Render};
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
const datalistHint = (input, hint) => {
|
||||
const selected = input.list.querySelector(`option[value="${input.value}"]`);
|
||||
if (selected === null)
|
||||
hint.innerText = ""
|
||||
else {
|
||||
hint.innerHTML = ""
|
||||
if (selected !== null) {
|
||||
const linkPrefix = input.list.dataset.linkPrefix;
|
||||
if (linkPrefix === undefined) {
|
||||
hint.innerHTML = selected.innerHTML
|
||||
|
@ -86,7 +85,32 @@
|
|||
})
|
||||
}
|
||||
|
||||
document.getElementById("add-event-modal").addEventListener("show.bs.modal", e => {
|
||||
document.getElementById("event").value = e.relatedTarget.dataset.eventType
|
||||
const addEventModal = document.getElementById("add-event-modal")
|
||||
if (addEventModal !== null) {
|
||||
addEventModal.addEventListener("show.bs.modal", e => {
|
||||
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
49
tests/licensing.rs
Normal 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 <simon@sbruder.de><"));
|
||||
assert!(body.contains(">RustCrypto Developers<"));
|
||||
|
||||
// license links (only partial, as I don’t 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<"##));
|
||||
}
|
Loading…
Reference in a new issue