diff --git a/Cargo.lock b/Cargo.lock index 719a171..47cc2b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,22 +1,16 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -65,10 +59,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -82,9 +75,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -103,9 +96,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -118,36 +111,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -164,9 +158,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -175,24 +169,24 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] @@ -221,31 +215,25 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -254,9 +242,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "binascii" @@ -272,9 +260,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" dependencies = [ "serde", ] @@ -299,9 +287,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "serde", @@ -309,15 +297,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.17.1" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" [[package]] name = "byteorder" @@ -327,15 +315,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.1.15" +version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ "shlex", ] @@ -348,9 +336,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", @@ -359,7 +347,7 @@ dependencies = [ "pure-rust-locales", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -369,7 +357,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" dependencies = [ "chrono", - "chrono-tz-build", + "chrono-tz-build 0.3.0", + "phf", +] + +[[package]] +name = "chrono-tz" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" +dependencies = [ + "chrono", + "chrono-tz-build 0.4.1", "phf", ] @@ -384,13 +383,23 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "chrono-tz-build" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "chumsky" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "stacker", ] @@ -406,9 +415,18 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] [[package]] name = "const-oid" @@ -423,7 +441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "aes-gcm", - "base64 0.22.1", + "base64", "hkdf", "percent-encoding", "rand", @@ -433,6 +451,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -451,9 +487,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -489,24 +525,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" dependencies = [ "chrono", - "nom", + "nom 7.1.3", "once_cell", ] [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -523,18 +559,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -549,9 +585,9 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -561,9 +597,9 @@ dependencies = [ [[package]] name = "csv-core" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" dependencies = [ "memchr", ] @@ -590,18 +626,18 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" dependencies = [ "powerfmt", ] [[package]] name = "deunicode" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" +checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d" [[package]] name = "devise" @@ -629,11 +665,11 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.77", + "syn", ] [[package]] @@ -648,6 +684,26 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -656,20 +712,20 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ "serde", ] [[package]] name = "email-encoding" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" +checksum = "20b9cde6a71f9f758440470f3de16db6c09a02c443ce66850d87f5410548fb8e" dependencies = [ - "base64 0.22.1", + "base64", "memchr", ] @@ -681,18 +737,18 @@ checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -700,31 +756,31 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -740,15 +796,20 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.3" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "figment" @@ -778,19 +839,19 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -803,6 +864,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -838,9 +905,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -853,9 +920,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -863,15 +930,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -891,38 +958,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -967,7 +1034,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -982,27 +1061,27 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "globset" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -1011,7 +1090,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "ignore", "walkdir", ] @@ -1046,22 +1125,30 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.8.4" +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "hashbrown", + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", ] [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1071,9 +1158,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "hex" @@ -1101,11 +1188,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1132,9 +1219,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1154,9 +1241,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1173,17 +1260,11 @@ dependencies = [ "libm", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1205,14 +1286,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1233,26 +1315,155 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ebec52f461ade2d19e7b594ecbcd0723ba0ab0eefa8aae2281b78ff461a91fa" [[package]] -name = "idna" -version = "0.5.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "ignore" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.9", "same-file", "walkdir", "winapi-util", @@ -1260,12 +1471,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", "serde", ] @@ -1297,22 +1508,22 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi 0.5.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1323,18 +1534,42 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "job_scheduler_ng" @@ -1349,10 +1584,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1387,11 +1623,11 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.7" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a62049a808f1c4e2356a2a380bd5f2aca3b011b0b482cf3b914ba1731426969" +checksum = "759bc2b8eabb6a30b235d6f716f7f36479f4b38cbe65b8747aefee51f89e8437" dependencies = [ - "base64 0.22.1", + "base64", "chumsky", "email-encoding", "email_address", @@ -1402,7 +1638,7 @@ dependencies = [ "idna", "mime", "native-tls", - "nom", + "nom 8.0.0", "percent-encoding", "quoted_printable", "socket2", @@ -1412,15 +1648,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1428,16 +1664,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "libc", - "redox_syscall 0.5.3", + "redox_syscall", ] [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -1446,9 +1682,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" @@ -1462,9 +1710,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "loom" @@ -1520,18 +1768,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] @@ -1544,19 +1783,18 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1569,7 +1807,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.1.0", + "http 1.3.1", "httparse", "memchr", "mime", @@ -1581,9 +1819,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -1606,6 +1844,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "normpath" version = "1.3.0" @@ -1621,7 +1868,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "crossbeam-channel", "filetime", "fsevent-sys", @@ -1709,18 +1956,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "opaque-debug" @@ -1730,11 +1977,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -1751,29 +1998,29 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.4.2+3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" dependencies = [ "cc", "libc", @@ -1788,6 +2035,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1806,7 +2059,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -1831,12 +2084,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pear" version = "0.2.9" @@ -1857,7 +2104,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.77", + "syn", ] [[package]] @@ -1877,9 +2124,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.11" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", "thiserror", @@ -1888,9 +2135,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.11" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -1898,22 +2145,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.11" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "pest_meta" -version = "2.7.11" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", @@ -1922,18 +2169,18 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", @@ -1941,9 +2188,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1951,18 +2198,18 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1993,9 +2240,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polyval" @@ -2009,6 +2256,21 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2017,18 +2279,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.24", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -2041,16 +2303,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", "version_check", "yansi", ] [[package]] name = "psm" -version = "0.1.22" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1f9bf148c15500d44581654fb9260bc9d82970f3ef777a79a40534f6aa784f" +checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" dependencies = [ "cc", ] @@ -2063,9 +2325,9 @@ checksum = "1190fd18ae6ce9e137184f207593877e70f39b015040156b1e05081cdfe3733a" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -2076,6 +2338,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -2103,57 +2371,48 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", ] [[package]] name = "ref-cast" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -2167,13 +2426,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -2184,21 +2443,20 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -2252,7 +2510,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.77", + "syn", "unicode-xid", "version_check", ] @@ -2303,7 +2561,7 @@ version = "0.1.0" dependencies = [ "argon2", "chrono", - "chrono-tz", + "chrono-tz 0.10.3", "csv", "env_logger", "futures", @@ -2326,9 +2584,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", @@ -2352,73 +2610,52 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.35" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.7", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.21.7", + "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -2427,15 +2664,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -2448,11 +2685,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2467,23 +2704,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", @@ -2492,9 +2719,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2502,29 +2729,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.209" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -2534,13 +2761,25 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2599,9 +2838,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" @@ -2624,15 +2863,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +dependencies = [ + "serde", +] [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2657,21 +2899,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2682,66 +2914,59 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ - "ahash", - "atoi", - "byteorder", "bytes", "chrono", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown 0.15.2", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", - "rustls 0.21.12", + "rustls", "rustls-pemfile", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", "thiserror", - "time", "tokio", "tokio-stream", "tracing", "url", - "webpki-roots 0.25.4", + "webpki-roots", ] [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" dependencies = [ "dotenvy", "either", @@ -2757,7 +2982,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", + "syn", "tempfile", "tokio", "url", @@ -2765,13 +2990,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", - "base64 0.21.7", - "bitflags 2.6.0", + "base64", + "bitflags 2.9.0", "byteorder", "bytes", "chrono", @@ -2802,20 +3027,19 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", - "time", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", - "base64 0.21.7", - "bitflags 2.6.0", + "base64", + "bitflags 2.9.0", "byteorder", "chrono", "crc", @@ -2823,7 +3047,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -2842,16 +3065,15 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", - "time", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", "chrono", @@ -2865,11 +3087,10 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", - "time", "tracing", "url", - "urlencoding", ] [[package]] @@ -2882,10 +3103,16 @@ dependencies = [ ] [[package]] -name = "stacker" -version = "0.1.17" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" dependencies = [ "cc", "cfg-if", @@ -2922,9 +3149,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -2932,24 +3159,24 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.77" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] name = "tempfile" -version = "3.12.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.2", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2962,7 +3189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" dependencies = [ "chrono", - "chrono-tz", + "chrono-tz 0.9.0", "globwalk", "humansize", "lazy_static", @@ -2979,22 +3206,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] @@ -3009,9 +3236,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -3024,25 +3251,35 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -3055,14 +3292,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio 1.0.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -3072,20 +3309,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3094,9 +3331,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", @@ -3107,9 +3344,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -3128,9 +3365,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", @@ -3147,9 +3384,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3159,20 +3396,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -3191,9 +3428,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -3215,9 +3452,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ubyte" @@ -3230,9 +3467,9 @@ dependencies = [ [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uncased" @@ -3296,48 +3533,36 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" - -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" @@ -3357,27 +3582,42 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.10.1" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +checksum = "4b0351ca625c7b41a8e4f9bb6c5d9755f67f62c2187ebedecacd9974674b271d" dependencies = [ - "base64 0.22.1", + "base64", + "cookie_store", "flate2", "log", - "once_cell", - "rustls 0.23.12", + "percent-encoding", + "rustls", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", - "url", - "webpki-roots 0.26.5", + "ureq-proto", + "utf-8", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae239d0a3341aebc94259414d1dc67cfce87d41cbebc816772c91b77902fafa4" +dependencies = [ + "base64", + "http 1.3.1", + "httparse", + "log", ] [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3390,6 +3630,24 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3398,18 +3656,18 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom", + "getrandom 0.3.2", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -3448,6 +3706,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -3456,35 +3723,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3492,45 +3759,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "webpki-roots" -version = "0.26.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall", "wasite", ] @@ -3593,6 +3857,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-sys" version = "0.48.0" @@ -3743,13 +4013,34 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "yansi" version = "1.0.1" @@ -3759,14 +4050,46 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -3777,7 +4100,39 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", ] [[package]] @@ -3785,3 +4140,25 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index d75e236..4a16639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rot" version = "0.1.0" -edition = "2021" +edition = "2024" [features] default = ["rest", "rowing-tera" ] @@ -13,20 +13,20 @@ rocket = { version = "0.5.0", features = ["secrets"]} rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true } log = "0.4" env_logger = "0.11" -sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] } +sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] } argon2 = "0.5" serde = { version = "1.0", features = [ "derive" ]} serde_json = "1.0" chrono = { version = "0.4", features = ["serde"]} -chrono-tz = "0.9" +chrono-tz = "0.10" tera = { version = "1.18", features = ["date-locale"], optional = true} ics = "0.5" futures = "0.3" lettre = "0.11" csv = "1.3" -itertools = "0.13" +itertools = "0.14" job_scheduler_ng = "2.0" -ureq = { version = "2.9", features = ["json"] } +ureq = { version = "3.0", features = ["json"] } regex = "1.10" urlencoding = "2.1" diff --git a/frontend/main.ts b/frontend/main.ts index 82f59e9..e8d528c 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -23,6 +23,7 @@ document.addEventListener("DOMContentLoaded", function () { addRelationMagic(document.querySelector("body")); reloadPage(); setCurrentdate(document.querySelector("#departure")); + initDropdown(); }); function changeTheme() { @@ -795,3 +796,21 @@ function replaceStrings() { weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag"); }); } + +function initDropdown() { + const popoverTriggerList = document.querySelectorAll('[data-dropdown]'); + + popoverTriggerList.forEach((popoverTriggerEl: Element) => { + const id = popoverTriggerEl.getAttribute('data-dropdown'); + + if (id) { + const element = document.getElementById(id); + if (element) { + // Toggle visibility of the dropdown when clicked + popoverTriggerEl.addEventListener('click', () => { + element.classList.toggle('hidden'); + }); + } + } + }); +} diff --git a/seeds.sql b/seeds.sql index 2641243..b4b2894 100644 --- a/seeds.sql +++ b/seeds.sql @@ -53,6 +53,7 @@ INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES(' INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox'); INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2); +INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'same trip_details as id=1'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '💪'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '⛱'); diff --git a/src/model/boathouse.rs b/src/model/boathouse.rs new file mode 100644 index 0000000..03d0f78 --- /dev/null +++ b/src/model/boathouse.rs @@ -0,0 +1,144 @@ +use rocket::serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +use crate::tera::board::boathouse::FormBoathouseToAdd; + +use super::boat::Boat; + +#[derive(Debug, Serialize, Deserialize)] +pub struct BoathousePlace { + boat: Boat, + boathouse_id: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BoathouseRack { + boats: [Option; 12], +} + +impl BoathouseRack { + fn new() -> Self { + let boats = [ + None, None, None, None, None, None, None, None, None, None, None, None, + ]; + Self { boats } + } + + async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) { + self.boats[boathouse.level as usize] = Some(BoathousePlace { + boat: Boat::find_by_id(db, boathouse.boat_id as i32) + .await + .unwrap(), + boathouse_id: boathouse.id, + }); + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BoathouseSide { + mountain: BoathouseRack, + water: BoathouseRack, +} + +impl BoathouseSide { + fn new() -> Self { + Self { + mountain: BoathouseRack::new(), + water: BoathouseRack::new(), + } + } + + async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) { + match boathouse.side.as_str() { + "mountain" => self.mountain.add(db, boathouse).await, + "water" => self.water.add(db, boathouse).await, + _ => panic!("db constraint failed"), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BoathouseAisles { + mountain: BoathouseSide, + middle: BoathouseSide, + water: BoathouseSide, +} + +impl BoathouseAisles { + fn new() -> Self { + Self { + mountain: BoathouseSide::new(), + middle: BoathouseSide::new(), + water: BoathouseSide::new(), + } + } + + async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) { + match boathouse.aisle.as_str() { + "water" => self.water.add(db, boathouse).await, + "middle" => self.middle.add(db, boathouse).await, + "mountain" => self.mountain.add(db, boathouse).await, + _ => panic!("db constraint failed"), + }; + } + + pub async fn from(db: &SqlitePool, boathouses: Vec) -> Self { + let mut ret = BoathouseAisles::new(); + + for boathouse in boathouses { + ret.add(db, boathouse).await; + } + ret + } +} + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct Boathouse { + pub id: i64, + pub boat_id: i64, + pub aisle: String, + pub side: String, + pub level: i64, +} + +impl Boathouse { + pub async fn get(db: &SqlitePool) -> BoathouseAisles { + let boathouses = sqlx::query_as!( + Boathouse, + "SELECT id, boat_id, aisle, side, level FROM boathouse" + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + BoathouseAisles::from(db, boathouses).await + } + + pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> { + sqlx::query!( + "INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)", + data.boat_id, + data.aisle, + data.side, + data.level + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + sqlx::query_as!(Self, "SELECT * FROM boathouse WHERE id like ?", id) + .fetch_one(db) + .await + .ok() + } + + pub async fn delete(&self, db: &SqlitePool) { + sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a Boat of a valid id + } +} diff --git a/src/model/boatreservation.rs b/src/model/boatreservation.rs new file mode 100644 index 0000000..036dd21 --- /dev/null +++ b/src/model/boatreservation.rs @@ -0,0 +1,272 @@ +use std::collections::HashMap; + +use crate::model::{boat::Boat, user::User}; +use crate::tera::boatreservation::ReservationEditForm; +use chrono::NaiveDate; +use chrono::NaiveDateTime; +use rocket::serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +use super::log::Log; +use super::notification::Notification; +use super::role::Role; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct BoatReservation { + pub id: i64, + pub boat_id: i64, + pub start_date: NaiveDate, + pub end_date: NaiveDate, + pub time_desc: String, + pub usage: String, + pub user_id_applicant: i64, + pub user_id_confirmation: Option, + pub created_at: NaiveDateTime, +} + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct BoatReservationWithDetails { + #[serde(flatten)] + reservation: BoatReservation, + boat: Boat, + user_applicant: User, + user_confirmation: Option, +} + +#[derive(Debug)] +pub struct BoatReservationToAdd<'r> { + pub boat: &'r Boat, + pub start_date: NaiveDate, + pub end_date: NaiveDate, + pub time_desc: &'r str, + pub usage: &'r str, + pub user_applicant: &'r User, +} + +impl BoatReservation { + pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + sqlx::query_as!( + Self, + "SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at + FROM boat_reservation + WHERE id like ?", + id + ) + .fetch_one(db) + .await + .ok() + } + pub async fn for_day(db: &SqlitePool, day: NaiveDate) -> Vec { + let boatreservations = sqlx::query_as!( + Self, + " +SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at +FROM boat_reservation +WHERE end_date >= ? AND start_date <= ? + ", day, day + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + let mut res = Vec::new(); + for reservation in boatreservations { + let user_confirmation = match reservation.user_id_confirmation { + Some(id) => { + let user = User::find_by_id(db, id as i32).await; + Some(user.unwrap()) + } + None => None, + }; + let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32) + .await + .unwrap(); + let boat = Boat::find_by_id(db, reservation.boat_id as i32) + .await + .unwrap(); + + res.push(BoatReservationWithDetails { + reservation, + boat, + user_applicant, + user_confirmation, + }); + } + res + } + + pub async fn all_future(db: &SqlitePool) -> Vec { + let boatreservations = sqlx::query_as!( + Self, + " +SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at +FROM boat_reservation +WHERE end_date >= CURRENT_DATE ORDER BY end_date + " + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + let mut res = Vec::new(); + for reservation in boatreservations { + let user_confirmation = match reservation.user_id_confirmation { + Some(id) => { + let user = User::find_by_id(db, id as i32).await; + Some(user.unwrap()) + } + None => None, + }; + let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32) + .await + .unwrap(); + let boat = Boat::find_by_id(db, reservation.boat_id as i32) + .await + .unwrap(); + + res.push(BoatReservationWithDetails { + reservation, + boat, + user_applicant, + user_confirmation, + }); + } + res + } + + pub fn with_groups( + reservations: Vec, + ) -> HashMap> { + let mut grouped_reservations: HashMap> = + HashMap::new(); + + for reservation in reservations { + let key = format!( + "{}-{}-{}-{}-{}", + reservation.reservation.start_date, + reservation.reservation.end_date, + reservation.reservation.time_desc, + reservation.reservation.usage, + reservation.user_applicant.name + ); + + grouped_reservations + .entry(key) + .or_default() + .push(reservation); + } + + grouped_reservations + } + pub async fn all_future_with_groups( + db: &SqlitePool, + ) -> HashMap> { + let reservations = Self::all_future(db).await; + Self::with_groups(reservations) + } + + pub async fn create( + db: &SqlitePool, + boatreservation: BoatReservationToAdd<'_>, + ) -> Result<(), String> { + if Self::boat_reserved_between_dates( + db, + boatreservation.boat, + &boatreservation.start_date, + &boatreservation.end_date, + ) + .await + { + return Err("Boot in diesem Zeitraum bereits reserviert.".into()); + } + + Log::create(db, format!("New boat reservation: {boatreservation:?}")).await; + + sqlx::query!( + "INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)", + boatreservation.boat.id, + boatreservation.start_date, + boatreservation.end_date, + boatreservation.time_desc, + boatreservation.usage, + boatreservation.user_applicant.id, + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + + let board = + User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await; + for user in board { + let date = if boatreservation.start_date == boatreservation.end_date { + format!("am {}", boatreservation.start_date) + } else { + format!( + "von {} bis {}", + boatreservation.start_date, boatreservation.end_date + ) + }; + + Notification::create( + db, + &user, + &format!( + "{} hat eine neue Bootsreservierung für Boot '{}' {} angelegt. Zeit: {}; Zweck: {}", + boatreservation.user_applicant.name, + boatreservation.boat.name, + date, + boatreservation.time_desc, + boatreservation.usage + ), + "Neue Bootsreservierung", + None,None + ) + .await; + } + + Ok(()) + } + + pub async fn boat_reserved_between_dates( + db: &SqlitePool, + boat: &Boat, + start_date: &NaiveDate, + end_date: &NaiveDate, + ) -> bool { + sqlx::query!( + "SELECT COUNT(*) AS reservation_count +FROM boat_reservation +WHERE boat_id = ? +AND start_date <= ? AND end_date >= ?;", + boat.id, + end_date, + start_date + ) + .fetch_one(db) + .await + .unwrap() + .reservation_count + > 0 + } + + pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) { + let time_desc = data.time_desc.trim(); + let usage = data.usage.trim(); + sqlx::query!( + "UPDATE boat_reservation SET time_desc = ?, usage = ? where id = ?", + time_desc, + usage, + self.id + ) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + } + + pub async fn delete(&self, db: &SqlitePool) { + sqlx::query!("DELETE FROM boat_reservation WHERE id=?", self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a Boat of a valid id + } +} diff --git a/src/model/event.rs b/src/model/event.rs index 78218ae..1f5e1ac 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -96,8 +96,8 @@ FROM trip WHERE planned_event_id = ? .unwrap() .into_iter() .map(|r| Registration { - name: r.name, - registered_at: r.registered_at, + name: r.name.unwrap(), + registered_at: r.registered_at.unwrap(), is_guest: false, is_real_guest: false, }) diff --git a/src/model/family.rs b/src/model/family.rs new file mode 100644 index 0000000..55aa5eb --- /dev/null +++ b/src/model/family.rs @@ -0,0 +1,94 @@ +use std::ops::DerefMut; + +use serde::Serialize; +use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction}; + +use super::user::User; + +#[derive(FromRow, Serialize, Clone)] +pub struct Family { + id: i64, +} + +#[derive(Serialize, Clone)] +pub struct FamilyWithMembers { + id: i64, + names: Option, +} + +impl Family { + pub async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as!(Self, "SELECT id FROM role") + .fetch_all(db) + .await + .unwrap() + } + + pub async fn insert_tx(db: &mut Transaction<'_, Sqlite>) -> i64 { + let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES") + .execute(db.deref_mut()) + .await + .unwrap(); + + result.last_insert_rowid() + } + + pub async fn insert(db: &SqlitePool) -> i64 { + let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES") + .execute(db) + .await + .unwrap(); + + result.last_insert_rowid() + } + + pub async fn all_with_members(db: &SqlitePool) -> Vec { + sqlx::query_as!( + FamilyWithMembers, + " +SELECT + family.id as id, + GROUP_CONCAT(user.name, ', ') as names +FROM family +LEFT JOIN + user ON family.id = user.family_id +GROUP BY family.id;" + ) + .fetch_all(db) + .await + .unwrap() + } + + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { + sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id) + .fetch_one(db) + .await + .ok() + } + + pub async fn find_by_opt_id(db: &SqlitePool, id: Option) -> Option { + if let Some(id) = id { + Self::find_by_id(db, id).await + } else { + None + } + } + + pub async fn amount_family_members(&self, db: &SqlitePool) -> i64 { + sqlx::query!( + "SELECT COUNT(*) as count FROM user WHERE family_id = ?", + self.id + ) + .fetch_one(db) + .await + .unwrap() + .count + } + + pub async fn members(&self, db: &SqlitePool) -> Vec { + sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id) + .fetch_all(db) + .await + .unwrap() + } +} diff --git a/src/model/logbook.rs b/src/model/logbook.rs new file mode 100644 index 0000000..4b104bf --- /dev/null +++ b/src/model/logbook.rs @@ -0,0 +1,1288 @@ +use std::ops::DerefMut; + +use chrono::{Datelike, Duration, Local, NaiveDateTime}; +use rocket::FromForm; +use serde::Serialize; +use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; + +use super::{ + boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User, +}; + +#[derive(FromRow, Serialize, Clone, Debug)] +pub struct Logbook { + pub id: i64, + pub boat_id: i64, + pub shipmaster: i64, + pub steering_person: i64, + #[serde(default = "bool::default")] + pub shipmaster_only_steering: bool, + pub departure: NaiveDateTime, + pub arrival: Option, + pub destination: Option, + pub distance_in_km: Option, + pub comments: Option, + pub logtype: Option, +} + +impl PartialEq for Logbook { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +pub(crate) enum Filter { + SingleDayOnly, + MultiDayOnly, +} + +#[derive(FromForm, Debug, Clone)] +pub struct LogToAdd { + pub boat_id: i32, + pub shipmaster: Option, + pub steering_person: Option, + pub shipmaster_only_steering: bool, + pub departure: String, + pub arrival: Option, + pub destination: Option, + pub distance_in_km: Option, + pub comments: Option, + pub logtype: Option, + pub rowers: Vec, +} + +#[derive(FromForm, Debug)] +pub struct LogToFinalize { + pub shipmaster: Option, + pub steering_person: Option, + pub shipmaster_only_steering: bool, + pub departure: String, + pub arrival: String, + pub destination: String, + pub distance_in_km: i64, + pub comments: Option, + pub logtype: Option, + pub rowers: Vec, +} + +#[derive(FromForm, Debug, Clone)] +pub struct LogToUpdate { + pub id: i64, + pub boat_id: i64, + pub shipmaster: i64, + pub steering_person: i64, + pub shipmaster_only_steering: bool, + pub departure: String, + pub arrival: Option, + pub destination: Option, + pub distance_in_km: Option, + pub comments: Option, + pub logtype: Option, + pub rowers: Vec, +} + +impl TryFrom for LogToFinalize { + type Error = String; + + fn try_from(value: LogToAdd) -> Result { + if let (Some(arrival), Some(destination), Some(distance_in_km)) = + (value.arrival, value.destination, value.distance_in_km) + { + return Ok(LogToFinalize { + arrival, + destination, + distance_in_km, + shipmaster: value.shipmaster, + steering_person: value.steering_person, + shipmaster_only_steering: value.shipmaster_only_steering, + departure: value.departure, + comments: value.comments, + logtype: value.logtype, + rowers: value.rowers, + }); + } + Err("Arrival, destination or distance_in_km not set".into()) + } +} + +#[derive(Serialize, Debug)] +pub struct LogbookWithBoatAndRowers { + #[serde(flatten)] + pub logbook: Logbook, + pub boat: Boat, + pub shipmaster_user: User, + pub steering_user: User, + pub rowers: Vec, +} + +impl LogbookWithBoatAndRowers { + pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self { + let mut tx = db.begin().await.unwrap(); + let ret = Self::from_tx(&mut tx, log).await; + tx.commit().await.unwrap(); + ret + } + + pub(crate) async fn from_tx(db: &mut Transaction<'_, Sqlite>, log: Logbook) -> Self { + Self { + rowers: Rower::for_log_tx(db, &log).await, + boat: Boat::find_by_id_tx(db, log.boat_id as i32).await.unwrap(), + shipmaster_user: User::find_by_id_tx(db, log.shipmaster as i32) + .await + .unwrap(), + steering_user: User::find_by_id_tx(db, log.steering_person as i32) + .await + .unwrap(), + logbook: log, + } + } +} + +#[derive(Debug, PartialEq)] +pub enum LogbookAdminUpdateError { + NotAllowed, +} + +#[derive(Debug, PartialEq)] +pub enum LogbookUpdateError { + NotYourEntry, + TooManyRowers(usize, usize), + RowerCreateError(i64, String), + ArrivalNotAfterDeparture, + ShipmasterNotInRowers, + SteeringPersonNotInRowers, + UserNotAllowedToUseBoat, + OnlyAllowedToEndTripsEndingToday, + TooFast(i64, i64), + AlreadyFinalized, + ExternalSteeringPersonMustSteerOrShipmaster, + BoatAlreadyOnWater, +} + +#[derive(Debug, PartialEq)] +pub enum LogbookDeleteError { + NotYourEntry, +} + +#[derive(Debug, PartialEq)] +pub enum LogbookCreateError { + UserNotAllowedToUseBoat, + BoatAlreadyOnWater, + BoatLocked, + BoatNotFound, + TooManyRowers(usize, usize), + RowerAlreadyOnWater(Box), + RowerCreateError(i64, String), + ArrivalNotAfterDeparture, + SteeringPersonNotInRowers, + ShipmasterNotInRowers, + NotYourEntry, + ArrivalSetButNotRemainingTwo, + OnlyAllowedToEndTripsEndingToday, + CantChangeHandoperatableStatusForThisBoat, + TooFast(i64, i64), + AlreadyFinalized, + ExternalSteeringPersonMustSteerOrShipmaster, +} + +impl From for LogbookCreateError { + fn from(value: LogbookUpdateError) -> Self { + match value { + LogbookUpdateError::NotYourEntry => LogbookCreateError::NotYourEntry, + LogbookUpdateError::TooManyRowers(a, b) => LogbookCreateError::TooManyRowers(a, b), + LogbookUpdateError::RowerCreateError(a, b) => { + LogbookCreateError::RowerCreateError(a, b) + } + LogbookUpdateError::ArrivalNotAfterDeparture => { + LogbookCreateError::ArrivalNotAfterDeparture + } + LogbookUpdateError::ShipmasterNotInRowers => LogbookCreateError::ShipmasterNotInRowers, + LogbookUpdateError::SteeringPersonNotInRowers => { + LogbookCreateError::SteeringPersonNotInRowers + } + LogbookUpdateError::UserNotAllowedToUseBoat => { + LogbookCreateError::UserNotAllowedToUseBoat + } + LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => { + LogbookCreateError::OnlyAllowedToEndTripsEndingToday + } + LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min), + LogbookUpdateError::AlreadyFinalized => LogbookCreateError::AlreadyFinalized, + LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster => { + LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster + } + LogbookUpdateError::BoatAlreadyOnWater => LogbookCreateError::BoatAlreadyOnWater, + } + } +} + +impl Logbook { + pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option { + sqlx::query_as!( + Self, + " + SELECT id,boat_id,shipmaster,steering_person,shipmaster_only_steering,departure,arrival,destination,distance_in_km,comments,logtype + FROM logbook + WHERE id like ? + ", + id + ) + .fetch_one(db.deref_mut()) + .await + .ok() + } + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { + sqlx::query_as!( + Self, + " + SELECT id,boat_id,shipmaster,steering_person,shipmaster_only_steering,departure,arrival,destination,distance_in_km,comments,logtype + FROM logbook + WHERE id like ? + ", + id + ) + .fetch_one(db) + .await + .ok() + } + + pub async fn on_water(db: &SqlitePool) -> Vec { + let rows = sqlx::query!( + " +SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype +FROM logbook +WHERE arrival is null +ORDER BY departure DESC + " +) +.fetch_all(db) +.await +.unwrap(); //TODO: fixme + + let logs: Vec = rows + .into_iter() + .map(|row| Logbook { + id: row.id, + boat_id: row.boat_id, + shipmaster: row.shipmaster, + steering_person: row.steering_person, + shipmaster_only_steering: row.shipmaster_only_steering, + departure: row.departure, + arrival: row.arrival, + destination: row.destination, + distance_in_km: row.distance_in_km, + comments: row.comments, + logtype: row.logtype, + }) + .collect(); + + let mut ret = Vec::new(); + for log in logs { + ret.push(LogbookWithBoatAndRowers::from(db, log).await); + } + ret + } + + pub async fn completed_with_user( + db: &SqlitePool, + user: &User, + ) -> Vec { + let mut tx = db.begin().await.unwrap(); + let ret = Self::completed_with_user_tx(&mut tx, user).await; + tx.commit().await.unwrap(); + ret + } + + pub async fn completed_with_user_tx( + db: &mut Transaction<'_, Sqlite>, + user: &User, + ) -> Vec { + let logs = sqlx::query_as( + &format!(" + SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype + FROM logbook + JOIN rower ON logbook.id = rower.logbook_id + WHERE arrival is not null AND rower_id = {} + ORDER BY arrival DESC + ", user.id) + ) + .fetch_all(db.deref_mut()) + .await + .unwrap(); //TODO: fixme + + let mut ret = Vec::new(); + for log in logs { + ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await); + } + ret + } + + pub async fn year_first_logbook_entry(db: &SqlitePool, user: &User) -> Option { + let log: Option = sqlx::query_as( + &format!(" + SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype + FROM logbook + JOIN rower ON logbook.id = rower.logbook_id + WHERE arrival is not null AND rower_id = {} + ORDER BY arrival + LIMIT 1 + ", user.id) + ) + .fetch_optional(db) + .await + .unwrap(); //TODO: fixme + + if let Some(log) = log { + Some(log.arrival.unwrap().year()) + } else { + None + } + } + + pub async fn year_last_logbook_entry(db: &SqlitePool, user: &User) -> Option { + let log: Option = sqlx::query_as( + &format!(" + SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype + FROM logbook + JOIN rower ON logbook.id = rower.logbook_id + WHERE arrival is not null AND rower_id = {} + ORDER BY arrival DESC + LIMIT 1 + ", user.id) + ) + .fetch_optional(db) + .await + .unwrap(); //TODO: fixme + + if let Some(log) = log { + Some(log.arrival.unwrap().year()) + } else { + None + } + } + + pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year_tx( + db: &mut Transaction<'_, Sqlite>, + user: &User, + min_distance: i32, + year: i32, + filter: Filter, + exclude_last_log: bool, + ) -> Vec { + let logs: Vec = sqlx::query_as( + &format!(" + SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype + FROM logbook + JOIN rower ON logbook.id = rower.logbook_id + WHERE arrival is not null AND rower_id = {} AND logtype = 1 AND distance_in_km >= {} AND arrival like '{}-%' + ORDER BY arrival DESC + ", user.id, min_distance, year) + ) + .fetch_all(db.deref_mut()) + .await + .unwrap(); //TODO: fixme + + let mut ret = Vec::new(); + for log in logs { + let trip_days = log.arrival.unwrap() - log.departure; + let trip_days = trip_days.num_days(); + match filter { + Filter::SingleDayOnly => { + if trip_days == 0 { + ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await); + } + } + Filter::MultiDayOnly => { + if trip_days > 0 { + ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await); + } + } + } + } + if exclude_last_log { + ret.pop(); + } + + ret + } + + pub async fn completed(db: &SqlitePool) -> Vec { + let year = chrono::Local::now().year(); + Self::completed_in_year(db, year).await + } + + pub async fn completed_in_year(db: &SqlitePool, year: i32) -> Vec { + let logs = sqlx::query_as( + &format!(" + SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype + FROM logbook + WHERE arrival is not null AND arrival LIKE '{}-%' + ORDER BY arrival DESC + ", year) + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + let mut ret = Vec::new(); + for log in logs { + ret.push(LogbookWithBoatAndRowers::from(db, log).await); + } + ret + } + + pub async fn create( + db: &SqlitePool, + mut log: LogToAdd, + created_by_user: &User, + smtp_pw: &str, + ) -> Result { + let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { + return Err(LogbookCreateError::BoatNotFound); + }; + + if log.shipmaster_only_steering != boat.default_shipmaster_only_steering + && !boat.convert_handoperated_possible + { + return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat); + } + + if boat.amount_seats == 1 && log.rowers.is_empty() { + log.rowers = vec![created_by_user.id]; + } + + if boat.amount_seats == 1 { + log.shipmaster = Some(log.rowers[0]); + log.steering_person = Some(log.rowers[0]); + } + + if let Ok(log_to_finalize) = TryInto::::try_into(log.clone()) { + if !boat.shipmaster_allowed(db, created_by_user).await { + return Err(LogbookCreateError::UserNotAllowedToUseBoat); + } + + let mut tx = db.begin().await.unwrap(); + + let inserted_row = sqlx::query!( + "INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id", + log.boat_id, + log.shipmaster, + log.steering_person, + log.shipmaster_only_steering, + log.departure, + log.destination, + log.distance_in_km, + log.comments, + log.logtype + ) + .fetch_one(tx.deref_mut()) + .await.unwrap().id; + + let logbook = Logbook::find_by_id_tx(&mut tx, inserted_row as i32) + .await + .unwrap(); //ok + + return match logbook + .home_with_transaction(&mut tx, created_by_user, log_to_finalize, smtp_pw) + .await + { + Ok(_) => { + tx.commit().await.unwrap(); + Ok(String::new()) + } + Err(a) => Err(a.into()), + }; + } + if log.arrival.is_some() { + return Err(LogbookCreateError::ArrivalSetButNotRemainingTwo); + } + + if boat.is_locked(db).await { + return Err(LogbookCreateError::BoatLocked); + } + + if boat.on_water(db).await { + return Err(LogbookCreateError::BoatAlreadyOnWater); + } + + if !log.rowers.contains(&log.shipmaster.unwrap()) { + return Err(LogbookCreateError::ShipmasterNotInRowers); + } + if !log.rowers.contains(&log.steering_person.unwrap()) { + return Err(LogbookCreateError::SteeringPersonNotInRowers); + } + + if log.rowers.len() > boat.amount_seats as usize { + return Err(LogbookCreateError::TooManyRowers( + boat.amount_seats as usize, + log.rowers.len(), + )); + } + + for rower in &log.rowers { + let user = User::find_by_id(db, *rower as i32).await.unwrap(); + + if user.on_water(db).await { + return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user))); + } + + if user.name == "Externe Steuerperson" { + if let (Some(steering_id), Some(shipmaster_id)) = + (log.steering_person, log.shipmaster) + { + if steering_id != user.id && shipmaster_id != user.id { + return Err( + LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster, + ); + } + } + } + } + + if !boat.shipmaster_allowed(db, created_by_user).await { + return Err(LogbookCreateError::UserNotAllowedToUseBoat); + } + + //let departure = format!("{}+02:00", &log.departure); + Log::create(db, format!("New trip started: {log:?}")).await; + + let mut tx = db.begin().await.unwrap(); + + let inserted_row = sqlx::query!( + "INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id", + log.boat_id, + log.shipmaster, + log.steering_person, + log.shipmaster_only_steering, + log.departure, + log.arrival, + log.destination, + log.distance_in_km, + log.comments, + log.logtype + ) + .fetch_one(tx.deref_mut()) + .await.unwrap(); + + for rower in &log.rowers { + Rower::create(&mut tx, inserted_row.id, *rower) + .await + .map_err(|e| LogbookCreateError::RowerCreateError(*rower, e.to_string()))?; + } + + tx.commit().await.unwrap(); + + let mut ret = String::new(); + for rower in &log.rowers { + let user = User::find_by_id(db, *rower as i32).await.unwrap(); + if let Some(msg) = user.close_thousands_trip(db).await { + ret.push_str(&format!(" • {msg}")); + } + } + + Ok(ret) + } + + pub async fn update( + &self, + db: &SqlitePool, + data: LogToUpdate, + user: &User, + ) -> Result<(), LogbookAdminUpdateError> { + if !user.has_role(db, "Vorstand").await { + return Err(LogbookAdminUpdateError::NotAllowed); + } + + sqlx::query!( + "UPDATE logbook SET boat_id=?, shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, arrival=?, destination=?, distance_in_km=?, comments=?, logtype=? WHERE id=?", + data.boat_id, + data.shipmaster, + data.steering_person, + data.shipmaster_only_steering, + data.departure, + data.arrival, + data.destination, + data.distance_in_km, + data.comments, + data.logtype, + self.id + ) + .execute(db) + .await.unwrap(); + Ok(()) + } + + async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) { + sqlx::query!("DELETE FROM rower WHERE logbook_id=?", self.id) + .execute(db.deref_mut()) + .await + .unwrap(); + } + + #[cfg(test)] + pub async fn highest_id(db: &SqlitePool) -> i32 { + sqlx::query!("SELECT max(id) as id FROM logbook") + .fetch_one(db) + .await + .unwrap() + .id + .unwrap() as i32 + } + + pub async fn home( + &self, + db: &SqlitePool, + user: &User, + log: LogToFinalize, + smtp_pw: &str, + ) -> Result<(), LogbookUpdateError> { + let mut tx = db.begin().await.unwrap(); + self.home_with_transaction(&mut tx, user, log, smtp_pw) + .await?; + tx.commit().await.unwrap(); + Ok(()) + } + + async fn home_with_transaction( + &self, + db: &mut Transaction<'_, Sqlite>, + user: &User, + mut log: LogToFinalize, + smtp_pw: &str, + ) -> Result<(), LogbookUpdateError> { + //TODO: extract common tests with `create()` + if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster { + return Err(LogbookUpdateError::NotYourEntry); + } + + if self.arrival.is_some() { + return Err(LogbookUpdateError::AlreadyFinalized); + } + + let boat = Boat::find_by_id_tx(db, self.boat_id as i32).await.unwrap(); //ok + + if boat.amount_seats == 1 { + log.shipmaster = Some(log.rowers[0]); + log.steering_person = Some(log.rowers[0]); + } + + if !log.rowers.contains(&log.shipmaster.unwrap()) { + return Err(LogbookUpdateError::ShipmasterNotInRowers); + } + if !log.rowers.contains(&log.steering_person.unwrap()) { + return Err(LogbookUpdateError::SteeringPersonNotInRowers); + } + + if !boat.shipmaster_allowed_tx(db, user).await && self.shipmaster != user.id { + //second part: shipmaster has entered a different user, then the user should be able to + //`home` it + return Err(LogbookUpdateError::UserNotAllowedToUseBoat); + } + + if log.rowers.len() > boat.amount_seats as usize { + return Err(LogbookUpdateError::TooManyRowers( + boat.amount_seats as usize, + log.rowers.len(), + )); + } + + let dep = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%dT%H:%M").unwrap(); + let arr = NaiveDateTime::parse_from_str(&log.arrival, "%Y-%m-%dT%H:%M").unwrap(); + if arr.and_utc().timestamp() < dep.and_utc().timestamp() { + return Err(LogbookUpdateError::ArrivalNotAfterDeparture); + } + + if !boat.external && boat.on_water_between(db, dep, arr).await { + return Err(LogbookUpdateError::BoatAlreadyOnWater); + } + + let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60; + // Not possible to row < 1 min / 500 m = < 2 min / km + let possible_distance_km = duration_in_mins / 2; + if log.distance_in_km > possible_distance_km { + return Err(LogbookUpdateError::TooFast( + log.distance_in_km, + duration_in_mins, + )); + } + + let today = Local::now().date_naive(); + let day_diff = today - arr.date(); + let day_diff = day_diff.num_days(); + if day_diff >= 7 + && !user.has_role_tx(db, "admin").await + && !user + .has_role_tx(db, "allow-retroactive-logbookentries") + .await + { + return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday); + } + if day_diff < 0 && !user.has_role_tx(db, "admin").await { + return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday); + } + + Log::create_with_tx(db, format!("New trip: {log:?}")).await; + + self.remove_rowers(db).await; + for rower in &log.rowers { + let user = User::find_by_id_tx(db, *rower as i32).await.unwrap(); + if user.name == "Externe Steuerperson" { + if let (Some(steering_id), Some(shipmaster_id)) = + (log.steering_person, log.shipmaster) + { + if steering_id != user.id && shipmaster_id != user.id { + return Err( + LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster, + ); + } + } + } + + Rower::create(db, self.id, *rower) + .await + .map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?; + + let user = User::find_by_id_tx(db, *rower as i32).await.unwrap(); + Notification::create_with_tx( + db, + &user, + &format!( + "Ausfahrt am {}.{}.{}; Ziel: {} ({} km)", + dep.day(), + dep.month(), + dep.year(), + log.destination, + log.distance_in_km + ), + "Neuer Logbucheintrag", + None, + None, + ) + .await; + } + + sqlx::query!( + "UPDATE logbook SET shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, destination=?, distance_in_km=?, comments=?, logtype=?, arrival=? WHERE id=?", + log.shipmaster, + log.steering_person, + log.shipmaster_only_steering, + log.departure, + log.destination, + log.distance_in_km, + log.comments, + log.logtype, + log.arrival, + self.id + ) + .execute(db.deref_mut()) + .await.unwrap(); //TODO: fixme + + let duration = arr - dep; + if duration.num_days() > 0 { + let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap(); + + Notification::create_for_role_tx( + db, + &vorstand, + &format!("'{}' hat eine mehrtägige Ausfahrt vom {} bis {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,log.departure, log.arrival, log.distance_in_km, log.destination, log.comments.clone().unwrap_or("".into())), + "Mehrtägige Ausfahrt eingetragen", + None,None + ).await; + } + + if boat.external { + let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap(); + + Notification::create_for_role_tx( + db, + &vorstand, + &format!("'{}' hat eine Ausfahrt mit externem Boot '{}' am {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,boat.name,log.departure,log.distance_in_km, log.destination, log.comments.unwrap_or("".into())), + "Ausfahrt mit externem Boot eingetragen", + None,None, + ).await; + } + + for rower in &log.rowers { + let user = User::find_by_id_tx(db, *rower as i32).await.unwrap(); + user.received_new_logentry(db, smtp_pw).await; + } + + Ok(()) + } + + pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> { + Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await; + + if self.arrival.is_none() { + if user.has_role(db, "admin").await + || user.has_role(db, "Vorstand").await + || user.id == self.shipmaster + { + let now = Local::now().naive_local(); + let difference = now - self.departure; + if difference > Duration::hours(1) { + let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap(); + let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await; + let mut msg = format!("{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", user.name, logbook.steering_user.name, logbook.steering_user.name, logbook.logbook.departure.format("%Y-%m-%d %H:%M")); + if let Some(destination) = logbook.logbook.destination { + msg.push_str(&format!(", Ziel: {}", destination)); + } else { + msg.push_str(", kein Ziel eingegeben"); + } + msg.push_str(", Ruderer: "); + let mut it = logbook.rowers.clone().into_iter().peekable(); + while let Some(rower) = it.next() { + msg.push_str(&rower.name); + if it.peek().is_some() { + msg.push_str(" + "); + } + } + + Notification::create_for_role( + db, + &vorstand, + &msg, + "Ungewöhnliches Verhalten", + None, + None, + ) + .await; + } + + sqlx::query!("DELETE FROM logbook WHERE id=?", self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a Logbook of a valid id + return Ok(()); + } + } else { + // Only admins can delete completed logbook entries + if user.has_role(db, "admin").await { + sqlx::query!("DELETE FROM logbook WHERE id=?", self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a Logbook of a valid id + return Ok(()); + } + } + Err(LogbookDeleteError::NotYourEntry) + } +} + +#[cfg(test)] +mod test { + use super::{LogToAdd, Logbook, LogbookCreateError, LogbookUpdateError}; + use crate::model::user::User; + use crate::testdb; + + use chrono::Duration; + use sqlx::SqlitePool; + + #[sqlx::test] + fn test_find_correct_id() { + let pool = testdb!(); + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + assert_eq!(logbook.id, 1); + } + + #[sqlx::test] + fn test_find_wrong_id() { + let pool = testdb!(); + let logbook = Logbook::find_by_id(&pool, 1337).await; + assert_eq!(logbook, None); + } + + #[sqlx::test] + fn test_on_water() { + let pool = testdb!(); + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let logbook_with_details = Logbook::on_water(&pool).await; + + assert_eq!(logbook_with_details[0].logbook, logbook); + } + + #[sqlx::test] + fn test_completed() { + let pool = testdb!(); + let completed = Logbook::completed(&pool).await; + + assert_eq!( + completed[0].logbook, + Logbook::find_by_id(&pool, 2).await.unwrap() + ); + assert_eq!( + completed[1].logbook, + Logbook::find_by_id(&pool, 3).await.unwrap() + ); + } + + //#[sqlx::test] + //fn test_all() { + // let pool = testdb!(); + // let res = Boat::all(&pool).await; + // assert!(res.len() > 3); + //} + + #[sqlx::test] + fn test_succ_create() { + let pool = testdb!(); + + let msg = Logbook::create( + &pool, + LogToAdd { + boat_id: 3, + shipmaster: Some(4), + steering_person: Some(4), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![4], + }, + &User::find_by_id(&pool, 4).await.unwrap(), + "", + ) + .await + .unwrap(); + assert_eq!(msg, String::from("")); + } + + #[sqlx::test] + fn test_succ_create_with_thousands_msg() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let user = User::find_by_id(&pool, 2).await.unwrap(); + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + let start_date = chrono::Local::now() - Duration::days(3); + let start_date = start_date.format("%Y-%m-%d").to_string(); + logbook + .home( + &pool, + &user, + super::LogToFinalize { + destination: "new-destination".into(), + distance_in_km: 995, + comments: Some("Perfect water".into()), + logtype: None, + rowers: vec![2], + shipmaster: Some(2), + steering_person: Some(2), + shipmaster_only_steering: false, + departure: format!("{}T10:00", start_date), + arrival: format!("{}T12:00", current_date), + }, + "", + ) + .await + .unwrap(); + + let msg = Logbook::create( + &pool, + LogToAdd { + boat_id: 3, + shipmaster: Some(2), + steering_person: Some(2), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![2], + }, + &User::find_by_id(&pool, 1).await.unwrap(), + "", + ) + .await + .unwrap(); + assert_eq!( + msg, + String::from(" • rower braucht nur mehr 5 km bis die 1000 km voll sind 🤑") + ); + } + + #[sqlx::test] + fn test_create_boat_not_found() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 999, + shipmaster: Some(5), + steering_person: Some(5), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![5], + }, + &User::find_by_id(&pool, 4).await.unwrap(), + "", + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::BoatNotFound)); + } + + #[sqlx::test] + fn test_create_boat_locked() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 5, + shipmaster: Some(5), + steering_person: Some(5), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![5], + }, + &User::find_by_id(&pool, 4).await.unwrap(), + "", + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::BoatLocked)); + } + + #[sqlx::test] + fn test_create_boat_on_water() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 2, + shipmaster: Some(5), + steering_person: Some(5), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![5], + }, + &User::find_by_id(&pool, 5).await.unwrap(), + "", + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::BoatAlreadyOnWater)); + } + + #[sqlx::test] + fn test_create_boat_on_water_wrong_arrival() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 3, + shipmaster: Some(5), + steering_person: Some(5), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: Some("2128-05-20T11:00".into()), + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![5], + }, + &User::find_by_id(&pool, 5).await.unwrap(), + "", + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::ArrivalSetButNotRemainingTwo)); + } + + #[sqlx::test] + fn test_create_shipmaster_not_in_rowers() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 3, + shipmaster: Some(2), + steering_person: Some(2), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: Vec::new(), + }, + &User::find_by_id(&pool, 2).await.unwrap(), + "", + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::ShipmasterNotInRowers)); + } + + #[sqlx::test] + fn test_create_steering_person_not_in_rowers() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 3, + shipmaster: Some(5), + steering_person: Some(1), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![5], + }, + &User::find_by_id(&pool, 5).await.unwrap(), + "", + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::SteeringPersonNotInRowers)); + } + + #[sqlx::test] + fn test_create_too_many_rowers() { + let pool = testdb!(); + + let res = Logbook::create( + &pool, + LogToAdd { + boat_id: 1, + shipmaster: Some(5), + steering_person: Some(5), + shipmaster_only_steering: false, + departure: "2128-05-20T12:00".into(), + arrival: None, + destination: None, + distance_in_km: None, + comments: None, + logtype: None, + rowers: vec![1, 5], + }, + &User::find_by_id(&pool, 5).await.unwrap(), + "", + ) + .await; + + assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2))); + } + + #[sqlx::test] + fn test_succ_home() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let user = User::find_by_id(&pool, 2).await.unwrap(); + + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + logbook + .home( + &pool, + &user, + super::LogToFinalize { + destination: "new-destination".into(), + distance_in_km: 42, + comments: Some("Perfect water".into()), + logtype: None, + rowers: vec![2], + shipmaster: Some(2), + steering_person: Some(2), + shipmaster_only_steering: false, + departure: format!("{}T10:00", current_date), + arrival: format!("{}T12:00", current_date), + }, + "", + ) + .await + .unwrap(); + } + + #[sqlx::test] + fn test_home_wrong_user() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let user = User::find_by_id(&pool, 1).await.unwrap(); + + let res = logbook + .home( + &pool, + &user, + super::LogToFinalize { + destination: "new-destination".into(), + distance_in_km: 42, + comments: Some("Perfect water".into()), + logtype: None, + rowers: vec![1], + shipmaster: Some(1), + steering_person: Some(1), + shipmaster_only_steering: false, + departure: "1990-01-01T10:00".into(), + arrival: "1990-01-01T12:00".into(), + }, + "", + ) + .await; + + assert_eq!(res, Err(LogbookUpdateError::NotYourEntry)); + } + + #[sqlx::test] + fn test_home_too_many_rower() { + let pool = testdb!(); + + let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); + let user = User::find_by_id(&pool, 2).await.unwrap(); + + let res = logbook + .home( + &pool, + &user, + super::LogToFinalize { + destination: "new-destination".into(), + distance_in_km: 42, + comments: Some("Perfect water".into()), + logtype: None, + rowers: vec![1, 2], + shipmaster: Some(2), + steering_person: Some(2), + shipmaster_only_steering: false, + departure: "1990-01-01T10:00".into(), + arrival: "1990-01-01T12:00".into(), + }, + "", + ) + .await; + + assert_eq!(res, Err(LogbookUpdateError::TooManyRowers(1, 2))); + } +} diff --git a/src/model/mail.rs b/src/model/mail.rs new file mode 100644 index 0000000..3d3794b --- /dev/null +++ b/src/model/mail.rs @@ -0,0 +1,367 @@ +use std::{error::Error, fs}; + +use lettre::{ + message::{header::ContentType, Attachment, MultiPart, SinglePart}, + transport::smtp::authentication::Credentials, + Message, SmtpTransport, Transport, +}; +use sqlx::{Sqlite, SqlitePool, Transaction}; + +use crate::tera::admin::mail::MailToSend; + +use super::{family::Family, log::Log, role::Role, user::User}; + +pub struct Mail {} + +impl Mail { + pub async fn send_single( + db: &SqlitePool, + to: &str, + subject: &str, + body: String, + smtp_pw: &str, + ) -> Result<(), String> { + let mut tx = db.begin().await.unwrap(); + let ret = Self::send_single_tx(&mut tx, to, subject, body, smtp_pw).await; + tx.commit().await.unwrap(); + ret + } + + pub async fn send_single_tx( + db: &mut Transaction<'_, Sqlite>, + to: &str, + subject: &str, + body: String, + smtp_pw: &str, + ) -> Result<(), String> { + let mut email = Message::builder() + .from( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .reply_to( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .to("ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap()); + let splitted = to.split(','); + for single_rec in splitted { + match single_rec.parse() { + Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail), + Err(_) => { + Log::create_with_tx( + db, + format!("Mail not sent to {single_rec}, because it could not be parsed"), + ) + .await; + return Err(format!( + "Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist." + )); + } + } + } + + let email = email + .subject(subject) + .header(ContentType::TEXT_PLAIN) + .body(body) + .unwrap(); + + let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into()); + + let mailer = SmtpTransport::relay("mail.your-server.de") + .unwrap() + .credentials(creds) + .build(); + + // Send the email + mailer.send(&email).unwrap(); + + Ok(()) + } + + pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool { + let mut email = Message::builder() + .from( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .reply_to( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .to("ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap()); + let role = Role::find_by_id(db, data.role_id).await.unwrap(); + for rec in role.mails_from_role(db).await { + let splitted = rec.split(','); + for single_rec in splitted { + match single_rec.parse() { + Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail), + Err(_) => { + Log::create( + db, + format!("Mail not sent to {rec}, because it could not be parsed"), + ) + .await; + } + } + } + } + + let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body)); + + for temp_file in &data.files { + let content = fs::read(temp_file.path().unwrap()).unwrap(); + let media_type = format!("{}", temp_file.content_type().unwrap().media_type()); + let content_type = ContentType::parse(&media_type).unwrap(); + if let Some(name) = temp_file.name() { + let attachment = Attachment::new(format!( + "{}.{}", + name, + temp_file.content_type().unwrap().extension().unwrap() + )) + .body(content, content_type); + + multipart = multipart.singlepart(attachment); + } + } + + let email = email.subject(data.subject).multipart(multipart).unwrap(); + + let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw); + + let mailer = SmtpTransport::relay("mail.your-server.de") + .unwrap() + .credentials(creds) + .build(); + + // Send the email + match mailer.send(&email) { + Ok(_) => return true, + Err(e) => println!("{:?}", e.source()), + }; + false + } + + pub async fn fees(db: &SqlitePool, smtp_pw: String) { + let users = User::all_payer_groups(db).await; + for user in users { + if !user.has_role(db, "paid").await { + let mut is_family = false; + let mut send_to = String::new(); + match Family::find_by_opt_id(db, user.family_id).await { + Some(family) => { + is_family = true; + for member in family.members(db).await { + if let Some(mail) = member.mail { + send_to.push_str(&format!("{mail},")) + } + } + } + None => { + if let Some(mail) = &user.mail { + send_to.push_str(mail) + } + } + } + + let fees = user.fee(db).await; + if let Some(fees) = fees { + let mut content = format!( + "Liebes Vereinsmitglied, \n\n\ +dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€", + fees.sum_in_cents / 100, + ); + + if fees.parts.len() == 1 { + content.push_str(&format!(" ({}).\n", fees.parts[0].0)) + } else { + content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n"); + for (desc, fee_in_cents) in fees.parts { + content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100)) + } + } + if is_family { + content.push_str(&format!( + "Dieser gilt für die gesamte Familie ({}).\n", + fees.name + )) + } + content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\ +Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\ +Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n +Beste Grüße\n\ +Der Vorstand + "); + let mut email = Message::builder() + .from( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .reply_to( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .to("ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap()); + let splitted = send_to.split(','); + let mut send_mail = false; + for single_rec in splitted { + let single_rec = single_rec.trim(); + match single_rec.parse() { + Ok(val) => { + email = email.bcc(val); + send_mail = true; + } + Err(_) => { + println!("Error in mail: {single_rec}"); + } + } + } + + if send_mail { + let email = email + .subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren") + .header(ContentType::TEXT_PLAIN) + .body(content) + .unwrap(); + + let creds = + Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone()); + + let mailer = SmtpTransport::relay("mail.your-server.de") + .unwrap() + .credentials(creds) + .build(); + + // Send the email + mailer.send(&email).unwrap(); + } + } + } + } + } + + pub async fn fees_final(db: &SqlitePool, smtp_pw: String) { + let users = User::all_payer_groups(db).await; + for user in users { + if let Some(fee) = user.fee(db).await { + if !fee.paid { + let mut is_family = false; + let mut send_to = String::new(); + match Family::find_by_opt_id(db, user.family_id).await { + Some(family) => { + is_family = true; + for member in family.members(db).await { + if let Some(mail) = member.mail { + send_to.push_str(&format!("{mail},")) + } + } + } + None => { + if let Some(mail) = &user.mail { + send_to.push_str(mail) + } + } + } + + let fees = user.fee(db).await; + if let Some(fees) = fees { + let mut content = format!( + "Liebes Vereinsmitglied, \n\n\ +wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können. + +Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\ +Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€", + fees.sum_in_cents / 100, + ); + + if fees.parts.len() == 1 { + content.push_str(&format!(" ({}).\n", fees.parts[0].0)) + } else { + content + .push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n"); + for (desc, fee_in_cents) in fees.parts { + content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100)) + } + } + if is_family { + content.push_str(&format!( + "Dieser gilt für die gesamte Familie ({}). Diese Mail wird an alle Familienmitglieder verschickt, bezahlen müsst ihr natürlich nur 1x.\n", + fees.name + )) + } + content.push_str("\n\ +Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\ +Bei Fragen oder Problemen stehen wir gerne zur Verfügung. + +Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.) + +Mit freundlichen Grüßen,\n\ +Der Vorstand"); + let mut email = Message::builder() + .from( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .reply_to( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .to("ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap()); + let splitted = send_to.split(','); + let mut send_mail = false; + for single_rec in splitted { + let single_rec = single_rec.trim(); + match single_rec.parse() { + Ok(val) => { + email = email.bcc(val); + send_mail = true; + } + Err(_) => { + println!("Error in mail: {single_rec}"); + } + } + } + + if send_mail { + let email = email + .subject("Mahnung Vereinsgebühren | ASKÖ Ruderverein Donau Linz") + .header(ContentType::TEXT_PLAIN) + .body(content) + .unwrap(); + + let creds = Credentials::new( + "no-reply@rudernlinz.at".to_owned(), + smtp_pw.clone(), + ); + + let mailer = SmtpTransport::relay("mail.your-server.de") + .unwrap() + .credentials(creds) + .build(); + + // Send the email + mailer.send(&email).unwrap(); + } + } + } + } + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 0bf6094..80b42ac 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -11,6 +11,8 @@ use self::{ waterlevel::Waterlevel, weather::Weather, }; +use boatreservation::{BoatReservation, BoatReservationWithDetails}; +use std::collections::HashMap; pub mod event; pub mod log; @@ -34,6 +36,7 @@ pub struct Day { regular_sees_this_day: bool, max_waterlevel: Option, weather: Option, + boat_reservations: HashMap>, } impl Day { @@ -50,6 +53,9 @@ impl Day { regular_sees_this_day, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, weather: Weather::find_by_day(db, day).await, + boat_reservations: BoatReservation::with_groups( + BoatReservation::for_day(db, day).await, + ), } } else { Self { @@ -60,6 +66,9 @@ impl Day { regular_sees_this_day, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, weather: Weather::find_by_day(db, day).await, + boat_reservations: BoatReservation::with_groups( + BoatReservation::for_day(db, day).await, + ), } } } diff --git a/src/model/notification.rs b/src/model/notification.rs index 9aa5030..f72eb49 100644 --- a/src/model/notification.rs +++ b/src/model/notification.rs @@ -208,6 +208,15 @@ ORDER BY read_at DESC, created_at DESC; } } } + + pub(crate) async fn mark_all_read(db: &SqlitePool, user: &User) { + let notifications = Self::for_user(db, user).await; + + for notification in notifications { + notification.mark_read(db).await; + } + } + pub(crate) async fn delete_by_action(db: &sqlx::Pool, action: &str) { sqlx::query!( "DELETE FROM notification WHERE action_after_reading=? and read_at is null", @@ -289,7 +298,7 @@ mod test { assert_eq!(rower_notification.category, "Absage Ausfahrt"); assert_eq!( rower_notification.action_after_reading.as_deref(), - Some("remove_user_trip_with_trip_details_id:3") + Some("remove_user_trip_with_trip_details_id:4") ); // Cox received notification diff --git a/src/model/personal/equatorprice.rs b/src/model/personal/equatorprice.rs new file mode 100644 index 0000000..2568987 --- /dev/null +++ b/src/model/personal/equatorprice.rs @@ -0,0 +1,104 @@ +use crate::model::{logbook::Logbook, stat::Stat, user::User}; +use serde::Serialize; + +#[derive(Serialize, PartialEq, Debug)] +pub(crate) enum Level { + None, + Bronze, + Silver, + Gold, + Diamond, + Done, +} + +impl Level { + fn required_km(&self) -> i32 { + match self { + Level::Bronze => 40_000, + Level::Silver => 80_000, + Level::Gold => 100_000, + Level::Diamond => 200_000, + Level::Done => 0, + Level::None => 0, + } + } + + fn next_level(km: i32) -> Self { + if km < Level::Bronze.required_km() { + Level::Bronze + } else if km < Level::Silver.required_km() { + Level::Silver + } else if km < Level::Gold.required_km() { + Level::Gold + } else if km < Level::Diamond.required_km() { + Level::Diamond + } else { + Level::Done + } + } + + pub(crate) fn curr_level(km: i32) -> Self { + if km < Level::Bronze.required_km() { + Level::None + } else if km < Level::Silver.required_km() { + Level::Bronze + } else if km < Level::Gold.required_km() { + Level::Silver + } else if km < Level::Diamond.required_km() { + Level::Gold + } else { + Level::Diamond + } + } + + pub(crate) fn desc(&self) -> &str { + match self { + Level::Bronze => "Bronze", + Level::Silver => "Silber", + Level::Gold => "Gold", + Level::Diamond => "Diamant", + Level::Done => "", + Level::None => "-", + } + } +} + +#[derive(Serialize)] +pub(crate) struct Next { + level: Level, + desc: String, + missing_km: i32, + required_km: i32, + rowed_km: i32, +} + +impl Next { + pub(crate) fn new(rowed_km: i32) -> Self { + let level = Level::next_level(rowed_km); + let required_km = level.required_km(); + let missing_km = required_km - rowed_km; + Self { + desc: level.desc().to_string(), + level, + missing_km, + required_km, + rowed_km, + } + } +} + +pub(crate) async fn new_level_with_last_log( + db: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + user: &User, +) -> Option { + let rowed_km = Stat::total_km_tx(db, user).await.rowed_km; + + if let Some(last_logbookentry) = Logbook::completed_with_user_tx(db, user).await.last() { + let last_trip_km = last_logbookentry.logbook.distance_in_km.unwrap(); + if Level::curr_level(rowed_km) != Level::curr_level(rowed_km - last_trip_km as i32) { + return Some(Level::curr_level(rowed_km).desc().to_string()); + } + } + + None +} diff --git a/src/model/personal/rowingbadge.rs b/src/model/personal/rowingbadge.rs new file mode 100644 index 0000000..93a969a --- /dev/null +++ b/src/model/personal/rowingbadge.rs @@ -0,0 +1,221 @@ +use std::cmp; + +use chrono::{Datelike, Local, NaiveDate}; +use serde::Serialize; +use sqlx::{Sqlite, SqlitePool, Transaction}; + +use crate::model::{ + logbook::{Filter, Logbook, LogbookWithBoatAndRowers}, + stat::Stat, + user::User, +}; + +enum AgeBracket { + Till14, + From14Till18, + From19Till30, + From31Till60, + From61Till75, + From76, +} + +impl AgeBracket { + fn cat(&self) -> &str { + match self { + AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre", + AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre", + AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre", + AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre", + AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre", + AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre", + } + } + + fn dist_in_km(&self) -> i32 { + match self { + AgeBracket::Till14 => 500, + AgeBracket::From14Till18 => 1000, + AgeBracket::From19Till30 => 1200, + AgeBracket::From31Till60 => 1000, + AgeBracket::From61Till75 => 800, + AgeBracket::From76 => 600, + } + } + + fn required_dist_multi_day_in_km(&self) -> i32 { + match self { + AgeBracket::Till14 => 60, + AgeBracket::From14Till18 => 60, + AgeBracket::From19Till30 => 80, + AgeBracket::From31Till60 => 80, + AgeBracket::From61Till75 => 80, + AgeBracket::From76 => 80, + } + } + + fn required_dist_single_day_in_km(&self) -> i32 { + match self { + AgeBracket::Till14 => 30, + AgeBracket::From14Till18 => 30, + AgeBracket::From19Till30 => 40, + AgeBracket::From31Till60 => 40, + AgeBracket::From61Till75 => 40, + AgeBracket::From76 => 40, + } + } +} + +impl TryFrom<&User> for AgeBracket { + type Error = String; + + fn try_from(value: &User) -> Result { + let Some(birthdate) = value.birthdate.clone() else { + return Err("User has no birthdate".to_string()); + }; + + let Ok(birthdate) = NaiveDate::parse_from_str(&birthdate, "%Y-%m-%d") else { + return Err("Birthdate in wrong format...".to_string()); + }; + + let today = Local::now().date_naive(); + + let age = today.year() - birthdate.year(); + if age <= 14 { + Ok(AgeBracket::Till14) + } else if age <= 18 { + Ok(AgeBracket::From14Till18) + } else if age <= 30 { + Ok(AgeBracket::From19Till30) + } else if age <= 60 { + Ok(AgeBracket::From31Till60) + } else if age <= 75 { + Ok(AgeBracket::From61Till75) + } else { + Ok(AgeBracket::From76) + } + } +} + +#[derive(Serialize)] +pub(crate) struct Status { + pub(crate) year: i32, + rowed_km: i32, + category: String, + required_km: i32, + missing_km: i32, + multi_day_trips_over_required_distance: Vec, + multi_day_trips_required_distance: i32, + single_day_trips_over_required_distance: Vec, + single_day_trips_required_distance: i32, + achieved: bool, +} + +impl Status { + fn calc( + agebracket: &AgeBracket, + rowed_km: i32, + single_day_trips_over_required_distance: usize, + multi_day_trips_over_required_distance: usize, + year: i32, + ) -> Self { + let category = agebracket.cat().to_string(); + + let required_km = agebracket.dist_in_km(); + let missing_km = cmp::max(required_km - rowed_km, 0); + + let achieved = missing_km == 0 + && (multi_day_trips_over_required_distance >= 1 + || single_day_trips_over_required_distance >= 2); + + Self { + year, + rowed_km, + category, + required_km, + missing_km, + multi_day_trips_over_required_distance: vec![], + single_day_trips_over_required_distance: vec![], + multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(), + single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(), + achieved, + } + } + + pub(crate) async fn for_user_tx( + db: &mut Transaction<'_, Sqlite>, + user: &User, + exclude_last_log: bool, + ) -> Option { + let Ok(agebracket) = AgeBracket::try_from(user) else { + return None; + }; + + let year = if Local::now().month() == 1 { + Local::now().year() - 1 + } else { + Local::now().year() + }; + + let rowed_km = Stat::person_tx(db, Some(year), user).await.rowed_km; + let single_day_trips_over_required_distance = + Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx( + db, + user, + agebracket.required_dist_single_day_in_km(), + year, + Filter::SingleDayOnly, + exclude_last_log, + ) + .await; + let multi_day_trips_over_required_distance = + Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx( + db, + user, + agebracket.required_dist_multi_day_in_km(), + year, + Filter::MultiDayOnly, + exclude_last_log, + ) + .await; + + let ret = Self::calc( + &agebracket, + rowed_km, + single_day_trips_over_required_distance.len(), + multi_day_trips_over_required_distance.len(), + year, + ); + + Some(Self { + multi_day_trips_over_required_distance, + single_day_trips_over_required_distance, + ..ret + }) + } + + pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option { + let mut tx = db.begin().await.unwrap(); + let ret = Self::for_user_tx(&mut tx, user, false).await; + tx.commit().await.unwrap(); + ret + } + + pub(crate) async fn completed_with_last_log( + db: &mut Transaction<'_, Sqlite>, + user: &User, + ) -> bool { + if let Some(status) = Self::for_user_tx(db, user, false).await { + // if user has agebracket... + if status.achieved { + // ... and has achieved the 'Fahrtenabzeichen' + let without_last_entry = Self::for_user_tx(db, user, true).await.unwrap(); + if !without_last_entry.achieved { + // ... and this wasn't the case before the last logentry + return true; + } + } + } + + false + } +} diff --git a/src/model/stat.rs b/src/model/stat.rs new file mode 100644 index 0000000..2ed663a --- /dev/null +++ b/src/model/stat.rs @@ -0,0 +1,335 @@ +use std::{collections::HashMap, ops::DerefMut}; + +use crate::model::user::User; +use chrono::Datelike; +use serde::Serialize; +use sqlx::{FromRow, Row, Sqlite, SqlitePool, Transaction}; + +use super::boat::Boat; + +#[derive(Serialize, Clone)] +pub struct BoatStat { + pot_years: Vec, + boats: Vec, +} + +#[derive(Serialize, Clone)] +pub struct SingleBoatStat { + name: String, + cat: String, + location: String, + owner: String, + years: HashMap, +} + +impl BoatStat { + pub async fn get(db: &SqlitePool) -> BoatStat { + let mut years = Vec::new(); + let mut boat_stats_map: HashMap = HashMap::new(); + + let rows = sqlx::query( + " +SELECT + boat.id, + location.name AS location, + CAST(strftime('%Y', COALESCE(arrival, 'now')) AS INTEGER) AS year, + CAST(SUM(COALESCE(distance_in_km, 0)) AS INTEGER) AS rowed_km +FROM + boat +LEFT JOIN + logbook ON boat.id = logbook.boat_id AND logbook.arrival IS NOT NULL +LEFT JOIN + location ON boat.location_id = location.id +WHERE + not boat.external +GROUP BY + boat.id, year +ORDER BY + boat.name, year DESC; + ", + ) + .fetch_all(db) + .await + .unwrap(); + + for row in rows { + let id: i32 = row.get("id"); + let boat = Boat::find_by_id(db, id).await.unwrap(); + let owner = if let Some(owner) = boat.owner(db).await { + owner.name + } else { + String::from("Verein") + }; + let name = boat.name.clone(); + let location: String = row.get("location"); + let year: i32 = row.get("year"); + if year == 0 { + continue; // Boat still on water + } + + if !years.contains(&year) { + years.push(year); + } + + let year: String = format!("{year}"); + let cat = boat.cat(); + + let rowed_km: i32 = row.get("rowed_km"); + + let boat_stat = boat_stats_map + .entry(name.clone()) + .or_insert(SingleBoatStat { + name, + location, + owner, + cat, + years: HashMap::new(), + }); + boat_stat.years.insert(year, rowed_km); + } + + BoatStat { + pot_years: years, + boats: boat_stats_map.into_values().collect(), + } + } +} + +#[derive(FromRow, Serialize, Clone)] +pub struct Stat { + name: String, + pub(crate) amount_trips: i32, + pub(crate) rowed_km: i32, +} + +impl Stat { + pub async fn guest(db: &SqlitePool, year: Option) -> Stat { + let year = match year { + Some(year) => year, + None => chrono::Local::now().year(), + }; + //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) + // proper guests + let guests = sqlx::query(&format!( + " +SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km, + SUM(b.amount_seats - COALESCE(m.member_count, 0)) AS amount_trips +FROM logbook l +JOIN boat b ON l.boat_id = b.id +LEFT JOIN ( + SELECT logbook_id, COUNT(*) as member_count + FROM rower + GROUP BY logbook_id +) m ON l.id = m.logbook_id +WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external; +" + )) + .fetch_one(db) + .await + .unwrap(); + + let guest_km: i32 = guests.get(0); + let guest_amount_trips: i32 = guests.get(1); + + // e.g. scheckbücher + let guest_user = sqlx::query(&format!( + " +SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips +FROM user u +INNER JOIN rower r ON u.id = r.rower_id +INNER JOIN logbook l ON r.logbook_id = l.id +WHERE u.id NOT IN ( + SELECT ur.user_id + FROM user_role ur + INNER JOIN role ro ON ur.role_id = ro.id + WHERE ro.name = 'Donau Linz' +) +AND l.distance_in_km IS NOT NULL +AND l.arrival LIKE '{year}-%' +AND u.name != 'Externe Steuerperson'; +" + )) + .fetch_one(db) + .await + .unwrap(); + + let guest_user_km: i32 = guest_user.get(0); + let guest_user_amount_trips: i32 = guest_user.get(1); + + Stat { + name: "Gäste".into(), + amount_trips: guest_amount_trips + guest_user_amount_trips, + rowed_km: guest_km + guest_user_km, + } + } + + pub async fn trips_people(db: &SqlitePool, year: Option) -> i32 { + let stats = Self::people(db, year).await; + let mut sum = 0; + for stat in stats { + sum += stat.amount_trips; + } + + sum + } + pub async fn sum_people(db: &SqlitePool, year: Option) -> i32 { + let stats = Self::people(db, year).await; + let mut sum = 0; + for stat in stats { + sum += stat.rowed_km; + } + + sum + } + + pub async fn people(db: &SqlitePool, year: Option) -> Vec { + let year = match year { + Some(year) => year, + None => chrono::Local::now().year(), + }; + //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) + sqlx::query(&format!( + " +SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips +FROM ( + SELECT * FROM user + WHERE id IN ( + SELECT user_id FROM user_role + JOIN role ON user_role.role_id = role.id + WHERE role.name = 'Donau Linz' + ) +) u +INNER JOIN rower r ON u.id = r.rower_id +INNER JOIN logbook l ON r.logbook_id = l.id +WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson' +GROUP BY u.name +ORDER BY rowed_km DESC, u.name; +" + )) + .fetch_all(db) + .await + .unwrap() + .into_iter() + .map(|row| Stat { + name: row.get("name"), + amount_trips: row.get("amount_trips"), + rowed_km: row.get("rowed_km"), + }) + .collect() + } + + pub async fn total_km_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Stat { + //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) + let row = sqlx::query(&format!( + " +SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips +FROM ( + SELECT * FROM user + WHERE id={} +) u +INNER JOIN rower r ON u.id = r.rower_id +INNER JOIN logbook l ON r.logbook_id = l.id +WHERE l.distance_in_km IS NOT NULL; +", + user.id + )) + .fetch_one(db.deref_mut()) + .await + .unwrap(); + + Stat { + name: row.get("name"), + amount_trips: row.get("amount_trips"), + rowed_km: row.get("rowed_km"), + } + } + + pub async fn total_km(db: &SqlitePool, user: &User) -> Stat { + let mut tx = db.begin().await.unwrap(); + let ret = Self::total_km_tx(&mut tx, user).await; + tx.commit().await.unwrap(); + ret + } + + pub async fn person_tx( + db: &mut Transaction<'_, Sqlite>, + year: Option, + user: &User, + ) -> Stat { + let year = match year { + Some(year) => year, + None => chrono::Local::now().year(), + }; + //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) + let row = sqlx::query(&format!( + " +SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips +FROM ( + SELECT * FROM user + WHERE id={} +) u +INNER JOIN rower r ON u.id = r.rower_id +INNER JOIN logbook l ON r.logbook_id = l.id +WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%'; +", + user.id + )) + .fetch_one(db.deref_mut()) + .await + .unwrap(); + + Stat { + name: row.get("name"), + amount_trips: row.get("amount_trips"), + rowed_km: row.get("rowed_km"), + } + } + + pub async fn person(db: &SqlitePool, year: Option, user: &User) -> Stat { + let mut tx = db.begin().await.unwrap(); + let ret = Self::person_tx(&mut tx, year, user).await; + tx.commit().await.unwrap(); + ret + } +} + +#[derive(Debug, Serialize)] +pub struct PersonalStat { + date: String, + km: i32, +} + +pub async fn get_personal(db: &SqlitePool, user: &User) -> Vec { + sqlx::query(&format!( + " +SELECT + departure_date as date, + SUM(total_distance) OVER (ORDER BY departure_date) as km +FROM ( + SELECT + date(l.departure) as departure_date, + COALESCE(SUM(l.distance_in_km),0) as total_distance + FROM + logbook l + LEFT JOIN + rower r ON l.id = r.logbook_id + WHERE + r.rower_id = {} + GROUP BY + departure_date +) as subquery +ORDER BY + departure_date; +", + user.id + )) + .fetch_all(db) + .await + .unwrap() + .into_iter() + .map(|row| PersonalStat { + date: row.get("date"), + km: row.get("km"), + }) + .collect() +} diff --git a/src/model/trip.rs b/src/model/trip.rs index fb2e575..c2eeeec 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -81,33 +81,31 @@ impl Trip { trip_details.planned_starting_time, ) .await; - if same_starting_datetime.len() > 1 { - for notify in same_starting_datetime { - // don't notify oneself - if notify.id == trip_details.id { - continue; - } + for notify in same_starting_datetime { + // don't notify oneself + if notify.id == trip_details.id { + continue; + } - // don't notify people who have cancelled their trip - if notify.cancelled() { - continue; - } + // don't notify people who have cancelled their trip + if notify.cancelled() { + continue; + } - if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await { - let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); - Notification::create( - db, - &user, - &format!( - "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt", - user.name, trip.day, trip.planned_starting_time - ), - "Neue Ausfahrt zur selben Zeit", - None, - None, - ) - .await; - } + if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await { + let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); + Notification::create( + db, + &user_earlier_trip, + &format!( + "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt", + user.name, trip.day, trip.planned_starting_time + ), + "Neue Ausfahrt zur selben Zeit", + None, + None, + ) + .await; } } } @@ -277,10 +275,8 @@ WHERE day=? return Err(TripUpdateError::NotYourTrip); } - if update.trip_type != Some(4) { - if !update.cox.allowed_to_steer(db).await { - return Err(TripUpdateError::TripTypeNotAllowed); - } + if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await { + return Err(TripUpdateError::TripTypeNotAllowed); } let Some(trip_details_id) = update.trip.trip_details_id else { @@ -478,6 +474,7 @@ mod test { use crate::{ model::{ event::Event, + notification::Notification, trip::{self, TripDeleteError}, tripdetails::TripDetails, user::{SteeringUser, User}, @@ -509,6 +506,34 @@ mod test { assert!(Trip::find_by_id(&pool, 1).await.is_some()); } + #[sqlx::test] + fn test_notification_cox_if_same_datetime() { + let pool = testdb!(); + let cox = SteeringUser::new( + &pool, + User::find_by_name(&pool, "cox".into()).await.unwrap(), + ) + .await + .unwrap(); + let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); + Trip::new_own(&pool, &cox, trip_details).await; + + let cox2 = SteeringUser::new( + &pool, + User::find_by_name(&pool, "cox2".into()).await.unwrap(), + ) + .await + .unwrap(); + let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap(); + Trip::new_own(&pool, &cox2, trip_details).await; + + let last_notification = &Notification::for_user(&pool, &cox).await[0]; + + assert!(last_notification + .message + .starts_with("cox2 hat eine Ausfahrt zur selben Zeit")); + } + #[sqlx::test] fn test_get_day_cox_trip() { let pool = testdb!(); diff --git a/src/model/tripdetails.rs b/src/model/tripdetails.rs index df5204e..ff813cb 100644 --- a/src/model/tripdetails.rs +++ b/src/model/tripdetails.rs @@ -339,7 +339,7 @@ mod test { } ) .await, - 3, + 4, ); assert_eq!( TripDetails::create( @@ -354,7 +354,7 @@ mod test { } ) .await, - 4, + 5, ); } diff --git a/src/model/user/fee.rs b/src/model/user/fee.rs new file mode 100644 index 0000000..0228892 --- /dev/null +++ b/src/model/user/fee.rs @@ -0,0 +1,58 @@ +use super::User; +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct Fee { + pub sum_in_cents: i64, + pub parts: Vec<(String, i64)>, + pub name: String, + pub user_ids: String, + pub paid: bool, + pub users: Vec, +} + +impl Default for Fee { + fn default() -> Self { + Self::new() + } +} + +impl Fee { + pub fn new() -> Self { + Self { + sum_in_cents: 0, + name: "".into(), + parts: Vec::new(), + user_ids: "".into(), + users: Vec::new(), + paid: false, + } + } + + pub fn add(&mut self, desc: String, price_in_cents: i64) { + self.sum_in_cents += price_in_cents; + + self.parts.push((desc, price_in_cents)); + } + + pub fn add_person(&mut self, user: &User) { + if !self.name.is_empty() { + self.name.push_str(" + "); + self.user_ids.push('&'); + } + self.name.push_str(&user.name); + + self.user_ids.push_str(&format!("user_ids[]={}", user.id)); + self.users.push(user.clone()); + } + + pub fn paid(&mut self) { + self.paid = true; + } + + pub fn merge(&mut self, fee: Fee) { + for (desc, price_in_cents) in fee.parts { + self.add(desc, price_in_cents); + } + } +} diff --git a/src/model/user.rs b/src/model/user/mod.rs similarity index 97% rename from src/model/user.rs rename to src/model/user/mod.rs index 4e58bd5..66bd4c6 100644 --- a/src/model/user.rs +++ b/src/model/user/mod.rs @@ -31,7 +31,7 @@ pub struct User { pub struct UserWithDetails { #[serde(flatten)] pub user: User, - pub amount_unread_notifications: i32, + pub amount_unread_notifications: i64, pub allowed_to_steer: bool, pub roles: Vec, } @@ -205,10 +205,21 @@ FROM user WHERE deleted = 0 ORDER BY last_access DESC " - ) - .fetch_all(db) - .await - .unwrap() + SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token + FROM user + WHERE deleted = 0 + ORDER BY {} + ", + sort + ); + if !asc { + query.push_str(" DESC"); + } + + sqlx::query_as::<_, User>(&query) + .fetch_all(db) + .await + .unwrap() } pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec { diff --git a/src/scheduled/waterlevel.rs b/src/scheduled/waterlevel.rs index 705e531..d4a371e 100644 --- a/src/scheduled/waterlevel.rs +++ b/src/scheduled/waterlevel.rs @@ -80,8 +80,8 @@ fn fetch() -> Result { let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json"; match ureq::get(url).call() { - Ok(response) => { - let forecast: Result, _> = response.into_json(); + Ok(mut response) => { + let forecast: Result, _> = response.body_mut().read_json(); if let Ok(data) = forecast { if data.len() == 1 { diff --git a/src/scheduled/weather.rs b/src/scheduled/weather.rs index 1f6fc03..456f8fa 100644 --- a/src/scheduled/weather.rs +++ b/src/scheduled/weather.rs @@ -99,8 +99,8 @@ fn fetch(api_key: &str) -> Result { let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=47.766249&lon=13.367683&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}"); match ureq::get(&url).call() { - Ok(response) => { - let data: Result = response.into_json(); + Ok(mut response) => { + let data: Result = response.body_mut().read_json(); if let Ok(data) = data { Ok(data) diff --git a/src/tera/admin/boat.rs b/src/tera/admin/boat.rs new file mode 100644 index 0000000..56bf5fd --- /dev/null +++ b/src/tera/admin/boat.rs @@ -0,0 +1,319 @@ +use crate::model::{ + boat::{Boat, BoatToAdd, BoatToUpdate}, + location::Location, + log::Log, + user::{User, UserWithDetails, VorstandUser}, +}; +use rocket::{ + form::Form, + get, post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, Route, State, +}; +use rocket_dyn_templates::{tera::Context, Template}; +use sqlx::SqlitePool; + +#[get("/boat")] +async fn index( + db: &State, + admin: VorstandUser, + flash: Option>, +) -> Template { + let boats = Boat::all(db).await; + let locations = Location::all(db).await; + let users = User::all(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("boats", &boats); + context.insert("locations", &locations); + context.insert("users", &users); + context.insert( + "loggedin_user", + &UserWithDetails::from_user(admin.user, db).await, + ); + + Template::render("admin/boat/index", context.into_json()) +} + +#[get("/boat//delete")] +async fn delete(db: &State, admin: VorstandUser, boat: i32) -> Flash { + let boat = Boat::find_by_id(db, boat).await; + Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await; + + match boat { + Some(boat) => { + boat.delete(db).await; + Flash::success( + Redirect::to("/admin/boat"), + format!("Boot {} gelöscht", boat.name), + ) + } + None => Flash::error(Redirect::to("/admin/boat"), "Boat does not exist"), + } +} + +#[post("/boat/", data = "")] +async fn update( + db: &State, + data: Form>, + boat_id: i32, + _admin: VorstandUser, +) -> Flash { + let boat = Boat::find_by_id(db, boat_id).await; + let Some(boat) = boat else { + return Flash::error(Redirect::to("/admin/boat"), "Boat does not exist!"); + }; + + match boat.update(db, data.into_inner()).await { + Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot bearbeitet"), + Err(e) => Flash::error(Redirect::to("/admin/boat"), e), + } +} + +#[post("/boat/new", data = "")] +async fn create( + db: &State, + data: Form>, + _admin: VorstandUser, +) -> Flash { + match Boat::create(db, data.into_inner()).await { + Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"), + Err(e) => Flash::error(Redirect::to("/admin/boat"), e), + } +} + +pub fn routes() -> Vec { + routes![index, create, delete, update] +} + +#[cfg(test)] +mod test { + use rocket::{ + http::{ContentType, Status}, + local::asynchronous::Client, + }; + use sqlx::SqlitePool; + + use crate::tera::admin::boat::Boat; + use crate::testdb; + + #[sqlx::test] + fn test_boat_index() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/admin/boat"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let text = response.into_string().await.unwrap(); + assert!(&text.contains("Neues Boot")); + assert!(&text.contains("Kaputtes Boot :-(")); + assert!(&text.contains("Haichenbach")); + } + + #[sqlx::test] + fn test_succ_update() { + let db = testdb!(); + + let boat = Boat::find_by_id(&db, 1).await.unwrap(); + assert_eq!(boat.name, "Haichenbach"); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/1") + .header(ContentType::Form) + .body("name=Haichiii&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successBoot bearbeitet"); + + let boat = Boat::find_by_id(&db, 1).await.unwrap(); + assert_eq!(boat.name, "Haichiii"); + } + + #[sqlx::test] + fn test_update_wrong_boat() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/1337") + .header(ContentType::Form) + .body("name=Haichiii&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorBoat does not exist!"); + + let boat = Boat::find_by_id(&db, 1).await.unwrap(); + assert_eq!(boat.name, "Haichenbach"); + } + + #[sqlx::test] + fn test_update_wrong_foreign() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/1") + .header(ContentType::Form) + .body("name=Haichiii&amount_seats=1&location_id=999"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "5:errorerror returned from database: (code: 787) FOREIGN KEY constraint failed" + ); + } + + #[sqlx::test] + fn test_succ_create() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + assert!(Boat::find_by_name(&db, "completely-new-boat".into()) + .await + .is_none()); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/new") + .header(ContentType::Form) + .body("name=completely-new-boat&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successBoot hinzugefügt"); + + Boat::find_by_name(&db, "completely-new-boat".into()) + .await + .unwrap(); + } + + #[sqlx::test] + fn test_create_db_error() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post("/admin/boat/new") + .header(ContentType::Form) + .body("name=Haichenbach&amount_seats=1&location_id=1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!( + response.headers().get("Location").next(), + Some("/admin/boat") + ); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "5:errorerror returned from database: (code: 2067) UNIQUE constraint failed: boat.name" + ); + } +} diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 2c800ba..ecea5c8 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -33,13 +33,17 @@ impl<'r> FromRequest<'r> for Referer { } } -#[get("/user")] +#[get("/user?&")] async fn index( db: &State, user: ManageUserUser, flash: Option>, + sort: Option, + asc: bool, ) -> Template { - let user_futures: Vec<_> = User::all(db) + let sort_column = sort.unwrap_or_else(|| "last_access".to_string()); + + let user_futures: Vec<_> = User::all_with_order(db, &sort_column, asc) .await .into_iter() .map(|u| async move { UserWithDetails::from_user(u, db).await }) diff --git a/src/tera/board/achievement.rs b/src/tera/board/achievement.rs new file mode 100644 index 0000000..750ab25 --- /dev/null +++ b/src/tera/board/achievement.rs @@ -0,0 +1,46 @@ +use crate::model::{ + personal::Achievements, + role::Role, + user::{User, UserWithDetails, VorstandUser}, +}; +use rocket::{get, request::FlashMessage, routes, Route, State}; +use rocket_dyn_templates::{tera::Context, Template}; +use sqlx::SqlitePool; + +#[get("/achievement")] +async fn index( + db: &State, + admin: VorstandUser, + flash: Option>, +) -> Template { + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + let role = Role::find_by_name(db, "Donau Linz").await.unwrap(); + let users = User::all_with_role(db, &role).await; + let mut people = Vec::new(); + let mut rowingbadge_year = None; + for user in users { + let achievement = Achievements::for_user(db, &user).await; + if let Some(badge) = &achievement.rowingbadge { + rowingbadge_year = Some(badge.year); + } + people.push((user, achievement)); + } + + context.insert("people", &people); + context.insert("rowingbadge_year", &rowingbadge_year.unwrap()); + + context.insert( + "loggedin_user", + &UserWithDetails::from_user(admin.into_inner(), db).await, + ); + + Template::render("achievement", context.into_json()) +} + +pub fn routes() -> Vec { + routes![index] +} diff --git a/src/tera/boatdamage.rs b/src/tera/boatdamage.rs new file mode 100644 index 0000000..2e345ac --- /dev/null +++ b/src/tera/boatdamage.rs @@ -0,0 +1,181 @@ +use rocket::{ + form::Form, + get, post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::Template; +use sqlx::SqlitePool; +use tera::Context; + +use crate::{ + model::{ + boat::Boat, + boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, + user::{DonauLinzUser, SteeringUser, TechUser, User, UserWithDetails}, + }, + tera::log::KioskCookie, +}; + +#[get("/")] +async fn index_kiosk( + db: &State, + flash: Option>, + _kiosk: KioskCookie, +) -> Template { + let boatdamages = BoatDamage::all(db).await; + let boats = Boat::all(db).await; + let user = User::all(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + context.insert("boatdamages", &boatdamages); + context.insert("boats", &boats); + context.insert("user", &user); + context.insert("show_kiosk_header", &true); + + Template::render("boatdamages", context.into_json()) +} + +#[get("/", rank = 2)] +async fn index( + db: &State, + flash: Option>, + user: DonauLinzUser, +) -> Template { + let boatdamages = BoatDamage::all(db).await; + let boats = Boat::all(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + context.insert("boatdamages", &boatdamages); + context.insert("boats", &boats); + context.insert( + "loggedin_user", + &UserWithDetails::from_user(user.into_inner(), db).await, + ); + + Template::render("boatdamages", context.into_json()) +} + +#[derive(FromForm)] +pub struct FormBoatDamageToAdd<'r> { + pub boat_id: i64, + pub desc: &'r str, + pub lock_boat: bool, +} + +#[post("/", data = "", rank = 2)] +async fn create<'r>( + db: &State, + data: Form>, + user: DonauLinzUser, +) -> Flash { + let user: User = user.into_inner(); + let boatdamage_to_add = BoatDamageToAdd { + boat_id: data.boat_id, + desc: data.desc, + lock_boat: data.lock_boat, + user_id_created: user.id as i32, + }; + match BoatDamage::create(db, boatdamage_to_add).await { + Ok(_) => Flash::success( + Redirect::to("/boatdamage"), + "Bootsschaden erfolgreich hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")), + } +} + +#[derive(FromForm)] +pub struct FormBoatDamageToAddKiosk<'r> { + pub boat_id: i64, + pub desc: &'r str, + pub lock_boat: bool, + pub user_id: i32, +} + +#[post("/", data = "")] +async fn create_from_kiosk<'r>( + db: &State, + data: Form>, + _kiosk: KioskCookie, +) -> Flash { + let boatdamage_to_add = BoatDamageToAdd { + boat_id: data.boat_id, + desc: data.desc, + lock_boat: data.lock_boat, + user_id_created: data.user_id, + }; + match BoatDamage::create(db, boatdamage_to_add).await { + Ok(_) => Flash::success( + Redirect::to("/boatdamage"), + "Bootsschaden erfolgreich hinzugefügt", + ), + Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")), + } +} + +#[derive(FromForm)] +pub struct FormBoatDamageFixed<'r> { + pub desc: &'r str, +} + +#[post("//fixed", data = "")] +async fn fixed<'r>( + db: &State, + data: Form>, + boatdamage_id: i32, + coxuser: SteeringUser, +) -> Flash { + let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix + let boatdamage_fixed = BoatDamageFixed { + desc: data.desc, + user_id_fixed: coxuser.id as i32, + }; + match boatdamage.fixed(db, boatdamage_fixed).await { + Ok(_) => Flash::success(Redirect::to("/boatdamage"), "Bootsschaden behoben."), + Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")), + } +} + +#[derive(FromForm)] +pub struct FormBoatDamageVerified<'r> { + desc: &'r str, +} + +#[post("//verified", data = "")] +async fn verified<'r>( + db: &State, + data: Form>, + boatdamage_id: i32, + techuser: TechUser, +) -> Flash { + let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix + let boatdamage_verified = BoatDamageVerified { + desc: data.desc, + user_id_verified: techuser.id as i32, + }; + match boatdamage.verified(db, boatdamage_verified).await { + Ok(_) => Flash::success(Redirect::to("/boatdamage"), "Bootsschaden verifiziert"), + Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")), + } +} + +pub fn routes() -> Vec { + routes![ + index, + index_kiosk, + create, + fixed, + verified, + create_from_kiosk + ] +} diff --git a/src/tera/ergo.rs b/src/tera/ergo.rs new file mode 100644 index 0000000..31b3d0d --- /dev/null +++ b/src/tera/ergo.rs @@ -0,0 +1,365 @@ +use std::env; + +use chrono::{Datelike, Utc}; +use rocket::{ + form::Form, + fs::TempFile, + get, + http::ContentType, + post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::{context, Template}; +use serde::Serialize; +use sqlx::SqlitePool; +use tera::Context; + +use crate::model::{ + log::Log, + notification::Notification, + role::Role, + user::{AdminUser, User, UserWithDetails}, +}; + +#[derive(Serialize)] +struct ErgoStat { + id: i64, + name: String, + dob: Option, + weight: Option, + sex: Option, + result: Option, +} + +#[get("/final")] +async fn send(db: &State, _user: AdminUser) -> Template { + let thirty = sqlx::query_as!( + ErgoStat, + "SELECT id, name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + let dozen= sqlx::query_as!( + ErgoStat, + "SELECT id, name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + Template::render( + "ergo/final", + context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen), + ) +} + +#[get("/reset")] +async fn reset(db: &State, _user: AdminUser) -> Flash { + sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;") + .execute(db.inner()) + .await + .unwrap(); + + Flash::success( + Redirect::to("/ergo"), + "Erfolgreich zurückgesetzt (Bilder müssen manuell gelöscht werden!)", + ) +} + +#[get("//user//new?")] +async fn update( + db: &State, + _admin: AdminUser, + challenge: &str, + user_id: i64, + new: &str, +) -> Flash { + if challenge == "thirty" { + sqlx::query!("UPDATE user SET dirty_thirty = ? WHERE id=?", new, user_id) + .execute(db.inner()) + .await + .unwrap(); + Flash::success(Redirect::to("/ergo"), "Succ") + } else if challenge == "dozen" { + sqlx::query!("UPDATE user SET dirty_dozen = ? WHERE id=?", new, user_id) + .execute(db.inner()) + .await + .unwrap(); + Flash::success(Redirect::to("/ergo"), "Succ") + } else { + Flash::error( + Redirect::to("/ergo"), + "Challenge not found (should be thirty or dozen)", + ) + } +} + +#[get("/")] +async fn index(db: &State, user: User, flash: Option>) -> Template { + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert( + "loggedin_user", + &UserWithDetails::from_user(user.clone(), db).await, + ); + + if !user.has_role(db, "ergo").await { + return Template::render("ergo/missing-data", context.into_json()); + } + + let users = User::ergo(db).await; + + let thirty = sqlx::query_as!( + ErgoStat, + "SELECT id, name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + let dozen= sqlx::query_as!( + ErgoStat, + "SELECT id, name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC" + ) + .fetch_all(db.inner()) + .await + .unwrap(); + + context.insert("users", &users); + context.insert("thirty", &thirty); + context.insert("dozen", &dozen); + + Template::render("ergo/index", context.into_json()) +} + +#[derive(FromForm, Debug)] +pub struct UserAdd { + birthyear: i32, + weight: i64, + sex: String, +} + +#[post("/set-data", data = "")] +async fn new_user(db: &State, data: Form, user: User) -> Flash { + if user.has_role(db, "ergo").await { + return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at"); + } + + // check data + if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 { + return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr..."); + } + if data.weight < 20 || data.weight > 200 { + return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht..."); + } + if &data.sex != "f" && &data.sex != "m" { + return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht..."); + } + + // set data + user.update_ergo(db, data.birthyear, data.weight, &data.sex) + .await; + + // inform all other `ergo` users + let ergo = Role::find_by_name(db, "ergo").await.unwrap(); + Notification::create_for_role( + db, + &ergo, + &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name), + "Ergo Challenge", + None, + None, + ) + .await; + + // add to `ergo` group + user.add_role(db, &ergo).await.unwrap(); + + Flash::success( + Redirect::to("/ergo"), + "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)", + ) +} + +#[derive(FromForm, Debug)] +pub struct ErgoToAdd<'a> { + user: i64, + result: String, + proof: TempFile<'a>, +} + +#[post("/thirty", data = "", format = "multipart/form-data")] +async fn new_thirty( + db: &State, + mut data: Form>, + created_by: User, +) -> Flash { + let user = User::find_by_id(db, data.user as i32).await.unwrap(); + + let extension = if data.proof.content_type() == Some(&ContentType::JPEG) { + "jpg" + } else { + return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert"); + }; + let base_dir = env::current_dir().unwrap(); + let file_path = base_dir.join(format!( + "data-ergo/thirty/{}_{}.{extension}", + user.name, + Utc::now() + )); + if let Err(e) = data.proof.move_copy_to(file_path).await { + eprintln!("Failed to persist file: {:?}", e); + } + + let result = data.result.trim_start_matches(['0', ' ']); + + sqlx::query!( + "UPDATE user SET dirty_thirty = ? where id = ?", + result, + data.user + ) + .execute(db.inner()) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + Log::create( + db, + format!("{} created thirty-ergo entry: {data:?}", created_by.name), + ) + .await; + + let ergo = Role::find_by_name(db, "ergo").await.unwrap(); + Notification::create_for_role( + db, + &ergo, + &format!( + "{} ist gerade die Dirty Thirty Challenge gefahren 🥵", + user.name + ), + "Ergo Challenge", + Some("/ergo"), + None, + ) + .await; + + Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen") +} + +fn format_time(input: &str) -> String { + let input = if input.starts_with(":") { + &format!("00{input}") + } else { + input + }; + let mut parts: Vec<&str> = input.split(':').collect(); + + // If there's only seconds (e.g., "24.2"), treat it as "00:00:24.2" + if parts.len() == 1 { + parts.insert(0, "0"); // Add "0" for hours + parts.insert(0, "0"); // Add "0" for minutes + } + + // If there are two parts (e.g., "4:24.2"), treat it as "00:04:24.2" + if parts.len() == 2 { + parts.insert(0, "0"); // Add "0" for hours + } + + // Now parts should have [hours, minutes, seconds] + let hours = if parts[0].len() == 1 { + format!("0{}", parts[0]) + } else { + parts[0].to_string() + }; + let minutes = if parts[1].len() == 1 { + format!("0{}", parts[1]) + } else { + parts[1].to_string() + }; + let seconds = parts[2]; + + // Split seconds into whole and fractional parts + let (sec_int, sec_frac) = seconds.split_once('.').unwrap_or((seconds, "0")); + + // Format the time as "hh:mm:ss.s" + format!( + "{}:{}:{}.{:1}", + hours, + minutes, + sec_int, + sec_frac.chars().next().unwrap_or('0') + ) +} + +#[post("/dozen", data = "", format = "multipart/form-data")] +async fn new_dozen( + db: &State, + mut data: Form>, + created_by: User, +) -> Flash { + let user = User::find_by_id(db, data.user as i32).await.unwrap(); + + let extension = if data.proof.content_type() == Some(&ContentType::JPEG) { + "jpg" + } else { + return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert"); + }; + let base_dir = env::current_dir().unwrap(); + let file_path = base_dir.join(format!( + "data-ergo/dozen/{}_{}.{extension}", + user.name, + Utc::now() + )); + if let Err(e) = data.proof.move_copy_to(file_path).await { + eprintln!("Failed to persist file: {:?}", e); + } + let result = data.result.trim_start_matches(['0', ' ']); + let result = if result.contains(":") || result.contains(".") { + format_time(result) + } else { + result.to_string() + }; + + sqlx::query!( + "UPDATE user SET dirty_dozen = ? where id = ?", + result, + data.user + ) + .execute(db.inner()) + .await + .unwrap(); //Okay, because we can only create a User of a valid id + + Log::create( + db, + format!("{} created dozen-ergo entry: {data:?}", created_by.name), + ) + .await; + + let ergo = Role::find_by_name(db, "ergo").await.unwrap(); + Notification::create_for_role( + db, + &ergo, + &format!( + "{} ist gerade die Dirty Dozen Challenge gefahren 🥵", + user.name + ), + "Ergo Challenge", + Some("/ergo"), + None, + ) + .await; + + Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen") +} + +pub fn routes() -> Vec { + routes![index, new_thirty, new_dozen, send, reset, update, new_user] +} + +#[cfg(test)] +mod test {} diff --git a/src/tera/log.rs b/src/tera/log.rs new file mode 100644 index 0000000..6269819 --- /dev/null +++ b/src/tera/log.rs @@ -0,0 +1,1111 @@ +use std::net::IpAddr; + +use rocket::{ + form::Form, + get, + http::{Cookie, CookieJar}, + post, + request::{self, FlashMessage, FromRequest}, + response::{Flash, Redirect}, + routes, + time::{Duration, OffsetDateTime}, + Request, Route, State, +}; +use rocket_dyn_templates::{context, Template}; +use sqlx::SqlitePool; +use tera::Context; + +use crate::{ + model::{ + boat::Boat, + boatreservation::BoatReservation, + distance::Distance, + log::Log, + logbook::{ + LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError, + LogbookCreateError, LogbookDeleteError, LogbookUpdateError, + }, + logtype::LogType, + trip::Trip, + user::{DonauLinzUser, User, UserWithDetails, VorstandUser}, + }, + tera::Config, +}; + +pub struct KioskCookie(()); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for KioskCookie { + type Error = std::convert::Infallible; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + match request.cookies().get_private("kiosk") { + Some(_) => request::Outcome::Success(KioskCookie(())), + None => request::Outcome::Forward(rocket::http::Status::SeeOther), + } + } +} + +#[get("/", rank = 2)] +async fn index( + db: &State, + flash: Option>, + user: DonauLinzUser, +) -> Template { + let boats = Boat::for_user(db, &user).await; + + let mut coxes: Vec = futures::future::join_all( + User::cox(db) + .await + .into_iter() + .map(|user| UserWithDetails::from_user(user, db)), + ) + .await; + coxes.retain(|u| { + u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) + }); + + let mut users: Vec = futures::future::join_all( + User::all(db) + .await + .into_iter() + .map(|user| UserWithDetails::from_user(user, db)), + ) + .await; + users.retain(|u| { + u.roles.contains(&"Donau Linz".into()) + || u.roles.contains(&"scheckbuch".into()) + || u.user.name == "Externe Steuerperson" + }); + + let logtypes = LogType::all(db).await; + let distances = Distance::all(db).await; + + let on_water = Logbook::on_water(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + context.insert("boats", &boats); + context.insert("planned_trips", &Trip::get_for_today(db).await); + context.insert( + "reservations", + &BoatReservation::all_future_with_groups(db).await, + ); + context.insert("coxes", &coxes); + context.insert("users", &users); + context.insert("logtypes", &logtypes); + context.insert( + "loggedin_user", + &UserWithDetails::from_user(user.into_inner(), db).await, + ); + context.insert("on_water", &on_water); + context.insert("distances", &distances); + + Template::render("log", context.into_json()) +} + +#[get("/show", rank = 3)] +async fn show(db: &State, user: DonauLinzUser) -> Template { + let logs = Logbook::completed(db).await; + + Template::render( + "log.completed", + context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await), + ) +} + +#[get("/show?", rank = 2)] +async fn show_for_year(db: &State, user: VorstandUser, year: i32) -> Template { + let logs = Logbook::completed_in_year(db, year).await; + + Template::render( + "log.completed", + context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await), + ) +} + +#[get("/show")] +async fn show_kiosk(db: &State, _kiosk: KioskCookie) -> Template { + let logs = Logbook::completed(db).await; + + Template::render("log.completed", context!(logs, show_kiosk_header: true)) +} + +#[get("/kiosk/ekrv2019/")] +async fn new_kiosk( + db: &State, + cookies: &CookieJar<'_>, + loc: String, + ip: Option, +) -> Redirect { + Log::create( + db, + format!("New kiosk cookie set for loc '{loc}' (IP={ip:?})"), + ) + .await; + let mut cookie = Cookie::new("kiosk", loc); + cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12)); + cookies.add_private(cookie); + Redirect::to("/log") +} + +#[get("/")] +async fn kiosk( + db: &State, + flash: Option>, + _kiosk: KioskCookie, +) -> Template { + let boats = Boat::all(db).await; + let mut coxes: Vec = futures::future::join_all( + User::cox(db) + .await + .into_iter() + .map(|user| UserWithDetails::from_user(user, db)), + ) + .await; + + coxes.retain(|u| { + u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) + }); + + let mut users: Vec = futures::future::join_all( + User::all(db) + .await + .into_iter() + .map(|user| UserWithDetails::from_user(user, db)), + ) + .await; + + users.retain(|u| { + u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) + }); + + let logtypes = LogType::all(db).await; + let distances = Distance::all(db).await; + + let on_water = Logbook::on_water(db).await; + + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + context.insert("planned_trips", &Trip::get_for_today(db).await); + context.insert("boats", &boats); + context.insert( + "reservations", + &BoatReservation::all_future_with_groups(db).await, + ); + context.insert("coxes", &coxes); + context.insert("users", &users); + context.insert("logtypes", &logtypes); + context.insert("on_water", &on_water); + context.insert("distances", &distances); + context.insert("show_kiosk_header", &true); + + Template::render("kiosk", context.into_json()) +} + +async fn create_logbook( + db: &SqlitePool, + data: Form, + user: &DonauLinzUser, + smtp_pw: &str, +) -> Flash { + match Logbook::create( + db, + data.into_inner(), + user, smtp_pw + ) + .await + { + Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")), + Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"), + Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)), + Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"), + Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), "Boot gibt's ned"), + Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), + Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")), + Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), "Ankunftszeit kann nicht vor der Abfahrtszeit sein"), + Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), "Schiffsführer darf dieses Boot nicht verwenden"), + Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(Redirect::to("/log"), "Steuerperson nicht in Liste der Ruderer!"), + Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"), + Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"), + Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"), + Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at)."), + Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."), + Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")), + Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."), + Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"), + } +} + +#[post("/", data = "", rank = 2)] +async fn create( + db: &State, + data: Form, + user: DonauLinzUser, + config: &State, +) -> Flash { + Log::create( + db, + format!("User {} tries to create log entry={:?}", &user.name, data), + ) + .await; + + create_logbook(db, data, &user, &config.smtp_pw).await +} + +#[post("/", data = "")] +async fn create_kiosk( + db: &State, + data: Form, + _kiosk: KioskCookie, + config: &State, +) -> Flash { + let Some(boat) = Boat::find_by_id(db, data.boat_id).await else { + return Flash::error(Redirect::to("/log"), "Boot gibt's nicht"); + }; + let creator = if boat.amount_seats == 1 && boat.owner.is_some() { + User::find_by_id(db, boat.owner.unwrap() as i32) + .await + .unwrap() + } else if let Some(shipmaster) = data.shipmaster { + User::find_by_id(db, shipmaster as i32).await.unwrap() + } else { + let Some(rower) = data.rowers.first() else { + return Flash::error( + Redirect::to("/log"), + "Ausfahrt ohne Benutzer kann nicht angelegt werden.", + ); + }; + User::find_by_id(db, *rower as i32).await.unwrap() + }; + Log::create( + db, + format!( + "Kiosk tries to create log for shipmaster {} entry={:?}", + creator.name, data + ), + ) + .await; + + create_logbook( + db, + data, + &DonauLinzUser::new(db, creator).await.unwrap(), + &config.smtp_pw, + ) + .await + //TODO: fixme +} + +#[post("/update", data = "")] +async fn update( + db: &State, + data: Form, + user: VorstandUser, +) -> Flash { + let data = data.into_inner(); + + let Some(logbook) = Logbook::find_by_id(db, data.id).await else { + return Flash::error(Redirect::to("/log"), format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id)); + }; + + match logbook.update(db, data.clone(), &user.user).await { + Ok(()) => { + Log::create( + db, + format!( + "User {} updated log entry={:?} to {:?}", + &user.name, logbook, data + ), + ) + .await; + + Flash::success( + Redirect::to("/log/show"), + "Logbucheintrag erfolgreich bearbeitet".to_string(), + ) + } + Err(LogbookAdminUpdateError::NotAllowed) => Flash::error( + Redirect::to("/log/show"), + "Du hast keine Erlaubnis, diesen Logbucheintrag zu bearbeiten!".to_string(), + ), + } +} + +async fn home_logbook( + db: &SqlitePool, + data: Form, + logbook_id: i64, + user: &DonauLinzUser, + smtp_pw: &str, +) -> Flash { + let logbook: Option = Logbook::find_by_id(db, logbook_id).await; + let Some(logbook) = logbook else { + return Flash::error( + Redirect::to("/admin/log"), + format!("Log with ID {} does not exist!", logbook_id), + ); + }; + + match logbook.home(db,user, data.into_inner(), smtp_pw).await { + Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), + Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), + Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at."), + Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")), + Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."), + Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"), + Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."), + Err(e) => Flash::error( + Redirect::to("/log"), + format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"), + ), + } +} + +#[post("/", data = "")] +async fn home_kiosk( + db: &State, + data: Form, + logbook_id: i64, + _kiosk: KioskCookie, + config: &State, +) -> Flash { + let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme + + Log::create( + db, + format!("Kiosk tries to finish log entry {logbook_id} {data:?}"), + ) + .await; + + home_logbook( + db, + data, + logbook_id, + &DonauLinzUser::new( + db, + User::find_by_id(db, logbook.shipmaster as i32) + .await + .unwrap(), + ) + .await + .unwrap(), + &config.smtp_pw, + ) + .await +} + +#[post("/", data = "", rank = 2)] +async fn home( + db: &State, + data: Form, + logbook_id: i64, + user: DonauLinzUser, + config: &State, +) -> Flash { + Log::create( + db, + format!( + "User {} tries to finish log entry {logbook_id} {data:?}", + &user.name + ), + ) + .await; + + home_logbook(db, data, logbook_id, &user, &config.smtp_pw).await +} + +#[get("//delete", rank = 2)] +async fn delete(db: &State, logbook_id: i64, user: DonauLinzUser) -> Flash { + let logbook = Logbook::find_by_id(db, logbook_id).await; + if let Some(logbook) = logbook { + let redirect = if logbook.arrival.is_some() { + "/log/show" + } else { + "/log" + }; + Log::create( + db, + format!("User {} tries to delete log entry {logbook_id}", &user.name), + ) + .await; + match logbook.delete(db, &user).await { + Ok(_) => Flash::success( + Redirect::to(redirect), + format!("Eintrag {} von {} gelöscht!", logbook_id, user.name), + ), + Err(LogbookDeleteError::NotYourEntry) => Flash::error( + Redirect::to(redirect), + "Du hast nicht die Berechtigung, den Eintrag zu löschen!", + ), + } + } else { + Flash::error( + Redirect::to("/log"), + format!("Logbook with ID {} could not be found!", logbook_id), + ) + } +} + +#[get("//delete")] +async fn delete_kiosk( + db: &State, + logbook_id: i64, + _kiosk: KioskCookie, +) -> Flash { + let logbook = Logbook::find_by_id(db, logbook_id).await; + if let Some(logbook) = logbook { + let cox = User::find_by_id(db, logbook.shipmaster as i32) + .await + .unwrap(); + Log::create(db, format!("Kiosk tries to delete log entry {logbook_id} ")).await; + match logbook.delete(db, &cox).await { + Ok(_) => Flash::success( + Redirect::to("/log"), + format!("Eintrag {} gelöscht!", logbook_id), + ), + Err(LogbookDeleteError::NotYourEntry) => Flash::error( + Redirect::to("/log"), + "Du hast nicht die Berechtigung, den Eintrag zu löschen!", + ), + } + } else { + Flash::error( + Redirect::to("/log"), + format!("Logbook with ID {} could not be found!", logbook_id), + ) + } +} + +pub fn routes() -> Vec { + routes![ + index, + create, + create_kiosk, + home, + kiosk, + home_kiosk, + new_kiosk, + show, + show_kiosk, + show_for_year, + delete, + delete_kiosk, + update + ] +} + +#[cfg(test)] +mod test { + use rocket::http::ContentType; + use rocket::{http::Status, local::asynchronous::Client}; + use sqlx::SqlitePool; + + use crate::model::logbook::Logbook; + use crate::tera::{log::Boat, User}; + use crate::testdb; + + #[sqlx::test] + fn test_kiosk_cookie() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/auth")); + + let req = client.get("/log/kiosk/ekrv2019/Linz"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let req = client.get("/log"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Neue Ausfahrt")); + + //assert!(!text.contains("Ottensheim Boot")); + } + + #[sqlx::test] + fn test_kiosk_cookie_boat() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log/kiosk/ekrv2019/Ottensheim"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let req = client.get("/log"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Neue Ausfahrt")); + + assert!(text.contains("Ottensheim Boot")); + } + + #[sqlx::test] + fn test_index() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/log"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Neue Ausfahrt")); + } + + #[sqlx::test] + fn test_show() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/log/show"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + println!("{text:?}"); + assert!(text.contains("Logbuch")); + assert!(text.contains("Joe")); + } + + #[sqlx::test] + fn test_show_kiosk() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log/kiosk/ekrv2019/Linz"); + let _ = req.dispatch().await; + + let req = client.get("/log/show"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + assert!(text.contains("Logbuch")); + assert!(text.contains("Joe")); + } + + #[sqlx::test] + fn test_create() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=admin&password=admin"); // Add the form data to the request body; + login.dispatch().await; + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + let req = client.post("/log").header(ContentType::Form).body(format!( + "boat_id=1&shipmaster=4&departure={0}T10:00&steering_person=4&rowers[]=4", + current_date + )); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt erfolgreich hinzugefügt" + ); + } + + #[sqlx::test] + fn test_home_kiosk() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log/kiosk/ekrv2019/Linz"); + let _ = req.dispatch().await; + + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + let req = client + .post("/log/1") + .header(ContentType::Form) + .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster=2&steering_person=2&departure={0}T10:00&arrival={0}T12:00&rowers[]=2", current_date)); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt korrekt eingetragen" + ); + } + + //Kiosk mode + // i see all boats + #[sqlx::test] + fn test_kiosks_sees_all_boats() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log/kiosk/ekrv2019/Linz"); + let _ = req.dispatch().await; + + let req = client.get("/log"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + //Sees all boats stationed in Linz + assert!(text.contains("Haichenbach")); + assert!(text.contains("Joe")); + assert!(text.contains("Kaputtes Boot :-(")); + assert!(text.contains("Sehr kaputtes Boot :-((")); + assert!(text.contains("second_private_boat_from_rower")); + assert!(text.contains("private_boat_from_rower")); + + //Doesn't see the one's in Ottensheim + //assert!(!text.contains("Ottensheim Boot")); + } + + #[sqlx::test] + fn test_kiosks_can_start_trips_with_all_boats() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + + let mut client = Client::tracked(rocket).await.unwrap(); + let req = client.get("/log/kiosk/ekrv2019/Linz"); + let _ = req.dispatch().await; + + can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "admin".into()).await; + can_start_and_end_trip(&db, &mut client, "Joe".into(), "admin".into()).await; + can_start_and_end_trip(&db, &mut client, "Kaputtes Boot :-(".into(), "admin".into()).await; + cant_start_trip( + &db, + &mut client, + "Sehr kaputtes Boot :-((".into(), + "admin".into(), + "Boot gesperrt".into(), + ) + .await; + can_start_and_end_trip( + &db, + &mut client, + "second_private_boat_from_rower".into(), + "rower".into(), + ) + .await; + } + + #[sqlx::test] + fn test_shipowner_can_allow_others_to_drive() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + // Owner can start trip: + let boat_id = Boat::find_by_name(&db, "private_boat_from_rower".into()) + .await + .unwrap() + .id; + let shipmaster_id = User::find_by_name(&db, "rower2".into()).await.unwrap().id; + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + let req = client.post("/log").header(ContentType::Form).body(format!( + "boat_id={boat_id}&shipmaster={shipmaster_id}&departure={0}T10:00&steering_person={shipmaster_id}&rowers[]={shipmaster_id}", current_date + )); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt erfolgreich hinzugefügt" + ); + + // Shipmaster can end it + let log_id = Logbook::highest_id(&db).await; + + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower2&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client + .post(format!("/log/{log_id}")) + .header(ContentType::Form) + .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster={shipmaster_id}&steering_person={shipmaster_id}&departure={0}T10:00&arrival={0}T12:00&rowers[]={shipmaster_id}", current_date)); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt korrekt eingetragen" + ); + } + + #[sqlx::test] + fn test_normal_user_sees_appropriate_boats() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let mut client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/log"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + + //Sees all 1x + assert!(text.contains("Haichenbach")); + can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "rower".into()).await; + + assert!(text.contains("private_boat_from_rower")); + can_start_and_end_trip( + &db, + &mut client, + "private_boat_from_rower".into(), + "rower".into(), + ) + .await; + + assert!(text.contains("second_private_boat_from_rower")); + can_start_and_end_trip( + &db, + &mut client, + "second_private_boat_from_rower".into(), + "rower".into(), + ) + .await; + + //Don't see anything else + assert!(!text.contains("Joe")); + cant_start_trip( + &db, + &mut client, + "Joe".into(), + "rower".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + + assert!(!text.contains("Kaputtes Boot :-(")); + cant_start_trip( + &db, + &mut client, + "Kaputtes Boot :-(".into(), + "rower".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + + assert!(!text.contains("Sehr kaputtes Boot :-((")); + cant_start_trip( + &db, + &mut client, + "Sehr kaputtes Boot :-((".into(), + "rower".into(), + "Boot gesperrt".into(), + ) + .await; + + assert!(!text.contains("Ottensheim Boot")); + cant_start_trip( + &db, + &mut client, + "Ottensheim Boot".into(), + "rower".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + } + + #[sqlx::test] + fn test_cox_sees_appropriate_boats() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let mut client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=cox&password=cox"); // Add the form data to the request body; + login.dispatch().await; + + sqlx::query("DELETE FROM logbook;") + .execute(&db) + .await + .unwrap(); + + let req = client.get("/log"); + let response = req.dispatch().await; + + let text = response.into_string().await.unwrap(); + + //Sees all 1x + assert!(text.contains("Haichenbach")); + can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "cox".into()).await; + + assert!(text.contains("Joe")); + can_start_and_end_trip(&db, &mut client, "Joe".into(), "cox".into()).await; + + assert!(text.contains("Kaputtes Boot :-(")); + can_start_and_end_trip(&db, &mut client, "Kaputtes Boot :-(".into(), "cox".into()).await; + + assert!(text.contains("Sehr kaputtes Boot :-((")); + cant_start_trip( + &db, + &mut client, + "Sehr kaputtes Boot :-((".into(), + "cox".into(), + "Boot gesperrt".into(), + ) + .await; + + assert!(text.contains("Ottensheim Boot")); + can_start_and_end_trip(&db, &mut client, "Ottensheim Boot".into(), "cox".into()).await; + + //Can't use private boats + assert!(!text.contains("private_boat_from_rower")); + cant_start_trip( + &db, + &mut client, + "private_boat_from_rower".into(), + "cox".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + + assert!(!text.contains("second_private_boat_from_rower")); + cant_start_trip( + &db, + &mut client, + "second_private_boat_from_rower".into(), + "cox".into(), + "Schiffsführer darf dieses Boot nicht verwenden".into(), + ) + .await; + } + + #[sqlx::test] + fn test_cant_end_trip_other_user() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower2&password=rower"); // Add the form data to the request body; + login.dispatch().await; + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + let req = client + .post("/log/1") + .header(ContentType::Form) + .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster=1&steering_person=1&departure={0}T10:00&arrival={0}T12:00&rowers[]=1", current_date)); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "5:errorEintrag 1 konnte nicht abgesendet werden (Fehler: NotYourEntry)!" + ); + } + + async fn can_start_and_end_trip( + db: &SqlitePool, + client: &mut Client, + boat_name: String, + shipmaster_name: String, + ) { + let boat_id = Boat::find_by_name(db, boat_name).await.unwrap().id; + let shipmaster_id = User::find_by_name(db, &shipmaster_name).await.unwrap().id; + + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + let req = client.post("/log").header(ContentType::Form).body(format!( + "boat_id={boat_id}&shipmaster={shipmaster_id}&departure={current_date}T10:00&steering_person={shipmaster_id}&rowers[]={shipmaster_id}" + )); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt erfolgreich hinzugefügt" + ); + + let log_id = Logbook::highest_id(db).await; + + let req = client + .post(format!("/log/{log_id}")) + .header(ContentType::Form) + .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster={shipmaster_id}&steering_person={shipmaster_id}&departure={current_date}T10:00&arrival={current_date}T12:00&rowers[]={shipmaster_id}")); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!( + flash_cookie.value(), + "7:successAusfahrt korrekt eingetragen" + ); + } + + async fn cant_start_trip( + db: &SqlitePool, + client: &mut Client, + boat_name: String, + shipmaster_name: String, + reason: String, + ) { + let boat_id = Boat::find_by_name(db, boat_name).await.unwrap().id; + let shipmaster_id = User::find_by_name(db, &shipmaster_name).await.unwrap().id; + + let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + + let req = client.post("/log").header(ContentType::Form).body(format!( + "boat_id={boat_id}&shipmaster={shipmaster_id}&departure={current_date}T10:00&steering_person={shipmaster_id}&rowers[]={shipmaster_id}" + )); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/log")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), format!("5:error{}", reason)); + } +} diff --git a/src/tera/misc.rs b/src/tera/misc.rs index 271bf96..1c79971 100644 --- a/src/tera/misc.rs +++ b/src/tera/misc.rs @@ -38,7 +38,7 @@ async fn cal_registered( return Err("Invalid".into()); }; - if &user.user_token != uuid { + if user.user_token != uuid { return Err("Invalid".into()); } diff --git a/src/tera/notification.rs b/src/tera/notification.rs index bf4145a..4401878 100644 --- a/src/tera/notification.rs +++ b/src/tera/notification.rs @@ -30,6 +30,12 @@ async fn mark_read(db: &State, user: User, notification_id: i64) -> } } -pub fn routes() -> Vec { - routes![mark_read] +#[get("/read/all")] +async fn mark_all_read(db: &State, user: User) -> Flash { + Notification::mark_all_read(db, &user).await; + Flash::success(Redirect::to("/"), "Alle Nachrichten als gelesen markiert") +} + +pub fn routes() -> Vec { + routes![mark_read, mark_all_read] } diff --git a/src/tera/stat.rs b/src/tera/stat.rs new file mode 100644 index 0000000..3665b4c --- /dev/null +++ b/src/tera/stat.rs @@ -0,0 +1,91 @@ +use rocket::{get, routes, Route, State}; +use rocket_dyn_templates::{context, Template}; +use sqlx::SqlitePool; + +use crate::model::{ + stat::{self, BoatStat, Stat}, + user::{DonauLinzUser, UserWithDetails}, +}; + +use super::log::KioskCookie; + +#[get("/boats", rank = 2)] +async fn index_boat(db: &State, user: DonauLinzUser) -> Template { + let stat = BoatStat::get(db).await; + let kiosk = false; + + Template::render( + "stat.boats", + context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, kiosk), + ) +} + +#[get("/boats")] +async fn index_boat_kiosk(db: &State, _kiosk: KioskCookie) -> Template { + let stat = BoatStat::get(db).await; + let kiosk = true; + + Template::render("stat.boats", context!(stat, kiosk, show_kiosk_header: true)) +} + +#[get("/?", rank = 2)] +async fn index(db: &State, user: DonauLinzUser, year: Option) -> Template { + let stat = Stat::people(db, year).await; + let club_km = Stat::sum_people(db, year).await; + let club_trips = Stat::trips_people(db, year).await; + let guest_km = Stat::guest(db, year).await; + let personal = stat::get_personal(db, &user).await; + let kiosk = false; + + Template::render( + "stat.people", + context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km, club_trips), + ) +} + +#[get("/?")] +async fn index_kiosk(db: &State, _kiosk: KioskCookie, year: Option) -> Template { + let stat = Stat::people(db, year).await; + let club_km = Stat::sum_people(db, year).await; + let club_trips = Stat::trips_people(db, year).await; + let guest_km = Stat::guest(db, year).await; + let kiosk = true; + + Template::render( + "stat.people", + context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km, club_trips), + ) +} + +pub fn routes() -> Vec { + routes![index, index_kiosk, index_boat, index_boat_kiosk] +} + +#[cfg(test)] +mod test { + use rocket::{http::Status, local::asynchronous::Client}; + use sqlx::SqlitePool; + + use crate::testdb; + + #[sqlx::test] + fn test_kiosk_stat() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + // "Log in" + let req = client.get("/log/kiosk/ekrv2019/Linz"); + let _ = req.dispatch().await; + + // `/stat` should be viewable + let req = client.get("/stat"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::Ok); + let text = response.into_string().await.unwrap(); + assert!(text.contains("Statistik")); + } +} diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 4787fa2..3129483 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -4,9 +4,12 @@

