From c5b11481b39478ae3232429950d662c8ba2f1182 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 9 Apr 2026 20:53:47 -0400 Subject: [PATCH] feat: make scholarship filtering work on fafsa and essay requirements --- src/common/expandable_info.rs | 2 +- src/pages/api/students.rs | 63 ++++++++++++++ src/pages/provider_applicants.rs | 143 +++++++++++++++++++++++++++---- 3 files changed, 189 insertions(+), 19 deletions(-) diff --git a/src/common/expandable_info.rs b/src/common/expandable_info.rs index a209606..304c73f 100644 --- a/src/common/expandable_info.rs +++ b/src/common/expandable_info.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; /// 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, /// 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 { /// The main key member. Required and used to get/put the information in a database. pub subject: String, diff --git a/src/pages/api/students.rs b/src/pages/api/students.rs index 88f08cc..29291e9 100644 --- a/src/pages/api/students.rs +++ b/src/pages/api/students.rs @@ -323,3 +323,66 @@ pub async fn get_student_file_names( .collect::>() }) } + +/// # 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] +pub async fn get_all_input_files( + access_token: String, + form_name: String, + input_name: String, +) -> Result>, ServerFnError> { + use imports::*; + + let claims = validate_and_get_token_info(access_token).await?; + + if !claims.groups.contains(&"ScholarshipProviders".to_string()) { + return Err(ServerFnError::new("Access denied: user is not a provider")); + } + + 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::>::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::(); + + 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) +} diff --git a/src/pages/provider_applicants.rs b/src/pages/provider_applicants.rs index de9610a..c3a5aba 100644 --- a/src/pages/provider_applicants.rs +++ b/src/pages/provider_applicants.rs @@ -2,7 +2,10 @@ use crate::components::{ActionButton, Banner, Loading, ValueDisplay}; use crate::pages::UnauthenticatedPage; use crate::pages::api::get_comparison_info; -use crate::pages::api::students::{get_all_student_data, get_student_data}; +use crate::pages::api::students::{ + BatchGetStudentFiles, GetStudentFile, GetStudentFileNames, get_all_input_files, + get_all_student_data, get_student_data, get_student_file_names, +}; use leptos::ev::{Event, MouseEvent}; use leptos::html::Dialog; use leptos::logging::debug_log; @@ -267,6 +270,7 @@ pub fn ApplicantsStudentList() -> impl IntoView { view! {
"Showing students here."
impl IntoView { #[component] fn ApplicantsStudentListView( + #[prop(into)] access_token: Signal>, #[prop(into)] students: Signal>>, #[prop(into)] scholarship: Signal, #[prop(into)] requirements: Signal>, @@ -309,20 +314,79 @@ fn ApplicantsStudentListView( 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()); + } + + 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()); + } + + get_all_input_files( + access_token, + "scholarship_essays".to_string(), + scholarship.get().subject, + ) + .await + }, + ); + fn get_eligible_students( students: HashMap>, scholarship: ExpandableInfo, requirements: Vec, + essay_required: bool, + fafsa_required: bool, + fafsa_list: HashMap>, + essay_list: HashMap>, ) -> Result>, Error> { - let requires_essay = !scholarship - .data - .get("essay_prompt") - .unwrap_or(&ValueType::String(None)) - .as_string() - .map_err(|_| "Couldn't get essay as a string")? - .unwrap_or_default() - .is_empty(); - // Get the full map of requirements. Each key contains a list of IDs. let scholarship_requirements = scholarship .data @@ -366,6 +430,26 @@ fn ApplicantsStudentListView( .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; @@ -385,16 +469,30 @@ fn ApplicantsStudentListView( } Effect::new(move || { - debug_log!("Running eligibility check on students"); - let eligible_res = - get_eligible_students(students.get(), scholarship.get(), requirements.get()); + // 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); + 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}")), } - Err(e) => error_msg.set(format!("Failed to get eligible students list: {e}")), } }); @@ -463,6 +561,8 @@ fn StudentInformationDialog( let dialog_ref = NodeRef::::new(); + //#region Schema Definitions + let demographics_schema = StoredValue::new( SchemaNode::new(SchemaType::Map) .header("Demographics Information") @@ -517,6 +617,8 @@ fn StudentInformationDialog( let extracurricular_schema = StoredValue::new(SchemaNode::new(SchemaType::Map).header("Extracurricular Activities")); + //#endregion + Effect::new(move || { if let Some(dialog) = dialog_ref.get() { let open = visible.get(); @@ -530,7 +632,12 @@ fn StudentInformationDialog( } }); + // We want to get the student's uploaded files - only the ones that are relevant to the current + // scholarship, meaning their essays and/or FAFSA by request. + let on_click_fafsa = move |_| { + // We need to get the student's file names/keys and then tell the server that we want to + // download them. let fafsa_key = format!("financial_info/user_id/fafsa/file_name"); debug_log!("Getting FAFSA file with key {fafsa_key}"); };