diff --git a/src/app.rs b/src/app.rs index fd98e09..10c4c98 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,6 @@ use crate::components::ToastList; use crate::components::login::ProviderLoginContext; -use crate::pages::{ - AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage, - ApplicantsPageFallback, ApplicantsStudentList, AuthCallbackPage, LoanerBorrowForm, - LoanerFallback, LoanerReturnForm, LoanerShell, ProviderContactPage, ProviderPortal, - ScholarshipInfoPage, -}; +use crate::pages::{AdminApplicantsPageFallback, AdminApplicantsPageShell, AdminApplicantsStudentList, AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage, ApplicantsPageFallback, ApplicantsStudentList, AuthCallbackPage, LoanerBorrowForm, LoanerFallback, LoanerReturnForm, LoanerShell, ProviderContactPage, ProviderPortal, ScholarshipInfoPage}; use crate::pages::{ApplicantsPageShell, student}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; @@ -126,6 +121,10 @@ fn AdminRoutes() -> impl MatchNestedRoutes + Clone { + + + + } .into_inner() diff --git a/src/common/user.rs b/src/common/user.rs index 525572c..7ebae51 100644 --- a/src/common/user.rs +++ b/src/common/user.rs @@ -25,6 +25,6 @@ pub struct UserClaims { /// The intended use for the token. pub token_use: String, /// The list of the names of user pool groups that have this user as a member. - #[serde(rename = "cognito:groups")] + #[serde(rename = "cognito:groups", default = "Vec::new")] pub groups: Vec, } diff --git a/src/pages/admin/applicants_list.rs b/src/pages/admin/applicants_list.rs new file mode 100644 index 0000000..9a906d5 --- /dev/null +++ b/src/pages/admin/applicants_list.rs @@ -0,0 +1,579 @@ +use crate::common::{ + ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, + SchemaType, ValueType, +}; +use crate::components::{ActionButton, Banner, DataDisplay, Loading}; +use crate::pages::{StudentInformationDialog, UnauthenticatedPage}; +use crate::pages::api::get_comparison_info; +use crate::pages::api::students::{GetStudentFiles, admin_get_all_input_files, get_student_data}; +use base64::Engine; +use leptos::ev::{Event, MouseEvent}; +use leptos::html::Dialog; +use leptos::logging::debug_log; +use leptos::prelude::*; +use leptos::wasm_bindgen::JsCast; +use leptos::web_sys; +use leptos::web_sys::{HtmlAnchorElement, ScrollBehavior}; +use leptos_icons::Icon; +use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated}; +use leptos_router::components::Outlet; +use leptos_router::hooks::use_navigate; +use std::collections::HashMap; +use leptos_router::NavigateOptions; + +#[component] +pub fn AdminApplicantsPageFallback() -> impl IntoView { + view! { +
+

"Scholarship Applicants"

+

+ "Please select a scholarship from the left to view the eligible applicants." +

+
+ } +} + +#[component] +pub fn AdminApplicantsPageShell() -> impl IntoView { + // This shell needs to simply contain everything that will stay consistent across paths. + // The path will change to contain each scholarship's id, which will directly affect the content + // shown in a subroute for this page. + + // As a result, this page likely only needs to contain the banner and the list of scholarships. + // The content will be shown within the right side, but only as a result of some subroutes, + // each of which is still considered a single page. + + view! { +
+
+
+ +
+ +
+
+
+
+ } +} + +#[component] +fn AdminApplicantsScholarshipList() -> impl IntoView { + use crate::common::ExpandableInfo; + + #[server] + async fn admin_get_provider_scholarships( + access_token: String, + ) -> Result, ServerFnError> { + use crate::pages::api::SCHOLARSHIPS_TABLE; + use crate::pages::api::tokens::validate_and_get_token_info; + + let claims = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?; + + // Get the information from the database. + let client = crate::utils::server::create_dynamo_client().await; + + let output = client + .scan() + .table_name(SCHOLARSHIPS_TABLE) + .send() + .await? + .items + .unwrap_or_default() + .into_iter() + .filter_map(|item| serde_dynamo::from_item(item).ok()) + .collect::>(); + + Ok(output) + } + + // Use access token for user identification + let auth = expect_context::(); + let access_token = + Memo::new(move |_| auth.with(|auth| auth.authenticated().map(|a| a.access_token()))); + + // We need to get the scholarships from the API. We don't need the whole scholarship, just the + // name and ID. + let trigger = Trigger::new(); + let scholarships_res = Resource::new( + move || (trigger.track(), access_token.get()), + async move |(_, access_token)| { + admin_get_provider_scholarships(access_token.unwrap_or_default()).await + }, + ); + + let navigate = use_navigate(); + + // This closure returns another closure that's using the correct scholarship ID. + // It avoids having to define it within the Transition component. It's wrapped in a Callback + // because this makes it Send + Sync. + let on_view_click = Callback::new(move |scholarship_id: String| { + let path = format!("/admin/applicants/{}", scholarship_id); + navigate(&*path, Default::default()); + }); + + view! { +
+

