Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e0e47d72b | ||
|
|
287598bb79 | ||
|
|
fff1854067 | ||
|
|
6d6862a680 | ||
|
|
61b0c8b83b | ||
|
|
e9a42aa0fa | ||
|
|
5b44bfb914 | ||
|
|
8282ba4302 | ||
|
|
3cc67cb9e6 | ||
|
|
26b984904b | ||
|
|
4f1d1c5b1c | ||
|
|
39178d05a5 | ||
|
|
4b398c8445 | ||
|
|
6294b114df | ||
|
|
48f711a84d | ||
|
|
21c0df0ded | ||
|
|
eae8d2a26a | ||
|
|
07c1679e1a | ||
|
|
2b255ce968 | ||
|
|
5cda2f8070 | ||
|
|
f8d399f9ae | ||
|
|
c5b11481b3 | ||
|
|
3cf5e6342b | ||
|
|
93a4b54d42 | ||
|
|
c6386e54fc | ||
|
|
469f192feb | ||
|
|
3baee8cac7 | ||
|
|
17e339ea00 | ||
|
|
152c01124b | ||
|
|
f8502ba781 | ||
|
|
23c0029317 | ||
|
|
8a44513a04 | ||
|
|
2db09af903 | ||
|
|
467f8c9020 | ||
|
|
909f33b044 | ||
|
|
44450f407c | ||
|
|
70a86f4545 | ||
|
|
e6409cc423 | ||
|
|
4e3161a05f | ||
|
|
51dfa013df | ||
|
|
1d41b6022e | ||
|
|
f9d5b25436 | ||
|
|
891e324186 | ||
|
|
b502c2f96d | ||
|
|
681da8c326 | ||
|
|
05b865c608 | ||
|
|
628b607074 | ||
|
|
97be249826 | ||
|
|
e1bd472a36 | ||
|
|
2e134fb693 | ||
|
|
db99f39716 | ||
|
|
dd19631e33 | ||
|
|
a0b206a554 | ||
|
|
e9b0452d38 |
No files matched your search
Generated
+202
@@ -2,6 +2,23 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -739,6 +756,15 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
|
||||||
|
dependencies = [
|
||||||
|
"libbz2-rs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "camino"
|
name = "camino"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -789,6 +815,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
@@ -895,6 +931,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
|
checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -1173,6 +1215,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deflate64"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -1359,6 +1407,16 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"miniz_oxide",
|
||||||
|
"zlib-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -2237,6 +2295,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "interpolator"
|
name = "interpolator"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2683,6 +2750,12 @@ dependencies = [
|
|||||||
"tachys",
|
"tachys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libbz2-rs-sys"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.182"
|
version = "0.2.182"
|
||||||
@@ -2725,6 +2798,15 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzma-rust2"
|
||||||
|
version = "0.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "macros"
|
name = "macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2795,6 +2877,16 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -2958,6 +3050,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbkdf2"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "3.0.6"
|
version = "3.0.6"
|
||||||
@@ -3016,6 +3118,12 @@ dependencies = [
|
|||||||
"spki",
|
"spki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -3031,6 +3139,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppmd-rust"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -3621,6 +3735,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"csv",
|
"csv",
|
||||||
|
"futures",
|
||||||
"icondata",
|
"icondata",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"jsonwebtoken 10.3.0",
|
"jsonwebtoken 10.3.0",
|
||||||
@@ -3640,6 +3755,7 @@ dependencies = [
|
|||||||
"traits",
|
"traits",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3931,6 +4047,12 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simple_asn1"
|
name = "simple_asn1"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -4177,6 +4299,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"js-sys",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -4464,6 +4587,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-path"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
@@ -5311,8 +5440,81 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "8.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcab981e19633ebcf0b001ddd37dd802996098bc1864f90b7c5d970ce76c1d59"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"bzip2",
|
||||||
|
"constant_time_eq",
|
||||||
|
"crc32fast",
|
||||||
|
"deflate64",
|
||||||
|
"flate2",
|
||||||
|
"getrandom 0.4.1",
|
||||||
|
"hmac",
|
||||||
|
"indexmap",
|
||||||
|
"lzma-rust2",
|
||||||
|
"memchr",
|
||||||
|
"pbkdf2",
|
||||||
|
"ppmd-rust",
|
||||||
|
"sha1",
|
||||||
|
"time",
|
||||||
|
"typed-path",
|
||||||
|
"zeroize",
|
||||||
|
"zopfli",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "7.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.16+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
+4
-1
@@ -25,7 +25,6 @@ leptos_oidc = "0.9.0"
|
|||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
leptos_animate = "0.1"
|
leptos_animate = "0.1"
|
||||||
|
|
||||||
macros = { path = "./macros" }
|
macros = { path = "./macros" }
|
||||||
traits = { path = "./traits" }
|
traits = { path = "./traits" }
|
||||||
icondata = { version = "0.7.0", features = ["font-awesome"] }
|
icondata = { version = "0.7.0", features = ["font-awesome"] }
|
||||||
@@ -35,6 +34,8 @@ csv = "1.4.0"
|
|||||||
leptos-use = { version = "0.18.3", features = ["use_drop_zone"] }
|
leptos-use = { version = "0.18.3", features = ["use_drop_zone"] }
|
||||||
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"], optional = true }
|
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"], optional = true }
|
||||||
reqwest = { version = "0.13.2", features = ["json"] }
|
reqwest = { version = "0.13.2", features = ["json"] }
|
||||||
|
zip = { version = "8.5.1", optional = true }
|
||||||
|
futures = { version = "0.3.32", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["ssr"]
|
default = ["ssr"]
|
||||||
@@ -52,6 +53,8 @@ ssr = [
|
|||||||
"dep:aws-config",
|
"dep:aws-config",
|
||||||
"dep:serde_dynamo",
|
"dep:serde_dynamo",
|
||||||
"dep:jsonwebtoken",
|
"dep:jsonwebtoken",
|
||||||
|
"dep:zip",
|
||||||
|
"dep:futures",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
|
|||||||
+6
-2
@@ -27,14 +27,18 @@ RUN cargo binstall fnm -y \
|
|||||||
|
|
||||||
# Start second phase of build - install ca-certs and copy all build outputs from previous step into a
|
# Start second phase of build - install ca-certs and copy all build outputs from previous step into a
|
||||||
# Debian image in the /app directory.
|
# Debian image in the /app directory.
|
||||||
FROM debian:trixie-slim AS runtime
|
FROM rust:1.95-slim-trixie AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt-get install -y --no-install-recommends openssl ca-certificates \
|
&& apt-get install -y --no-install-recommends openssl ca-certificates curl \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& apt-get clean -y \
|
&& apt-get clean -y \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Cargo, then binstall, then typst installation.
|
||||||
|
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||||
|
RUN cargo binstall typst-cli -y
|
||||||
|
|
||||||
# Remember to replace project_name with the actual project name
|
# Remember to replace project_name with the actual project name
|
||||||
COPY --from=builder /app/target/release/scholarships-rs /app/
|
COPY --from=builder /app/target/release/scholarships-rs /app/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#set page(margin: 40pt)
|
||||||
|
#set block(breakable: false)
|
||||||
|
|
||||||
|
#let map_block(header, val) = {
|
||||||
|
block(
|
||||||
|
width: 100%,
|
||||||
|
fill: rgb(200, 0, 0),
|
||||||
|
radius: 4pt,
|
||||||
|
below: 0pt,
|
||||||
|
above: 4pt,
|
||||||
|
inset: 2pt,
|
||||||
|
[
|
||||||
|
#show heading: set text(white)
|
||||||
|
#show heading: set text(size: 15pt)
|
||||||
|
#stack(dir: ttb,
|
||||||
|
block(
|
||||||
|
inset: 7pt,
|
||||||
|
align(horizon)[= #header]
|
||||||
|
),
|
||||||
|
align(center)[#block(
|
||||||
|
width: 100%,
|
||||||
|
fill: white,
|
||||||
|
inset: 8pt,
|
||||||
|
radius: 3pt,
|
||||||
|
align(left)[#val]
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let info_line(subject, key, dict: student) = [
|
||||||
|
|
||||||
|
*#subject* #dict.at(key, default: "N/A")
|
||||||
|
]
|
||||||
|
|
||||||
|
#let info_line_list(subject, key, dict: student) = [
|
||||||
|
|
||||||
|
*#subject* #dict.at(key, default: "N/A").join(", ")
|
||||||
|
]
|
||||||
|
|
||||||
|
#show heading: set align(center)
|
||||||
|
#show heading: set text(size: 20pt)
|
||||||
|
#v(10cm)
|
||||||
|
= #student.first_name #student.last_name
|
||||||
|
#show heading: set text(size: 17pt)
|
||||||
|
== Region 15 General Scholarship Application
|
||||||
|
#pagebreak()
|
||||||
|
|
||||||
|
#show heading: set block(below: 20pt, above: 30pt)
|
||||||
|
#show heading: set text(size: 20pt)
|
||||||
|
= Student Demographic Information
|
||||||
|
|
||||||
|
#info_line("First Name:", "first_name")
|
||||||
|
#info_line("Last Name:", "last_name")
|
||||||
|
#info_line("Date of Birth:", "dob")
|
||||||
|
#info_line("Gender:", "gender")
|
||||||
|
#info_line("Phone Number:", "phone_number")
|
||||||
|
#info_line("Email:","email")
|
||||||
|
#info_line("Street Address:", "street_address")
|
||||||
|
#info_line("Town:", "town")
|
||||||
|
|
||||||
|
= Academic Information
|
||||||
|
|
||||||
|
#info_line("Unweighted GPA:", "unweighted_gpa")
|
||||||
|
#info_line("Weighted GPA:", "weighted_gpa")
|
||||||
|
#info_line("Highest SAT Score:", "sat_score")
|
||||||
|
#info_line("Highest ACT Score:", "act_score")
|
||||||
|
#info_line("Academic Honors:", "academic_honors")
|
||||||
|
|
||||||
|
= Sports Participation
|
||||||
|
#if student.at("sports_participation", default: ()).len() == 0 [
|
||||||
|
Student did not enter any sports participation.
|
||||||
|
] else {
|
||||||
|
for pair in student.sports_participation {
|
||||||
|
map_block(pair.at("sport_name"), [
|
||||||
|
#info_line("Special Achievements:", "achievements", dict: pair)
|
||||||
|
#info_line_list("Grades Participated:", "grades", dict: pair)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
= Extracurricular Activities
|
||||||
|
#if student.at("extracurricular", default: ()).len() == 0 [
|
||||||
|
Student did not enter any extracurricular activities.
|
||||||
|
] else {
|
||||||
|
for pair in student.extracurricular {
|
||||||
|
map_block(pair.at("activity_name"), [
|
||||||
|
#info_line("Number of hours per week:", "num_hours", dict: pair)
|
||||||
|
#info_line("Number of weeks participated:", "num_weeks", dict: pair)
|
||||||
|
#info_line("Special Involvement:", "special_involvement", dict: pair)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
= Work Experience
|
||||||
|
#if student.at("work_experience", default: ()).len() == 0 [
|
||||||
|
Student did not enter any work experience.
|
||||||
|
] else {
|
||||||
|
for pair in student.work_experience {
|
||||||
|
map_block(pair.at("employer"), [
|
||||||
|
#info_line("Job Title:", "job_title", dict: pair)
|
||||||
|
#info_line("Start Date:", "start_date", dict: pair)
|
||||||
|
#info_line("End Date:", "end_date", dict: pair)
|
||||||
|
#info_line("Number of hours per week:", "num_hours", dict: pair)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
= University Information
|
||||||
|
|
||||||
|
#info_line("University Name:", "college_name")
|
||||||
|
#info_line("University Street Address:", "college_city")
|
||||||
|
#info_line("University State:", "college_state")
|
||||||
|
#info_line("University ZIP:", "college_zip")
|
||||||
|
#info_line("Has the student received an acceptance:", "college_acceptance")
|
||||||
|
#info_line("Student's chosen major:", "major")
|
||||||
|
#info_line("Student's intended career:", "intended_career")
|
||||||
|
|
||||||
|
= Family Information
|
||||||
|
|
||||||
|
#info_line("Total number of children in student's family:", "num_children")
|
||||||
|
#info_line("Total number of children currently attending college:", "num_children_college")
|
||||||
|
#info_line("Parent/Guardian 1 Name:", "parent_one_name")
|
||||||
|
#info_line("Parent/Guardian 1 Relationship:", "parent_one_relationship")
|
||||||
|
#info_line("Parent/Guardian 1 Occupation:", "parent_one_occupation")
|
||||||
|
#info_line("Parent/Guardian 1 Employer:", "parent_one_employer")
|
||||||
|
#info_line("Parent/Guardian 2 Name:", "parent_two_name")
|
||||||
|
#info_line("Parent/Guardian 2 Relationship:", "parent_two_relationship")
|
||||||
|
#info_line("Parent/Guardian 2 Occupation:", "parent_two_occupation")
|
||||||
|
#info_line("Parent/Guardian 2 Employer:", "parent_two_employer")
|
||||||
|
|
||||||
|
= Extra Eligibility Information
|
||||||
|
|
||||||
|
#info_line("Did the student attend BAS:", "attend_bas")
|
||||||
|
#info_line("Is the student a member of Midd-South Catholic Church:", "middsouth_church")
|
||||||
|
#info_line("Does the student have a family member who has served in the US Military:", "family_military_service")
|
||||||
|
#info_line("Has the student participated in Pomperaug Youth Baseball:", "youth_baseball")
|
||||||
|
#info_line("Has the student participated in the Panthers Aquatic Club:", "aquatic_club")
|
||||||
|
#info_line("Does the student have a family member in the Region 15 PEA:", "pea_member")
|
||||||
|
#info_line("Has the student participated in the PHS Music Program:", "music_program")
|
||||||
+21
-27
@@ -1,11 +1,7 @@
|
|||||||
use crate::components::ToastList;
|
use crate::components::ToastList;
|
||||||
use crate::components::login::ProviderLoginContext;
|
use crate::components::login::ProviderLoginContext;
|
||||||
use crate::pages::student;
|
use crate::pages::{AdminApplicantsPageFallback, AdminApplicantsPageShell, AdminApplicantsStudentList, AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage, ApplicantsPageFallback, ApplicantsStudentList, AuthCallbackPage, LoanerBorrowForm, LoanerFallback, LoanerReturnForm, LoanerShell, ProviderContactPage, ProviderPortal, ScholarshipInfoPage};
|
||||||
use crate::pages::{
|
use crate::pages::{ApplicantsPageShell, student};
|
||||||
AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage,
|
|
||||||
ApplicantsPageFallback, AuthCallbackPage, LoanerBorrowForm, LoanerFallback, LoanerReturnForm,
|
|
||||||
LoanerShell, ProviderContactPage, ProviderPortal, ScholarshipInfoPage,
|
|
||||||
};
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
||||||
use leptos_oidc::AuthSignal;
|
use leptos_oidc::AuthSignal;
|
||||||
@@ -86,7 +82,10 @@ fn ProviderRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
view! {
|
view! {
|
||||||
<ParentRoute path=path!("/providers") view=ProviderLoginContext>
|
<ParentRoute path=path!("/providers") view=ProviderLoginContext>
|
||||||
<Route path=path!("") view=ProviderPortal />
|
<Route path=path!("") view=ProviderPortal />
|
||||||
<Route path=path!("/applicants") view=ApplicantsPageFallback />
|
<ParentRoute path=path!("/applicants") view=ApplicantsPageShell>
|
||||||
|
<Route path=path!("") view=ApplicantsPageFallback />
|
||||||
|
<Route path=path!("/:scholarship_id") view=ApplicantsStudentList />
|
||||||
|
</ParentRoute>
|
||||||
<Route path=path!("/callback") view=AuthCallbackPage />
|
<Route path=path!("/callback") view=AuthCallbackPage />
|
||||||
<Route path=path!("/profile") view=ProviderContactPage />
|
<Route path=path!("/profile") view=ProviderContactPage />
|
||||||
// <Route path=path!("/scholarships") view=FormClosedPage />
|
// <Route path=path!("/scholarships") view=FormClosedPage />
|
||||||
@@ -122,6 +121,10 @@ fn AdminRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
<Route path=path!("providers") view=AdminProviderPage />
|
<Route path=path!("providers") view=AdminProviderPage />
|
||||||
<Route path=path!("scholarships") view=AdminScholarshipPage />
|
<Route path=path!("scholarships") view=AdminScholarshipPage />
|
||||||
<Route path=path!("utilities") view=AdminUtilsPage />
|
<Route path=path!("utilities") view=AdminUtilsPage />
|
||||||
|
<ParentRoute path=path!("applicants") view=AdminApplicantsPageShell>
|
||||||
|
<Route path=path!("") view=AdminApplicantsPageFallback />
|
||||||
|
<Route path=path!("/:scholarship_id") view=AdminApplicantsStudentList />
|
||||||
|
</ParentRoute>
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
}
|
}
|
||||||
.into_inner()
|
.into_inner()
|
||||||
@@ -134,26 +137,17 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
<ParentRoute path=path!("students") view=student::StudentShell>
|
<ParentRoute path=path!("students") view=student::StudentShell>
|
||||||
<Route path=path!("callback") view=AuthCallbackPage />
|
<Route path=path!("callback") view=AuthCallbackPage />
|
||||||
<Route path=path!("home") view=student::StudentHomePage />
|
<Route path=path!("home") view=student::StudentHomePage />
|
||||||
<Route path=path!("demographics") view=student::StudentDemographicsPage />
|
<Route path=path!("demographics") view=student::ClosedForm />
|
||||||
<Route path=path!("eligibility") view=student::StudentEligibilityPage />
|
<Route path=path!("eligibility") view=student::ClosedForm />
|
||||||
<Route path=path!("additional") view=student::AdditionalPage />
|
<Route path=path!("additional") view=student::ClosedForm />
|
||||||
<Route path=path!("additional/academics") view=student::StudentAcademicsPage />
|
<Route path=path!("additional/academics") view=student::ClosedForm />
|
||||||
<Route path=path!("additional/athletics") view=student::StudentAthleticsPage />
|
<Route path=path!("additional/athletics") view=student::ClosedForm />
|
||||||
<Route
|
<Route path=path!("additional/extracurriculars") view=student::ClosedForm />
|
||||||
path=path!("additional/extracurriculars")
|
<Route path=path!("additional/work-experience") view=student::ClosedForm />
|
||||||
view=student::StudentExtracurricularsPage
|
<Route path=path!("additional/university") view=student::ClosedForm />
|
||||||
/>
|
<Route path=path!("additional/family-info") view=student::ClosedForm />
|
||||||
<Route
|
<Route path=path!("additional/financials") view=student::ClosedForm />
|
||||||
path=path!("additional/work-experience")
|
<Route path=path!("additional/specifics") view=student::ClosedForm />
|
||||||
view=student::StudentWorkExperiencePage
|
|
||||||
/>
|
|
||||||
<Route path=path!("additional/university") view=student::StudentUniversityPage />
|
|
||||||
<Route path=path!("additional/family-info") view=student::StudentFamilyPage />
|
|
||||||
<Route path=path!("additional/financials") view=student::StudentFinancialPage />
|
|
||||||
<Route
|
|
||||||
path=path!("additional/specifics")
|
|
||||||
view=student::StudentSpecificRequirementsPage
|
|
||||||
/>
|
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
}
|
}
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use std::collections::HashMap;
|
|||||||
/// now, the table will simply create a single massive `Map` field that contains all the stored
|
/// now, the table will simply create a single massive `Map` field that contains all the stored
|
||||||
/// information in the `HashMap`. We'd prefer to be able to get single values if possible,
|
/// information in the `HashMap`. We'd prefer to be able to get single values if possible,
|
||||||
/// especially for interoperability with other applications, should they be created.
|
/// especially for interoperability with other applications, should they be created.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Reactive)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Reactive)]
|
||||||
pub struct ExpandableInfo {
|
pub struct ExpandableInfo {
|
||||||
/// The main key member. Required and used to get/put the information in a database.
|
/// The main key member. Required and used to get/put the information in a database.
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
|
|||||||
+170
-15
@@ -1,13 +1,35 @@
|
|||||||
use std::collections::HashMap;
|
use indexmap::IndexMap;
|
||||||
use indexmap::IndexMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||||
pub enum SchemaType {
|
pub enum SchemaType {
|
||||||
|
/// Indicates a `ValueType::String` value.
|
||||||
|
#[default]
|
||||||
String,
|
String,
|
||||||
|
/// Indicates a `ValueType::Number` value.
|
||||||
Number,
|
Number,
|
||||||
List,
|
/// Indicates a `ValueType::List` containing only strings or numbers.
|
||||||
Map
|
PrimitiveList,
|
||||||
|
/// Indicates a `ValueType::List` containing only maps.
|
||||||
|
MapList,
|
||||||
|
/// Indicates a `ValueType::Map`.
|
||||||
|
Map,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||||
|
pub enum SchemaContainerStyle {
|
||||||
|
#[default]
|
||||||
|
Header,
|
||||||
|
Capsule,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||||
|
pub enum SchemaHeaderStyle {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
MainHeader,
|
||||||
|
SubsectionHeader,
|
||||||
|
Bold,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Schema Node
|
/// # Schema Node
|
||||||
@@ -55,12 +77,16 @@ pub enum SchemaType {
|
|||||||
/// This schema does not currently support display of a single nested member, like a single map
|
/// This schema does not currently support display of a single nested member, like a single map
|
||||||
/// within a list. This means that you will have to display any listed or mapped information as a
|
/// within a list. This means that you will have to display any listed or mapped information as a
|
||||||
/// list. Consider this when designing the data.
|
/// list. Consider this when designing the data.
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||||
pub struct SchemaNode {
|
pub struct SchemaNode {
|
||||||
|
/// The display style of the data. Refer to [`SchemaContainerStyle`] for more information.
|
||||||
|
pub container_style: SchemaContainerStyle,
|
||||||
/// The datatype of the data found using the `data_member` key.
|
/// The datatype of the data found using the `data_member` key.
|
||||||
pub data_type: SchemaType,
|
pub data_type: SchemaType,
|
||||||
/// The header of this display.
|
/// The header of this display.
|
||||||
pub header: String,
|
pub header: String,
|
||||||
|
/// The style of the header's display.
|
||||||
|
pub header_style: SchemaHeaderStyle,
|
||||||
/// When used with a SchemaType::List, this determines the datatype and children for every entry
|
/// When used with a SchemaType::List, this determines the datatype and children for every entry
|
||||||
/// in the list. Some examples include a list of maps, where we use the `item_template` to define
|
/// in the list. Some examples include a list of maps, where we use the `item_template` to define
|
||||||
/// the SchemaType::Map, and all its children. This is then mapped across all entries in the list,
|
/// the SchemaType::Map, and all its children. This is then mapped across all entries in the list,
|
||||||
@@ -69,16 +95,28 @@ pub struct SchemaNode {
|
|||||||
/// When used with a SchemaType::Map, this determines what children from the map are displayed
|
/// When used with a SchemaType::Map, this determines what children from the map are displayed
|
||||||
/// and how. It is ignored with all other SchemaType variants.
|
/// and how. It is ignored with all other SchemaType variants.
|
||||||
pub children: IndexMap<String, SchemaNode>,
|
pub children: IndexMap<String, SchemaNode>,
|
||||||
|
/// The primary key for iteration over maps. Used when there is a list of maps.
|
||||||
|
pub primary_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SchemaNode {
|
impl SchemaNode {
|
||||||
pub fn new(data_type: SchemaType) -> Self {
|
/// Creates a new [`SchemaBuilder`].
|
||||||
SchemaNode {
|
pub fn builder() -> SchemaBuilder {
|
||||||
data_type,
|
SchemaBuilder::default()
|
||||||
header: "".to_string(),
|
}
|
||||||
item_template: None,
|
}
|
||||||
children: IndexMap::new(),
|
|
||||||
}
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct SchemaBuilder {
|
||||||
|
container_style: SchemaContainerStyle,
|
||||||
|
header: String,
|
||||||
|
header_style: SchemaHeaderStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchemaBuilder {
|
||||||
|
pub fn container(mut self, style: SchemaContainerStyle) -> Self {
|
||||||
|
self.container_style = style;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header(mut self, header: impl Into<String>) -> Self {
|
pub fn header(mut self, header: impl Into<String>) -> Self {
|
||||||
@@ -86,13 +124,130 @@ impl SchemaNode {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn child(mut self, data_member: impl Into<String>, node: SchemaNode) -> Self {
|
pub fn header_style(mut self, header_style: SchemaHeaderStyle) -> Self {
|
||||||
self.children.insert(data_member.into(), node);
|
self.header_style = header_style;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn primitive_list(self) -> SchemaPrimitiveListBuilder {
|
||||||
|
SchemaPrimitiveListBuilder::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_list(self) -> SchemaMapListBuilder {
|
||||||
|
SchemaMapListBuilder::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map(self) -> SchemaMapBuilder {
|
||||||
|
SchemaMapBuilder::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string(self) -> SchemaPrimitiveBuilder {
|
||||||
|
SchemaPrimitiveBuilder::new(self, SchemaType::String)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn number(self) -> SchemaPrimitiveBuilder {
|
||||||
|
SchemaPrimitiveBuilder::new(self, SchemaType::Number)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_node(self) -> SchemaNode {
|
||||||
|
SchemaNode {
|
||||||
|
container_style: self.container_style,
|
||||||
|
header: self.header,
|
||||||
|
header_style: self.header_style,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct SchemaPrimitiveListBuilder {
|
||||||
|
builder: SchemaBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchemaPrimitiveListBuilder {
|
||||||
|
fn new(builder: SchemaBuilder) -> Self {
|
||||||
|
Self { builder }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct SchemaMapListBuilder {
|
||||||
|
builder: SchemaBuilder,
|
||||||
|
item_template: Option<Box<SchemaNode>>,
|
||||||
|
primary_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchemaMapListBuilder {
|
||||||
|
fn new(builder: SchemaBuilder) -> Self {
|
||||||
|
Self {
|
||||||
|
builder,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn item_template(mut self, node: SchemaNode) -> Self {
|
pub fn item_template(mut self, node: SchemaNode) -> Self {
|
||||||
self.item_template = Some(Box::new(node));
|
self.item_template = Some(Box::new(node));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn primary_key(mut self, primary_key: impl Into<String>) -> Self {
|
||||||
|
self.primary_key = primary_key.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> SchemaNode {
|
||||||
|
SchemaNode {
|
||||||
|
data_type: SchemaType::MapList,
|
||||||
|
item_template: self.item_template,
|
||||||
|
primary_key: Some(self.primary_key),
|
||||||
|
..self.builder.into_node()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct SchemaMapBuilder {
|
||||||
|
builder: SchemaBuilder,
|
||||||
|
children: IndexMap<String, SchemaNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchemaMapBuilder {
|
||||||
|
fn new(builder: SchemaBuilder) -> Self {
|
||||||
|
Self {
|
||||||
|
builder,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn child(mut self, data_member: impl Into<String>, node: SchemaNode) -> Self {
|
||||||
|
self.children.insert(data_member.into(), node);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> SchemaNode {
|
||||||
|
SchemaNode {
|
||||||
|
data_type: SchemaType::Map,
|
||||||
|
children: self.children,
|
||||||
|
..self.builder.into_node()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct SchemaPrimitiveBuilder {
|
||||||
|
builder: SchemaBuilder,
|
||||||
|
data_type: SchemaType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchemaPrimitiveBuilder {
|
||||||
|
fn new(builder: SchemaBuilder, data_type: SchemaType) -> Self {
|
||||||
|
Self { data_type, builder }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> SchemaNode {
|
||||||
|
SchemaNode {
|
||||||
|
data_type: self.data_type,
|
||||||
|
..self.builder.into_node()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,4 +24,7 @@ pub struct UserClaims {
|
|||||||
pub username: String,
|
pub username: String,
|
||||||
/// The intended use for the token.
|
/// The intended use for the token.
|
||||||
pub token_use: String,
|
pub token_use: String,
|
||||||
|
/// The list of the names of user pool groups that have this user as a member.
|
||||||
|
#[serde(rename = "cognito:groups", default = "Vec::new")]
|
||||||
|
pub groups: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,13 @@ pub fn ActionButton(
|
|||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<button
|
<button
|
||||||
class="m-1.5 p-2.5 min-w-20 rounded-lg font-semibold text-white transition-all duration-400 cursor-pointer"
|
class="flex-1 m-1.5 p-2.5 min-w-20 rounded-lg font-semibold text-white transition-all duration-400 cursor-pointer"
|
||||||
class=(["bg-red-800", "hover:bg-red-900"], move || !disabled.get())
|
class=(["bg-red-800", "hover:bg-red-900"], move || !disabled.get())
|
||||||
class=(["bg-gray-600", "pointer-events-none"], move || disabled.get())
|
class=(["bg-gray-600", "pointer-events-none"], move || disabled.get())
|
||||||
r#type=button_type
|
r#type=button_type
|
||||||
disabled=move || disabled.get()
|
disabled=move || disabled.get()
|
||||||
>
|
>
|
||||||
{ children() }
|
{children()}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+207
-217
@@ -1,263 +1,253 @@
|
|||||||
use crate::common::{SchemaNode, SchemaType, ValueType};
|
use crate::common::{SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType};
|
||||||
use indexmap::IndexMap;
|
|
||||||
use leptos::logging::debug_log;
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// # Value Display Component
|
|
||||||
/// This component displays any data from a given `HashMap<String, ValueType>`. It requires a schema
|
|
||||||
/// to tell it how to display the information, which is covered in the [`SchemaNode`] documentation.
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ValueDisplay(
|
pub fn DataDisplay(
|
||||||
#[prop(into)] schema: Signal<SchemaNode>,
|
/// The schema that this component should use to display the given data.
|
||||||
#[prop(into, optional)] data_member: Signal<String>,
|
#[prop(into)]
|
||||||
#[prop(into)] data_map: Signal<HashMap<String, ValueType>>,
|
schema: Signal<SchemaNode>,
|
||||||
|
/// The starting key in the data map.
|
||||||
|
#[prop(into, optional)]
|
||||||
|
data_member: Signal<String>,
|
||||||
|
/// A `HashMap` containing all the data that should be displayed.
|
||||||
|
#[prop(into)]
|
||||||
|
data_map: Signal<HashMap<String, ValueType>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let data_value = Memo::new(move |_| {
|
// The schema should contain information about the header type and styling, along with the
|
||||||
if data_member.get().is_empty() {
|
// information display. This is rendered as part of the DataDisplayWrapper, which is simply a
|
||||||
ValueType::Map(Some(data_map.get()))
|
// wrapper around the information to be displayed.
|
||||||
|
|
||||||
|
// The children are all SchemaNodes as well, meaning that this component needs to call itself
|
||||||
|
// for each of these children. They will of course contain all the same information that we
|
||||||
|
// expect here.
|
||||||
|
|
||||||
|
// The actual displayed information is controlled through a DataDisplayView, which will accept
|
||||||
|
// settings for how the information is parsed. For example, rendering a list may be done by
|
||||||
|
// a string join operation, or it may be a list of maps, each of which needs to be displayed with
|
||||||
|
// a specific set of children. The SchemaNode must be able to handle this, and these options
|
||||||
|
// should be given to the DataDisplayView somehow.
|
||||||
|
|
||||||
|
// As the display_value here is actually an Option<ValueType>, we want to be able to handle cases
|
||||||
|
// where the display is given no values. In this case, the SchemaNode needs to specify whether
|
||||||
|
// a missing value is expected or an error.
|
||||||
|
|
||||||
|
let display_value = Memo::new(move |_| {
|
||||||
|
let member = data_member.get();
|
||||||
|
if member.is_empty() {
|
||||||
|
Some(ValueType::Map(Some(data_map.get())))
|
||||||
} else {
|
} else {
|
||||||
data_map
|
data_map.get().get(&member).cloned()
|
||||||
.get()
|
|
||||||
.get(&data_member.get())
|
|
||||||
.map(|v| v.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
{move || {
|
<DataDisplayWrapper
|
||||||
match schema.get().data_type {
|
container_style=schema.get().container_style
|
||||||
SchemaType::String | SchemaType::Number => {
|
header_style=schema.get().header_style
|
||||||
view! {
|
header_text=schema.get().header
|
||||||
<PrimitiveDisplay
|
>
|
||||||
header=move || schema.get().header
|
<DataDisplayView display_data=display_value schema=schema />
|
||||||
value=data_value
|
</DataDisplayWrapper>
|
||||||
nested=true
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
SchemaType::List => {
|
|
||||||
view! {
|
|
||||||
<ListDisplay
|
|
||||||
header=move || schema.get().header
|
|
||||||
value=data_value
|
|
||||||
item_template=move || {
|
|
||||||
schema
|
|
||||||
.get()
|
|
||||||
.item_template
|
|
||||||
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)))
|
|
||||||
}
|
|
||||||
nested=true
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
SchemaType::Map => {
|
|
||||||
view! {
|
|
||||||
<MapDisplay
|
|
||||||
value=data_value
|
|
||||||
header=move || schema.get().header
|
|
||||||
schema=move || schema.get().children
|
|
||||||
nested=true
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Display Wrapper Component
|
|
||||||
/// This component is designed to wrap information for the [`ValueDisplay`] component. It allows
|
|
||||||
/// for simple uniform styling of the header and capsule, along with the ability for users to
|
|
||||||
/// collapse the information.
|
|
||||||
#[component]
|
#[component]
|
||||||
fn DisplayWrapper(
|
fn DataDisplayWrapper(
|
||||||
#[prop(into, optional)] header: Signal<String>,
|
#[prop(into)] container_style: SchemaContainerStyle,
|
||||||
#[prop(into, optional)] nested: Signal<bool>,
|
#[prop(into)] header_style: SchemaHeaderStyle,
|
||||||
|
#[prop(into)] header_text: Signal<String>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
// This component takes a wrapper style and displays the "chrome" of the interface. It has two
|
||||||
|
// options: Text Headers or Capsules. This will then render a DataDisplayHeader, which handles
|
||||||
|
// the styling for the text inside the header specifically.
|
||||||
|
|
||||||
|
// The capsule style is a colored header and a rounded container.
|
||||||
|
// The text header style is an invisible container.
|
||||||
|
|
||||||
let expanded = RwSignal::new(true);
|
let expanded = RwSignal::new(true);
|
||||||
|
|
||||||
view! {
|
match container_style {
|
||||||
<div class="m-3 rounded-lg shadow-lg flex flex-col">
|
SchemaContainerStyle::Header => {
|
||||||
<div
|
// Creates a simple container that only has a header and a series of children.
|
||||||
class="p-2 px-3 font-bold rounded-t-lg cursor-pointer"
|
// The spacing between the header and children is controlled by the header style.
|
||||||
class=(["bg-gray-300"], move || !nested.get())
|
// This type of container is not collapsable at the moment.
|
||||||
class=(["text-white", "bg-red-900"], move || nested.get())
|
view! {
|
||||||
class=(["rounded-lg"], move || !expanded.get())
|
<div class="flex flex-col w-full text-left">
|
||||||
on:click=move |_| expanded.update(|b| *b = !*b)
|
<div class="self-start w-full">
|
||||||
>
|
<DataDisplayHeader header_text=header_text header_style=header_style />
|
||||||
{header}
|
<div class="p-2 flex flex-col gap-1">{children()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="grid transition-all ease-in-out"
|
|
||||||
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
|
|
||||||
>
|
|
||||||
<div class="min-h-0 overflow-hidden">
|
|
||||||
<div class="p-2">{children()}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
SchemaContainerStyle::Capsule => {
|
||||||
|
// Creates a capsule container - it is collapsable, with rounded corners and a colored
|
||||||
/// # Primitive Value Display Component
|
// header.
|
||||||
/// This component allows for the display of a `ValueType::String` or `ValueType::Number`.
|
view! {
|
||||||
#[component]
|
<div class="self-stretch rounded-lg shadow-lg flex flex-col w-full text-center">
|
||||||
fn PrimitiveDisplay(
|
<div
|
||||||
#[prop(into)] header: Signal<String>,
|
class="p-2 px-3 rounded-t-lg cursor-pointer bg-red-800 text-white"
|
||||||
#[prop(into)] value: Signal<ValueType>,
|
class=(["rounded-lg"], move || !expanded.get())
|
||||||
#[prop(into, optional)] nested: Signal<bool>,
|
on:click=move |_| expanded.update(|b| *b = !*b)
|
||||||
) -> impl IntoView {
|
>
|
||||||
let display_string = Memo::new(move |_| {
|
<DataDisplayHeader header_text=header_text header_style=header_style />
|
||||||
let value = value.get();
|
</div>
|
||||||
match value {
|
<div
|
||||||
ValueType::String(_) | ValueType::Number(_) => value.to_string(),
|
class="grid transition-all ease-in-out"
|
||||||
_ => "Value is not a string.".to_string(),
|
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
|
||||||
|
>
|
||||||
|
<div class="min-h-0 overflow-hidden">
|
||||||
|
<div class="p-2 flex flex-col gap-1 flex-1">{children()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<DisplayWrapper header=header nested=nested>
|
|
||||||
<div>{display_string}</div>
|
|
||||||
</DisplayWrapper>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # List Value Display Component
|
|
||||||
/// This component allows for the display of a `ValueType::List`. It will show an error if passed
|
|
||||||
/// anything other than a list. It requires an item template that tells the list how to display
|
|
||||||
/// each value inside of it, most specifically the type of data inside of it.
|
|
||||||
#[component]
|
#[component]
|
||||||
fn ListDisplay(
|
fn DataDisplayHeader(
|
||||||
#[prop(into)] item_template: Signal<Box<SchemaNode>>,
|
#[prop(into)] header_style: SchemaHeaderStyle,
|
||||||
#[prop(into)] value: Signal<ValueType>,
|
#[prop(into)] header_text: Signal<String>,
|
||||||
#[prop(into)] header: Signal<String>,
|
|
||||||
#[prop(into, optional)] nested: Signal<bool>,
|
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
// Unwrap the value and check if it's the correct type. To be displayed with this component, it
|
// This component will take a header style and displays the corresponding text. It has a few
|
||||||
// MUST be a ValueType::List.
|
// options, all of which are defined in the HeaderStyle enum.
|
||||||
let display_list = Memo::new(move |_| {
|
|
||||||
let ValueType::List(Some(list)) = value.get() else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
list.into_iter()
|
match header_style {
|
||||||
.enumerate()
|
SchemaHeaderStyle::MainHeader => {
|
||||||
.collect::<Vec<(usize, ValueType)>>()
|
view! { <div class="text-3xl font-bold my-2">{header_text}</div> }.into_any()
|
||||||
});
|
}
|
||||||
|
SchemaHeaderStyle::SubsectionHeader => {
|
||||||
let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::List(_)));
|
view! { <div class="text-xl font-bold">{header_text}</div> }.into_any()
|
||||||
|
}
|
||||||
let item_header = Memo::new(move |_| item_template.get().header);
|
SchemaHeaderStyle::Bold => view! { <div class="font-bold">{header_text}</div> }.into_any(),
|
||||||
|
SchemaHeaderStyle::None => view! {}.into_any(),
|
||||||
view! {
|
|
||||||
<DisplayWrapper header=header nested=nested>
|
|
||||||
<For
|
|
||||||
each=move || display_list.get()
|
|
||||||
key=|(idx, _)| idx.clone()
|
|
||||||
children=move |(idx, entry)| {
|
|
||||||
match item_template.get().data_type {
|
|
||||||
SchemaType::String | SchemaType::Number => {
|
|
||||||
view! { <PrimitiveDisplay value=entry header=item_header /> }.into_any()
|
|
||||||
}
|
|
||||||
SchemaType::List => {
|
|
||||||
view! {
|
|
||||||
<div>"Nested lists are not currently supported for display."</div>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
SchemaType::Map => {
|
|
||||||
let map_header = format!("{} {}", item_template.get().header, idx + 1);
|
|
||||||
view! {
|
|
||||||
<MapDisplay
|
|
||||||
value=entry
|
|
||||||
header=map_header
|
|
||||||
schema=move || item_template.get().children
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</DisplayWrapper>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Map Data Display Component
|
|
||||||
/// This component allows for the display of a `ValueType::Map`. The nested information must be defined
|
|
||||||
/// by a [`SchemaNode`], which tells the display what keys in the value map to search for the
|
|
||||||
/// display information.
|
|
||||||
#[component]
|
#[component]
|
||||||
fn MapDisplay(
|
fn DataDisplayView(
|
||||||
#[prop(into)] value: Signal<ValueType>,
|
#[prop(into)] display_data: Signal<Option<ValueType>>,
|
||||||
#[prop(into)] header: Signal<String>,
|
#[prop(into)] schema: Signal<SchemaNode>,
|
||||||
#[prop(into)] schema: Signal<IndexMap<String, SchemaNode>>,
|
|
||||||
#[prop(into, optional)] nested: Signal<bool>,
|
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
// Unwrap the value and check if it is the correct type. It must be a ValueType::Map.
|
// This is the part of the data display that does most of the work. It will take the values
|
||||||
let display_map = Memo::new(move |_| {
|
// found in the DataDisplay component and perform the logic that actually displays the
|
||||||
let ValueType::Map(Some(map)) = value.get() else {
|
// information. This component is only displayed if there is Some(value) in the DataDisplay.
|
||||||
return HashMap::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
map
|
// In the case that this is displaying a Map or List, it will recursively call the DataDisplay
|
||||||
});
|
// component.
|
||||||
|
|
||||||
Effect::new(move || {
|
// If this is displaying a PrimitiveList, it will flatten and join all the values into a single
|
||||||
debug_log!("Current map values: {:?}", display_map.get());
|
// string, then display that string.
|
||||||
});
|
|
||||||
|
|
||||||
let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::Map(_)));
|
let data_type = schema.get().data_type;
|
||||||
|
|
||||||
view! {
|
// Matches the specific data type. Displays errors if the given display data does not match.
|
||||||
<DisplayWrapper header=header nested=nested>
|
match data_type {
|
||||||
<For
|
SchemaType::String => {
|
||||||
each=move || schema.get()
|
let value = Signal::derive(move || {
|
||||||
key=|(k, _)| k.clone()
|
display_data
|
||||||
children=move |(member, child_schema)| {
|
.get()
|
||||||
let display_value = Memo::new(move |_| {
|
.map(|v| v.as_string().ok().flatten())
|
||||||
display_map.get().get(&member).map(|v| v.clone()).unwrap_or_default()
|
.flatten()
|
||||||
});
|
.unwrap_or("N/A".to_string())
|
||||||
match child_schema.data_type {
|
});
|
||||||
SchemaType::String | SchemaType::Number => {
|
view! { <div>{value}</div> }.into_any()
|
||||||
view! {
|
}
|
||||||
<PrimitiveDisplay value=display_value header=child_schema.header />
|
SchemaType::Number => {
|
||||||
}
|
let value = Signal::derive(move || {
|
||||||
.into_any()
|
display_data
|
||||||
|
.get()
|
||||||
|
.map(|v| v.as_number().ok().flatten())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or("N/A".to_string())
|
||||||
|
});
|
||||||
|
view! { <div>{value}</div> }.into_any()
|
||||||
|
}
|
||||||
|
SchemaType::PrimitiveList => {
|
||||||
|
let value = Signal::derive(move || {
|
||||||
|
display_data
|
||||||
|
.get()
|
||||||
|
.map(|v| v.as_list().ok().flatten())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
});
|
||||||
|
view! { <div>{value}</div> }.into_any()
|
||||||
|
}
|
||||||
|
SchemaType::MapList => {
|
||||||
|
let list = Signal::derive(move || {
|
||||||
|
display_data
|
||||||
|
.get()
|
||||||
|
.map(|v| v.as_list().ok().flatten())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_map().ok().flatten().clone())
|
||||||
|
.collect::<Vec<HashMap<String, ValueType>>>()
|
||||||
|
});
|
||||||
|
let primary_key = StoredValue::new(schema.get().primary_key.unwrap_or_default());
|
||||||
|
view! {
|
||||||
|
<Show
|
||||||
|
when=move || !list.get().is_empty()
|
||||||
|
fallback=|| view! { <div class="w-full text-center">"List is empty."</div> }
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each=move || list.get()
|
||||||
|
key=move |map| {
|
||||||
|
map.get(&primary_key.get_value())
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
SchemaType::List => {
|
children=move |map| {
|
||||||
let template = child_schema
|
|
||||||
.item_template
|
|
||||||
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)));
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ListDisplay
|
<DataDisplay
|
||||||
value=display_value
|
schema=*schema.get().item_template.unwrap_or_default()
|
||||||
item_template=template
|
data_map=map
|
||||||
header=child_schema.header
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
SchemaType::Map => {
|
/>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
SchemaType::Map => {
|
||||||
|
let map = Signal::derive(move || {
|
||||||
|
display_data
|
||||||
|
.get()
|
||||||
|
.map(|v| v.as_map().ok().flatten())
|
||||||
|
.flatten()
|
||||||
|
});
|
||||||
|
view! {
|
||||||
|
<Show
|
||||||
|
when=move || map.get().is_some()
|
||||||
|
fallback=|| view! { <div class="text-red-500">"Map was not found."</div> }
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each=move || schema.get().children
|
||||||
|
key=|(key, _)| key.clone()
|
||||||
|
children=move |(key, schema)| {
|
||||||
view! {
|
view! {
|
||||||
<MapDisplay
|
<DataDisplay
|
||||||
value=display_value
|
schema=schema
|
||||||
schema=child_schema.children
|
data_member=key
|
||||||
header=child_schema.header
|
data_map=Signal::derive(move || map.get().unwrap_or_default())
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
}
|
</Show>
|
||||||
/>
|
}
|
||||||
</DisplayWrapper>
|
.into_any()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::pages::api::files::{DeleteFile, upload_file};
|
use crate::pages::api::files::{DeleteFile, upload_file};
|
||||||
use leptos::html::{Input, Label};
|
use leptos::html::{Input, Label};
|
||||||
use leptos::logging::debug_log;
|
use leptos::logging::{debug_error, debug_log};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::web_sys::{File, FormData};
|
use leptos::web_sys::{File, FormData};
|
||||||
use leptos_icons::Icon;
|
use leptos_icons::Icon;
|
||||||
@@ -28,6 +28,7 @@ pub fn FileDrop(
|
|||||||
|
|
||||||
let file_name_list = RwSignal::new(Vec::new());
|
let file_name_list = RwSignal::new(Vec::new());
|
||||||
let hovering = RwSignal::new(false);
|
let hovering = RwSignal::new(false);
|
||||||
|
let error_msg = RwSignal::new(String::new());
|
||||||
let name = StoredValue::new(name);
|
let name = StoredValue::new(name);
|
||||||
let form_id = StoredValue::new(form_id);
|
let form_id = StoredValue::new(form_id);
|
||||||
let auth = expect_context::<AuthSignal>();
|
let auth = expect_context::<AuthSignal>();
|
||||||
@@ -172,10 +173,18 @@ pub fn FileDrop(
|
|||||||
existing_files
|
existing_files
|
||||||
.get()
|
.get()
|
||||||
.map(|result| {
|
.map(|result| {
|
||||||
if let Ok(files) = result {
|
match result {
|
||||||
file_name_list.set(files);
|
Ok(files) => {
|
||||||
|
error_msg.set("".to_string());
|
||||||
|
file_name_list.set(files);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.to_string();
|
||||||
|
debug_error!("Error while getting files: {}", msg);
|
||||||
|
error_msg.set(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}}
|
}}
|
||||||
<label
|
<label
|
||||||
node_ref=zone_ref
|
node_ref=zone_ref
|
||||||
@@ -194,6 +203,9 @@ pub fn FileDrop(
|
|||||||
<div class="text-sm text-gray-400">"Or click to select a file"</div>
|
<div class="text-sm text-gray-400">"Or click to select a file"</div>
|
||||||
<input node_ref=input_ref type="file" class="hidden" on:change=on_input_change />
|
<input node_ref=input_ref type="file" class="hidden" on:change=on_input_change />
|
||||||
</label>
|
</label>
|
||||||
|
<Show when=move || !error_msg.get().is_empty()>
|
||||||
|
<div class="flex-1">{error_msg}</div>
|
||||||
|
</Show>
|
||||||
<For
|
<For
|
||||||
each=move || file_name_list.get()
|
each=move || file_name_list.get()
|
||||||
key=|file| file.clone()
|
key=|file| file.clone()
|
||||||
|
|||||||
@@ -0,0 +1,577 @@
|
|||||||
|
use crate::common::{
|
||||||
|
ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode,
|
||||||
|
SchemaType, ValueType,
|
||||||
|
};
|
||||||
|
use crate::components::{ActionButton, Banner, DataDisplay, Loading};
|
||||||
|
use crate::pages::{StudentInformationDialog, UnauthenticatedPage};
|
||||||
|
use crate::pages::api::get_comparison_info;
|
||||||
|
use crate::pages::api::students::{GetStudentFiles, admin_get_all_input_files, get_student_data};
|
||||||
|
use base64::Engine;
|
||||||
|
use leptos::ev::{Event, MouseEvent};
|
||||||
|
use leptos::html::Dialog;
|
||||||
|
use leptos::logging::debug_log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos::wasm_bindgen::JsCast;
|
||||||
|
use leptos::web_sys;
|
||||||
|
use leptos::web_sys::{HtmlAnchorElement, ScrollBehavior};
|
||||||
|
use leptos_icons::Icon;
|
||||||
|
use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated};
|
||||||
|
use leptos_router::components::Outlet;
|
||||||
|
use leptos_router::hooks::use_navigate;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use leptos_router::NavigateOptions;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminApplicantsPageFallback() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col flex-1 gap-2">
|
||||||
|
<h1 class="flex-1 text-center font-bold text-2xl">"Scholarship Applicants"</h1>
|
||||||
|
<p class="text-center text-lg">
|
||||||
|
"Please select a scholarship from the left to view the eligible applicants."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminApplicantsPageShell() -> impl IntoView {
|
||||||
|
// This shell needs to simply contain everything that will stay consistent across paths.
|
||||||
|
// The path will change to contain each scholarship's id, which will directly affect the content
|
||||||
|
// shown in a subroute for this page.
|
||||||
|
|
||||||
|
// As a result, this page likely only needs to contain the banner and the list of scholarships.
|
||||||
|
// The content will be shown within the right side, but only as a result of some subroutes,
|
||||||
|
// each of which is still considered a single page.
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex-1" />
|
||||||
|
<div class="flex-3 flex flex-row items-start">
|
||||||
|
<AdminApplicantsScholarshipList />
|
||||||
|
<div class="flex-3 text-center">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn AdminApplicantsScholarshipList() -> impl IntoView {
|
||||||
|
use crate::common::ExpandableInfo;
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn admin_get_provider_scholarships(
|
||||||
|
access_token: String,
|
||||||
|
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||||
|
use crate::pages::api::SCHOLARSHIPS_TABLE;
|
||||||
|
use crate::pages::api::tokens::validate_and_get_token_info;
|
||||||
|
|
||||||
|
let claims = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?;
|
||||||
|
|
||||||
|
// Get the information from the database.
|
||||||
|
let client = crate::utils::server::create_dynamo_client().await;
|
||||||
|
|
||||||
|
let output = client
|
||||||
|
.scan()
|
||||||
|
.table_name(SCHOLARSHIPS_TABLE)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.items
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|item| serde_dynamo::from_item(item).ok())
|
||||||
|
.collect::<Vec<ExpandableInfo>>();
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use access token for user identification
|
||||||
|
let auth = expect_context::<AuthSignal>();
|
||||||
|
let access_token =
|
||||||
|
Memo::new(move |_| auth.with(|auth| auth.authenticated().map(|a| a.access_token())));
|
||||||
|
|
||||||
|
// We need to get the scholarships from the API. We don't need the whole scholarship, just the
|
||||||
|
// name and ID.
|
||||||
|
let trigger = Trigger::new();
|
||||||
|
let scholarships_res = Resource::new(
|
||||||
|
move || (trigger.track(), access_token.get()),
|
||||||
|
async move |(_, access_token)| {
|
||||||
|
admin_get_provider_scholarships(access_token.unwrap_or_default()).await
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let navigate = use_navigate();
|
||||||
|
|
||||||
|
// This closure returns another closure that's using the correct scholarship ID.
|
||||||
|
// It avoids having to define it within the Transition component. It's wrapped in a Callback
|
||||||
|
// because this makes it Send + Sync.
|
||||||
|
let on_view_click = Callback::new(move |scholarship_id: String| {
|
||||||
|
let path = format!("/admin/applicants/{}", scholarship_id);
|
||||||
|
navigate(&*path, Default::default());
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col flex-1 rounded-md shadow-lg/33 p-2 gap-3">
|
||||||
|
<h2 class="text-xl font-bold flex-1 text-center">"Scholarships"</h2>
|
||||||
|
<Transition fallback=Loading>
|
||||||
|
{move || {
|
||||||
|
scholarships_res
|
||||||
|
.get()
|
||||||
|
.map(|items_res| {
|
||||||
|
let items = match items_res {
|
||||||
|
Ok(items) => items,
|
||||||
|
Err(e) => {
|
||||||
|
return view! {
|
||||||
|
<div>
|
||||||
|
{format!("Couldn't get scholarships: {}", e.to_string())}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<For
|
||||||
|
each=move || items.clone()
|
||||||
|
key=|item| item.subject.clone()
|
||||||
|
let(item)
|
||||||
|
>
|
||||||
|
<ApplicantsScholarshipEntry
|
||||||
|
item=item
|
||||||
|
on_view_click=on_view_click
|
||||||
|
/>
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
.collect_view()
|
||||||
|
}}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ApplicantsScholarshipEntry(
|
||||||
|
#[prop()] item: ExpandableInfo,
|
||||||
|
#[prop(into)] on_view_click: Callback<String, ()>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
// Store the item's subject.
|
||||||
|
let subject = StoredValue::new(item.subject);
|
||||||
|
let scholarship_name = Memo::new(move |_| {
|
||||||
|
item.data
|
||||||
|
.get("name")
|
||||||
|
.map(|name| name.as_string().ok().flatten())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or("<unnamed scholarship>".to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_click = move |_| {
|
||||||
|
on_view_click.run(subject.get_value());
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col rounded-md shadow-md hover:shadow-lg/33 transition-shadow">
|
||||||
|
<div class="text-center p-2">{scholarship_name}</div>
|
||||||
|
<div
|
||||||
|
class="flex-1 font-bold text-white rounded-b-md text-center cursor-pointer bg-red-800 hover:bg-red-900 p-2 transition-bg"
|
||||||
|
on:click=on_click
|
||||||
|
>
|
||||||
|
"View Applicants"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminApplicantsStudentList() -> impl IntoView {
|
||||||
|
use crate::pages::api::get_scholarship_info;
|
||||||
|
use crate::pages::api::students::admin_get_completed_students;
|
||||||
|
use leptos_router::hooks::use_params;
|
||||||
|
use leptos_router::params::Params;
|
||||||
|
|
||||||
|
/// The parameters for the page that displays this component. This may be moved to the page-level
|
||||||
|
/// rather than the component-level.
|
||||||
|
#[derive(Params, PartialEq)]
|
||||||
|
struct StudentListParams {
|
||||||
|
scholarship_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is in charge of finding all student information and running the comparisons.
|
||||||
|
// It will then display all the eligible students.
|
||||||
|
|
||||||
|
let params = use_params::<StudentListParams>();
|
||||||
|
let auth = expect_context::<AuthSignal>();
|
||||||
|
let access_token =
|
||||||
|
Memo::new(move |_| auth.with(|a| a.authenticated().map(|a| a.access_token())));
|
||||||
|
|
||||||
|
let scholarship_id = Memo::new(move |_| {
|
||||||
|
params
|
||||||
|
.read()
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|params| params.scholarship_id.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn get_eligibility_info(
|
||||||
|
token: String,
|
||||||
|
scholarship_id: String,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
HashMap<String, HashMap<String, ValueType>>,
|
||||||
|
ExpandableInfo,
|
||||||
|
Vec<ComparisonData>,
|
||||||
|
),
|
||||||
|
ServerFnError,
|
||||||
|
> {
|
||||||
|
let (students, scholarships, requirements_list) = tokio::join!(
|
||||||
|
admin_get_completed_students(token),
|
||||||
|
get_scholarship_info(scholarship_id),
|
||||||
|
get_comparison_info()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((students?, scholarships?, requirements_list?))
|
||||||
|
}
|
||||||
|
|
||||||
|
let resource = Resource::new(
|
||||||
|
move || (access_token.get(), scholarship_id.get()),
|
||||||
|
move |(access_token, scholarship_id)| async move {
|
||||||
|
let Some(token) = access_token else {
|
||||||
|
return Err(ServerFnError::new("Couldn't find access token"));
|
||||||
|
};
|
||||||
|
let Some(scholarship_id) = scholarship_id else {
|
||||||
|
return Err(ServerFnError::new("Couldn't find scholarship ID"));
|
||||||
|
};
|
||||||
|
|
||||||
|
get_eligibility_info(token, scholarship_id).await
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<Suspense fallback=Loading>
|
||||||
|
{move || {
|
||||||
|
resource
|
||||||
|
.get()
|
||||||
|
.map(|result| {
|
||||||
|
match result {
|
||||||
|
Ok((students, scholarship, requirements_list)) => {
|
||||||
|
view! {
|
||||||
|
<h1 class="text-2xl font-bold">"Eligible Students"</h1>
|
||||||
|
<div class="text-lg">
|
||||||
|
"Click on a student's name to view their information and download any requested files."
|
||||||
|
</div>
|
||||||
|
<div class="text-lg">
|
||||||
|
"If your scholarship requires a transcript, please send an email to Sara Smith at "
|
||||||
|
<a
|
||||||
|
class="text-blue-500 underline"
|
||||||
|
href="mailto:ssmith@region15.org"
|
||||||
|
>
|
||||||
|
"ssmith@region15.org"
|
||||||
|
</a>"."
|
||||||
|
</div>
|
||||||
|
<AdminApplicantsStudentListView
|
||||||
|
access_token=access_token
|
||||||
|
students=students
|
||||||
|
scholarship=scholarship
|
||||||
|
requirements=requirements_list
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
"Error while getting eligible students: "{e.to_string()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn AdminApplicantsStudentListView(
|
||||||
|
#[prop(into)] access_token: Signal<Option<String>>,
|
||||||
|
#[prop(into)] students: Signal<HashMap<String, HashMap<String, ValueType>>>,
|
||||||
|
#[prop(into)] scholarship: Signal<ExpandableInfo>,
|
||||||
|
#[prop(into)] requirements: Signal<Vec<ComparisonData>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let eligible_students = RwSignal::new(HashMap::new());
|
||||||
|
let error_msg = RwSignal::new(String::new());
|
||||||
|
// let (current_student_id, set_current_student_id) = query_signal::<String>("student_id");
|
||||||
|
let current_student_id = RwSignal::new(None);
|
||||||
|
let navigate_student = Callback::new(move |id: String| {
|
||||||
|
let options = web_sys::ScrollToOptions::new();
|
||||||
|
options.set_top(0.0);
|
||||||
|
options.set_behavior(ScrollBehavior::Smooth);
|
||||||
|
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
window.scroll_to_with_scroll_to_options(&options);
|
||||||
|
}
|
||||||
|
|
||||||
|
current_student_id.set(Some(id));
|
||||||
|
});
|
||||||
|
let dialog_visible =
|
||||||
|
Signal::derive(move || !current_student_id.get().unwrap_or_default().is_empty());
|
||||||
|
let on_dialog_close = Callback::new(move |_| {
|
||||||
|
current_student_id.set(None);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fafsa_required = Memo::new(move |_| {
|
||||||
|
let required = scholarship
|
||||||
|
.get()
|
||||||
|
.data
|
||||||
|
.get("fafsa_required")
|
||||||
|
.map(|v| v.as_string().ok().flatten())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
required == "Yes"
|
||||||
|
});
|
||||||
|
|
||||||
|
let essay_required = Memo::new(move |_| {
|
||||||
|
let prompt = scholarship
|
||||||
|
.get()
|
||||||
|
.data
|
||||||
|
.get("essay_prompt")
|
||||||
|
.map(|v| v.as_string().ok().flatten())
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
!prompt.is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
let fafsa_resource = Resource::new(
|
||||||
|
move || (fafsa_required.get(), access_token.get()),
|
||||||
|
move |(fafsa_required, access_token)| async move {
|
||||||
|
// Only get this information if the FAFSA is required. Otherwise, return a blank result.
|
||||||
|
let Some(access_token) = access_token else {
|
||||||
|
return Err(ServerFnError::new("Couldn't find access token"));
|
||||||
|
};
|
||||||
|
if !fafsa_required {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
admin_get_all_input_files(
|
||||||
|
access_token,
|
||||||
|
"financial_info".to_string(),
|
||||||
|
"fafsa".to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let essay_resource = Resource::new(
|
||||||
|
move || (essay_required.get(), access_token.get()),
|
||||||
|
move |(essay_required, access_token)| async move {
|
||||||
|
// Only get this information if the scholarship requested it. Otherwise, return a blank
|
||||||
|
// result.
|
||||||
|
let Some(access_token) = access_token else {
|
||||||
|
return Err(ServerFnError::new("Couldn't find access token"));
|
||||||
|
};
|
||||||
|
if !essay_required {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
admin_get_all_input_files(
|
||||||
|
access_token,
|
||||||
|
"scholarship_essays".to_string(),
|
||||||
|
scholarship.get().subject,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
fn get_eligible_students(
|
||||||
|
students: HashMap<String, HashMap<String, ValueType>>,
|
||||||
|
scholarship: ExpandableInfo,
|
||||||
|
requirements: Vec<ComparisonData>,
|
||||||
|
essay_required: bool,
|
||||||
|
fafsa_required: bool,
|
||||||
|
fafsa_list: HashMap<String, Vec<String>>,
|
||||||
|
essay_list: HashMap<String, Vec<String>>,
|
||||||
|
) -> Result<HashMap<String, HashMap<String, ValueType>>, Error> {
|
||||||
|
// Get the full map of requirements. Each key contains a list of IDs.
|
||||||
|
let scholarship_requirements = scholarship
|
||||||
|
.data
|
||||||
|
.get("requirements")
|
||||||
|
.map(|val| val.as_map().ok()?)
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Get the lists of requirements. Maps into string IDs and discards invalid ValueTypes.
|
||||||
|
let requirement_lists = scholarship_requirements
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_, list_val)| {
|
||||||
|
let list = list_val.as_list().ok().flatten()?;
|
||||||
|
let string_list = list
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|v| v.as_string().ok().flatten())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
Some(string_list)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<String>>>();
|
||||||
|
|
||||||
|
// Resolve the requirement IDs. Discards all IDs that do not resolve.
|
||||||
|
let resolved_requirements = requirement_lists
|
||||||
|
.into_iter()
|
||||||
|
.map(|list| {
|
||||||
|
list.into_iter()
|
||||||
|
.filter_map(|id| {
|
||||||
|
requirements
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.find(|requirement| requirement.id == id)
|
||||||
|
})
|
||||||
|
.collect::<Vec<ComparisonData>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<ComparisonData>>>();
|
||||||
|
|
||||||
|
// Check student eligibility. These are all students that have completed the demographics
|
||||||
|
// forms.
|
||||||
|
let eligible_students = students
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(id, student)| {
|
||||||
|
// The student needs to pass one requirement from each list.
|
||||||
|
// The easiest ones to check first are whether the scholarship requires a student
|
||||||
|
// essay and/or their fafsa information. If the student does not have these things,
|
||||||
|
// they are skipped.
|
||||||
|
|
||||||
|
if fafsa_required && !fafsa_list.contains_key(&id) {
|
||||||
|
// Get the student files related to their fafsa and continue. Return false if not there.
|
||||||
|
debug_log!(
|
||||||
|
"Student with id {id} failed - FAFSA is required but was not found."
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if essay_required && !essay_list.contains_key(&id) {
|
||||||
|
// Get the student's files related to their essays and continue. Return false if not there.
|
||||||
|
debug_log!(
|
||||||
|
"Student with id {id} failed - essay is required but was not found."
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = resolved_requirements.iter().all(|list| {
|
||||||
|
if list.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
list.iter().any(|requirement| {
|
||||||
|
let result = requirement.compare(&student).unwrap_or(false);
|
||||||
|
|
||||||
|
if !result {
|
||||||
|
debug_log!(
|
||||||
|
"Student with id {id} failed - requirement with id {} failed.",
|
||||||
|
requirement.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if result { Some((id, student)) } else { None }
|
||||||
|
})
|
||||||
|
.collect::<HashMap<String, HashMap<String, ValueType>>>();
|
||||||
|
|
||||||
|
// debug_log!("Total number of eligible students: {}", eligible_students.len());
|
||||||
|
|
||||||
|
Ok(eligible_students)
|
||||||
|
}
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
// We have to wait until we've gotten fafsa/essay requirements.
|
||||||
|
debug_log!("Waiting on essay and FAFSA resources...");
|
||||||
|
if let (Some(fafsa_list), Some(essay_list)) = (fafsa_resource.get(), essay_resource.get()) {
|
||||||
|
debug_log!(
|
||||||
|
"Running eligibility check on {} students",
|
||||||
|
students.get().len()
|
||||||
|
);
|
||||||
|
let eligible_res = get_eligible_students(
|
||||||
|
students.get(),
|
||||||
|
scholarship.get(),
|
||||||
|
requirements.get(),
|
||||||
|
essay_required.get(),
|
||||||
|
fafsa_required.get(),
|
||||||
|
fafsa_list.unwrap_or_default(),
|
||||||
|
essay_list.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match eligible_res {
|
||||||
|
Ok(list) => {
|
||||||
|
debug_log!("Total number of eligible students: {}", list.len());
|
||||||
|
eligible_students.set(list);
|
||||||
|
}
|
||||||
|
Err(e) => error_msg.set(format!("Failed to get eligible students list: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is actually just a display of all the information from the students list.
|
||||||
|
// As defined in the ApplicantsStudentList component, this will already be wrapped in a
|
||||||
|
// flex container.
|
||||||
|
view! {
|
||||||
|
<StudentInformationDialog
|
||||||
|
scholarship_id=scholarship.get().subject
|
||||||
|
student_id=current_student_id
|
||||||
|
visible=dialog_visible
|
||||||
|
on_close=on_dialog_close
|
||||||
|
essay_required=essay_required
|
||||||
|
fafsa_required=fafsa_required
|
||||||
|
/>
|
||||||
|
<Show when=move || !error_msg.get().is_empty()>
|
||||||
|
<div>"Error checking student eligibility: "{error_msg}</div>
|
||||||
|
</Show>
|
||||||
|
<div class="m-3 flex flex-col">
|
||||||
|
<div class="flex flex-row p-1 border-b-1 border-black">
|
||||||
|
<div class="pl-5 flex-1 text-lg font-bold text-left">"Last Name"</div>
|
||||||
|
<div class="pr-5 flex-1 text-lg font-bold text-left">"First Name"</div>
|
||||||
|
</div>
|
||||||
|
<For
|
||||||
|
each=move || eligible_students.get()
|
||||||
|
key=|(id, _)| id.clone()
|
||||||
|
children=move |(student_id, student)| {
|
||||||
|
let student = StoredValue::new(student);
|
||||||
|
let first_name = Memo::new(move |_| {
|
||||||
|
student
|
||||||
|
.get_value()
|
||||||
|
.get("first_name")
|
||||||
|
.map(|n| n.as_string().ok().flatten())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let last_name = Memo::new(move |_| {
|
||||||
|
student
|
||||||
|
.get_value()
|
||||||
|
.get("last_name")
|
||||||
|
.map(|n| n.as_string().ok().flatten())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let on_click = move |_| {
|
||||||
|
navigate_student.run(student_id.clone());
|
||||||
|
};
|
||||||
|
// For now, we'll just display each student's first and last name.
|
||||||
|
// Now I'll add a container with some links. These links will just be appended
|
||||||
|
// to the end of the current path.
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="flex flex-row p-1 border-y-1 border-gray-200 cursor-pointer hover:bg-yellow-100 transition-all duration-200"
|
||||||
|
on:click=on_click
|
||||||
|
>
|
||||||
|
<div class="pl-5 flex-1 text-lg text-left">{last_name}</div>
|
||||||
|
<div class="pr-5 flex-1 text-lg text-left">{first_name}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@ mod home_page;
|
|||||||
mod provider_page;
|
mod provider_page;
|
||||||
mod scholarship_page;
|
mod scholarship_page;
|
||||||
mod utils_page;
|
mod utils_page;
|
||||||
|
mod applicants_list;
|
||||||
|
|
||||||
pub use shell::*;
|
pub use shell::*;
|
||||||
pub use home_page::*;
|
pub use home_page::*;
|
||||||
pub use provider_page::*;
|
pub use provider_page::*;
|
||||||
pub use scholarship_page::*;
|
pub use scholarship_page::*;
|
||||||
pub use utils_page::*;
|
pub use utils_page::*;
|
||||||
|
pub use applicants_list::*;
|
||||||
@@ -25,6 +25,7 @@ pub fn AdminShell() -> impl IntoView {
|
|||||||
TabInfo::new("Providers", "providers", None),
|
TabInfo::new("Providers", "providers", None),
|
||||||
TabInfo::new("Scholarships", "scholarships", None),
|
TabInfo::new("Scholarships", "scholarships", None),
|
||||||
TabInfo::new("Utilities", "utilities", None),
|
TabInfo::new("Utilities", "utilities", None),
|
||||||
|
TabInfo::new("Scholarship Applicants", "applicants", None),
|
||||||
]
|
]
|
||||||
/>
|
/>
|
||||||
</Authenticated>
|
</Authenticated>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use leptos::prelude::*;
|
|||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use leptos::wasm_bindgen::JsCast;
|
use leptos::wasm_bindgen::JsCast;
|
||||||
use leptos::web_sys::{Blob, HtmlAnchorElement, Url, js_sys};
|
use leptos::web_sys::{Blob, HtmlAnchorElement, Url, js_sys};
|
||||||
|
use crate::pages::api::students::{get_student_info_json, get_student_pdf};
|
||||||
|
|
||||||
fn get_important_dates() -> Vec<DateInfo> {
|
fn get_important_dates() -> Vec<DateInfo> {
|
||||||
vec![
|
vec![
|
||||||
@@ -100,6 +101,36 @@ pub fn AdminUtilsPage() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_click_json = move |_| {
|
||||||
|
spawn_local(async move {
|
||||||
|
let (file_name, file_bytes) = get_student_pdf("54d82438-c051-709d-30f7-c60e97f5112a".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let uint_arr = js_sys::Uint8Array::new_with_length(file_bytes.len() as u32);
|
||||||
|
uint_arr.copy_from(&file_bytes);
|
||||||
|
|
||||||
|
let blob_parts = js_sys::Array::new();
|
||||||
|
blob_parts.push(&uint_arr);
|
||||||
|
|
||||||
|
let blob = Blob::new_with_u8_array_sequence(&blob_parts).unwrap();
|
||||||
|
|
||||||
|
let blob_url = Url::create_object_url_with_blob(&blob).unwrap();
|
||||||
|
|
||||||
|
let window = window();
|
||||||
|
let document = window.document().unwrap();
|
||||||
|
let a: HtmlAnchorElement = document.create_element("a").unwrap().dyn_into().unwrap();
|
||||||
|
|
||||||
|
a.set_href(&blob_url);
|
||||||
|
a.set_download(&file_name);
|
||||||
|
document.body().unwrap().append_child(&a).unwrap();
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
document.body().unwrap().remove_child(&a).unwrap();
|
||||||
|
Url::revoke_object_url(&blob_url).unwrap();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="mx-auto px-6">
|
<div class="mx-auto px-6">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
@@ -109,6 +140,7 @@ pub fn AdminUtilsPage() -> impl IntoView {
|
|||||||
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
||||||
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
||||||
<ActionButton on:click=on_click_export>"Scholarship Export"</ActionButton>
|
<ActionButton on:click=on_click_export>"Scholarship Export"</ActionButton>
|
||||||
|
<ActionButton on:click=on_click_json>"Get student JSON"</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
|
|||||||
@@ -435,6 +435,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
leptos::logging::error!("{}", msg);
|
||||||
return Err(ServerFnError::new(msg));
|
return Err(ServerFnError::new(msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,7 +454,6 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
|
|||||||
match client.scan().table_name(COMPARISONS_TABLE).send().await {
|
match client.scan().table_name(COMPARISONS_TABLE).send().await {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
if let Some(items) = output.items {
|
if let Some(items) = output.items {
|
||||||
log!("Found comparisons from API: {:?}", items);
|
|
||||||
Ok(serde_dynamo::from_items(items)?)
|
Ok(serde_dynamo::from_items(items)?)
|
||||||
} else {
|
} else {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
@@ -461,6 +461,7 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
leptos::logging::error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,11 +494,14 @@ pub async fn get_comparisons_categorized()
|
|||||||
|
|
||||||
Ok(categorized)
|
Ok(categorized)
|
||||||
} else {
|
} else {
|
||||||
Err(ServerFnError::new("Couldn't find any comparisons."))
|
let msg = "Couldn't find any comparisons.";
|
||||||
|
leptos::logging::error!("{}", msg);
|
||||||
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
leptos::logging::error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-32
@@ -1,61 +1,60 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use leptos::logging::log;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use crate::common::DateInfo;
|
use crate::common::DateInfo;
|
||||||
|
use leptos::logging::{error, log};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use super::DATES_TABLE;
|
use super::DATES_TABLE;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
use aws_sdk_dynamodb::{
|
|
||||||
types::{WriteRequest, PutRequest},
|
|
||||||
Client as DynamoClient
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use crate::utils::server::create_dynamo_client;
|
use crate::utils::server::create_dynamo_client;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use std::sync::Arc;
|
use aws_sdk_dynamodb::{
|
||||||
|
Client as DynamoClient,
|
||||||
|
error::ProvideErrorMetadata,
|
||||||
|
types::{PutRequest, WriteRequest},
|
||||||
|
};
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> {
|
pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> {
|
||||||
// Serialize and put dates into the database.
|
// Serialize and put dates into the database.
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
log!("Creating dates: {:?}", dates);
|
log!("Creating dates: {:?}", dates);
|
||||||
|
|
||||||
let requests = dates
|
let requests = dates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|date| {
|
.filter_map(|date| {
|
||||||
let item = serde_dynamo::to_item(&date)
|
let item = serde_dynamo::to_item(&date).unwrap_or_default();
|
||||||
.unwrap_or_default();
|
|
||||||
|
let put_request = PutRequest::builder().set_item(Some(item)).build().ok()?;
|
||||||
let put_request = PutRequest::builder()
|
|
||||||
.set_item(Some(item))
|
Some(WriteRequest::builder().put_request(put_request).build())
|
||||||
.build()
|
|
||||||
.expect("Couldn't build request, item is not present");
|
|
||||||
|
|
||||||
WriteRequest::builder().put_request(put_request).build()
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<WriteRequest>>();
|
.collect::<Vec<WriteRequest>>();
|
||||||
|
|
||||||
let mut request_map = HashMap::new();
|
let mut request_map = HashMap::new();
|
||||||
request_map.insert(DATES_TABLE.to_string(), requests);
|
request_map.insert(DATES_TABLE.to_string(), requests);
|
||||||
|
|
||||||
client
|
client
|
||||||
.batch_write_item()
|
.batch_write_item()
|
||||||
.set_request_items(Some(request_map))
|
.set_request_items(Some(request_map))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(ServerFnError::from)
|
.map_err(|err| {
|
||||||
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
|
pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
|
||||||
|
log!("Getting important dates list");
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
client
|
client
|
||||||
.scan()
|
.scan()
|
||||||
.table_name(DATES_TABLE)
|
.table_name(DATES_TABLE)
|
||||||
@@ -65,14 +64,15 @@ pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
|
|||||||
if let Some(items) = output.items {
|
if let Some(items) = output.items {
|
||||||
items
|
items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|item| {
|
.filter_map(|item| serde_dynamo::from_item::<_, DateInfo>(item).ok())
|
||||||
serde_dynamo::from_item::<_, DateInfo>(item)
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.collect::<Vec<DateInfo>>()
|
.collect::<Vec<DateInfo>>()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map_err(ServerFnError::from)
|
.map_err(|err| {
|
||||||
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,6 @@ pub async fn get_scholarship_csv() -> Result<Vec<u8>, ServerFnError> {
|
|||||||
writer.flush()?;
|
writer.flush()?;
|
||||||
|
|
||||||
let result = writer.into_inner()?;
|
let result = writer.into_inner()?;
|
||||||
debug_log!("{:?}", String::from_utf8(result.clone()));
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
+44
-26
@@ -1,4 +1,5 @@
|
|||||||
use leptos::logging::debug_log;
|
use leptos::leptos_dom::error;
|
||||||
|
use leptos::logging::debug_log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use server_fn::codec::{MultipartData, MultipartFormData};
|
use server_fn::codec::{MultipartData, MultipartFormData};
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
|
|||||||
return Err(ServerFnError::new("Missing file contents"));
|
return Err(ServerFnError::new("Missing file contents"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_claims = validate_and_get_token_info(access_token).await?;
|
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
|
||||||
let subject = user_claims.subject;
|
let subject = user_claims.subject;
|
||||||
|
|
||||||
let key = format!("{form_id}/{subject}/{input_name}/{file_name}");
|
let key = format!("{form_id}/{subject}/{input_name}/{file_name}");
|
||||||
@@ -83,10 +84,12 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
|
|||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
ServerFnError::new(format!(
|
let msg = format!(
|
||||||
"Couldn't put file to S3: {}",
|
"Couldn't put file to S3: {}",
|
||||||
e.message().unwrap_or_default()
|
e.message().unwrap_or("Unknown error occurred")
|
||||||
))
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Besides uploading the file to S3, we also want to store a FILE entry in Dynamo.
|
// Besides uploading the file to S3, we also want to store a FILE entry in Dynamo.
|
||||||
@@ -113,11 +116,13 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
|
|||||||
.item("file_key".to_string(), AttributeValue::S(key.clone()))
|
.item("file_key".to_string(), AttributeValue::S(key.clone()))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|err| {
|
||||||
ServerFnError::new(format!(
|
let msg = format!(
|
||||||
"Couldn't put file to Dynamo, file rolled back: {}",
|
"Couldn't put file to Dynamo, file rolled back: {}",
|
||||||
e.message().unwrap_or_default()
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
))
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
});
|
});
|
||||||
|
|
||||||
// If Dynamo fails, we want to handle the error by rolling back the file. Then, we return a failure.
|
// If Dynamo fails, we want to handle the error by rolling back the file. Then, we return a failure.
|
||||||
@@ -128,11 +133,13 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
|
|||||||
.key(&key)
|
.key(&key)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|err| {
|
||||||
ServerFnError::new(format!(
|
let msg = format!(
|
||||||
"Couldn't roll back S3 operation: {}",
|
"Failed to rollback Dynamo entry: {}",
|
||||||
e.message().unwrap_or_default()
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
))
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
return Err(err);
|
return Err(err);
|
||||||
@@ -150,7 +157,7 @@ pub async fn delete_file(
|
|||||||
) -> Result<String, ServerFnError> {
|
) -> Result<String, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let user_claims = validate_and_get_token_info(access_token).await?;
|
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
|
||||||
let subject = user_claims.subject;
|
let subject = user_claims.subject;
|
||||||
|
|
||||||
let entry_hk = format!("STUDENT#{subject}");
|
let entry_hk = format!("STUDENT#{subject}");
|
||||||
@@ -179,7 +186,14 @@ pub async fn delete_file(
|
|||||||
.key(key)
|
.key(key)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(ServerFnError::from);
|
.map_err(|err| {
|
||||||
|
let msg = format!(
|
||||||
|
"Failed to put file into S3: {}",
|
||||||
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
});
|
||||||
|
|
||||||
if let Err(err) = s3_result {
|
if let Err(err) = s3_result {
|
||||||
// Add the key back to Dynamo and return an error.
|
// Add the key back to Dynamo and return an error.
|
||||||
@@ -190,10 +204,12 @@ pub async fn delete_file(
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
ServerFnError::new(format!(
|
let msg = format!(
|
||||||
"Couldn't roll back Dynamo operation: {}",
|
"Failed to rollback Dynamo entry: {}",
|
||||||
err.message().unwrap_or_default()
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
))
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
return Err(err);
|
return Err(err);
|
||||||
@@ -210,7 +226,7 @@ pub async fn list_files(
|
|||||||
) -> Result<Vec<String>, ServerFnError> {
|
) -> Result<Vec<String>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let user_claims = validate_and_get_token_info(access_token).await?;
|
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
|
||||||
let subject = user_claims.subject;
|
let subject = user_claims.subject;
|
||||||
|
|
||||||
let entry_hk = format!("STUDENT#{subject}");
|
let entry_hk = format!("STUDENT#{subject}");
|
||||||
@@ -242,10 +258,12 @@ pub async fn list_files(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
})
|
})
|
||||||
.map_err(|e| {
|
.map_err(|err| {
|
||||||
ServerFnError::new(format!(
|
let msg = format!(
|
||||||
"Couldn't list files: {}",
|
"Failed to get files: {}",
|
||||||
e.message().unwrap_or_default()
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
))
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,11 @@ mod admin;
|
|||||||
mod comparisons;
|
mod comparisons;
|
||||||
mod dates;
|
mod dates;
|
||||||
pub mod exports;
|
pub mod exports;
|
||||||
|
pub mod files;
|
||||||
mod providers;
|
mod providers;
|
||||||
mod scholarships;
|
mod scholarships;
|
||||||
pub mod students;
|
pub mod students;
|
||||||
pub mod files;
|
pub(crate) mod tokens;
|
||||||
mod tokens;
|
|
||||||
|
|
||||||
pub use admin::*;
|
pub use admin::*;
|
||||||
pub use comparisons::*;
|
pub use comparisons::*;
|
||||||
|
|||||||
+23
-12
@@ -1,8 +1,8 @@
|
|||||||
use leptos::prelude::*;
|
use crate::common::ValueType;
|
||||||
use leptos::logging::debug_log;
|
use leptos::logging::{debug_log, error};
|
||||||
use std::collections::HashMap;
|
use leptos::prelude::*;
|
||||||
use leptos::server_fn::codec::Json;
|
use leptos::server_fn::codec::Json;
|
||||||
use crate::common::ValueType;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
||||||
@@ -12,8 +12,8 @@ static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
|||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
|
pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
|
||||||
use aws_sdk_dynamodb::types::AttributeValue;
|
|
||||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
use aws_sdk_dynamodb::types::AttributeValue;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
@@ -31,29 +31,39 @@ pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueTyp
|
|||||||
return Ok(HashMap::new());
|
return Ok(HashMap::new());
|
||||||
};
|
};
|
||||||
|
|
||||||
let map = item.iter().map(|(key, val)| {
|
let map = item
|
||||||
let val_type = ValueType::from(val);
|
.iter()
|
||||||
|
.map(|(key, val)| {
|
||||||
|
let val_type = ValueType::from(val);
|
||||||
|
|
||||||
(key.clone(), val_type)
|
(key.clone(), val_type)
|
||||||
}).collect::<HashMap<String, ValueType>>();
|
})
|
||||||
|
.collect::<HashMap<String, ValueType>>();
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||||
|
error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(input = Json)]
|
#[server(input = Json)]
|
||||||
pub async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
|
pub async fn put_provider_contact(
|
||||||
|
id: String,
|
||||||
|
contact_info: HashMap<String, ValueType>,
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
let mut item = into_attr_map(contact_info);
|
let mut item = into_attr_map(contact_info);
|
||||||
item.insert("subject".to_owned(), aws_sdk_dynamodb::types::AttributeValue::S(id));
|
item.insert(
|
||||||
|
"subject".to_owned(),
|
||||||
|
aws_sdk_dynamodb::types::AttributeValue::S(id),
|
||||||
|
);
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.put_item()
|
.put_item()
|
||||||
@@ -65,7 +75,8 @@ pub async fn put_provider_contact(id: String, contact_info: HashMap<String, Valu
|
|||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||||
|
error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
mod imports {
|
mod imports {
|
||||||
pub use leptos::logging::debug_log;
|
|
||||||
pub use aws_sdk_dynamodb::{
|
|
||||||
types::AttributeValue,
|
|
||||||
error::ProvideErrorMetadata,
|
|
||||||
};
|
|
||||||
pub use leptos::leptos_dom::log;
|
|
||||||
pub use uuid::Uuid;
|
|
||||||
pub use crate::utils::server::{create_dynamo_client};
|
|
||||||
pub use crate::common::ValueType;
|
|
||||||
pub use super::super::SCHOLARSHIPS_TABLE;
|
pub use super::super::SCHOLARSHIPS_TABLE;
|
||||||
|
pub use crate::common::ValueType;
|
||||||
|
pub use crate::utils::server::create_dynamo_client;
|
||||||
|
pub use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue};
|
||||||
|
pub use leptos::logging::{debug_log, error, log};
|
||||||
|
pub use uuid::Uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::common::ExpandableInfo;
|
use crate::common::ExpandableInfo;
|
||||||
@@ -20,7 +16,7 @@ use leptos::server_fn::codec::Json;
|
|||||||
#[server(GetScholarshipInfo)]
|
#[server(GetScholarshipInfo)]
|
||||||
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
// Perform the operation - we just want to return all data that's contained in this entry,
|
// Perform the operation - we just want to return all data that's contained in this entry,
|
||||||
@@ -39,11 +35,14 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
|
|||||||
Ok(serde_dynamo::from_item(item)?)
|
Ok(serde_dynamo::from_item(item)?)
|
||||||
} else {
|
} else {
|
||||||
log!("Couldn't find scholarship with ID {:?}", id);
|
log!("Couldn't find scholarship with ID {:?}", id);
|
||||||
Err(ServerFnError::new("Couldn't find scholarship with given ID."))
|
Err(ServerFnError::new(
|
||||||
|
"Couldn't find scholarship with given ID.",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,12 +50,15 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
|
|||||||
|
|
||||||
#[server(CreateScholarshipInfo, input = Json)]
|
#[server(CreateScholarshipInfo, input = Json)]
|
||||||
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
||||||
use imports::*;
|
|
||||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
log!("Creating or updating scholarship with ID {:?}", info.subject);
|
log!(
|
||||||
|
"Creating or updating scholarship with ID {:?}",
|
||||||
|
info.subject
|
||||||
|
);
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.put_item()
|
.put_item()
|
||||||
@@ -68,6 +70,7 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
|
|||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,24 +79,20 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
|
|||||||
#[server(GetAllScholarshipInfo)]
|
#[server(GetAllScholarshipInfo)]
|
||||||
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
debug_log!("Getting all scholarship info");
|
debug_log!("Getting all scholarship info");
|
||||||
match client
|
match client.scan().table_name(SCHOLARSHIPS_TABLE).send().await {
|
||||||
.scan()
|
|
||||||
.table_name(SCHOLARSHIPS_TABLE)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
if let Some(items) = output.items {
|
if let Some(items) = output.items {
|
||||||
Ok(serde_dynamo::from_items(items)?)
|
Ok(serde_dynamo::from_items(items)?)
|
||||||
} else {
|
} else {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,17 +100,25 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
|
|||||||
|
|
||||||
/// Gets all scholarships that are associated with the given scholarship provider's ID.
|
/// Gets all scholarships that are associated with the given scholarship provider's ID.
|
||||||
#[server(GetProviderScholarships)]
|
#[server(GetProviderScholarships)]
|
||||||
pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
pub async fn get_provider_scholarships(
|
||||||
|
provider_id: String,
|
||||||
|
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
debug_log!("Getting provider scholarships for provider with ID {:?}", provider_id);
|
debug_log!(
|
||||||
|
"Getting provider scholarships for provider with ID {:?}",
|
||||||
|
provider_id
|
||||||
|
);
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.scan()
|
.scan()
|
||||||
.table_name(SCHOLARSHIPS_TABLE)
|
.table_name(SCHOLARSHIPS_TABLE)
|
||||||
.expression_attribute_values(":id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?)
|
.expression_attribute_values(
|
||||||
|
":id",
|
||||||
|
serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?,
|
||||||
|
)
|
||||||
.filter_expression("provider_id = :id")
|
.filter_expression("provider_id = :id")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -124,7 +131,8 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,14 +142,20 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
|
|||||||
#[server(RegisterScholarship)]
|
#[server(RegisterScholarship)]
|
||||||
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
|
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
debug_log!("Creating scholarship for provider with ID {:?}", provider_id);
|
debug_log!(
|
||||||
|
"Creating scholarship for provider with ID {:?}",
|
||||||
|
provider_id
|
||||||
|
);
|
||||||
|
|
||||||
let mut current_uuid = Uuid::new_v4().to_string();
|
let mut current_uuid = Uuid::new_v4().to_string();
|
||||||
let mut item = ExpandableInfo::new(current_uuid.clone());
|
let mut item = ExpandableInfo::new(current_uuid.clone());
|
||||||
item.data.insert("provider_id".to_string(), ValueType::String(Some(provider_id)));
|
item.data.insert(
|
||||||
|
"provider_id".to_string(),
|
||||||
|
ValueType::String(Some(provider_id)),
|
||||||
|
);
|
||||||
loop {
|
loop {
|
||||||
let ser_item = serde_dynamo::to_item(&item)?;
|
let ser_item = serde_dynamo::to_item(&item)?;
|
||||||
match client
|
match client
|
||||||
@@ -165,10 +179,12 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
|
|||||||
log!("Current error: {:?}", err);
|
log!("Current error: {:?}", err);
|
||||||
current_uuid = Uuid::new_v4().to_string();
|
current_uuid = Uuid::new_v4().to_string();
|
||||||
item.subject = current_uuid.clone();
|
item.subject = current_uuid.clone();
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ServerFnError::new(err.message().unwrap_or("An unknown error occurred")));
|
return Err(ServerFnError::new(
|
||||||
|
err.message().unwrap_or("Unknown error occurred"),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,41 +194,51 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
|
|||||||
|
|
||||||
/// Deletes a provider's scholarship given their provider ID and scholarship ID.
|
/// Deletes a provider's scholarship given their provider ID and scholarship ID.
|
||||||
#[server(DeleteProviderScholarship)]
|
#[server(DeleteProviderScholarship)]
|
||||||
pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: String) -> Result<(), ServerFnError> {
|
pub async fn delete_provider_scholarship(
|
||||||
|
provider_id: String,
|
||||||
|
scholarship_id: String,
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
debug_log!("Deleting scholarship with ID {:?} for provider with ID {:?}", scholarship_id, provider_id);
|
debug_log!(
|
||||||
|
"Deleting scholarship with ID {:?} for provider with ID {:?}",
|
||||||
|
scholarship_id,
|
||||||
|
provider_id
|
||||||
|
);
|
||||||
|
|
||||||
// When we delete a scholarship, we need to ensure that the provider's ID matches the scholarship,
|
// When we delete a scholarship, we need to ensure that the provider's ID matches the scholarship,
|
||||||
// otherwise everyone can delete anyone else's scholarships.
|
// otherwise everyone can delete anyone else's scholarships.
|
||||||
match client
|
match client
|
||||||
.delete_item()
|
.delete_item()
|
||||||
.table_name(SCHOLARSHIPS_TABLE)
|
.table_name(SCHOLARSHIPS_TABLE)
|
||||||
.key("subject", AttributeValue::S(scholarship_id))
|
.key("subject", AttributeValue::S(scholarship_id))
|
||||||
.expression_attribute_values(":provider_id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?)
|
.expression_attribute_values(
|
||||||
|
":provider_id",
|
||||||
|
serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?,
|
||||||
|
)
|
||||||
.condition_expression("provider_id = :provider_id")
|
.condition_expression("provider_id = :provider_id")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => Ok(()),
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
pub async fn get_all_scholarships()
|
||||||
|
-> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
client
|
client
|
||||||
.scan()
|
.scan()
|
||||||
.table_name(SCHOLARSHIPS_TABLE)
|
.table_name(SCHOLARSHIPS_TABLE)
|
||||||
@@ -222,12 +248,20 @@ pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<Stri
|
|||||||
let Some(items) = output.items else {
|
let Some(items) = output.items else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
items.iter().map(|item| {
|
items
|
||||||
// Each item should be simply converted from an AttributeValue to a ValueType.
|
.iter()
|
||||||
item.iter().map(|(k, v)| (k.clone(), ValueType::from(v)))
|
.map(|item| {
|
||||||
.collect::<HashMap<String, ValueType>>()
|
// Each item should be simply converted from an AttributeValue to a ValueType.
|
||||||
}).collect::<Vec<_>>()
|
item.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||||
|
.collect::<HashMap<String, ValueType>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.map_err(|err| {
|
||||||
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
})
|
})
|
||||||
.map_err(ServerFnError::from)
|
|
||||||
}
|
}
|
||||||
+603
-37
@@ -1,38 +1,45 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::server_fn::codec::Json;
|
use leptos::server_fn::codec::Json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
mod imports {
|
mod imports {
|
||||||
pub use crate::utils::server::*;
|
|
||||||
pub use aws_sdk_dynamodb::types::AttributeValue;
|
|
||||||
pub use super::super::MAIN_TABLE_NAME;
|
pub use super::super::MAIN_TABLE_NAME;
|
||||||
pub use crate::common::ValueType;
|
pub use crate::common::ValueType;
|
||||||
|
pub use crate::pages::api::tokens::validate_and_get_token_info;
|
||||||
|
pub use crate::utils::server::*;
|
||||||
|
pub use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
pub use aws_sdk_dynamodb::types::AttributeValue;
|
||||||
|
pub use leptos::logging::{debug_log, error};
|
||||||
|
pub use leptos::serde_json;
|
||||||
pub use std::collections::HashMap;
|
pub use std::collections::HashMap;
|
||||||
pub use leptos::logging::debug_log;
|
pub use std::process::Command;
|
||||||
|
pub use zip::write::SimpleFileOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PDF_TEMPLATE: &str = include_str!("../../../pdf_template.typ");
|
||||||
|
|
||||||
#[server(input = Json)]
|
#[server(input = Json)]
|
||||||
pub async fn put_student_data(
|
pub async fn put_student_data(
|
||||||
subject: String,
|
subject: String,
|
||||||
data_type: String,
|
data_type: String,
|
||||||
data_map: std::collections::HashMap<String, crate::common::ValueType>
|
data_map: HashMap<String, crate::common::ValueType>,
|
||||||
) -> Result<(), ServerFnError> {
|
) -> Result<(), ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
let mut data_map_attr = into_attr_map(data_map);
|
let mut data_map_attr = into_attr_map(data_map);
|
||||||
data_map_attr.insert(
|
data_map_attr.insert(
|
||||||
"HK".into(),
|
"HK".into(),
|
||||||
AttributeValue::S(format!("STUDENT#{}", subject))
|
AttributeValue::S(format!("STUDENT#{}", subject)),
|
||||||
);
|
);
|
||||||
data_map_attr.insert(
|
data_map_attr.insert("SK".into(), AttributeValue::S(data_type.to_uppercase()));
|
||||||
"SK".into(),
|
|
||||||
AttributeValue::S(data_type.to_uppercase())
|
|
||||||
);
|
|
||||||
|
|
||||||
debug_log!("Inserting this item: {:?}", data_map_attr);
|
debug_log!("Inserting this item: {:?}", data_map_attr);
|
||||||
|
|
||||||
client
|
client
|
||||||
.put_item()
|
.put_item()
|
||||||
.table_name(MAIN_TABLE_NAME)
|
.table_name(MAIN_TABLE_NAME)
|
||||||
@@ -40,18 +47,25 @@ pub async fn put_student_data(
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(ServerFnError::from)
|
.map_err(|err| {
|
||||||
|
let msg = format!(
|
||||||
|
"Couldn't put item into Dynamo: {}",
|
||||||
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_student_data(
|
pub async fn get_student_data(
|
||||||
subject: String,
|
subject: String,
|
||||||
data_type: String,
|
data_type: String,
|
||||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
) -> Result<HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
client
|
client
|
||||||
.query()
|
.query()
|
||||||
.table_name(MAIN_TABLE_NAME)
|
.table_name(MAIN_TABLE_NAME)
|
||||||
@@ -65,37 +79,42 @@ pub async fn get_student_data(
|
|||||||
let Some(list) = output.items else {
|
let Some(list) = output.items else {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
let _ = list
|
let _ = list.into_iter().flatten().for_each(|(k, v)| {
|
||||||
.into_iter()
|
map.insert(k, ValueType::from(&v));
|
||||||
.flatten()
|
});
|
||||||
.for_each(|(k, v)| {
|
|
||||||
map.insert(k, ValueType::from(&v));
|
|
||||||
});
|
|
||||||
|
|
||||||
map
|
map
|
||||||
})
|
})
|
||||||
.map_err(ServerFnError::from)
|
.map_err(|err| {
|
||||||
|
let msg = format!(
|
||||||
|
"Couldn't get item from Dynamo: {}",
|
||||||
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets and flattens all of a student's information from the database, regardless of their defined
|
/// Gets and flattens all of a student's information from the database, regardless of their defined
|
||||||
/// sort key.
|
/// sort key.
|
||||||
///
|
///
|
||||||
/// Note: if sorted data from the `put_student_data` function contains the same fields, this
|
/// Note: if sorted data from the `put_student_data` function contains the same fields, this
|
||||||
/// function will have unexpected behavior. For example, if a `first_name` field appears in multiple
|
/// function will have unexpected behavior. For example, if a `first_name` field appears in multiple
|
||||||
/// locations in the database, all for this single student, they will overwrite each other in no
|
/// locations in the database, all for this single student, they will overwrite each other in no
|
||||||
/// specific order.
|
/// specific order.
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_all_student_data(
|
pub async fn get_all_student_data(
|
||||||
subject: String
|
subject: String,
|
||||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
) -> Result<HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
client.query()
|
client
|
||||||
|
.query()
|
||||||
.table_name(MAIN_TABLE_NAME)
|
.table_name(MAIN_TABLE_NAME)
|
||||||
.key_condition_expression("HK = :hk")
|
.key_condition_expression("HK = :hk")
|
||||||
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
|
||||||
@@ -105,10 +124,557 @@ pub async fn get_all_student_data(
|
|||||||
let Some(items) = output.items else {
|
let Some(items) = output.items else {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
items.iter().flatten()
|
items
|
||||||
.map(|(|k, v)| (k.clone(), ValueType::from(v)))
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||||
.collect::<HashMap<String, ValueType>>()
|
.collect::<HashMap<String, ValueType>>()
|
||||||
})
|
})
|
||||||
.map_err(ServerFnError::from)
|
.map_err(|err| {
|
||||||
|
let msg = format!(
|
||||||
|
"Couldn't get data from Dynamo: {}",
|
||||||
|
err.message().unwrap_or("Unknown error occurred")
|
||||||
|
);
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn provider_get_completed_students(
|
||||||
|
access_token: String,
|
||||||
|
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let claims =
|
||||||
|
validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
|
||||||
|
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
|
||||||
|
return Err(ServerFnError::new(
|
||||||
|
"User is not in the ScholarshipProviders group",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
get_completed_students().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn admin_get_completed_students(
|
||||||
|
access_token: String,
|
||||||
|
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?;
|
||||||
|
|
||||||
|
get_completed_students().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn get_completed_students()
|
||||||
|
-> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
// We want to get all student information. The requirements are that the students have completed
|
||||||
|
// the demographics form - everything else may bed from this.
|
||||||
|
// The easiest way is to get all the information and filter on this side, instead of bookkeeping
|
||||||
|
// on the database's side.
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.scan()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(items) = response.items else {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output = HashMap::<String, HashMap<String, ValueType>>::new();
|
||||||
|
|
||||||
|
items.into_iter().for_each(|mut form_info| {
|
||||||
|
let student_id_full = form_info
|
||||||
|
.get("HK")
|
||||||
|
.map(|v| v.as_s().cloned().unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let student_id = student_id_full.split("STUDENT#").collect::<String>();
|
||||||
|
|
||||||
|
form_info.remove("HK");
|
||||||
|
form_info.remove("SK");
|
||||||
|
|
||||||
|
let form_info_convert = form_info
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, ValueType::from(&v)))
|
||||||
|
.collect::<HashMap<String, ValueType>>();
|
||||||
|
|
||||||
|
// We want to insert all the remaining information into the output map
|
||||||
|
output
|
||||||
|
.entry(student_id)
|
||||||
|
.and_modify(|v| {
|
||||||
|
v.extend(form_info_convert.clone());
|
||||||
|
})
|
||||||
|
.or_insert(form_info_convert);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't love this, but it does verify that the student has completed the demographics form
|
||||||
|
let output = output
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(k, student_info)| {
|
||||||
|
student_info
|
||||||
|
.get("first_name")
|
||||||
|
.and_then(|_| Some((k, student_info.clone())))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get File by Key API
|
||||||
|
/// This function gets a file from the corresponding S3 file key. If the subject found from the
|
||||||
|
/// `access_token` does not match the owner of the requested file, the requesting user must have
|
||||||
|
/// provider-level access or higher.
|
||||||
|
///
|
||||||
|
/// ## Possible errors
|
||||||
|
/// Please check the [`validate_and_get_token_info`] function for all token parsing errors. Otherwise,
|
||||||
|
/// if the requesting user does not have the correct permissions, it will return a message indicating
|
||||||
|
/// such. If the file is not found, an error containing "File not found" will be returned.
|
||||||
|
#[server]
|
||||||
|
pub async fn get_file_by_key(
|
||||||
|
access_token: String,
|
||||||
|
file_key: String,
|
||||||
|
) -> Result<Vec<u8>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let user_claims =
|
||||||
|
validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
|
||||||
|
|
||||||
|
// Check if the user's subject is contained in the file_key (since all files are keyed by the
|
||||||
|
// user's subject)
|
||||||
|
if !file_key.contains(&user_claims.subject) {
|
||||||
|
// Check that the user is a provider.
|
||||||
|
if !user_claims
|
||||||
|
.groups
|
||||||
|
.contains(&"ScholarshipProviders".to_string())
|
||||||
|
{
|
||||||
|
return Err(ServerFnError::new(
|
||||||
|
"Access denied: user is not the student or a provider",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We now just need to get the actual file.
|
||||||
|
let client = aws_sdk_s3::Client::new(&create_aws_config().await);
|
||||||
|
|
||||||
|
let result = client
|
||||||
|
.get_object()
|
||||||
|
.key(file_key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let bytes = result.body.collect().await?;
|
||||||
|
|
||||||
|
Ok(bytes.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get Student File Names API
|
||||||
|
/// Gets a list of file names given a student's ID and the corresponding input ID from the forms.
|
||||||
|
/// These file names can be used to construct a file key for S3.
|
||||||
|
///
|
||||||
|
/// This API is only accessible by provider-level users.
|
||||||
|
#[server]
|
||||||
|
pub async fn get_student_files(
|
||||||
|
access_token: String,
|
||||||
|
student_id: String,
|
||||||
|
form_name: String,
|
||||||
|
question_id: String,
|
||||||
|
file_name_postfix: String,
|
||||||
|
) -> Result<(String, Vec<u8>), ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
match validate_and_get_token_info(access_token.clone(), "us-east-1_Lfjuy5zaM", "us-east-1")
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(claims) => {
|
||||||
|
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
|
||||||
|
return Err(ServerFnError::new("Access denied: user is not a provider"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let demographics_res = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.expression_attribute_values(":sk", AttributeValue::S("DEMOGRAPHICS".to_string()))
|
||||||
|
.key_condition_expression("HK = :hk and SK = :sk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let student_demographics = demographics_res
|
||||||
|
.items
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(ServerFnError::new(
|
||||||
|
"Failed to find student demographic information.",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let first_name = student_demographics
|
||||||
|
.get("first_name")
|
||||||
|
.and_then(|v| v.as_s().ok().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let last_name = student_demographics
|
||||||
|
.get("last_name")
|
||||||
|
.and_then(|v| v.as_s().ok().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let file_keys = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.expression_attribute_values(
|
||||||
|
":sk",
|
||||||
|
AttributeValue::S(format!("FILE#{form_name}#{question_id}")),
|
||||||
|
)
|
||||||
|
.key_condition_expression("HK = :hk and begins_with(SK, :sk)")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
|
.map(|output| {
|
||||||
|
let Some(items) = output.items else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|item| item.get("file_key")?.as_s().ok().cloned())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
debug_log!("Await student file futures...");
|
||||||
|
let futures: Vec<_> = file_keys
|
||||||
|
.into_iter()
|
||||||
|
.map(|file_key| async move {
|
||||||
|
debug_log!("Requesting file from API with key {file_key}");
|
||||||
|
// Get all files from S3. This is a batch operation.
|
||||||
|
let s3_client = aws_sdk_s3::Client::new(&create_aws_config().await);
|
||||||
|
let file_output = s3_client
|
||||||
|
.get_object()
|
||||||
|
.bucket("leptos-scholarships")
|
||||||
|
.key(&file_key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let bytes = file_output.body.collect().await.ok()?.to_vec();
|
||||||
|
|
||||||
|
Some((file_key, bytes))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let results = futures::future::join_all(futures)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<(String, Vec<u8>)>>();
|
||||||
|
|
||||||
|
let cur = std::io::Cursor::new(Vec::new());
|
||||||
|
let mut writer = zip::ZipWriter::new(cur);
|
||||||
|
|
||||||
|
debug_log!("Writing {} files to zip file...", results.len());
|
||||||
|
for (key, file_bytes) in results {
|
||||||
|
let file_name = key
|
||||||
|
.split('/')
|
||||||
|
.last()
|
||||||
|
.ok_or(ServerFnError::new("Failed to parse file name"))?
|
||||||
|
.split(".")
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let file_ext = key
|
||||||
|
.split('.')
|
||||||
|
.last()
|
||||||
|
.ok_or(ServerFnError::new("Failed to parse file extension"))?;
|
||||||
|
|
||||||
|
// Construct the real file name. This name should be under 150 characters.
|
||||||
|
let temp = format!("{first_name} {last_name} - {file_name}");
|
||||||
|
let file_name_no_ext = temp.chars().take(150).collect::<String>();
|
||||||
|
let file_name_complete = format!("{file_name_no_ext}.{file_ext}");
|
||||||
|
|
||||||
|
writer
|
||||||
|
.start_file(file_name_complete, SimpleFileOptions::default())
|
||||||
|
.map_err(ServerFnError::new)?;
|
||||||
|
writer.write_all(&file_bytes).map_err(ServerFnError::new)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_log!("Getting finished file");
|
||||||
|
let finished = writer.finish().map_err(ServerFnError::new)?;
|
||||||
|
|
||||||
|
let file_name_hint = format!("{first_name}{last_name}_{file_name_postfix}.zip");
|
||||||
|
|
||||||
|
Ok((file_name_hint, finished.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Get All Input Files API
|
||||||
|
/// Gets all files that have been uploaded to the given input. For example, getting all the FAFSA
|
||||||
|
/// files that were uploaded to the FAFSA file input on the Financial Info page.
|
||||||
|
///
|
||||||
|
/// Returns a `HashMap` keyed by the submitting user's ID, with a value of all files that were
|
||||||
|
/// uploaded by that user.
|
||||||
|
///
|
||||||
|
/// This is only usable by users with provider-level access.
|
||||||
|
#[server]
|
||||||
|
async fn get_all_input_files(
|
||||||
|
form_name: String,
|
||||||
|
input_name: String,
|
||||||
|
) -> Result<HashMap<String, Vec<String>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.scan()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.filter_expression("begins_with(SK, :sk)")
|
||||||
|
.expression_attribute_values(
|
||||||
|
":sk",
|
||||||
|
AttributeValue::S(format!("FILE#{form_name}#{input_name}")),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut result_map = HashMap::<String, Vec<String>>::new();
|
||||||
|
res.items.unwrap_or_default().iter().for_each(|item| {
|
||||||
|
let id = item
|
||||||
|
.get("HK")
|
||||||
|
.map(|v| v.as_s().cloned().unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.split("STUDENT#")
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let file_key = item
|
||||||
|
.get("file_key")
|
||||||
|
.map(|v| v.as_s().cloned().unwrap_or_default())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
result_map
|
||||||
|
.entry(id)
|
||||||
|
.and_modify(|v| v.push(file_key.clone()))
|
||||||
|
.or_insert(vec![file_key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(result_map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn admin_get_all_input_files(
|
||||||
|
access_token: String,
|
||||||
|
form_name: String,
|
||||||
|
input_name: String,
|
||||||
|
) -> Result<HashMap<String, Vec<String>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?;
|
||||||
|
|
||||||
|
get_all_input_files(form_name, input_name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn provider_get_all_input_files(
|
||||||
|
access_token: String,
|
||||||
|
form_name: String,
|
||||||
|
input_name: String,
|
||||||
|
) -> Result<HashMap<String, Vec<String>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let claims =
|
||||||
|
validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
|
||||||
|
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
|
||||||
|
return Err(ServerFnError::new("Access denied: user is not a provider"));
|
||||||
|
}
|
||||||
|
|
||||||
|
get_all_input_files(form_name, input_name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_student_pdf(student_id: String) -> Result<(String, Vec<u8>), ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
// NOTE: this API requires that the server has the typst-cli available ON PATH.
|
||||||
|
|
||||||
|
let student_info_str = get_student_info_json(student_id.clone()).await?;
|
||||||
|
let student_info = get_student_data(student_id, "DEMOGRAPHICS".to_string()).await?;
|
||||||
|
|
||||||
|
let first_name = student_info
|
||||||
|
.get("first_name")
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let last_name = student_info
|
||||||
|
.get("last_name")
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let typst_string = student_info_str
|
||||||
|
.replace("{", "(")
|
||||||
|
.replace("}", ")")
|
||||||
|
.replace("[", "(")
|
||||||
|
.replace("]", ")")
|
||||||
|
// Ensures trailing commas for lists of dictionaries
|
||||||
|
.replace("))", "),)")
|
||||||
|
.replace("\")", "\",)");
|
||||||
|
|
||||||
|
debug_log!("New string: {}", typst_string);
|
||||||
|
|
||||||
|
let pdf_string = format!("#let student = {typst_string}\n{PDF_TEMPLATE}");
|
||||||
|
debug_log!("PDF string: {}", pdf_string);
|
||||||
|
let mut child = Command::new("typst")
|
||||||
|
.arg("compile")
|
||||||
|
.arg("-")
|
||||||
|
.arg("-")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let stdin = child.stdin.as_mut().unwrap();
|
||||||
|
stdin.write_all(pdf_string.as_bytes())?;
|
||||||
|
stdin.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let msg = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
error!("Failed to generate PDF: {}", msg);
|
||||||
|
Err(ServerFnError::new(format!("Failed to generate PDF: {msg}")))
|
||||||
|
} else {
|
||||||
|
let bytes = output.stdout;
|
||||||
|
let file_name = format!("{first_name}{last_name}_Application.pdf");
|
||||||
|
Ok((file_name, bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_student_info_json(student_id: String) -> Result<String, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.key_condition_expression("HK = :hk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let work_exp = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.expression_attribute_values(":sk", AttributeValue::S("WORKEXP".to_string()))
|
||||||
|
.key_condition_expression("HK = :hk AND SK = :sk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
|
.map(|output| {
|
||||||
|
let items = output.items.unwrap_or_default();
|
||||||
|
debug_log!("Number of items: {}", items.len());
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.get("extracurricular")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(AttributeValue::S("".to_string()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let extracurricular = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.expression_attribute_values(":sk", AttributeValue::S("DEMOGRAPHICS".to_string()))
|
||||||
|
.key_condition_expression("HK = :hk AND SK = :sk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
|
.map(|output| {
|
||||||
|
let items = output.items.unwrap_or_default();
|
||||||
|
debug_log!("Number of items: {}", items.len());
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.get("extracurricular")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(AttributeValue::S("".to_string()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(items) = response.items else {
|
||||||
|
return Err(ServerFnError::new("Failed to find student information."));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Join items together into a single object.
|
||||||
|
let mut data = items.into_iter().fold(HashMap::new(), |mut map, item| {
|
||||||
|
item.into_iter().for_each(|(k, v)| {
|
||||||
|
map.insert(k, v);
|
||||||
|
});
|
||||||
|
map
|
||||||
|
});
|
||||||
|
|
||||||
|
data.insert("work_experience".to_string(), work_exp);
|
||||||
|
data.insert("extracurricular".to_string(), extracurricular);
|
||||||
|
|
||||||
|
let json_value: serde_json::Value = serde_dynamo::from_item(data)?;
|
||||||
|
let json_string = serde_json::to_string(&json_value)?;
|
||||||
|
println!("{}", json_string);
|
||||||
|
|
||||||
|
Ok(json_string)
|
||||||
}
|
}
|
||||||
+26
-18
@@ -1,29 +1,35 @@
|
|||||||
#[cfg(feature = "ssr")]
|
use crate::common::UserClaims;
|
||||||
use jsonwebtoken::{jwk::JwkSet, DecodingKey};
|
#[cfg(feature = "ssr")]
|
||||||
|
use jsonwebtoken::{DecodingKey, jwk::JwkSet};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use crate::common::UserClaims;
|
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn fetch_jwks(user_pool_id: String, region: String) -> Result<JwkSet, ServerFnError> {
|
pub async fn fetch_jwks(user_pool_id: String, region: String) -> Result<JwkSet, ServerFnError> {
|
||||||
let request_url = format!(
|
let request_url = format!(
|
||||||
"https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json",
|
"https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json",
|
||||||
region,
|
region, user_pool_id
|
||||||
user_pool_id
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let jwks = reqwest::get(&request_url).await?
|
let jwks = reqwest::get(&request_url).await?.json::<JwkSet>().await?;
|
||||||
.json::<JwkSet>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(jwks)
|
Ok(jwks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates a JWT, and, if successful, decodes it into a series of user claims.
|
/// Validates a JWT, and, if successful, decodes it into a series of user claims.
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn validate_and_get_token_info(token: String) -> Result<UserClaims, ServerFnError> {
|
pub async fn validate_and_get_token_info(
|
||||||
use jsonwebtoken::{decode, Algorithm, Validation};
|
token: String,
|
||||||
|
pool_id: impl Into<String>,
|
||||||
|
pool_region: impl Into<String>
|
||||||
|
) -> Result<UserClaims, ServerFnError> {
|
||||||
|
use jsonwebtoken::{Algorithm, Validation, decode};
|
||||||
|
|
||||||
let jwks = fetch_jwks("us-east-1_Lfjuy5zaM".into(), "us-east-1".into()).await?;
|
let pool_id = pool_id.into();
|
||||||
|
|
||||||
|
leptos::logging::debug_log!("Validating token using group {}", pool_id);
|
||||||
|
|
||||||
|
// let jwks = fetch_jwks("us-east-1_Lfjuy5zaM".into(), "us-east-1".into()).await?;
|
||||||
|
let jwks = fetch_jwks(pool_id, pool_region.into()).await?;
|
||||||
|
|
||||||
let validation = Validation::new(Algorithm::RS256);
|
let validation = Validation::new(Algorithm::RS256);
|
||||||
// validation.set_audience(&["scholarships-rs"]);
|
// validation.set_audience(&["scholarships-rs"]);
|
||||||
@@ -31,16 +37,18 @@ pub async fn validate_and_get_token_info(token: String) -> Result<UserClaims, Se
|
|||||||
let header = jsonwebtoken::decode_header(&token)
|
let header = jsonwebtoken::decode_header(&token)
|
||||||
.map_err(|e| ServerFnError::new(format!("Invalid token header: {e}")))?;
|
.map_err(|e| ServerFnError::new(format!("Invalid token header: {e}")))?;
|
||||||
|
|
||||||
let kid = header.kid.ok_or_else(|| ServerFnError::new("Missing kid header"))?;
|
let kid = header
|
||||||
|
.kid
|
||||||
|
.ok_or_else(|| ServerFnError::new("Missing kid header"))?;
|
||||||
|
|
||||||
let jwk = jwks.find(&kid)
|
let jwk = jwks
|
||||||
|
.find(&kid)
|
||||||
.ok_or_else(|| ServerFnError::new("Couldn't find matching key in JWKS"))?;
|
.ok_or_else(|| ServerFnError::new("Couldn't find matching key in JWKS"))?;
|
||||||
|
|
||||||
let token_data = decode::<UserClaims>(
|
leptos::logging::debug_log!("Parsing token: {:?}", token);
|
||||||
&token,
|
|
||||||
&DecodingKey::from_jwk(&jwk)?,
|
let token_data = decode::<UserClaims>(&token, &DecodingKey::from_jwk(&jwk)?, &validation)
|
||||||
&validation
|
.map_err(|err| ServerFnError::new(format!("Invalid token found: {err}")))?;
|
||||||
).map_err(|_| ServerFnError::new("Invalid token found".to_string()))?;
|
|
||||||
|
|
||||||
if token_data.claims.token_use != "access" {
|
if token_data.claims.token_use != "access" {
|
||||||
return Err(ServerFnError::new("Token type mismatch"));
|
return Err(ServerFnError::new("Token type mismatch"));
|
||||||
|
|||||||
+1343
-10
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,13 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ClosedForm() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col items-center mx-auto mt-10">
|
||||||
|
<h1 class="text-2xl font-bold">"Form Closed"</h1>
|
||||||
|
<p class="text-lg">
|
||||||
|
"This form is now closed. If you believe this is an error, please contact the guidance office."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use crate::pages::api::files::list_files;
|
|||||||
use crate::pages::api::students::get_all_student_data;
|
use crate::pages::api::students::get_all_student_data;
|
||||||
use crate::pages::api::{get_all_scholarship_info, get_comparison_info};
|
use crate::pages::api::{get_all_scholarship_info, get_comparison_info};
|
||||||
use crate::utils::get_user_claims;
|
use crate::utils::get_user_claims;
|
||||||
use leptos::logging::debug_log;
|
use leptos::logging::{debug_error, debug_log};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_oidc::AuthSignal;
|
use leptos_oidc::AuthSignal;
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
|
|||||||
let auth = expect_context::<AuthSignal>();
|
let auth = expect_context::<AuthSignal>();
|
||||||
let user_claims = get_user_claims();
|
let user_claims = get_user_claims();
|
||||||
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
|
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
|
||||||
|
let access_token = Memo::new(move |_| auth.get().authenticated().map(|a| a.access_token()));
|
||||||
|
|
||||||
let form_id = StoredValue::new("scholarship_essays".to_string());
|
let form_id = StoredValue::new("scholarship_essays".to_string());
|
||||||
|
|
||||||
@@ -63,8 +64,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
|
|||||||
.get("name")
|
.get("name")
|
||||||
.unwrap_or(&ValueType::String(None))
|
.unwrap_or(&ValueType::String(None))
|
||||||
.as_string()
|
.as_string()
|
||||||
.ok()
|
.ok()?
|
||||||
.flatten()
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
debug_log!("Checking scholarship: {}", scholarship_name);
|
debug_log!("Checking scholarship: {}", scholarship_name);
|
||||||
let requirements_map = scholarship
|
let requirements_map = scholarship
|
||||||
@@ -72,8 +72,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
|
|||||||
.get("requirements")
|
.get("requirements")
|
||||||
.unwrap_or(&ValueType::Map(None))
|
.unwrap_or(&ValueType::Map(None))
|
||||||
.as_map()
|
.as_map()
|
||||||
.ok()
|
.ok()?
|
||||||
.flatten()
|
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.values()
|
.values()
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -104,38 +103,50 @@ pub fn StudentEligibilityPage() -> impl IntoView {
|
|||||||
debug_log!(
|
debug_log!(
|
||||||
"Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>()
|
"Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>()
|
||||||
);
|
);
|
||||||
let valid = resolved_requirements
|
let valid = !scholarship_name.is_empty()
|
||||||
.iter()
|
&& resolved_requirements
|
||||||
.all(|list| {
|
.iter()
|
||||||
list.iter()
|
.all(|list| {
|
||||||
.fold(
|
debug_log!("Checking list: {:?}", list);
|
||||||
false,
|
if list.is_empty() {
|
||||||
|prev, requirement| {
|
true
|
||||||
let result = requirement
|
} else {
|
||||||
.compare(&student_info)
|
list.iter()
|
||||||
.unwrap_or(false);
|
.any(|requirement| {
|
||||||
if prev { prev } else { result }
|
let result = requirement.compare(&student_info);
|
||||||
},
|
match result.as_ref() {
|
||||||
)
|
Ok(status) => {
|
||||||
});
|
debug_log!(
|
||||||
|
"Requirement with id {} is Ok({})", requirement.id, status
|
||||||
|
);
|
||||||
|
status.clone()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug_error!(
|
||||||
|
"Requirement with id {} failed: {}", requirement.id, e
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
if valid {
|
if valid {
|
||||||
debug_log!("Scholarship is valid.");
|
debug_log!("Scholarship is valid.");
|
||||||
let scholarship_id = StoredValue::new(scholarship.subject.clone());
|
let scholarship_id = StoredValue::new(scholarship.subject.clone());
|
||||||
let resource = Resource::new(
|
let resource = Resource::new(
|
||||||
move || (
|
move || (
|
||||||
auth
|
access_token.get(),
|
||||||
.try_with(|a| a.authenticated().map(|a| a.access_token()))
|
|
||||||
.flatten(),
|
|
||||||
form_id.get_value(),
|
form_id.get_value(),
|
||||||
scholarship_id.get_value(),
|
scholarship_id.get_value(),
|
||||||
),
|
),
|
||||||
async move |(access_token, form_id, scholarship_id)| {
|
async move |(access_token, form_id, scholarship_id)| {
|
||||||
list_files(
|
let Some(access_token) = access_token else {
|
||||||
access_token.unwrap_or_default(),
|
return Err(
|
||||||
form_id,
|
ServerFnError::new("Couldn't find access token"),
|
||||||
scholarship_id,
|
);
|
||||||
)
|
};
|
||||||
.await
|
list_files(access_token, form_id, scholarship_id).await
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Some((scholarship.clone(), resource))
|
Some((scholarship.clone(), resource))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use leptos::prelude::*;
|
|
||||||
use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm};
|
use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm};
|
||||||
use crate::pages::student::form_setup::use_student_form;
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn StudentFamilyPage() -> impl IntoView {
|
pub fn StudentFamilyPage() -> impl IntoView {
|
||||||
let controller = use_student_form("family", true);
|
let controller = use_student_form("family", true);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||||
<div class="flex flex-1" />
|
<div class="flex flex-1" />
|
||||||
@@ -115,4 +115,4 @@ pub fn StudentFamilyPage() -> impl IntoView {
|
|||||||
<div class="flex flex-1" />
|
<div class="flex flex-1" />
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use crate::common::ValueType;
|
use crate::common::ValueType;
|
||||||
use crate::components::{Toast, ToastContext};
|
use crate::components::{Toast, ToastContext};
|
||||||
use crate::pages::api::students::{get_student_data, PutStudentData};
|
use crate::pages::api::students::{PutStudentData, get_student_data};
|
||||||
use crate::utils::get_user_claims;
|
use crate::utils::get_user_claims;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct StudentFormInfo {
|
pub struct StudentFormInfo {
|
||||||
pub data_map: RwSignal<HashMap<String, ValueType>>,
|
pub data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
@@ -20,22 +20,21 @@ pub fn use_student_form(
|
|||||||
enable_toasts: bool,
|
enable_toasts: bool,
|
||||||
) -> StudentFormInfo {
|
) -> StudentFormInfo {
|
||||||
let form_type = form_type.into();
|
let form_type = form_type.into();
|
||||||
|
|
||||||
let user_claims = get_user_claims();
|
let user_claims = get_user_claims();
|
||||||
let user_id = Memo::new(move |_| {
|
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
|
||||||
user_claims.get()
|
|
||||||
.map(|info| info.claims.subject.clone())
|
|
||||||
});
|
|
||||||
|
|
||||||
let data_map = RwSignal::new(HashMap::new());
|
let data_map = RwSignal::new(HashMap::new());
|
||||||
|
|
||||||
let refresh_trigger = Trigger::new();
|
let refresh_trigger = Trigger::new();
|
||||||
let data_resource = Resource::new(
|
let data_resource = Resource::new(
|
||||||
move || (user_id.get().unwrap_or_default(), form_type.get(), refresh_trigger.track()),
|
move || (user_id.get(), form_type.get(), refresh_trigger.track()),
|
||||||
async move |(id, form_type, _)| get_student_data(
|
async move |(id, form_type, _)| {
|
||||||
id,
|
let Some(id) = id else {
|
||||||
form_type,
|
return Ok(HashMap::new());
|
||||||
).await
|
};
|
||||||
|
get_student_data(id, form_type).await
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Effect::new(move || {
|
Effect::new(move || {
|
||||||
@@ -50,13 +49,15 @@ pub fn use_student_form(
|
|||||||
|
|
||||||
let submit_action = ServerAction::<PutStudentData>::new();
|
let submit_action = ServerAction::<PutStudentData>::new();
|
||||||
let on_submit = move || {
|
let on_submit = move || {
|
||||||
submit_action.dispatch(PutStudentData {
|
if let Some(id) = user_id.get() {
|
||||||
subject: user_id.get().unwrap_or_default(),
|
submit_action.dispatch(PutStudentData {
|
||||||
data_type: form_type.get(),
|
subject: id,
|
||||||
data_map: data_map.get()
|
data_type: form_type.get(),
|
||||||
});
|
data_map: data_map.get(),
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if enable_toasts {
|
if enable_toasts {
|
||||||
let mut toasts = expect_context::<ToastContext>();
|
let mut toasts = expect_context::<ToastContext>();
|
||||||
Effect::watch(
|
Effect::watch(
|
||||||
@@ -74,13 +75,13 @@ pub fn use_student_form(
|
|||||||
Err(e) => Toast::new()
|
Err(e) => Toast::new()
|
||||||
.id(Uuid::new_v4())
|
.id(Uuid::new_v4())
|
||||||
.header("Submission Failed")
|
.header("Submission Failed")
|
||||||
.msg(e.to_string())
|
.msg(e.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
toasts.toast(toast);
|
toasts.toast(toast);
|
||||||
untrack(move || submit_action.clear());
|
untrack(move || submit_action.clear());
|
||||||
},
|
},
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +91,6 @@ pub fn use_student_form(
|
|||||||
data_resource,
|
data_resource,
|
||||||
user_id,
|
user_id,
|
||||||
submit_action: on_submit.into(),
|
submit_action: on_submit.into(),
|
||||||
submit_pending: submit_action.pending()
|
submit_pending: submit_action.pending(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
mod academics_page;
|
mod academics_page;
|
||||||
mod additional_page;
|
mod additional_page;
|
||||||
mod athletics_page;
|
mod athletics_page;
|
||||||
|
mod closed_form;
|
||||||
mod demographics_page;
|
mod demographics_page;
|
||||||
mod eligibility_page;
|
mod eligibility_page;
|
||||||
mod extracurriculars_page;
|
mod extracurriculars_page;
|
||||||
@@ -9,14 +10,14 @@ mod financial_page;
|
|||||||
mod form_setup;
|
mod form_setup;
|
||||||
mod home_page;
|
mod home_page;
|
||||||
mod shell;
|
mod shell;
|
||||||
mod test_display;
|
mod specific_requirements_page;
|
||||||
mod university_page;
|
mod university_page;
|
||||||
mod work_experience_page;
|
mod work_experience_page;
|
||||||
mod specific_requirements_page;
|
|
||||||
|
|
||||||
pub use academics_page::*;
|
pub use academics_page::*;
|
||||||
pub use additional_page::*;
|
pub use additional_page::*;
|
||||||
pub use athletics_page::*;
|
pub use athletics_page::*;
|
||||||
|
pub use closed_form::*;
|
||||||
pub use demographics_page::*;
|
pub use demographics_page::*;
|
||||||
pub use eligibility_page::*;
|
pub use eligibility_page::*;
|
||||||
pub use extracurriculars_page::*;
|
pub use extracurriculars_page::*;
|
||||||
@@ -24,7 +25,6 @@ pub use family_info_page::*;
|
|||||||
pub use financial_page::*;
|
pub use financial_page::*;
|
||||||
pub use home_page::*;
|
pub use home_page::*;
|
||||||
pub use shell::*;
|
pub use shell::*;
|
||||||
pub use test_display::*;
|
pub use specific_requirements_page::*;
|
||||||
pub use university_page::*;
|
pub use university_page::*;
|
||||||
pub use work_experience_page::*;
|
pub use work_experience_page::*;
|
||||||
pub use specific_requirements_page::*;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
use crate::common::{SchemaNode, SchemaType};
|
|
||||||
use crate::components::{Loading, ValueDisplay};
|
|
||||||
use crate::pages::api::students::get_all_student_data;
|
|
||||||
use crate::utils::get_user_claims;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn StudentTestDisplay() -> impl IntoView {
|
|
||||||
let create_student_display_schema = move || {
|
|
||||||
SchemaNode::new(SchemaType::Map)
|
|
||||||
.header("Student Information")
|
|
||||||
.child(
|
|
||||||
"first_name",
|
|
||||||
SchemaNode::new(SchemaType::String).header("First Name"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
"last_name",
|
|
||||||
SchemaNode::new(SchemaType::String).header("Last Name"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
"sports_participation",
|
|
||||||
SchemaNode::new(SchemaType::List)
|
|
||||||
.header("Sports Activities")
|
|
||||||
.item_template(
|
|
||||||
SchemaNode::new(SchemaType::Map)
|
|
||||||
.header("Sport")
|
|
||||||
.child(
|
|
||||||
"sport_name",
|
|
||||||
SchemaNode::new(SchemaType::String).header("Sport Name"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
"achievements",
|
|
||||||
SchemaNode::new(SchemaType::String).header("Special Achievements"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_claims = get_user_claims();
|
|
||||||
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
|
|
||||||
|
|
||||||
let resource = Resource::new(
|
|
||||||
move || user_id.get().unwrap_or_default(),
|
|
||||||
async move |id| get_all_student_data(id).await,
|
|
||||||
);
|
|
||||||
|
|
||||||
let schema = StoredValue::new(create_student_display_schema());
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div class="flex flex-1" />
|
|
||||||
<Suspense fallback=Loading>
|
|
||||||
<div class="flex flex-2 flex-col">
|
|
||||||
{move || {
|
|
||||||
resource
|
|
||||||
.get()
|
|
||||||
.map(|data_map| {
|
|
||||||
let Ok(map) = data_map else {
|
|
||||||
return view! {}.into_any();
|
|
||||||
};
|
|
||||||
let data_map = StoredValue::new(map);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div>
|
|
||||||
<ValueDisplay
|
|
||||||
schema=schema.get_value()
|
|
||||||
data_map=data_map.get_value()
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
|
||||||
<div class="flex flex-1" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user