Mitglieder

{% if allowed_to_edit %} -
+ Neue Person hinzufügen + + class="flex mt-4 rounded-md sm:flex items-end justify-between">

Neues Mitglied hinzufügen

@@ -19,15 +22,17 @@
-
+
+ + {% endif %} -
+
- {% set aisle = aisle_name ~ "-aisle" %} - {% set place = boathouse[aisle][side_name] %} + {% set place = boathouse[aisle_name][side_name].boats %} {% if place[level] %} - {{ place[level].1.name }} + {{ place[level].boat.name }} {% if "admin" in loggedin_user.roles %} X + href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X {% endif %} {% elif boats | length > 0 %} {% if "admin" in loggedin_user.roles %} diff --git a/templates/ergo/index.html.tera b/templates/ergo/index.html.tera new file mode 100644 index 0000000..e13a86b --- /dev/null +++ b/templates/ergo/index.html.tera @@ -0,0 +1,241 @@ +{% import "includes/macros" as macros %} +{% extends "base" %} +{% block content %} +
+

Ergo Challenges

+
+ +
+

+ Neuer Eintrag +

+
+ Dirty Thirty +
+
+
+ + +
+ {{ macros::input(label="Distanz [m]", name="result", required=true, type="number", class="input rounded-md") }} +
+ + +
+
+ +
+
+
+
+
+ Dirty Dozen +
+
+
+ + +
+ {{ macros::input(label="Zeit [hh:mm:ss.s] oder Distanz [m]", name="result", required=true, type="text", class="input rounded-md", pattern="(?:\d+:\d{2}:\d{2}\.\d+|\d{1,2}:\d{2}\.\d+|\d+(\.\d+)?)") }} +
+ + +
+
+ +
+
+
+
+
+
+