"Scholarships"

+ + {move || { + scholarships_res + .get() + .map(|items_res| { + let items = match items_res { + Ok(items) => items, + Err(e) => { + return view! { +
+ {format!("Couldn't get scholarships: {}", e.to_string())} +
+ } + .into_any(); + } + }; + + view! { + + + + } + .into_any() + }) + .collect_view() + }} +
+
+ } +} + +#[component] +fn ApplicantsScholarshipEntry( + #[prop()] item: ExpandableInfo, + #[prop(into)] on_view_click: Callback, +) -> impl IntoView { + // Store the item's subject. + let subject = StoredValue::new(item.subject); + let scholarship_name = Memo::new(move |_| { + item.data + .get("name") + .map(|name| name.as_string().ok().flatten()) + .flatten() + .unwrap_or("".to_string()) + }); + + let on_click = move |_| { + on_view_click.run(subject.get_value()); + }; + + view! { +
+
{scholarship_name}
+
+ "View Applicants" +
+
+ } +} + +#[component] +pub fn AdminApplicantsStudentList() -> impl IntoView { + use crate::pages::api::get_scholarship_info; + use crate::pages::api::students::admin_get_completed_students; + use leptos_router::hooks::use_params; + use leptos_router::params::Params; + + /// The parameters for the page that displays this component. This may be moved to the page-level + /// rather than the component-level. + #[derive(Params, PartialEq)] + struct StudentListParams { + scholarship_id: Option, + } + + // This is in charge of finding all student information and running the comparisons. + // It will then display all the eligible students. + + let params = use_params::(); + let auth = expect_context::(); + let access_token = + Memo::new(move |_| auth.with(|a| a.authenticated().map(|a| a.access_token()))); + + let scholarship_id = Memo::new(move |_| { + params + .read() + .as_ref() + .ok() + .and_then(|params| params.scholarship_id.clone()) + }); + + #[server] + async fn get_eligibility_info( + token: String, + scholarship_id: String, + ) -> Result< + ( + HashMap>, + ExpandableInfo, + Vec, + ), + ServerFnError, + > { + let (students, scholarships, requirements_list) = tokio::join!( + admin_get_completed_students(token), + get_scholarship_info(scholarship_id), + get_comparison_info() + ); + + Ok((students?, scholarships?, requirements_list?)) + } + + let resource = Resource::new( + move || (access_token.get(), scholarship_id.get()), + move |(access_token, scholarship_id)| async move { + let Some(token) = access_token else { + return Err(ServerFnError::new("Couldn't find access token")); + }; + let Some(scholarship_id) = scholarship_id else { + return Err(ServerFnError::new("Couldn't find scholarship ID")); + }; + + get_eligibility_info(token, scholarship_id).await + }, + ); + + view! { +
+ + {move || { + resource + .get() + .map(|result| { + match result { + Ok((students, scholarship, requirements_list)) => { + view! { +

"Eligible Students"

+
+ "Click on a student's name to view their information and download any requested files." +
+
+ "If your scholarship requires a transcript, please send an email to Sara Smith at " + + "ssmith@region15.org" + "." +
+ + } + .into_any() + } + Err(e) => { + view! { +
+ "Error while getting eligible students: "{e.to_string()} +
+ } + .into_any() + } + } + }) + }} +
+
+ } +} + +#[component] +fn AdminApplicantsStudentListView( + #[prop(into)] access_token: Signal>, + #[prop(into)] students: Signal>>, + #[prop(into)] scholarship: Signal, + #[prop(into)] requirements: Signal>, +) -> impl IntoView { + let eligible_students = RwSignal::new(HashMap::new()); + let error_msg = RwSignal::new(String::new()); + // let (current_student_id, set_current_student_id) = query_signal::("student_id"); + let current_student_id = RwSignal::new(None); + let navigate_student = Callback::new(move |id: String| { + let options = web_sys::ScrollToOptions::new(); + options.set_top(0.0); + options.set_behavior(ScrollBehavior::Smooth); + + if let Some(window) = web_sys::window() { + window.scroll_to_with_scroll_to_options(&options); + } + + current_student_id.set(Some(id)); + }); + let dialog_visible = + Signal::derive(move || !current_student_id.get().unwrap_or_default().is_empty()); + let on_dialog_close = Callback::new(move |_| { + 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()); + } + + admin_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()); + } + + admin_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> { + // Get the full map of requirements. Each key contains a list of IDs. + let scholarship_requirements = scholarship + .data + .get("requirements") + .map(|val| val.as_map().ok()?) + .flatten() + .unwrap_or_default(); + + // Get the lists of requirements. Maps into string IDs and discards invalid ValueTypes. + let requirement_lists = scholarship_requirements + .into_iter() + .filter_map(|(_, list_val)| { + let list = list_val.as_list().ok().flatten()?; + let string_list = list + .into_iter() + .filter_map(|v| v.as_string().ok().flatten()) + .collect::>(); + + Some(string_list) + }) + .collect::>>(); + + // Resolve the requirement IDs. Discards all IDs that do not resolve. + let resolved_requirements = requirement_lists + .into_iter() + .map(|list| { + list.into_iter() + .filter_map(|id| { + requirements + .iter() + .cloned() + .find(|requirement| requirement.id == id) + }) + .collect::>() + }) + .collect::>>(); + + // Check student eligibility. These are all students that have completed the demographics + // forms. + let eligible_students = students + .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; + } + list.iter().any(|requirement| { + let result = requirement.compare(&student).unwrap_or(false); + + if !result { + debug_log!( + "Student with id {id} failed - requirement with id {} failed.", + requirement.id + ); + } + + result + }) + }); + + if result { Some((id, student)) } else { None } + }) + .collect::>>(); + + // debug_log!("Total number of eligible students: {}", eligible_students.len()); + + Ok(eligible_students) + } + + Effect::new(move || { + // 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); + } + Err(e) => error_msg.set(format!("Failed to get eligible students list: {e}")), + } + } + }); + + // This is actually just a display of all the information from the students list. + // As defined in the ApplicantsStudentList component, this will already be wrapped in a + // flex container. + view! { + + +
"Error checking student eligibility: "{error_msg}
+
+
+
+
"Last Name"
+
"First Name"
+
+ +
{last_name}
+
{first_name}
+
+ } + } + /> +
+ } +} diff --git a/src/pages/admin/mod.rs b/src/pages/admin/mod.rs index 3bb847a..d090b82 100644 --- a/src/pages/admin/mod.rs +++ b/src/pages/admin/mod.rs @@ -3,9 +3,11 @@ mod home_page; mod provider_page; mod scholarship_page; mod utils_page; +mod applicants_list; pub use shell::*; pub use home_page::*; pub use provider_page::*; pub use scholarship_page::*; pub use utils_page::*; +pub use applicants_list::*; diff --git a/src/pages/admin/shell.rs b/src/pages/admin/shell.rs index 7bdca5c..d97f00b 100644 --- a/src/pages/admin/shell.rs +++ b/src/pages/admin/shell.rs @@ -25,6 +25,7 @@ pub fn AdminShell() -> impl IntoView { TabInfo::new("Providers", "providers", None), TabInfo::new("Scholarships", "scholarships", None), TabInfo::new("Utilities", "utilities", None), + TabInfo::new("Scholarship Applicants", "applicants", None), ] /> diff --git a/src/pages/api/files.rs b/src/pages/api/files.rs index aac22b9..fe8aad2 100644 --- a/src/pages/api/files.rs +++ b/src/pages/api/files.rs @@ -66,7 +66,7 @@ pub async fn upload_file(data: MultipartData) -> Result { return Err(ServerFnError::new("Missing file contents")); } - let user_claims = validate_and_get_token_info(access_token).await?; + let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?; let subject = user_claims.subject; let key = format!("{form_id}/{subject}/{input_name}/{file_name}"); @@ -157,7 +157,7 @@ pub async fn delete_file( ) -> Result { use imports::*; - let user_claims = validate_and_get_token_info(access_token).await?; + let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?; let subject = user_claims.subject; let entry_hk = format!("STUDENT#{subject}"); @@ -226,7 +226,7 @@ pub async fn list_files( ) -> Result, ServerFnError> { use imports::*; - let user_claims = validate_and_get_token_info(access_token).await?; + let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?; let subject = user_claims.subject; let entry_hk = format!("STUDENT#{subject}"); diff --git a/src/pages/api/students.rs b/src/pages/api/students.rs index f83e76c..1b1dc3d 100644 --- a/src/pages/api/students.rs +++ b/src/pages/api/students.rs @@ -137,24 +137,41 @@ pub async fn get_all_student_data( } #[server] -pub async fn get_completed_students( +pub async fn provider_get_completed_students( access_token: String, ) -> Result>, ServerFnError> { use imports::*; + let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?; + if !claims.groups.contains(&"ScholarshipProviders".to_string()) { + return Err(ServerFnError::new( + "User is not in the ScholarshipProviders group", + )); + } + + get_completed_students().await +} + +#[server] +pub async fn admin_get_completed_students( + access_token: String, +) -> Result>, ServerFnError> { + use imports::*; + + let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?; + + get_completed_students().await +} + +#[server] +async fn get_completed_students() -> Result>, ServerFnError> { + use imports::*; + // We want to get all student information. The requirements are that the students have completed // the demographics form - everything else may bed from this. // The easiest way is to get all the information and filter on this side, instead of bookkeeping // on the database's side. - // We want to verify the access token first, and make sure that the user has the correct access. - let claims = validate_and_get_token_info(access_token).await?; - if !claims.groups.contains(&"ScholarshipProviders".to_string()) { - return Err(ServerFnError::new( - "User is not in the ScholarshipProviders group", - )); - } - let client = create_dynamo_client().await; let response = client @@ -229,7 +246,7 @@ pub async fn get_file_by_key( ) -> Result, ServerFnError> { use imports::*; - let user_claims = validate_and_get_token_info(access_token).await?; + let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?; // Check if the user's subject is contained in the file_key (since all files are keyed by the // user's subject) @@ -279,7 +296,7 @@ pub async fn get_student_files( ) -> Result<(String, Vec), ServerFnError> { use imports::*; - let claims = validate_and_get_token_info(access_token).await?; + let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?; // Check if the user has permission to get these files. if !claims.groups.contains(&"ScholarshipProviders".to_string()) { @@ -420,19 +437,12 @@ pub async fn get_student_files( /// /// This is only usable by users with provider-level access. #[server] -pub async fn get_all_input_files( - access_token: String, +async fn get_all_input_files( 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 @@ -473,3 +483,32 @@ pub async fn get_all_input_files( Ok(result_map) } + +#[server] +pub async fn admin_get_all_input_files( + access_token: String, + form_name: String, + input_name: String, +) -> Result>, ServerFnError> { + use imports::*; + + let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?; + + get_all_input_files(form_name, input_name).await +} + +#[server] +pub async fn provider_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, "us-east-1_Lfjuy5zaM", "us-east-1").await?; + if !claims.groups.contains(&"ScholarshipProviders".to_string()) { + return Err(ServerFnError::new("Access denied: user is not a provider")); + } + + get_all_input_files(form_name, input_name).await +} diff --git a/src/pages/api/tokens.rs b/src/pages/api/tokens.rs index e71db7e..ea45aff 100644 --- a/src/pages/api/tokens.rs +++ b/src/pages/api/tokens.rs @@ -17,10 +17,19 @@ pub async fn fetch_jwks(user_pool_id: String, region: String) -> Result Result { +pub async fn validate_and_get_token_info( + token: String, + pool_id: impl Into, + pool_region: impl Into +) -> Result { use jsonwebtoken::{Algorithm, Validation, decode}; - let jwks = fetch_jwks("us-east-1_Lfjuy5zaM".into(), "us-east-1".into()).await?; + let pool_id = pool_id.into(); + + leptos::logging::debug_log!("Validating token using group {}", pool_id); + + // let jwks = fetch_jwks("us-east-1_Lfjuy5zaM".into(), "us-east-1".into()).await?; + let jwks = fetch_jwks(pool_id, pool_region.into()).await?; let validation = Validation::new(Algorithm::RS256); // validation.set_audience(&["scholarships-rs"]); diff --git a/src/pages/provider_applicants.rs b/src/pages/provider_applicants.rs index c13530c..cc83dd6 100644 --- a/src/pages/provider_applicants.rs +++ b/src/pages/provider_applicants.rs @@ -1,11 +1,8 @@ -use crate::common::{ - ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, - SchemaType, ValueType, -}; +use crate::common::{ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType}; use crate::components::{ActionButton, Banner, DataDisplay, Loading}; use crate::pages::UnauthenticatedPage; use crate::pages::api::get_comparison_info; -use crate::pages::api::students::{GetStudentFiles, get_all_input_files, get_student_data}; +use crate::pages::api::students::{GetStudentFiles, provider_get_all_input_files, get_student_data}; use base64::Engine; use leptos::ev::{Event, MouseEvent}; use leptos::html::Dialog; @@ -71,12 +68,12 @@ fn ApplicantsScholarshipList() -> impl IntoView { #[server] async fn get_provider_scholarships( - access_token: String, + access_token: String ) -> Result, ServerFnError> { use crate::pages::api::SCHOLARSHIPS_TABLE; use crate::pages::api::tokens::validate_and_get_token_info; - let claims = validate_and_get_token_info(access_token).await?; + let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM".to_string(), "us-east-1".to_string()).await?; // Get the information from the database. let client = crate::utils::server::create_dynamo_client().await; @@ -110,8 +107,10 @@ fn ApplicantsScholarshipList() -> impl IntoView { let trigger = Trigger::new(); let scholarships_res = Resource::new( move || (trigger.track(), access_token.get()), - async move |(_, access_token)| { - get_provider_scholarships(access_token.unwrap_or_default()).await + move |(_, access_token)| async move { + get_provider_scholarships( + access_token.unwrap_or_default() + ).await }, ); @@ -201,7 +200,7 @@ fn ApplicantsScholarshipEntry( #[component] pub fn ApplicantsStudentList() -> impl IntoView { use crate::pages::api::get_scholarship_info; - use crate::pages::api::students::get_completed_students; + use crate::pages::api::students::provider_get_completed_students; use leptos_router::hooks::use_params; use leptos_router::params::Params; @@ -241,7 +240,7 @@ pub fn ApplicantsStudentList() -> impl IntoView { ServerFnError, > { let (students, scholarships, requirements_list) = tokio::join!( - get_completed_students(token), + provider_get_completed_students(token), get_scholarship_info(scholarship_id), get_comparison_info() ); @@ -374,7 +373,7 @@ fn ApplicantsStudentListView( return Ok(HashMap::new()); } - get_all_input_files( + provider_get_all_input_files( access_token, "financial_info".to_string(), "fafsa".to_string(), @@ -394,7 +393,7 @@ fn ApplicantsStudentListView( return Ok(HashMap::new()); } - get_all_input_files( + provider_get_all_input_files( access_token, "scholarship_essays".to_string(), scholarship.get().subject, @@ -592,7 +591,7 @@ fn ApplicantsStudentListView( } #[component] -fn StudentInformationDialog( +pub fn StudentInformationDialog( #[prop(into)] scholarship_id: Signal, #[prop(into)] student_id: Signal>, #[prop(into)] visible: Signal, @@ -1287,7 +1286,10 @@ fn StudentInformationDialog( }) })} {move || Suspend::new(async move { - get_student_data(student_id.get().unwrap_or_default(), "family".to_string()) + get_student_data( + student_id.get().unwrap_or_default(), + "family".to_string() + ) .await .map(|student_info| { view! {