feat: make scholarship filtering work on fafsa and essay requirements
This commit is contained in:
1 parent
93a4b54d42
commit
c5b11481b3
3 files changed
+189
-19
No files matched your search
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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}");
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user