PR with too many features #58
No files matched your search
@@ -125,6 +125,7 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
<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::StudentDemographicsPage />
|
||||||
|
<Route path=path!("eligibility") view=student::StudentEligibilityPage />
|
||||||
<Route path=path!("additional") view=student::AdditionalPage />
|
<Route path=path!("additional") view=student::AdditionalPage />
|
||||||
<Route path=path!("additional/academics") view=student::StudentAcademicsPage />
|
<Route path=path!("additional/academics") view=student::StudentAcademicsPage />
|
||||||
<Route path=path!("additional/athletics") view=student::StudentAthleticsPage />
|
<Route path=path!("additional/athletics") view=student::StudentAthleticsPage />
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
use leptos::prelude::*;
|
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(
|
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: std::collections::HashMap<String, crate::common::ValueType>
|
||||||
) -> Result<(), ServerFnError> {
|
) -> Result<(), ServerFnError> {
|
||||||
use leptos::logging::log;
|
use leptos::logging::log;
|
||||||
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
use imports::*;
|
||||||
use aws_sdk_dynamodb::types::AttributeValue;
|
|
||||||
use super::MAIN_TABLE_NAME;
|
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
@@ -40,11 +48,7 @@ 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<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||||
use crate::utils::server::create_dynamo_client;
|
use imports::*;
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::common::ValueType;
|
|
||||||
use aws_sdk_dynamodb::types::AttributeValue;
|
|
||||||
use super::MAIN_TABLE_NAME;
|
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
@@ -75,3 +79,36 @@ pub async fn get_student_data(
|
|||||||
})
|
})
|
||||||
.map_err(ServerFnError::from)
|
.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<std::collections::HashMap<String, crate::common::ValueType>, 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::<HashMap<String, ValueType>>()
|
||||||
|
})
|
||||||
|
.map_err(ServerFnError::from)
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ pub fn ComparisonTestPage() -> impl IntoView {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let comparison_outcome = match comp_opt {
|
let comparison_outcome = match comp_opt {
|
||||||
Some(comp) => comp.compare(&student),
|
Some(comp) => comp.compare(&student.data),
|
||||||
_ => Err("".to_string()),
|
_ => Err("".to_string()),
|
||||||
};
|
};
|
||||||
match comparison_outcome {
|
match comparison_outcome {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub fn StudentAthleticsPage() -> impl IntoView {
|
|||||||
label="Athletics Activities:"
|
label="Athletics Activities:"
|
||||||
description="Leave blank if not applicable."
|
description="Leave blank if not applicable."
|
||||||
data_map=controller.data_map
|
data_map=controller.data_map
|
||||||
data_member="athletic_participation"
|
data_member="sports_participation"
|
||||||
schema=vec![
|
schema=vec![
|
||||||
input!(
|
input!(
|
||||||
Select, "sport_name", "Sport Name:", true, [
|
Select, "sport_name", "Sport Name:", true, [
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
<div class="flex-1" />
|
||||||
|
<div>
|
||||||
|
<Suspense fallback=move || {
|
||||||
|
view! { <div>"Checking eligibility..."</div> }
|
||||||
|
}>
|
||||||
|
{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::<Vec<ValueType>>();
|
||||||
|
|
||||||
|
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::<Vec<ComparisonData>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<ComparisonData>>>();
|
||||||
|
|
||||||
|
log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>());
|
||||||
|
|
||||||
|
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::<Vec<ExpandableInfo>>();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>{format!("Valid scholarships: {}", valid_list.len())}</div>
|
||||||
|
<For
|
||||||
|
each=move || valid_list.clone()
|
||||||
|
key=|scholarship| {
|
||||||
|
scholarship
|
||||||
|
.subject
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
children=move |scholarship| {
|
||||||
|
let scholarship = StoredValue::new(scholarship);
|
||||||
|
let scholarship_name = Memo::new(move |_| {
|
||||||
|
scholarship
|
||||||
|
.read_value()
|
||||||
|
.data
|
||||||
|
.get("name")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.as_string()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let scholarship_essay_prompt = Memo::new(move |_| {
|
||||||
|
scholarship
|
||||||
|
.read_value()
|
||||||
|
.data
|
||||||
|
.get("essay_prompt")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.as_string()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let essay_required = Memo::new(move |_| {
|
||||||
|
!scholarship_essay_prompt.get().is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-row gap-2 p-3">
|
||||||
|
<div>{scholarship_name}</div>
|
||||||
|
<Show when=move || essay_required.get()>
|
||||||
|
<div>{scholarship_essay_prompt}</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}.into_any()
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1" />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ mod work_experience_page;
|
|||||||
mod university_page;
|
mod university_page;
|
||||||
mod family_info_page;
|
mod family_info_page;
|
||||||
mod form_setup;
|
mod form_setup;
|
||||||
|
mod eligibility_page;
|
||||||
|
|
||||||
pub use shell::*;
|
pub use shell::*;
|
||||||
pub use home_page::*;
|
pub use home_page::*;
|
||||||
@@ -20,3 +21,4 @@ pub use extracurriculars_page::*;
|
|||||||
pub use work_experience_page::*;
|
pub use work_experience_page::*;
|
||||||
pub use university_page::*;
|
pub use university_page::*;
|
||||||
pub use family_info_page::*;
|
pub use family_info_page::*;
|
||||||
|
pub use eligibility_page::*;
|
||||||
@@ -38,6 +38,7 @@ pub fn StudentShell() -> impl IntoView {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
TabInfo::new("Eligible Scholarships", "eligibility", None)
|
||||||
]
|
]
|
||||||
/>
|
/>
|
||||||
</Authenticated>
|
</Authenticated>
|
||||||
|
|||||||
Reference in new issue
Block a user