diff --git a/src/app.rs b/src/app.rs index 9960cae..a9be970 100644 --- a/src/app.rs +++ b/src/app.rs @@ -125,6 +125,7 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone { + diff --git a/src/pages/api/students.rs b/src/pages/api/students.rs index c965d56..da59e23 100644 --- a/src/pages/api/students.rs +++ b/src/pages/api/students.rs @@ -1,15 +1,23 @@ use leptos::prelude::*; +use leptos::server_fn::codec::Json; -#[server] +#[cfg(feature = "ssr")] +mod imports { + pub use crate::utils::server::*; + pub use aws_sdk_dynamodb::types::AttributeValue; + pub use super::super::MAIN_TABLE_NAME; + pub use crate::common::ValueType; + pub use std::collections::HashMap; +} + +#[server(input = Json)] pub async fn put_student_data( subject: String, data_type: String, data_map: std::collections::HashMap ) -> Result<(), ServerFnError> { use leptos::logging::log; - use crate::utils::server::{create_dynamo_client, into_attr_map}; - use aws_sdk_dynamodb::types::AttributeValue; - use super::MAIN_TABLE_NAME; + use imports::*; let client = create_dynamo_client().await; @@ -40,11 +48,7 @@ pub async fn get_student_data( subject: String, data_type: String, ) -> Result, ServerFnError> { - use crate::utils::server::create_dynamo_client; - use std::collections::HashMap; - use crate::common::ValueType; - use aws_sdk_dynamodb::types::AttributeValue; - use super::MAIN_TABLE_NAME; + use imports::*; let client = create_dynamo_client().await; @@ -75,3 +79,36 @@ pub async fn get_student_data( }) .map_err(ServerFnError::from) } + +/// Gets and flattens all of a student's information from the database, regardless of their defined +/// sort key. +/// +/// 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 +/// locations in the database, all for this single student, they will overwrite each other in no +/// specific order. +#[server] +pub async fn get_all_student_data( + subject: String +) -> Result, ServerFnError> { + use imports::*; + + let client = create_dynamo_client().await; + + client.query() + .table_name(MAIN_TABLE_NAME) + .key_condition_expression("HK = :hk") + .expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject))) + .send() + .await + .map(|output| { + let Some(items) = output.items else { + return HashMap::new(); + }; + + items.iter().flatten() + .map(|(|k, v)| (k.clone(), ValueType::from(v))) + .collect::>() + }) + .map_err(ServerFnError::from) +} diff --git a/src/pages/comparison_test.rs b/src/pages/comparison_test.rs index 50dd46f..28ce397 100644 --- a/src/pages/comparison_test.rs +++ b/src/pages/comparison_test.rs @@ -88,7 +88,7 @@ pub fn ComparisonTestPage() -> impl IntoView { _ => None, }; let comparison_outcome = match comp_opt { - Some(comp) => comp.compare(&student), + Some(comp) => comp.compare(&student.data), _ => Err("".to_string()), }; match comparison_outcome { diff --git a/src/pages/student/athletics_page.rs b/src/pages/student/athletics_page.rs index b311f1a..3c74f87 100644 --- a/src/pages/student/athletics_page.rs +++ b/src/pages/student/athletics_page.rs @@ -19,7 +19,7 @@ pub fn StudentAthleticsPage() -> impl IntoView { label="Athletics Activities:" description="Leave blank if not applicable." data_map=controller.data_map - data_member="athletic_participation" + data_member="sports_participation" schema=vec![ input!( Select, "sport_name", "Sport Name:", true, [ diff --git a/src/pages/student/eligibility_page.rs b/src/pages/student/eligibility_page.rs new file mode 100644 index 0000000..64ab926 --- /dev/null +++ b/src/pages/student/eligibility_page.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; +use leptos::logging::log; +use leptos::prelude::*; +use crate::common::{ComparisonData, ExpandableInfo, ValueType}; +use crate::pages::api::{get_all_scholarship_info, get_comparison_info}; +use crate::pages::api::students::get_all_student_data; +use crate::utils::get_user_claims; + +#[component] +pub fn StudentEligibilityPage() -> impl IntoView { + // This page will show all scholarships that the student is eligible for. + // The scholarships are obviously pulled using a readonly API, and we'll also need to resolve + // all of the relation IDs that are stored inside of it. + // Then, for each scholarship found, we'll check every relation and filter out scholarships that + // do not pass the check. We'll show some sort of simple list to indicate what scholarships passed + // the check, and another to indicate which ones didn't. + + // My only reservation with showing the scholarships that aren't valid is that people will want + // to know why. That's hard information to keep track of right now. + + let user_claims = get_user_claims(); + let user_id = Memo::new(move |_| { + user_claims.get() + .map(|info| info.claims.subject.clone()) + }); + + let refresh_trigger = Trigger::new(); + + let scholarships_resource = Resource::new( + move || refresh_trigger.track(), + async move |_| { + get_all_scholarship_info().await + } + ); + let relations_resource = Resource::new( + move || refresh_trigger.track(), + async move |_| { + get_comparison_info().await + } + ); + let student_resource = Resource::new( + move || (user_id.get().unwrap_or_default(), refresh_trigger.track()), + async move |(user_id, _)| { + get_all_student_data(user_id).await + } + ); + + view! { +
+
+ "Checking eligibility..."
} + }> + {move || { + let Some(Ok(scholarships_list)) = scholarships_resource.get() else { + return view! {}.into_any(); + }; + let Some(Ok(relations_list)) = relations_resource.get() else { + return view! {}.into_any(); + }; + let Some(Ok(student_info)) = student_resource.get() else { + return view! {}.into_any(); + }; + + let valid_list = scholarships_list.iter() + .filter_map(|scholarship| { + let scholarship_name = scholarship.data.get("name") + .unwrap_or(&ValueType::String(None)) + .as_string().ok().flatten().unwrap_or_default(); + log!("Checking scholarship: {}", scholarship_name); + + // Each scholarship will return Some(itself), or None. This is will be automatically + // filtered based on the information. Invalid scholarships are None, along with those + // that fail validation for any reason. Scholarships that are valid or did not choose + // any requirements are Some(scholarship). + let requirements_map = scholarship.data.get("requirements") + .unwrap_or(&ValueType::Map(None)) + .as_map().ok().flatten() + .unwrap_or_default() + .values().cloned() + .collect::>(); + + log!("Requirements list after conversion: {:?}", requirements_map); + + let resolved_requirements = requirements_map + .iter() + .map(|list_val| { + // We want to cast to a list, and then map the list to a new + // one that contains the correct ComparisonData. + let list = list_val.as_list().ok().flatten().unwrap_or_default(); + list.iter().filter_map(|v| { + let id_string = v.as_string().ok().flatten().unwrap_or_default(); + relations_list.iter() + .find(|relation| relation.id == id_string) + }) + .cloned() + .collect::>() + }) + .collect::>>(); + + log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::>()); + + let valid = resolved_requirements.iter() + .all(|list| { + list.iter().fold(false, |prev, requirement| { + let result = requirement.compare(&student_info) + .unwrap_or(false); + + if prev { prev } else { result } + }) + }); + + if valid { + log!("Scholarship is valid."); + Some(scholarship.clone()) + } else { + None + } + }) + .collect::>(); + + view! { +
{format!("Valid scholarships: {}", valid_list.len())}
+ +
{scholarship_name}
+ +
{scholarship_essay_prompt}
+
+
+ } + } + /> + }.into_any() + }} + + +
+ } +} diff --git a/src/pages/student/mod.rs b/src/pages/student/mod.rs index 61275ce..5c3e5fa 100644 --- a/src/pages/student/mod.rs +++ b/src/pages/student/mod.rs @@ -9,6 +9,7 @@ mod work_experience_page; mod university_page; mod family_info_page; mod form_setup; +mod eligibility_page; pub use shell::*; pub use home_page::*; @@ -19,4 +20,5 @@ pub use academics_page::*; pub use extracurriculars_page::*; pub use work_experience_page::*; pub use university_page::*; -pub use family_info_page::*; \ No newline at end of file +pub use family_info_page::*; +pub use eligibility_page::*; \ No newline at end of file diff --git a/src/pages/student/shell.rs b/src/pages/student/shell.rs index ee33082..ba0c9d6 100644 --- a/src/pages/student/shell.rs +++ b/src/pages/student/shell.rs @@ -38,6 +38,7 @@ pub fn StudentShell() -> impl IntoView { ], ), ), + TabInfo::new("Eligible Scholarships", "eligibility", None) ] />