Aktuelle Woche

+
+ + Dirty Thirty ({{ thirty | length }}) + +
+
    + {% for stat in thirty %} +
  1. + {{ stat.name }}: {{ stat.result }} +
  2. + {% endfor %} +
+
+
+
+ + Dirty Dozen ({{ dozen | length }}) + +
+
    + {% for stat in dozen %} +
  1. + {{ stat.name }}: {{ stat.result }} +
  2. + {% endfor %} +
+
+
+
+ {% if "admin" in loggedin_user.roles %} +
+

Update

+
+ + Dirty Thirty ({{ thirty | length }}) + +
+
    + {% for stat in thirty %} +
  1. +
    + {{ stat.name }}: + + +
    +
  2. + {% endfor %} +
+
+
+
+ + Dirty Dozen ({{ dozen | length }}) + +
+
    + {% for stat in dozen %} +
  1. +
    + {{ stat.name }}: + + +
    +
  2. + {% endfor %} +
+
+
+
+
+ {% endif %} +
+
+ {% endblock content %} diff --git a/templates/log.completed.html.tera b/templates/log.completed.html.tera new file mode 100644 index 0000000..622a34b --- /dev/null +++ b/templates/log.completed.html.tera @@ -0,0 +1,65 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/log" as log %} +{% extends "base" %} +{% block content %} +
+

