From 1cfd0b126ceff514bfde2a4fb1200ec2ac085d69 Mon Sep 17 00:00:00 2001 From: Dietrich Date: Tue, 30 Nov 2021 12:44:49 +0100 Subject: [PATCH] Add a statistics graph * add query to server to get the statistics * render the statistics to in wasm svg * add text about the total clicks (translatable) * make cached generic over the type of the data and the type of the cache * bumping versions * rearranging some functions --- Cargo.lock | 323 +++++++++++++------------------- app/Cargo.toml | 7 +- app/src/pages/list_links.rs | 286 ++++++++++++++++++++++------ locales/Cargo.toml | 2 +- locales/de/main.ftl | 3 +- locales/en/main.ftl | 3 +- pslink/Cargo.toml | 10 +- pslink/sqlx-data.json | 36 ++++ pslink/src/bin/pslink/main.rs | 4 + pslink/src/bin/pslink/views.rs | 19 +- pslink/src/models.rs | 58 +++++- pslink/src/queries.rs | 28 ++- pslink/static/admin.css | 16 ++ shared/Cargo.toml | 6 +- shared/src/apirequests/links.rs | 5 + shared/src/datatypes.rs | 75 +++++++- 16 files changed, 610 insertions(+), 271 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40a9315..9eeb4a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,19 +41,18 @@ dependencies = [ "log", "memchr", "pin-project-lite 0.2.7", - "tokio 1.13.0", + "tokio 1.14.0", "tokio-util 0.6.9", ] [[package]] name = "actix-files" -version = "0.6.0-beta.8" +version = "0.6.0-beta.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3754d4632de16dd68f6a5d379cf9d7200ec26020482e26aa16560a258e92f3" +checksum = "e963fdf6f2108738b0cce01d5df9cd2a78afa03f362ee8be08c0f948f75d1cd7" dependencies = [ "actix-http", "actix-service 2.0.1", - "actix-utils 3.0.0", "actix-web", "askama_escape", "bitflags", @@ -65,18 +64,18 @@ dependencies = [ "mime", "mime_guess", "percent-encoding", + "pin-project-lite 0.2.7", ] [[package]] name = "actix-http" -version = "3.0.0-beta.11" +version = "3.0.0-beta.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b510d35f13987537289f38bf136e7e702a5c87cc28760310cc459544f40afd" +checksum = "1bc3f9d97e32d75fae3ad7d955ac005eea3fd3ea60a89132768700911a60fd94" dependencies = [ "actix-codec 0.4.1", - "actix-rt 2.4.0", + "actix-rt 2.5.0", "actix-service 2.0.1", - "actix-tls", "actix-utils 3.0.0", "ahash", "base64 0.13.0", @@ -98,22 +97,20 @@ dependencies = [ "local-channel", "log", "mime", - "once_cell", "percent-encoding", "pin-project 1.0.8", "pin-project-lite 0.2.7", "rand 0.8.4", "sha-1", "smallvec", - "tokio 1.13.0", "zstd", ] [[package]] name = "actix-identity" -version = "0.4.0-beta.3" +version = "0.4.0-beta.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bfff0a087e890d3b9bed74951a554dc709c3ac92f855b07a6767d42bca9c7c2" +checksum = "7858dff821616d585dbc6ca0d64a3d571c8e567ca0a19f77342f9b8bb29a04be" dependencies = [ "actix-service 2.0.1", "actix-web", @@ -174,13 +171,13 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0c218d0a17c120f10ee0c69c9f0c45d87319e8f66b1f065e8412b612fc3e24" +checksum = "05c2f80ce8d0c990941c7a7a931f69fd0701b76d521f8d36298edf59cd3fbf1f" dependencies = [ "actix-macros 0.2.3", "futures-core", - "tokio 1.13.0", + "tokio 1.14.0", ] [[package]] @@ -205,18 +202,20 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.0.0-beta.6" +version = "2.0.0-beta.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7367665785765b066ad16e1086d26a087f696bc7c42b6f93004ced6cfcf1eeca" +checksum = "411dd3296dd317ff5eff50baa13f31923ea40ec855dd7f2d3ed8639948f0195f" dependencies = [ - "actix-rt 2.4.0", + "actix-rt 2.5.0", "actix-service 2.0.1", "actix-utils 3.0.0", "futures-core", + "futures-util", "log", "mio 0.7.14", "num_cpus", - "tokio 1.13.0", + "socket2 0.4.2", + "tokio 1.14.0", ] [[package]] @@ -255,23 +254,6 @@ dependencies = [ "threadpool", ] -[[package]] -name = "actix-tls" -version = "3.0.0-beta.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af84e13e4600829858a3e68079be710d1ada461431e1e4c5ae663479ea0a3c" -dependencies = [ - "actix-codec 0.4.1", - "actix-rt 2.4.0", - "actix-service 2.0.1", - "actix-utils 3.0.0", - "derive_more", - "futures-core", - "http", - "log", - "tokio-util 0.6.9", -] - [[package]] name = "actix-utils" version = "2.0.0" @@ -304,16 +286,16 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.0.0-beta.10" +version = "4.0.0-beta.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a4b9d00991d8da308070a5cea7f1bbaa153a91c3fb5567937d99b9f46d601e" +checksum = "e87cfc4efaad42f8a054e269d1b85046397ff4e8707e49128dea3f99a512a9d6" dependencies = [ "actix-codec 0.4.1", "actix-http", "actix-macros 0.2.3", "actix-router", - "actix-rt 2.4.0", - "actix-server 2.0.0-beta.6", + "actix-rt 2.5.0", + "actix-server 2.0.0-beta.9", "actix-service 2.0.1", "actix-utils 3.0.0", "actix-web-codegen", @@ -339,7 +321,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2 0.4.2", - "time 0.3.4", + "time 0.3.5", "url", ] @@ -363,7 +345,7 @@ dependencies = [ "actix-service 2.0.1", "actix-web", "derive_more", - "futures 0.3.17", + "futures 0.3.18", "static-files", ] @@ -462,15 +444,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -482,9 +455,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63" +checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" [[package]] name = "argonautica" @@ -717,9 +690,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" dependencies = [ "jobserver", ] @@ -767,11 +740,13 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "js-sys", "libc", "num-integer", "num-traits", "serde", "time 0.1.43", + "wasm-bindgen", "winapi 0.3.9", ] @@ -797,11 +772,11 @@ dependencies = [ [[package]] name = "clap" -version = "2.33.3" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "ansi_term 0.11.0", + "ansi_term", "atty", "bitflags", "strsim", @@ -878,9 +853,9 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b4ac5559dd39f7bdc516f769cb412b151585d8886d216871a8435ed7f862cd" +checksum = "b3f7034c0932dc36f5bd8ec37368d971346809435824f277cb3b8299fc56167c" dependencies = [ "cookie 0.15.1", "idna", @@ -946,9 +921,9 @@ checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" [[package]] name = "crc32fast" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +checksum = "3825b1e8580894917dc4468cb634a1b4e9745fddc854edad72d9c04644c0319f" dependencies = [ "cfg-if 1.0.0", ] @@ -1047,14 +1022,14 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.16" +version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", "proc-macro2 1.0.32", "quote 1.0.10", - "rustc_version 0.3.3", + "rustc_version 0.4.0", "syn", ] @@ -1386,9 +1361,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" dependencies = [ "futures-channel", "futures-core", @@ -1401,9 +1376,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" dependencies = [ "futures-core", "futures-sink", @@ -1411,9 +1386,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" [[package]] name = "futures-cpupool" @@ -1427,9 +1402,9 @@ dependencies = [ [[package]] name = "futures-executor" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" dependencies = [ "futures-core", "futures-task", @@ -1449,18 +1424,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" dependencies = [ - "autocfg 1.0.1", - "proc-macro-hack", "proc-macro2 1.0.32", "quote 1.0.10", "syn", @@ -1468,23 +1441,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" dependencies = [ - "autocfg 1.0.1", "futures-channel", "futures-core", "futures-io", @@ -1494,8 +1466,6 @@ dependencies = [ "memchr", "pin-project-lite 0.2.7", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -1645,7 +1615,7 @@ dependencies = [ "http", "indexmap", "slab", - "tokio 1.13.0", + "tokio 1.14.0", "tokio-util 0.6.9", "tracing", ] @@ -1764,9 +1734,9 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "humantime" @@ -1779,9 +1749,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.14" +version = "0.14.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" +checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" dependencies = [ "bytes 1.1.0", "futures-channel", @@ -1795,7 +1765,7 @@ dependencies = [ "itoa", "pin-project-lite 0.2.7", "socket2 0.4.2", - "tokio 1.13.0", + "tokio 1.14.0", "tower-service", "tracing", "want", @@ -1810,7 +1780,7 @@ dependencies = [ "bytes 1.1.0", "hyper", "native-tls", - "tokio 1.13.0", + "tokio 1.14.0", "tokio-native-tls", ] @@ -1988,9 +1958,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" [[package]] name = "libloading" @@ -2352,9 +2322,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.70" +version = "0.9.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf" +checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" dependencies = [ "autocfg 1.0.1", "cc", @@ -2371,7 +2341,7 @@ checksum = "e1cf9b1c4e9a6c4de793c632496fa490bdc0e1eea73f0c91394f7b6990935d22" dependencies = [ "async-trait", "crossbeam-channel", - "futures 0.3.17", + "futures 0.3.18", "js-sys", "lazy_static", "percent-encoding", @@ -2462,9 +2432,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" +checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" [[package]] name = "path-matchers" @@ -2493,15 +2463,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - [[package]] name = "pin-project" version = "0.4.28" @@ -2597,9 +2558,9 @@ checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "predicates" -version = "2.0.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474" +checksum = "95e5a7689e456ab905c22c2b48225bb921aba7c8dfa58440d68ba13f6222a715" dependencies = [ "difflib", "float-cmp", @@ -2655,12 +2616,6 @@ version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - [[package]] name = "proc-macro2" version = "0.4.30" @@ -2681,17 +2636,17 @@ dependencies = [ [[package]] name = "psl-types" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b398073e7cdd6f05934389a8f5961e3aabfa66675b6f440df4e2c793d51a4f" +checksum = "01e985332847890fc9ff26986900c036c8b3fde8da6a31d8ca315cc116405b8c" [[package]] name = "pslink" -version = "0.4.5" +version = "0.4.6" dependencies = [ "actix-files", "actix-identity", - "actix-rt 2.4.0", + "actix-rt 2.5.0", "actix-server 1.0.4", "actix-web", "actix-web-static-files", @@ -2720,7 +2675,7 @@ dependencies = [ "tempdir", "test_bin", "thiserror", - "tokio 1.13.0", + "tokio 1.14.0", "tracing", "tracing-actix-web", "tracing-opentelemetry", @@ -2729,8 +2684,9 @@ dependencies = [ [[package]] name = "pslink-app" -version = "0.4.5" +version = "0.4.6" dependencies = [ + "chrono", "enum-map", "fluent 0.16.0", "image", @@ -2747,7 +2703,7 @@ dependencies = [ [[package]] name = "pslink-locales" -version = "0.4.5" +version = "0.4.6" dependencies = [ "fluent 0.16.0", "pslink-shared", @@ -2757,7 +2713,7 @@ dependencies = [ [[package]] name = "pslink-shared" -version = "0.4.5" +version = "0.4.6" dependencies = [ "chrono", "enum-map", @@ -3141,7 +3097,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "time 0.2.27", - "tokio 1.13.0", + "tokio 1.14.0", "tokio-native-tls", "url", "wasm-bindgen", @@ -3198,11 +3154,11 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 0.11.0", + "semver 1.0.4", ] [[package]] @@ -3219,10 +3175,16 @@ dependencies = [ ] [[package]] -name = "ryu" +name = "rustversion" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + +[[package]] +name = "ryu" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" [[package]] name = "same-file" @@ -3304,7 +3266,7 @@ dependencies = [ "cookie 0.14.4", "dbg", "enclose", - "futures 0.3.17", + "futures 0.3.18", "gloo-file", "gloo-timers", "indexmap", @@ -3322,9 +3284,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55267dd945ff7d9388dd56c5a6a97477bdb6f2584be5ba45fdde7207b7cac3a6" +checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" [[package]] name = "semver" @@ -3332,17 +3294,14 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser 0.7.0", + "semver-parser", ] [[package]] name = "semver" -version = "0.11.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser 0.10.2", -] +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "semver-parser" @@ -3350,15 +3309,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "serde" version = "1.0.130" @@ -3381,9 +3331,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.69" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "indexmap", "itoa", @@ -3597,7 +3547,7 @@ checksum = "4dc33c35d54774eed73d54568d47a6ac099aed8af5e1556a017c131be88217d5" dependencies = [ "dotenv", "either", - "futures 0.3.17", + "futures 0.3.18", "heck", "hex", "once_cell", @@ -3618,9 +3568,9 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d1bd069de53442e7a320f525a6d4deb8bb0621ac7a55f7eccbc2b58b57f43d0" dependencies = [ - "actix-rt 2.4.0", + "actix-rt 2.5.0", "once_cell", - "tokio 1.13.0", + "tokio 1.14.0", "tokio-rustls", ] @@ -3717,19 +3667,20 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strum" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" [[package]] name = "strum_macros" -version = "0.22.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" dependencies = [ "heck", "proc-macro2 1.0.32", "quote 1.0.10", + "rustversion", "syn", ] @@ -3741,9 +3692,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" dependencies = [ "proc-macro2 1.0.32", "quote 1.0.10", @@ -3905,9 +3856,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99beeb0daeac2bd1e86ac2c21caddecb244b39a093594da1a661ec2060c7aedd" +checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" dependencies = [ "itoa", "libc", @@ -3944,9 +3895,9 @@ checksum = "29738eedb4388d9ea620eeab9384884fc3f06f586a2eddb56bedc5885126c7c1" [[package]] name = "tinyvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -3978,9 +3929,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg 1.0.1", "bytes 1.1.0", @@ -4002,7 +3953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" dependencies = [ "native-tls", - "tokio 1.13.0", + "tokio 1.14.0", ] [[package]] @@ -4012,7 +3963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls", - "tokio 1.13.0", + "tokio 1.14.0", "webpki", ] @@ -4024,7 +3975,7 @@ checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", "pin-project-lite 0.2.7", - "tokio 1.13.0", + "tokio 1.14.0", ] [[package]] @@ -4052,7 +4003,7 @@ dependencies = [ "futures-sink", "log", "pin-project-lite 0.2.7", - "tokio 1.13.0", + "tokio 1.14.0", ] [[package]] @@ -4076,9 +4027,9 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.4.0-beta.16" +version = "0.5.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c2b77e219084424d5928fc42557bdaa011a8ef00a1c8492adcfc8fb8bb83c6" +checksum = "994e4a59135823bdca121a8d086e3fcc71741c8677b47fa95a6afdd15e8f646f" dependencies = [ "actix-web", "pin-project 1.0.8", @@ -4157,7 +4108,7 @@ version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" dependencies = [ - "ansi_term 0.12.1", + "ansi_term", "chrono", "lazy_static", "matchers", @@ -4194,12 +4145,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - [[package]] name = "unic-langid" version = "0.9.0" @@ -4515,9 +4460,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.1.5" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483a59fee1a93fec90eb08bc2eb4315ef10f4ebc478b3a5fadc969819cb66117" +checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" dependencies = [ "wasm-bindgen", "web-sys", @@ -4587,18 +4532,18 @@ dependencies = [ [[package]] name = "zstd" -version = "0.7.0+zstd.1.4.9" +version = "0.9.0+zstd.1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9428752481d8372e15b1bf779ea518a179ad6c771cca2d2c60e4fbff3cc2cd52" +checksum = "07749a5dc2cb6b36661290245e350f15ec3bbb304e493db54a1d354480522ccd" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "3.1.0+zstd.1.4.9" +version = "4.1.1+zstd.1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa1926623ad7fe406e090555387daf73db555b948134b4d73eac5eb08fb666d" +checksum = "c91c90f2c593b003603e5e0493c837088df4469da25aafff8bce42ba48caf079" dependencies = [ "libc", "zstd-sys", @@ -4606,9 +4551,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.5.0+zstd.1.4.9" +version = "1.6.1+zstd.1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6c094340240369025fc6b731b054ee2a834328fa584310ac96aa4baebdc465" +checksum = "615120c7a2431d16cf1cf979e7fc31ba7a5b5e5707b29c8a99e5dbf8a8392a33" dependencies = [ "cc", "libc", diff --git a/app/Cargo.toml b/app/Cargo.toml index 3b51339..a746e31 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["url", "link", "webpage", "actix", "web"] license = "MIT OR Apache-2.0" readme = "README.md" repository = "https://github.com/enaut/pslink/" -version = "0.4.5" +version = "0.4.6" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -20,11 +20,12 @@ fluent = "0.16" seed = "0.8" serde = {version="1.0", features = ["derive"]} unic-langid = "0.9" -strum_macros = "0.22" -strum = "0.22" +strum_macros = "0.23" +strum = "0.23" enum-map = "1" qrcode = "0.12" image = "0.23" +chrono = {version="0.4", features=["wasmbind"]} pslink-shared = { version="0.4", path = "../shared" } pslink-locales = { version="0.4", path = "../locales" } diff --git a/app/src/pages/list_links.rs b/app/src/pages/list_links.rs index fb678cc..39d7985 100644 --- a/app/src/pages/list_links.rs +++ b/app/src/pages/list_links.rs @@ -1,14 +1,15 @@ //! List all the links the own links editable or if an admin is logged in all links editable. use std::ops::Deref; +use chrono::Datelike; use enum_map::EnumMap; use fluent::fluent_args; use image::{DynamicImage, ImageOutputFormat, Luma}; use pslink_locales::I18n; use qrcode::{render::svg, QrCode}; use seed::{ - a, attrs, div, h1, img, input, log, nodes, prelude::*, raw, section, span, table, td, th, tr, - Url, C, IF, + a, attrs, div, h1, img, input, log, nodes, path, prelude::*, raw, section, span, svg, table, + td, th, tr, Url, C, IF, }; use web_sys::{IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit}; @@ -16,9 +17,9 @@ use pslink_shared::{ apirequests::general::Ordering, apirequests::{ general::{EditMode, Message, Operation, Status}, - links::{LinkDelta, LinkOverviewColumns, LinkRequestForm}, + links::{LinkDelta, LinkOverviewColumns, LinkRequestForm, StatisticsRequest}, }, - datatypes::{FullLink, Lang, Loadable, User}, + datatypes::{Clicks, Count, FullLink, Lang, Loadable, Statistics, User, WeekCount}, }; use crate::{get_host, unwrap_or_return}; @@ -56,7 +57,7 @@ pub fn init(mut url: Url, orders: &mut impl Orders, i18n: I18n) -> Model { #[derive(Debug)] pub struct Model { - links: Vec>, // will contain the links to display + links: Vec>, // will contain the links to display load_more: ElRef, i18n: I18n, // to translate formconfig: LinkRequestForm, // when requesting links the form is stored here @@ -78,12 +79,18 @@ impl Model { } #[derive(Debug)] -pub struct Cached { +pub struct LinkCache { + qr: String, + stats: Option>, +} + +#[derive(Debug)] +pub struct Cached { data: T, - cache: String, + cache: S, } -impl Deref for Cached { +impl Deref for Cached { type Target = T; fn deref(&self) -> &Self::Target { @@ -145,7 +152,7 @@ struct FilterInput { } /// A message can either edit or query. (or set a dialog) -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Msg { Query(QueryMsg), // Messages related to querying links Edit(EditMsg), // Messages related to editing links @@ -156,10 +163,12 @@ pub enum Msg { } /// All the messages related to requesting information from the server. -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum QueryMsg { Fetch, FetchAdditional, + GetStatistics(i64), + ReceivedStatistics(Statistics), OrderBy(LinkOverviewColumns), Received(Vec), ReceivedAdditional(Vec), @@ -262,70 +271,42 @@ pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut imp consecutive_load(model, orders); } // Default to ascending ordering but if the links are already sorted according to this column toggle between ascending and descending ordering. - QueryMsg::OrderBy(column) => { - model.formconfig.order = model.formconfig.order.as_ref().map_or_else( - || { - Some(Operation { - column: column.clone(), - value: Ordering::Ascending, - }) - }, - |order| { - Some(Operation { - column: column.clone(), - value: if order.column == column && order.value == Ordering::Ascending { - Ordering::Descending - } else { - Ordering::Ascending - }, - }) - }, - ); - // After setting up the ordering fetch the links from the server again with the new filter settings. - // If the new filters and ordering include more links the list would be incomplete otherwise. - orders.send_msg(Msg::Query(QueryMsg::Fetch)); - - // Also sort the links locally - can probably removed... - model.links.sort_by(match column { - LinkOverviewColumns::Code => { - |o: &Cached, t: &Cached| o.link.code.cmp(&t.link.code) - } - LinkOverviewColumns::Description => { - |o: &Cached, t: &Cached| o.link.title.cmp(&t.link.title) - } - LinkOverviewColumns::Target => { - |o: &Cached, t: &Cached| o.link.target.cmp(&t.link.target) - } - LinkOverviewColumns::Author => |o: &Cached, t: &Cached| { - o.user.username.cmp(&t.user.username) - }, - LinkOverviewColumns::Statistics => |o: &Cached, t: &Cached| { - o.clicks.number.cmp(&t.clicks.number) - }, - }); - } + QueryMsg::OrderBy(ref column) => order_columns(orders, model, column), QueryMsg::Received(response) => { model.links = response .into_iter() .map(|l| { let cache = generate_qr_from_code(&l.link.code); - Cached { data: l, cache } + Cached { + data: l, + cache: LinkCache { + qr: cache, + stats: None, + }, + } }) .collect(); + request_all_statistics(orders, model); } QueryMsg::ReceivedAdditional(response) => { if response.len() < model.formconfig.amount { - log!("There are no more links! "); model.everything_loaded = true; }; let mut new_links = response .into_iter() .map(|l| { let cache = generate_qr_from_code(&l.link.code); - Cached { data: l, cache } + Cached { + data: l, + cache: LinkCache { + qr: cache, + stats: None, + }, + } }) .collect(); model.links.append(&mut new_links); + request_all_statistics(orders, model); } QueryMsg::CodeFilterChanged(s) => { log!("Filter is: ", &s); @@ -351,9 +332,113 @@ pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut imp model.formconfig.filter[LinkOverviewColumns::Author].sieve = sanit; orders.send_msg(Msg::Query(QueryMsg::Fetch)); } + QueryMsg::GetStatistics(link_id) => { + orders.skip(); // No need to rerender + request_statistics(orders, link_id); + } + QueryMsg::ReceivedStatistics(statistics) => { + for i in 1..model.links.len() { + if model.links[i].data.link.id == statistics.link_id { + model.links[i].data.clicks = Clicks::Extended(statistics.clone()); + + model.links[i].cache.stats = statistics + .values + .iter() + .max() + .map(|maximum| render_stats(statistics.clone(), maximum)); + } + } + } } } +fn order_columns(orders: &mut impl Orders, model: &mut Model, column: &LinkOverviewColumns) { + model.formconfig.order = model.formconfig.order.as_ref().map_or_else( + || { + Some(Operation { + column: column.clone(), + value: Ordering::Ascending, + }) + }, + |order| { + Some(Operation { + column: column.clone(), + value: if &order.column == column && order.value == Ordering::Ascending { + Ordering::Descending + } else { + Ordering::Ascending + }, + }) + }, + ); + // After setting up the ordering fetch the links from the server again with the new filter settings. + // If the new filters and ordering include more links the list would be incomplete otherwise. + orders.send_msg(Msg::Query(QueryMsg::Fetch)); + + // Also sort the links locally - can probably removed... + model.links.sort_by(match column { + LinkOverviewColumns::Code => { + |o: &Cached, t: &Cached| o.link.code.cmp(&t.link.code) + } + LinkOverviewColumns::Description => { + |o: &Cached, t: &Cached| o.link.title.cmp(&t.link.title) + } + LinkOverviewColumns::Target => { + |o: &Cached, t: &Cached| o.link.target.cmp(&t.link.target) + } + LinkOverviewColumns::Author => { + |o: &Cached, t: &Cached| o.user.username.cmp(&t.user.username) + } + LinkOverviewColumns::Statistics => { + |o: &Cached, t: &Cached| o.clicks.cmp(&t.clicks) + } + }); +} + +fn request_all_statistics(orders: &mut impl Orders, model: &Model) { + for m in &model.links { + match m.data.clicks { + Clicks::Count(_) => { + let id = m.link.id; + orders.perform_cmd(cmds::timeout(500, move || { + Msg::Query(QueryMsg::GetStatistics(id)) + })); + } + Clicks::Extended(_) => (), + } + } +} + +fn request_statistics(orders: &mut impl Orders, link_id: i64) { + let data = StatisticsRequest { link_id }; + orders.perform_cmd(async move { + let data = data; + let url = "/admin/json/get_link_statistics/"; + // create a request + let request = unwrap_or_return!( + Request::new(url).method(Method::Post).json(&data), + Msg::SetMessage("Failed to parse data".to_string()) + ); + // send the request and receive a response + let response = unwrap_or_return!( + fetch(request).await, + Msg::SetMessage("Failed to send data".to_string()) + ); + // check the html status to be 200 + let response = unwrap_or_return!( + response.check_status(), + Msg::SetMessage("Wrong response code".to_string()) + ); + // unpack the response into the `Vec` + let statistics: Statistics = unwrap_or_return!( + response.json().await, + Msg::SetMessage("Invalid response".to_string()) + ); + // The message that is sent by perform_cmd after this async block is completed + Msg::Query(QueryMsg::ReceivedStatistics(statistics)) + }); +} + fn initial_load(model: &Model, orders: &mut impl Orders) { let mut data = model.formconfig.clone(); data.offset = 0; @@ -622,7 +707,10 @@ pub fn view(model: &Model, logged_in_user: &User) -> Node { // Add filter fields right below the headlines view_link_table_filter_input(model, &t), // Add all the content lines - model.links.iter().map(|l| { view_link(l, logged_in_user) }) + model + .links + .iter() + .map(|l| { view_link(l, logged_in_user, t) }) ], if not(model.everything_loaded) { a![ @@ -729,7 +817,11 @@ fn view_link_table_filter_input String>(model: &Model, t: F) -> N } /// display a single table row containing one link -fn view_link(l: &Cached, logged_in_user: &User) -> Node { +fn view_link String>( + l: &Cached, + logged_in_user: &User, + t: F, +) -> Node { use pslink_shared::apirequests::users::Role; let link = LinkDelta::from(l.data.clone()); tr![ @@ -740,7 +832,25 @@ fn view_link(l: &Cached, logged_in_user: &User) -> Node { td![&l.link.title], td![&l.link.target], td![&l.user.username], - td![&l.clicks.number], + match &l.clicks { + Clicks::Count(Count { number }) => td![number], + Clicks::Extended(statistics) => + if let Some(nodes) = l.cache.stats.clone() { + td![ + span!( + C!("stats_total"), + t("total_clicks"), + match &l.data.clicks { + Clicks::Count(c) => c.number, + Clicks::Extended(e) => e.total.number, + } + ), + nodes + ] + } else { + td!(statistics.total.number) + }, + }, { td![ C!["table_qr"], @@ -748,7 +858,7 @@ fn view_link(l: &Cached, logged_in_user: &User) -> Node { ev(Ev::Click, |event| event.stop_propagation()), attrs![At::Href => format!("/admin/download/png/{}", &l.link.code), At::Download => true.as_at_value()], - raw!(&l.cache) + raw!(&l.cache.qr) ] ] }, @@ -769,6 +879,64 @@ fn view_link(l: &Cached, logged_in_user: &User) -> Node { ] } +/// Render stats is best performed in the update rather than in the view cycle to avoid needless recalculations. Since the database only sends a list of weeks that contain clicks this function has to add the weeks that do not contain clicks. This is more easily said than done since the first and last week index are not constant, the ordering has to be right, and not every year has 52 weeks. But we ignore the last issue. +fn render_stats(q: Statistics, maximum: &WeekCount) -> Node { + let factor = 40.0 / f64::max(f64::from(maximum.total.number), 1.0); + let mut full: Vec = Vec::new(); + let mut week = chrono::Utc::now() - chrono::Duration::weeks(52); + + for with_clicks in q.values { + loop { + #[allow(clippy::cast_possible_wrap)] + let cw = week.iso_week().week() as i32; + if with_clicks.week == cw { + full.push(with_clicks); + week = week + chrono::Duration::weeks(1); + break; + } + // otherwise add another empty week + let nstat = WeekCount { + month: week.naive_local(), + total: Count { number: 0 }, + week: cw, + }; + full.push(nstat); + week = week + chrono::Duration::weeks(1); + } + } + loop { + #[allow(clippy::cast_possible_wrap)] + let cw = week.iso_week().week() as i32; + if week < chrono::Utc::now() { + let nstat = WeekCount { + month: week.naive_local(), + total: Count { number: 0 }, + week: cw, + }; + full.push(nstat); + week = week + chrono::Duration::weeks(1); + } else { + break; + } + } + #[allow(clippy::cast_possible_truncation)] + let normalized: Vec = full + .iter() + .map(|v| (40.0 - f64::from(v.total.number) * factor).round() as i64) + .collect(); + let mut points = Vec::new(); + points.push(format!("M 0 {}", &normalized[0])); + #[allow(clippy::needless_range_loop)] + for i in 1..normalized.len() { + points.push(format!("L {} {}", i * 2, &normalized[i])); + } + + svg![ + C!("statistics_graph"), + path![attrs![At::D => points.join(" "), At::Stroke => "green", At::Fill => "transparent"]] + ] +} + /// display a link editing dialog with save and close button fn edit_or_create_link String>( link: &LinkDelta, diff --git a/locales/Cargo.toml b/locales/Cargo.toml index f67a57d..cccf246 100644 --- a/locales/Cargo.toml +++ b/locales/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["url", "link", "webpage", "actix", "web"] license = "MIT OR Apache-2.0" readme = "README.md" repository = "https://github.com/enaut/pslink/" -version = "0.4.5" +version = "0.4.6" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/locales/de/main.ftl b/locales/de/main.ftl index 554b566..731c35f 100644 --- a/locales/de/main.ftl +++ b/locales/de/main.ftl @@ -49,4 +49,5 @@ make-user-regular = Zurückstufen zum normalen Nutzer role = Rolle userid = Benutzernummer -statistics = Statistik \ No newline at end of file +statistics = Statistik +total_clicks = Klicks insgesamt: \ No newline at end of file diff --git a/locales/en/main.ftl b/locales/en/main.ftl index f06d850..8e8c33d 100644 --- a/locales/en/main.ftl +++ b/locales/en/main.ftl @@ -50,4 +50,5 @@ make-user-regular = Demote to regular role = Role userid = User ID -statistics = Statistics \ No newline at end of file +statistics = Statistics +total_clicks = total clicks: \ No newline at end of file diff --git a/pslink/Cargo.toml b/pslink/Cargo.toml index a4777ea..2275359 100644 --- a/pslink/Cargo.toml +++ b/pslink/Cargo.toml @@ -9,18 +9,18 @@ license = "MIT OR Apache-2.0" name = "pslink" readme = "README.md" repository = "https://github.com/enaut/pslink/" -version = "0.4.5" +version = "0.4.6" [build-dependencies] actix-web-static-files = { git = "https://github.com/enaut/actix-web-static-files.git", branch="master" } static-files = { version = "0.2", default-features = false } [dependencies] -actix-identity = "0.4.0-beta.3" +actix-identity = "0.4.0-beta.4" actix-rt = "2.2" -actix-web = "4.0.0-beta.10" +actix-web = "4.0.0-beta.12" actix-web-static-files = { git = "https://github.com/enaut/actix-web-static-files.git", branch="master" } -actix-files = "0.6.0-beta.8" +actix-files = "0.6.0-beta.9" argonautica = "0.2" clap = "2.33" dotenv = "0.15.0" @@ -34,7 +34,7 @@ rpassword = "5.0" serde = {version="1.0", features = ["derive"]} static-files = { version = "0.2", default-features = false } thiserror = "1.0" -tracing-actix-web = "0.4.0-beta.16" +tracing-actix-web = "0.5.0-beta.3" tracing-opentelemetry = "0.15" async-trait = "0.1" enum-map = {version="1", features = ["serde"]} diff --git a/pslink/sqlx-data.json b/pslink/sqlx-data.json index 2ae7b05..2922a0b 100644 --- a/pslink/sqlx-data.json +++ b/pslink/sqlx-data.json @@ -289,5 +289,41 @@ }, "nullable": [] } + }, + "eb5c92f5a47a730bf82d92fc0107913cb37d57d99f0170538f1b1da14665bbb5": { + "query": "select code from links where id=?", + "describe": { + "columns": [ + { + "name": "code", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + } + }, + "f6cd43935b88accd904538e59f59684786b9960d7b8cd7b7d924cfcfb65e1dbe": { + "query": "select count(*) as number from clicks join links on clicks.link = links.id where links.code = ?", + "describe": { + "columns": [ + { + "name": "number", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + } } } \ No newline at end of file diff --git a/pslink/src/bin/pslink/main.rs b/pslink/src/bin/pslink/main.rs index e119a05..7e881de 100644 --- a/pslink/src/bin/pslink/main.rs +++ b/pslink/src/bin/pslink/main.rs @@ -160,6 +160,10 @@ pub async fn webservice( "/get_logged_user/", web::post().to(views::get_logged_user_json), ) + .route( + "/get_link_statistics/", + web::post().to(views::get_statistics), + ) .route("/login_user/", web::post().to(views::process_login_json)), ) .default_service(web::to(views::to_admin)), diff --git a/pslink/src/bin/pslink/views.rs b/pslink/src/bin/pslink/views.rs index 5489129..6b38d7e 100644 --- a/pslink/src/bin/pslink/views.rs +++ b/pslink/src/bin/pslink/views.rs @@ -12,11 +12,11 @@ use fluent_langneg::{ }; use fluent_templates::LanguageIdentifier; use image::{DynamicImage, ImageOutputFormat, Luma}; -use pslink::queries::{authenticate, RoleGuard}; +use pslink::queries::{authenticate, Item, RoleGuard}; use pslink_shared::{ apirequests::{ general::{Message, Status}, - links::{LinkDelta, LinkRequestForm}, + links::{LinkDelta, LinkRequestForm, StatisticsRequest}, users::{LoginUser, UserDelta, UserRequestForm}, }, datatypes::Lang, @@ -177,6 +177,21 @@ pub async fn download_png( } } +#[instrument(skip(id))] +pub async fn get_statistics( + id: Identity, + config: web::Data, + link_id: web::Json, +) -> Result { + match queries::get_statistics(&id, link_id.link_id, &config).await { + Ok(Item { + user: _, + item: stats, + }) => Ok(HttpResponse::Ok().json(stats)), + Err(e) => Err(e), + } +} + #[instrument(skip(id))] pub async fn process_create_user_json( config: web::Data, diff --git a/pslink/src/models.rs b/pslink/src/models.rs index 24f8464..5750ffe 100644 --- a/pslink/src/models.rs +++ b/pslink/src/models.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use pslink_shared::{ apirequests::{links::LinkDelta, users::Role}, - datatypes::{Count, Lang, Link, User}, + datatypes::{Count, Lang, Link, Statistics, User, WeekCount}, }; use sqlx::Row; use tracing::{error, info, instrument}; @@ -257,10 +257,66 @@ pub trait LinkDbOperations { server_config: &ServerConfig, ) -> Result<(), ServerError>; async fn update_link(&self, server_config: &ServerConfig) -> Result<(), ServerError>; + async fn get_statistics( + code: i64, + server_config: &ServerConfig, + ) -> Result; } #[async_trait] impl LinkDbOperations for Link { + /// Get a link by its code (the short url code) + /// + /// # Errors + /// fails with [`ServerError`] if the database cannot be acessed or the link is not found. + #[instrument()] + async fn get_statistics( + link_id: i64, + server_config: &ServerConfig, + ) -> Result { + // Verify that the code exists to avoid injections in the next query + let code = sqlx::query!("select code from links where id=?", link_id) + .fetch_one(&server_config.db_pool) + .await? + .code; + // The query to get the statistics carefully check code before to avoid injections. + let qry = format!( + r#"SELECT created_at AS month, + cast(strftime('%W', created_at) AS String) AS week, + count(*) AS total +FROM clicks +WHERE month > date('now', 'start of month', '-1 year') + AND link = '{}' +GROUP BY week +ORDER BY month"#, + link_id + ); + // Execute and map the query to the desired type + let values: Vec = sqlx::query(&qry) + .fetch_all(&server_config.db_pool) + .await? + .into_iter() + .map(|c| WeekCount { + month: c.get("month"), + total: Count { + number: c.get("total"), + }, + week: c.get("week"), + }) + .collect(); + let total = sqlx::query_as!( + Count, + "select count(*) as number from clicks join links on clicks.link = links.id where links.code = ?", + code + ).fetch_one(&server_config.db_pool).await?; + tracing::info!("Found Statistics: {:?}", &values); + Ok(Statistics { + link_id, + total, + values, + }) + } + /// Get a link by its code (the short url code) /// /// # Errors diff --git a/pslink/src/queries.rs b/pslink/src/queries.rs index eeb43b3..80ea1ba 100644 --- a/pslink/src/queries.rs +++ b/pslink/src/queries.rs @@ -8,7 +8,7 @@ use pslink_shared::{ links::{LinkDelta, LinkOverviewColumns, LinkRequestForm}, users::{Role, UserDelta, UserOverviewColumns, UserRequestForm}, }, - datatypes::{Count, FullLink, Lang, Link, Secret, User}, + datatypes::{Clicks, Count, FullLink, Lang, Link, Secret, Statistics, User}, }; use serde::Serialize; use sqlx::Row; @@ -137,9 +137,9 @@ pub async fn list_all_allowed( role: Role::convert(v.get("urole")), language: Lang::from_str(v.get("ulang")).expect("Should parse"), }, - clicks: Count { + clicks: Clicks::Count(Count { number: v.get("counter"), /* count is never None */ - }, + }), }); // show all links let all_links: Vec = links.collect(); @@ -561,6 +561,28 @@ pub async fn get_link( } } +/// Get monthly statistics for one link if permissions are accordingly. +/// +/// # Errors +/// Fails with [`ServerError`] if access to the database fails or this user does not have permissions. +#[instrument(skip(id))] +pub async fn get_statistics( + id: &Identity, + link_id: i64, + server_config: &ServerConfig, +) -> Result, ServerError> { + match authenticate(id, server_config).await? { + RoleGuard::Admin { user } | RoleGuard::Regular { user } => { + let stats = Link::get_statistics(link_id, server_config).await?; + Ok(Item { user, item: stats }) + } + RoleGuard::Disabled | RoleGuard::NotAuthenticated => { + warn!("User could not be authenticated!"); + Err(ServerError::User("Not Allowed".to_owned())) + } + } +} + /// Get one link if permissions are accordingly. /// /// # Errors diff --git a/pslink/static/admin.css b/pslink/static/admin.css index aed7b85..2bcdc40 100644 --- a/pslink/static/admin.css +++ b/pslink/static/admin.css @@ -64,6 +64,22 @@ td { text-align: center; border: 1px solid #ccc; padding: 10px; + position: relative; +} + +svg.statistics_graph { + width: 120px; + height: 50px; +} + +.stats_total { + display: block; + position: absolute; + font-size: 0.7em; + background-color: rgba(255, 255, 255, 0.75); + padding: 3px; + border-radius: 14px; + bottom: 0; } td.table_qr svg { diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 5a43481..8106538 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -8,11 +8,11 @@ license = "MIT OR Apache-2.0" name = "pslink-shared" readme = "../pslink/README.md" repository = "https://github.com/enaut/pslink/" -version = "0.4.5" +version = "0.4.6" [dependencies] serde = {version="1.0", features = ["derive"]} chrono = {version = "0.4", features = ["serde"] } enum-map = {version="1", features = ["serde"]} -strum_macros = "0.22" -strum = "0.22" +strum_macros = "0.23" +strum = "0.23" diff --git a/shared/src/apirequests/links.rs b/shared/src/apirequests/links.rs index 75d03a2..ce59ff1 100644 --- a/shared/src/apirequests/links.rs +++ b/shared/src/apirequests/links.rs @@ -79,6 +79,11 @@ pub enum LinkOverviewColumns { Statistics, } +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] +pub struct StatisticsRequest { + pub link_id: i64, +} + /// A struct to request a qr-code from the server #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] pub struct QrCodeRequest { diff --git a/shared/src/datatypes.rs b/shared/src/datatypes.rs index 49a5bb3..446b9d4 100644 --- a/shared/src/datatypes.rs +++ b/shared/src/datatypes.rs @@ -13,13 +13,56 @@ pub struct ListWithOwner { } /// A link together with its author and its click-count. -#[derive(Clone, Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct FullLink { pub link: Link, pub user: User, - pub clicks: Count, + pub clicks: Clicks, } +#[derive(Deserialize, Serialize, Clone, Debug)] +pub enum Clicks { + Count(Count), + Extended(Statistics), +} + +impl PartialEq for Clicks { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Count(l0), Self::Count(r0)) => l0.number == r0.number, + (Self::Extended(l0), Self::Extended(r0)) => l0.total.number == r0.total.number, + (Clicks::Count(l0), Clicks::Extended(r0)) => l0.number == r0.total.number, + (Clicks::Extended(l0), Clicks::Count(r0)) => l0.total.number == r0.number, + } + } +} + +impl PartialOrd for Clicks { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Count(l0), Self::Count(r0)) => l0.number.partial_cmp(&r0.number), + (Self::Extended(l0), Self::Extended(r0)) => { + l0.total.number.partial_cmp(&r0.total.number) + } + (Clicks::Count(l0), Clicks::Extended(r0)) => l0.number.partial_cmp(&r0.total.number), + (Clicks::Extended(l0), Clicks::Count(r0)) => l0.total.number.partial_cmp(&r0.number), + } + } +} + +impl Ord for Clicks { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (Self::Count(l0), Self::Count(r0)) => l0.number.cmp(&r0.number), + (Self::Extended(l0), Self::Extended(r0)) => l0.total.number.cmp(&r0.total.number), + (Clicks::Count(l0), Clicks::Extended(r0)) => l0.number.cmp(&r0.total.number), + (Clicks::Extended(l0), Clicks::Count(r0)) => l0.total.number.cmp(&r0.number), + } + } +} + +impl Eq for Clicks {} + /// A User of the pslink service #[derive(PartialEq, Serialize, Deserialize, Clone, Debug)] pub struct User { @@ -43,11 +86,37 @@ pub struct Link { } /// When statistics are counted -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] pub struct Count { pub number: i32, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct WeekCount { + pub month: chrono::NaiveDateTime, + pub total: Count, + pub week: i32, +} +impl Eq for WeekCount {} + +impl PartialOrd for WeekCount { + fn partial_cmp(&self, other: &Self) -> std::option::Option { + self.total.number.partial_cmp(&other.total.number) + } +} +impl Ord for WeekCount { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.total.number.cmp(&other.total.number) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Statistics { + pub link_id: i64, + pub total: Count, + pub values: Vec, +} + /// Every time a short url is clicked record it for statistical evaluation. #[derive(Serialize, Debug)] pub struct Click {