feat: make scholarship filtering work on fafsa and essay requirements

This commit is contained in:
alphafetus committed 2026-04-09 20:53:47 -04:00
1 parent 93a4b54d42
commit c5b11481b3
3 files changed
+189 -19

No files matched your search

+1 -1
View File
@@ -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,
+63
View File
@@ -323,3 +323,66 @@ pub async fn get_student_file_names(
.collect::<Vec<String>>() .collect::<Vec<String>>()
}) })
} }
/// # 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<HashMap<String, Vec<String>>, 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::<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)
}
+125 -18
View File
@@ -2,7 +2,10 @@
use crate::components::{ActionButton, Banner, Loading, ValueDisplay}; use crate::components::{ActionButton, Banner, Loading, ValueDisplay};
use crate::pages::UnauthenticatedPage; use crate::pages::UnauthenticatedPage;
use crate::pages::api::get_comparison_info; 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::ev::{Event, MouseEvent};
use leptos::html::Dialog; use leptos::html::Dialog;
use leptos::logging::debug_log; use leptos::logging::debug_log;
@@ -267,6 +270,7 @@ pub fn ApplicantsStudentList() -> impl IntoView {
view! { view! {
<div>"Showing students here."</div> <div>"Showing students here."</div>
<ApplicantsStudentListView <ApplicantsStudentListView
access_token=access_token
students=students students=students
scholarship=scholarship scholarship=scholarship
requirements=requirements_list requirements=requirements_list
@@ -292,6 +296,7 @@ pub fn ApplicantsStudentList() -> impl IntoView {
#[component] #[component]
fn ApplicantsStudentListView( fn ApplicantsStudentListView(
#[prop(into)] access_token: Signal<Option<String>>,
#[prop(into)] students: Signal<HashMap<String, HashMap<String, ValueType>>>, #[prop(into)] students: Signal<HashMap<String, HashMap<String, ValueType>>>,
#[prop(into)] scholarship: Signal<ExpandableInfo>, #[prop(into)] scholarship: Signal<ExpandableInfo>,
#[prop(into)] requirements: Signal<Vec<ComparisonData>>, #[prop(into)] requirements: Signal<Vec<ComparisonData>>,
@@ -309,20 +314,79 @@ fn ApplicantsStudentListView(
current_student_id.set(None); 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( fn get_eligible_students(
students: HashMap<String, HashMap<String, ValueType>>, students: HashMap<String, HashMap<String, ValueType>>,
scholarship: ExpandableInfo, scholarship: ExpandableInfo,
requirements: Vec<ComparisonData>, 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> { ) -> Result<HashMap<String, HashMap<String, ValueType>>, 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. // Get the full map of requirements. Each key contains a list of IDs.
let scholarship_requirements = scholarship let scholarship_requirements = scholarship
.data .data
@@ -366,6 +430,26 @@ fn ApplicantsStudentListView(
.into_iter() .into_iter()
.filter_map(|(id, student)| { .filter_map(|(id, student)| {
// The student needs to pass one requirement from each list. // 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| { let result = resolved_requirements.iter().all(|list| {
if list.is_empty() { if list.is_empty() {
return true; return true;
@@ -385,16 +469,30 @@ fn ApplicantsStudentListView(
} }
Effect::new(move || { Effect::new(move || {
debug_log!("Running eligibility check on students"); // We have to wait until we've gotten fafsa/essay requirements.
let eligible_res = debug_log!("Waiting on essay and FAFSA resources...");
get_eligible_students(students.get(), scholarship.get(), requirements.get()); 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 { match eligible_res {
Ok(list) => { Ok(list) => {
debug_log!("Total number of eligible students: {}", list.len()); debug_log!("Total number of eligible students: {}", list.len());
eligible_students.set(list); 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::<Dialog>::new(); let dialog_ref = NodeRef::<Dialog>::new();
//#region Schema Definitions
let demographics_schema = StoredValue::new( let demographics_schema = StoredValue::new(
SchemaNode::new(SchemaType::Map) SchemaNode::new(SchemaType::Map)
.header("Demographics Information") .header("Demographics Information")
@@ -517,6 +617,8 @@ fn StudentInformationDialog(
let extracurricular_schema = let extracurricular_schema =
StoredValue::new(SchemaNode::new(SchemaType::Map).header("Extracurricular Activities")); StoredValue::new(SchemaNode::new(SchemaType::Map).header("Extracurricular Activities"));
//#endregion
Effect::new(move || { Effect::new(move || {
if let Some(dialog) = dialog_ref.get() { if let Some(dialog) = dialog_ref.get() {
let open = visible.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 |_| { 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"); let fafsa_key = format!("financial_info/user_id/fafsa/file_name");
debug_log!("Getting FAFSA file with key {fafsa_key}"); debug_log!("Getting FAFSA file with key {fafsa_key}");
}; };