+ Logbuch + {% if loggedin_user and "Vorstand" in loggedin_user.roles %} + + {% endif %} +

+
+
+ + +
+
+ {% for log in logs %} + {% set_global allowed_to_edit = false %} + {% if loggedin_user %} + {% if "Vorstand" in loggedin_user.roles %} + {% set_global allowed_to_edit = true %} + {% endif %} + {% endif %} + {{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=allowed_to_edit) }} + {% endfor %} +
+
+ +{% endblock content %} diff --git a/templates/planned.html.tera b/templates/planned.html.tera new file mode 100644 index 0000000..4310925 --- /dev/null +++ b/templates/planned.html.tera @@ -0,0 +1,490 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/log" as log %} +{% extends "base" %} +{% block content %} +
+ {% if "paid" not in loggedin_user.roles and "Donau Linz" in loggedin_user.roles %} +
+ +
+ {% endif %} +

Ausfahrten

+ {% include "includes/buttons" %} + {% for day in days %} + {% set amount_trips = day.events | length + day.trips | length %} + {% set_global day_cox_needed = false %} + {% if day.events | length > 0 %} + {% for event in day.events %} + {% if event.cox_needed %} + {% set_global day_cox_needed = true %} + {% endif %} + {% endfor %} + {% endif %} +
+
+

+ {{ day.day| date(format="%d.%m.%Y") }} + {{ day.day | date(format="%A", locale="de_AT") }} + {% if day.max_waterlevel %} + • 🌊{{ day.max_waterlevel.avg }} ± {{ day.max_waterlevel.fluctuation }} cm + {% endif %} + + {% if day.weather %} + + Temp: {{ day.weather.max_temp | round }}° • Windböe: {{ day.weather.wind_gust | round }} km/h • Regen: {{ day.weather.rain_mm | round }} mm + + {% endif %} +

+ {% if day.events | length > 0 or day.trips | length > 0 or day.boat_reservations | length > 0 %} +
+ {# --- START Boatreservations--- #} + {% for _, reservations_for_event in day.boat_reservations %} + {% set reservation = reservations_for_event[0] %} +
+
+
+ + ⏳ {{ reservation.time_desc }} ({{ reservation.user_applicant.name }})
+ + {% for reservation in reservations_for_event -%} + {{ reservation.boat.name }} + {%- if not loop.last %} + {% endif -%} + {% endfor -%} + +
+ (Reservierung - {{ reservation.usage}}) +
+
+
+ {% endfor %} + {# --- END Boatreservations--- #} + {# --- START Events --- #} + {% if day.events | length > 0 %} + {% for event in day.events | sort(attribute="planned_starting_time") %} + {% set amount_cur_cox = event.cox | length %} + {% set amount_cox_missing = event.planned_amount_cox - amount_cur_cox %} +
+
+
+ {% if event.always_show and not day.regular_sees_this_day %} + 🔮 + {% endif -%} + {%- if event.max_people == 0 %} + ⚠ Absage + {{ event.planned_starting_time }} + Uhr + + ({{ event.name }} + {%- if event.trip_type %} + - {{ event.trip_type.icon | safe }} {{ event.trip_type.name }} + {%- endif -%} + ) + {% else %} + + {{ event.planned_starting_time }} + Uhr + + ({{ event.name }} + {%- if event.trip_type %} + - {{ event.trip_type.icon | safe }} {{ event.trip_type.name }} + {%- endif -%} + ) + {% endif %} +
+ + Details + +
+
+ {# --- START Row Buttons --- #} + {% set_global cur_user_participates = false %} + {% for rower in event.rower %} + {% if rower.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + Abmelden + {% endif %} + {% if event.max_people > event.rower | length and cur_user_participates == false %} + Mitrudern + {% endif %} + {# --- END Row Buttons --- #} + {# --- START Cox Buttons --- #} + {% if loggedin_user.allowed_to_steer %} + {% set_global cur_user_participates = false %} + {% for cox in event.cox %} + {% if cox.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + + {% include "includes/cox-icon" %} + Abmelden + + {% elif event.planned_amount_cox > 0 %} + + {% include "includes/cox-icon" %} + Steuern + + {% endif %} + {% endif %} + {# --- END Cox Buttons --- #} +
+
+ {# --- START Sidebar Content --- #} + + {# --- END Sidebar Content --- #} +
+ {% endfor %} + {% endif %} + {# --- END Events --- #} + {# --- START Trips --- #} + {% if day.trips | length > 0 %} + {% for trip in day.trips | sort(attribute="planned_starting_time") %} +
+
+
+ {% if trip.always_show and not day.regular_sees_this_day %} + 🔮 + {% endif -%} + {% if trip.max_people == 0 %} + ⚠ + {{ trip.planned_starting_time }} + Uhr + (Absage + {{ trip.cox_name -}} + {% if trip.trip_type %} + - + {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }} + {%- endif -%} + ) + {% else %} + {{ trip.planned_starting_time }} + Uhr + ({{ trip.cox_name -}} + {% if trip.trip_type %} + - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }} + {%- endif -%} + ) + {% endif %} +
+ + Details + +
+
+ {% set_global cur_user_participates = false %} + {% for rower in trip.rower %} + {% if rower.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + Abmelden + {% endif %} + {% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false %} + Mitrudern + {% endif %} +
+
+ {# --- START Sidebar Content --- #} + + {# --- END Sidebar Content --- #} +
+ {% endfor %} + {% endif %} + {# --- END Trips --- #} +
+ {% endif %} +
+ {# --- START Add Buttons --- #} + {% if "manage_events" in loggedin_user.roles or loggedin_user.allowed_to_steer or "ergo" in loggedin_user.roles %} +
+ {% if "manage_events" in loggedin_user.roles %} + + {% include "includes/plus-icon" %} + Event + + {% endif %} + {% if loggedin_user.allowed_to_steer or "ergo" in loggedin_user.roles %} + + {% include "includes/plus-icon" %} + {% if not loggedin_user.allowed_to_steer %}Ergo-Session + {%- else -%} + Ausfahrt{%endif%} + + {% endif %} +
+ {% endif %} + {# --- END Add Buttons --- #} +
+{% endfor %} +
+
+{% if loggedin_user.allowed_to_steer or "ergo" in loggedin_user.roles %} + {% include "forms/trip" %} +{% endif %} +{% if "manage_events" in loggedin_user.roles %} + {% include "forms/event" %} +{% endif %} +{% endblock content %} diff --git a/templates/stat.people.html.tera b/templates/stat.people.html.tera new file mode 100644 index 0000000..ce947a7 --- /dev/null +++ b/templates/stat.people.html.tera @@ -0,0 +1,103 @@ +{% import "includes/macros" as macros %} +{% extends "base" %} +{% block content %} +
+

+ Statistik + +

+
+ + +
+
+
+
+ # + Name + km + Fahrten +
+ {% set_global km = 0 %} {% set_global km = 0 %} {% set_global index = 1 %} + {% for s in stat %} +
+ + {% if km != s.rowed_km %} + {{ loop.index }} + {% set_global index = loop.index %} + {% else %} + {{ index }} + {% endif %} + + {{ s.name }} + {{ s.rowed_km }} + {{ s.amount_trips }} + {% set_global km = s.rowed_km %} +
+ {% endfor %} +
+ + Summe Vereinsmitglieder + {{ club_km }} + {{ club_trips }} +
+
+ + Summe {{ guest_km.name }} + {{ guest_km.rowed_km }} + {{ guest_km.amount_trips }} +
+
+ + Gesamtsumme + {{ club_km + guest_km.rowed_km }} + {{ guest_km.amount_trips + club_trips }} +
+
+
+ +{% endblock content %}