Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b0c8b83b | ||
|
|
e9a42aa0fa | ||
|
|
5b44bfb914 | ||
|
|
8282ba4302 | ||
|
|
3cc67cb9e6 | ||
|
|
26b984904b | ||
|
|
4f1d1c5b1c |
No files matched your search
+6
-2
@@ -27,14 +27,18 @@ RUN cargo binstall fnm -y \
|
|||||||
|
|
||||||
# Start second phase of build - install ca-certs and copy all build outputs from previous step into a
|
# Start second phase of build - install ca-certs and copy all build outputs from previous step into a
|
||||||
# Debian image in the /app directory.
|
# Debian image in the /app directory.
|
||||||
FROM debian:trixie-slim AS runtime
|
FROM rust:1.95-slim-trixie AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt-get install -y --no-install-recommends openssl ca-certificates \
|
&& apt-get install -y --no-install-recommends openssl ca-certificates curl \
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
&& apt-get clean -y \
|
&& apt-get clean -y \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Cargo, then binstall, then typst installation.
|
||||||
|
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||||
|
RUN cargo binstall typst-cli -y
|
||||||
|
|
||||||
# Remember to replace project_name with the actual project name
|
# Remember to replace project_name with the actual project name
|
||||||
COPY --from=builder /app/target/release/scholarships-rs /app/
|
COPY --from=builder /app/target/release/scholarships-rs /app/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#set page(margin: 40pt)
|
||||||
|
#set block(breakable: false)
|
||||||
|
|
||||||
|
#let map_block(header, val) = {
|
||||||
|
block(
|
||||||
|
width: 100%,
|
||||||
|
fill: rgb(200, 0, 0),
|
||||||
|
radius: 4pt,
|
||||||
|
below: 0pt,
|
||||||
|
above: 4pt,
|
||||||
|
inset: 2pt,
|
||||||
|
[
|
||||||
|
#show heading: set text(white)
|
||||||
|
#show heading: set text(size: 15pt)
|
||||||
|
#stack(dir: ttb,
|
||||||
|
block(
|
||||||
|
inset: 7pt,
|
||||||
|
align(horizon)[= #header]
|
||||||
|
),
|
||||||
|
align(center)[#block(
|
||||||
|
width: 100%,
|
||||||
|
fill: white,
|
||||||
|
inset: 8pt,
|
||||||
|
radius: 3pt,
|
||||||
|
align(left)[#val]
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let info_line(subject, key, dict: student) = [
|
||||||
|
|
||||||
|
*#subject* #dict.at(key, default: "N/A")
|
||||||
|
]
|
||||||
|
|
||||||
|
#let info_line_list(subject, key, dict: student) = [
|
||||||
|
|
||||||
|
*#subject* #dict.at(key, default: "N/A").join(", ")
|
||||||
|
]
|
||||||
|
|
||||||
|
#show heading: set align(center)
|
||||||
|
#show heading: set text(size: 20pt)
|
||||||
|
#v(10cm)
|
||||||
|
= #student.first_name #student.last_name
|
||||||
|
#show heading: set text(size: 17pt)
|
||||||
|
== Region 15 General Scholarship Application
|
||||||
|
#pagebreak()
|
||||||
|
|
||||||
|
#show heading: set block(below: 20pt, above: 30pt)
|
||||||
|
#show heading: set text(size: 20pt)
|
||||||
|
= Student Demographic Information
|
||||||
|
|
||||||
|
#info_line("First Name:", "first_name")
|
||||||
|
#info_line("Last Name:", "last_name")
|
||||||
|
#info_line("Date of Birth:", "dob")
|
||||||
|
#info_line("Gender:", "gender")
|
||||||
|
#info_line("Phone Number:", "phone_number")
|
||||||
|
#info_line("Email:","email")
|
||||||
|
#info_line("Street Address:", "street_address")
|
||||||
|
#info_line("Town:", "town")
|
||||||
|
|
||||||
|
= Academic Information
|
||||||
|
|
||||||
|
#info_line("Unweighted GPA:", "unweighted_gpa")
|
||||||
|
#info_line("Weighted GPA:", "weighted_gpa")
|
||||||
|
#info_line("Highest SAT Score:", "sat_score")
|
||||||
|
#info_line("Highest ACT Score:", "act_score")
|
||||||
|
#info_line("Academic Honors:", "academic_honors")
|
||||||
|
|
||||||
|
= Sports Participation
|
||||||
|
#if student.at("sports_participation", default: ()).len() == 0 [
|
||||||
|
Student did not enter any sports participation.
|
||||||
|
] else {
|
||||||
|
for pair in student.sports_participation {
|
||||||
|
map_block(pair.at("sport_name"), [
|
||||||
|
#info_line("Special Achievements:", "achievements", dict: pair)
|
||||||
|
#info_line_list("Grades Participated:", "grades", dict: pair)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
= Extracurricular Activities
|
||||||
|
#if student.at("extracurricular", default: ()).len() == 0 [
|
||||||
|
Student did not enter any extracurricular activities.
|
||||||
|
] else {
|
||||||
|
for pair in student.extracurricular {
|
||||||
|
map_block(pair.at("activity_name"), [
|
||||||
|
#info_line("Number of hours per week:", "num_hours", dict: pair)
|
||||||
|
#info_line("Number of weeks participated:", "num_weeks", dict: pair)
|
||||||
|
#info_line("Special Involvement:", "special_involvement", dict: pair)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
= Work Experience
|
||||||
|
#if student.at("work_experience", default: ()).len() == 0 [
|
||||||
|
Student did not enter any work experience.
|
||||||
|
] else {
|
||||||
|
for pair in student.work_experience {
|
||||||
|
map_block(pair.at("employer"), [
|
||||||
|
#info_line("Job Title:", "job_title", dict: pair)
|
||||||
|
#info_line("Start Date:", "start_date", dict: pair)
|
||||||
|
#info_line("End Date:", "end_date", dict: pair)
|
||||||
|
#info_line("Number of hours per week:", "num_hours", dict: pair)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
= University Information
|
||||||
|
|
||||||
|
#info_line("University Name:", "college_name")
|
||||||
|
#info_line("University Street Address:", "college_city")
|
||||||
|
#info_line("University State:", "college_state")
|
||||||
|
#info_line("University ZIP:", "college_zip")
|
||||||
|
#info_line("Has the student received an acceptance:", "college_acceptance")
|
||||||
|
#info_line("Student's chosen major:", "major")
|
||||||
|
#info_line("Student's intended career:", "intended_career")
|
||||||
|
|
||||||
|
= Family Information
|
||||||
|
|
||||||
|
#info_line("Total number of children in student's family:", "num_children")
|
||||||
|
#info_line("Total number of children currently attending college:", "num_children_college")
|
||||||
|
#info_line("Parent/Guardian 1 Name:", "parent_one_name")
|
||||||
|
#info_line("Parent/Guardian 1 Relationship:", "parent_one_relationship")
|
||||||
|
#info_line("Parent/Guardian 1 Occupation:", "parent_one_occupation")
|
||||||
|
#info_line("Parent/Guardian 1 Employer:", "parent_one_employer")
|
||||||
|
#info_line("Parent/Guardian 2 Name:", "parent_two_name")
|
||||||
|
#info_line("Parent/Guardian 2 Relationship:", "parent_two_relationship")
|
||||||
|
#info_line("Parent/Guardian 2 Occupation:", "parent_two_occupation")
|
||||||
|
#info_line("Parent/Guardian 2 Employer:", "parent_two_employer")
|
||||||
|
|
||||||
|
= Extra Eligibility Information
|
||||||
|
|
||||||
|
#info_line("Did the student attend BAS:", "attend_bas")
|
||||||
|
#info_line("Is the student a member of Midd-South Catholic Church:", "middsouth_church")
|
||||||
|
#info_line("Does the student have a family member who has served in the US Military:", "family_military_service")
|
||||||
|
#info_line("Has the student participated in Pomperaug Youth Baseball:", "youth_baseball")
|
||||||
|
#info_line("Has the student participated in the Panthers Aquatic Club:", "aquatic_club")
|
||||||
|
#info_line("Does the student have a family member in the Region 15 PEA:", "pea_member")
|
||||||
|
#info_line("Has the student participated in the PHS Music Program:", "music_program")
|
||||||
+5
-6
@@ -1,11 +1,6 @@
|
|||||||
use crate::components::ToastList;
|
use crate::components::ToastList;
|
||||||
use crate::components::login::ProviderLoginContext;
|
use crate::components::login::ProviderLoginContext;
|
||||||
use crate::pages::{
|
use crate::pages::{AdminApplicantsPageFallback, AdminApplicantsPageShell, AdminApplicantsStudentList, AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage, ApplicantsPageFallback, ApplicantsStudentList, AuthCallbackPage, LoanerBorrowForm, LoanerFallback, LoanerReturnForm, LoanerShell, ProviderContactPage, ProviderPortal, ScholarshipInfoPage};
|
||||||
AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage,
|
|
||||||
ApplicantsPageFallback, ApplicantsStudentList, AuthCallbackPage, LoanerBorrowForm,
|
|
||||||
LoanerFallback, LoanerReturnForm, LoanerShell, ProviderContactPage, ProviderPortal,
|
|
||||||
ScholarshipInfoPage,
|
|
||||||
};
|
|
||||||
use crate::pages::{ApplicantsPageShell, student};
|
use crate::pages::{ApplicantsPageShell, student};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
||||||
@@ -126,6 +121,10 @@ fn AdminRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
<Route path=path!("providers") view=AdminProviderPage />
|
<Route path=path!("providers") view=AdminProviderPage />
|
||||||
<Route path=path!("scholarships") view=AdminScholarshipPage />
|
<Route path=path!("scholarships") view=AdminScholarshipPage />
|
||||||
<Route path=path!("utilities") view=AdminUtilsPage />
|
<Route path=path!("utilities") view=AdminUtilsPage />
|
||||||
|
<ParentRoute path=path!("applicants") view=AdminApplicantsPageShell>
|
||||||
|
<Route path=path!("") view=AdminApplicantsPageFallback />
|
||||||
|
<Route path=path!("/:scholarship_id") view=AdminApplicantsStudentList />
|
||||||
|
</ParentRoute>
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
}
|
}
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
|||||||
+1
-1
@@ -25,6 +25,6 @@ pub struct UserClaims {
|
|||||||
/// The intended use for the token.
|
/// The intended use for the token.
|
||||||
pub token_use: String,
|
pub token_use: String,
|
||||||
/// The list of the names of user pool groups that have this user as a member.
|
/// 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<String>,
|
pub groups: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ pub fn ActionButton(
|
|||||||
r#type=button_type
|
r#type=button_type
|
||||||
disabled=move || disabled.get()
|
disabled=move || disabled.get()
|
||||||
>
|
>
|
||||||
{ children() }
|
{children()}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
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! {
|
||||||
|
<div class="flex flex-col flex-1 gap-2">
|
||||||
|
<h1 class="flex-1 text-center font-bold text-2xl">"Scholarship Applicants"</h1>
|
||||||
|
<p class="text-center text-lg">
|
||||||
|
"Please select a scholarship from the left to view the eligible applicants."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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! {
|
||||||
|
<div class="flex-1" />
|
||||||
|
<div class="flex-3 flex flex-row items-start">
|
||||||
|
<AdminApplicantsScholarshipList />
|
||||||
|
<div class="flex-3 text-center">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn AdminApplicantsScholarshipList() -> impl IntoView {
|
||||||
|
use crate::common::ExpandableInfo;
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn admin_get_provider_scholarships(
|
||||||
|
access_token: String,
|
||||||
|
) -> Result<Vec<ExpandableInfo>, 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::<Vec<ExpandableInfo>>();
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use access token for user identification
|
||||||
|
let auth = expect_context::<AuthSignal>();
|
||||||
|
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! {
|
||||||
|
<div class="flex flex-col flex-1 rounded-md shadow-lg/33 p-2 gap-3">
|
||||||
|
<h2 class="text-xl font-bold flex-1 text-center">"Scholarships"</h2>
|
||||||
|
<Transition fallback=Loading>
|
||||||
|
{move || {
|
||||||
|
scholarships_res
|
||||||
|
.get()
|
||||||
|
.map(|items_res| {
|
||||||
|
let items = match items_res {
|
||||||
|
Ok(items) => items,
|
||||||
|
Err(e) => {
|
||||||
|
return view! {
|
||||||
|
<div>
|
||||||
|
{format!("Couldn't get scholarships: {}", e.to_string())}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<For
|
||||||
|
each=move || items.clone()
|
||||||
|
key=|item| item.subject.clone()
|
||||||
|
let(item)
|
||||||
|
>
|
||||||
|
<ApplicantsScholarshipEntry
|
||||||
|
item=item
|
||||||
|
on_view_click=on_view_click
|
||||||
|
/>
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
})
|
||||||
|
.collect_view()
|
||||||
|
}}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ApplicantsScholarshipEntry(
|
||||||
|
#[prop()] item: ExpandableInfo,
|
||||||
|
#[prop(into)] on_view_click: Callback<String, ()>,
|
||||||
|
) -> 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("<unnamed scholarship>".to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_click = move |_| {
|
||||||
|
on_view_click.run(subject.get_value());
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col rounded-md shadow-md hover:shadow-lg/33 transition-shadow">
|
||||||
|
<div class="text-center p-2">{scholarship_name}</div>
|
||||||
|
<div
|
||||||
|
class="flex-1 font-bold text-white rounded-b-md text-center cursor-pointer bg-red-800 hover:bg-red-900 p-2 transition-bg"
|
||||||
|
on:click=on_click
|
||||||
|
>
|
||||||
|
"View Applicants"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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::<StudentListParams>();
|
||||||
|
let auth = expect_context::<AuthSignal>();
|
||||||
|
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<String, HashMap<String, ValueType>>,
|
||||||
|
ExpandableInfo,
|
||||||
|
Vec<ComparisonData>,
|
||||||
|
),
|
||||||
|
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! {
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<Suspense fallback=Loading>
|
||||||
|
{move || {
|
||||||
|
resource
|
||||||
|
.get()
|
||||||
|
.map(|result| {
|
||||||
|
match result {
|
||||||
|
Ok((students, scholarship, requirements_list)) => {
|
||||||
|
view! {
|
||||||
|
<h1 class="text-2xl font-bold">"Eligible Students"</h1>
|
||||||
|
<div class="text-lg">
|
||||||
|
"Click on a student's name to view their information and download any requested files."
|
||||||
|
</div>
|
||||||
|
<div class="text-lg">
|
||||||
|
"If your scholarship requires a transcript, please send an email to Sara Smith at "
|
||||||
|
<a
|
||||||
|
class="text-blue-500 underline"
|
||||||
|
href="mailto:ssmith@region15.org"
|
||||||
|
>
|
||||||
|
"ssmith@region15.org"
|
||||||
|
</a>"."
|
||||||
|
</div>
|
||||||
|
<AdminApplicantsStudentListView
|
||||||
|
access_token=access_token
|
||||||
|
students=students
|
||||||
|
scholarship=scholarship
|
||||||
|
requirements=requirements_list
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
"Error while getting eligible students: "{e.to_string()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn AdminApplicantsStudentListView(
|
||||||
|
#[prop(into)] access_token: Signal<Option<String>>,
|
||||||
|
#[prop(into)] students: Signal<HashMap<String, HashMap<String, ValueType>>>,
|
||||||
|
#[prop(into)] scholarship: Signal<ExpandableInfo>,
|
||||||
|
#[prop(into)] requirements: Signal<Vec<ComparisonData>>,
|
||||||
|
) -> 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::<String>("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<String, HashMap<String, ValueType>>,
|
||||||
|
scholarship: ExpandableInfo,
|
||||||
|
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> {
|
||||||
|
// 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::<Vec<String>>();
|
||||||
|
|
||||||
|
Some(string_list)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<String>>>();
|
||||||
|
|
||||||
|
// 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::<Vec<ComparisonData>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<ComparisonData>>>();
|
||||||
|
|
||||||
|
// 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::<HashMap<String, HashMap<String, ValueType>>>();
|
||||||
|
|
||||||
|
// 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! {
|
||||||
|
<StudentInformationDialog
|
||||||
|
scholarship_id=scholarship.get().subject
|
||||||
|
student_id=current_student_id
|
||||||
|
visible=dialog_visible
|
||||||
|
on_close=on_dialog_close
|
||||||
|
essay_required=essay_required
|
||||||
|
fafsa_required=fafsa_required
|
||||||
|
/>
|
||||||
|
<Show when=move || !error_msg.get().is_empty()>
|
||||||
|
<div>"Error checking student eligibility: "{error_msg}</div>
|
||||||
|
</Show>
|
||||||
|
<div class="m-3 flex flex-col">
|
||||||
|
<div class="flex flex-row p-1 border-b-1 border-black">
|
||||||
|
<div class="pl-5 flex-1 text-lg font-bold text-left">"Last Name"</div>
|
||||||
|
<div class="pr-5 flex-1 text-lg font-bold text-left">"First Name"</div>
|
||||||
|
</div>
|
||||||
|
<For
|
||||||
|
each=move || eligible_students.get()
|
||||||
|
key=|(id, _)| id.clone()
|
||||||
|
children=move |(student_id, student)| {
|
||||||
|
let student = StoredValue::new(student);
|
||||||
|
let first_name = Memo::new(move |_| {
|
||||||
|
student
|
||||||
|
.get_value()
|
||||||
|
.get("first_name")
|
||||||
|
.map(|n| n.as_string().ok().flatten())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let last_name = Memo::new(move |_| {
|
||||||
|
student
|
||||||
|
.get_value()
|
||||||
|
.get("last_name")
|
||||||
|
.map(|n| n.as_string().ok().flatten())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let on_click = move |_| {
|
||||||
|
navigate_student.run(student_id.clone());
|
||||||
|
};
|
||||||
|
// For now, we'll just display each student's first and last name.
|
||||||
|
// Now I'll add a container with some links. These links will just be appended
|
||||||
|
// to the end of the current path.
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="flex flex-row p-1 border-y-1 border-gray-200 cursor-pointer hover:bg-yellow-100 transition-all duration-200"
|
||||||
|
on:click=on_click
|
||||||
|
>
|
||||||
|
<div class="pl-5 flex-1 text-lg text-left">{last_name}</div>
|
||||||
|
<div class="pr-5 flex-1 text-lg text-left">{first_name}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@ mod home_page;
|
|||||||
mod provider_page;
|
mod provider_page;
|
||||||
mod scholarship_page;
|
mod scholarship_page;
|
||||||
mod utils_page;
|
mod utils_page;
|
||||||
|
mod applicants_list;
|
||||||
|
|
||||||
pub use shell::*;
|
pub use shell::*;
|
||||||
pub use home_page::*;
|
pub use home_page::*;
|
||||||
pub use provider_page::*;
|
pub use provider_page::*;
|
||||||
pub use scholarship_page::*;
|
pub use scholarship_page::*;
|
||||||
pub use utils_page::*;
|
pub use utils_page::*;
|
||||||
|
pub use applicants_list::*;
|
||||||
@@ -25,6 +25,7 @@ pub fn AdminShell() -> impl IntoView {
|
|||||||
TabInfo::new("Providers", "providers", None),
|
TabInfo::new("Providers", "providers", None),
|
||||||
TabInfo::new("Scholarships", "scholarships", None),
|
TabInfo::new("Scholarships", "scholarships", None),
|
||||||
TabInfo::new("Utilities", "utilities", None),
|
TabInfo::new("Utilities", "utilities", None),
|
||||||
|
TabInfo::new("Scholarship Applicants", "applicants", None),
|
||||||
]
|
]
|
||||||
/>
|
/>
|
||||||
</Authenticated>
|
</Authenticated>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use leptos::prelude::*;
|
|||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use leptos::wasm_bindgen::JsCast;
|
use leptos::wasm_bindgen::JsCast;
|
||||||
use leptos::web_sys::{Blob, HtmlAnchorElement, Url, js_sys};
|
use leptos::web_sys::{Blob, HtmlAnchorElement, Url, js_sys};
|
||||||
|
use crate::pages::api::students::{get_student_info_json, get_student_pdf};
|
||||||
|
|
||||||
fn get_important_dates() -> Vec<DateInfo> {
|
fn get_important_dates() -> Vec<DateInfo> {
|
||||||
vec![
|
vec![
|
||||||
@@ -100,6 +101,36 @@ pub fn AdminUtilsPage() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_click_json = move |_| {
|
||||||
|
spawn_local(async move {
|
||||||
|
let (file_name, file_bytes) = get_student_pdf("54d82438-c051-709d-30f7-c60e97f5112a".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let uint_arr = js_sys::Uint8Array::new_with_length(file_bytes.len() as u32);
|
||||||
|
uint_arr.copy_from(&file_bytes);
|
||||||
|
|
||||||
|
let blob_parts = js_sys::Array::new();
|
||||||
|
blob_parts.push(&uint_arr);
|
||||||
|
|
||||||
|
let blob = Blob::new_with_u8_array_sequence(&blob_parts).unwrap();
|
||||||
|
|
||||||
|
let blob_url = Url::create_object_url_with_blob(&blob).unwrap();
|
||||||
|
|
||||||
|
let window = window();
|
||||||
|
let document = window.document().unwrap();
|
||||||
|
let a: HtmlAnchorElement = document.create_element("a").unwrap().dyn_into().unwrap();
|
||||||
|
|
||||||
|
a.set_href(&blob_url);
|
||||||
|
a.set_download(&file_name);
|
||||||
|
document.body().unwrap().append_child(&a).unwrap();
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
document.body().unwrap().remove_child(&a).unwrap();
|
||||||
|
Url::revoke_object_url(&blob_url).unwrap();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="mx-auto px-6">
|
<div class="mx-auto px-6">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
@@ -109,6 +140,7 @@ pub fn AdminUtilsPage() -> impl IntoView {
|
|||||||
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
||||||
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
||||||
<ActionButton on:click=on_click_export>"Scholarship Export"</ActionButton>
|
<ActionButton on:click=on_click_export>"Scholarship Export"</ActionButton>
|
||||||
|
<ActionButton on:click=on_click_json>"Get student JSON"</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
|
|||||||
return Err(ServerFnError::new("Missing file contents"));
|
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 subject = user_claims.subject;
|
||||||
|
|
||||||
let key = format!("{form_id}/{subject}/{input_name}/{file_name}");
|
let key = format!("{form_id}/{subject}/{input_name}/{file_name}");
|
||||||
@@ -157,7 +157,7 @@ pub async fn delete_file(
|
|||||||
) -> Result<String, ServerFnError> {
|
) -> Result<String, ServerFnError> {
|
||||||
use imports::*;
|
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 subject = user_claims.subject;
|
||||||
|
|
||||||
let entry_hk = format!("STUDENT#{subject}");
|
let entry_hk = format!("STUDENT#{subject}");
|
||||||
@@ -226,7 +226,7 @@ pub async fn list_files(
|
|||||||
) -> Result<Vec<String>, ServerFnError> {
|
) -> Result<Vec<String>, ServerFnError> {
|
||||||
use imports::*;
|
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 subject = user_claims.subject;
|
||||||
|
|
||||||
let entry_hk = format!("STUDENT#{subject}");
|
let entry_hk = format!("STUDENT#{subject}");
|
||||||
|
|||||||
+268
-23
@@ -2,6 +2,7 @@ use leptos::prelude::*;
|
|||||||
use leptos::server_fn::codec::Json;
|
use leptos::server_fn::codec::Json;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
mod imports {
|
mod imports {
|
||||||
@@ -14,8 +15,12 @@ mod imports {
|
|||||||
pub use leptos::logging::{debug_log, error};
|
pub use leptos::logging::{debug_log, error};
|
||||||
pub use std::collections::HashMap;
|
pub use std::collections::HashMap;
|
||||||
pub use zip::write::SimpleFileOptions;
|
pub use zip::write::SimpleFileOptions;
|
||||||
|
pub use leptos::serde_json;
|
||||||
|
pub use std::process::Command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PDF_TEMPLATE: &str = include_str!("../../../pdf_template.typ");
|
||||||
|
|
||||||
#[server(input = Json)]
|
#[server(input = Json)]
|
||||||
pub async fn put_student_data(
|
pub async fn put_student_data(
|
||||||
subject: String,
|
subject: String,
|
||||||
@@ -137,24 +142,41 @@ pub async fn get_all_student_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_completed_students(
|
pub async fn provider_get_completed_students(
|
||||||
access_token: String,
|
access_token: String,
|
||||||
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
use imports::*;
|
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<HashMap<String, HashMap<String, crate::common::ValueType>>, 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<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
// We want to get all student information. The requirements are that the students have completed
|
// 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 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
|
// The easiest way is to get all the information and filter on this side, instead of bookkeeping
|
||||||
// on the database's side.
|
// 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 client = create_dynamo_client().await;
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
@@ -229,7 +251,7 @@ pub async fn get_file_by_key(
|
|||||||
) -> Result<Vec<u8>, ServerFnError> {
|
) -> Result<Vec<u8>, ServerFnError> {
|
||||||
use imports::*;
|
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
|
// Check if the user's subject is contained in the file_key (since all files are keyed by the
|
||||||
// user's subject)
|
// user's subject)
|
||||||
@@ -275,10 +297,11 @@ pub async fn get_student_files(
|
|||||||
student_id: String,
|
student_id: String,
|
||||||
form_name: String,
|
form_name: String,
|
||||||
question_id: String,
|
question_id: String,
|
||||||
) -> Result<Vec<u8>, ServerFnError> {
|
file_name_postfix: String,
|
||||||
|
) -> Result<(String, Vec<u8>), ServerFnError> {
|
||||||
use imports::*;
|
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.
|
// Check if the user has permission to get these files.
|
||||||
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
|
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
|
||||||
@@ -287,6 +310,39 @@ pub async fn get_student_files(
|
|||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let demographics_res = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.expression_attribute_values(":sk", AttributeValue::S("DEMOGRAPHICS".to_string()))
|
||||||
|
.key_condition_expression("HK = :hk and SK = :sk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let student_demographics = demographics_res
|
||||||
|
.items
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(ServerFnError::new(
|
||||||
|
"Failed to find student demographic information.",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let first_name = student_demographics
|
||||||
|
.get("first_name")
|
||||||
|
.and_then(|v| v.as_s().ok().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let last_name = student_demographics
|
||||||
|
.get("last_name")
|
||||||
|
.and_then(|v| v.as_s().ok().cloned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let file_keys = client
|
let file_keys = client
|
||||||
.query()
|
.query()
|
||||||
.table_name(MAIN_TABLE_NAME)
|
.table_name(MAIN_TABLE_NAME)
|
||||||
@@ -346,9 +402,25 @@ pub async fn get_student_files(
|
|||||||
|
|
||||||
debug_log!("Writing {} files to zip file...", results.len());
|
debug_log!("Writing {} files to zip file...", results.len());
|
||||||
for (key, file_bytes) in results {
|
for (key, file_bytes) in results {
|
||||||
let file_name = key.split('/').last().unwrap_or_default();
|
let file_name = key
|
||||||
|
.split('/')
|
||||||
|
.last()
|
||||||
|
.ok_or(ServerFnError::new("Failed to parse file name"))?
|
||||||
|
.split(".")
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let file_ext = key
|
||||||
|
.split('.')
|
||||||
|
.last()
|
||||||
|
.ok_or(ServerFnError::new("Failed to parse file extension"))?;
|
||||||
|
|
||||||
|
// Construct the real file name. This name should be under 150 characters.
|
||||||
|
let temp = format!("{first_name} {last_name} - {file_name}");
|
||||||
|
let file_name_no_ext = temp.chars().take(150).collect::<String>();
|
||||||
|
let file_name_complete = format!("{file_name_no_ext}.{file_ext}");
|
||||||
|
|
||||||
writer
|
writer
|
||||||
.start_file(file_name, SimpleFileOptions::default())
|
.start_file(file_name_complete, SimpleFileOptions::default())
|
||||||
.map_err(ServerFnError::new)?;
|
.map_err(ServerFnError::new)?;
|
||||||
writer.write_all(&file_bytes).map_err(ServerFnError::new)?;
|
writer.write_all(&file_bytes).map_err(ServerFnError::new)?;
|
||||||
}
|
}
|
||||||
@@ -356,7 +428,9 @@ pub async fn get_student_files(
|
|||||||
debug_log!("Getting finished file");
|
debug_log!("Getting finished file");
|
||||||
let finished = writer.finish().map_err(ServerFnError::new)?;
|
let finished = writer.finish().map_err(ServerFnError::new)?;
|
||||||
|
|
||||||
Ok(finished.into_inner())
|
let file_name_hint = format!("{first_name}{last_name}_{file_name_postfix}.zip");
|
||||||
|
|
||||||
|
Ok((file_name_hint, finished.into_inner()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Get All Input Files API
|
/// # Get All Input Files API
|
||||||
@@ -368,19 +442,12 @@ pub async fn get_student_files(
|
|||||||
///
|
///
|
||||||
/// This is only usable by users with provider-level access.
|
/// This is only usable by users with provider-level access.
|
||||||
#[server]
|
#[server]
|
||||||
pub async fn get_all_input_files(
|
async fn get_all_input_files(
|
||||||
access_token: String,
|
|
||||||
form_name: String,
|
form_name: String,
|
||||||
input_name: String,
|
input_name: String,
|
||||||
) -> Result<HashMap<String, Vec<String>>, ServerFnError> {
|
) -> Result<HashMap<String, Vec<String>>, ServerFnError> {
|
||||||
use imports::*;
|
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 client = create_dynamo_client().await;
|
||||||
|
|
||||||
let res = client
|
let res = client
|
||||||
@@ -421,3 +488,181 @@ pub async fn get_all_input_files(
|
|||||||
|
|
||||||
Ok(result_map)
|
Ok(result_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn admin_get_all_input_files(
|
||||||
|
access_token: String,
|
||||||
|
form_name: String,
|
||||||
|
input_name: String,
|
||||||
|
) -> Result<HashMap<String, Vec<String>>, 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<HashMap<String, Vec<String>>, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_student_pdf(
|
||||||
|
student_id: String,
|
||||||
|
) -> Result<(String, Vec<u8>), ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
// NOTE: this API requires that the server has the typst-cli available ON PATH.
|
||||||
|
|
||||||
|
let student_info_str = get_student_info_json(student_id.clone()).await?;
|
||||||
|
let student_info = get_student_data(student_id, "DEMOGRAPHICS".to_string()).await?;
|
||||||
|
|
||||||
|
let first_name = student_info.get("first_name")
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let last_name = student_info.get("last_name")
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let typst_string = student_info_str
|
||||||
|
.replace("{", "(")
|
||||||
|
.replace("}", ")")
|
||||||
|
.replace("[", "(")
|
||||||
|
.replace("]", ")")
|
||||||
|
// Ensures trailing commas for lists of dictionaries
|
||||||
|
.replace("))", "),)");
|
||||||
|
|
||||||
|
debug_log!("New string: {}", typst_string);
|
||||||
|
|
||||||
|
let pdf_string = format!("#let student = {typst_string}\n{PDF_TEMPLATE}");
|
||||||
|
debug_log!("PDF string: {}", pdf_string);
|
||||||
|
let mut child = Command::new("typst")
|
||||||
|
.arg("compile")
|
||||||
|
.arg("-")
|
||||||
|
.arg("-")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let stdin = child.stdin.as_mut().unwrap();
|
||||||
|
stdin.write_all(pdf_string.as_bytes())?;
|
||||||
|
stdin.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let msg = String::from_utf8_lossy(&output.stderr).to_string();
|
||||||
|
error!("Failed to generate PDF: {}", msg);
|
||||||
|
Err(ServerFnError::new(format!("Failed to generate PDF: {msg}")))
|
||||||
|
} else {
|
||||||
|
let bytes = output.stdout;
|
||||||
|
let file_name = format!("{first_name}{last_name}_Application.pdf");
|
||||||
|
Ok((file_name, bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_student_info_json(
|
||||||
|
student_id: String,
|
||||||
|
) -> Result<String, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.key_condition_expression("HK = :hk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let work_exp = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.expression_attribute_values(":sk", AttributeValue::S("WORKEXP".to_string()))
|
||||||
|
.key_condition_expression("HK = :hk AND SK = :sk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
|
.map(|output| {
|
||||||
|
let items = output.items.unwrap_or_default();
|
||||||
|
debug_log!("Number of items: {}", items.len());
|
||||||
|
items.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.get("extracurricular")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(AttributeValue::S("".to_string()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let extracurricular = client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{student_id}")))
|
||||||
|
.expression_attribute_values(":sk", AttributeValue::S("DEMOGRAPHICS".to_string()))
|
||||||
|
.key_condition_expression("HK = :hk AND SK = :sk")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
let msg = e.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})
|
||||||
|
.map(|output| {
|
||||||
|
let items = output.items.unwrap_or_default();
|
||||||
|
debug_log!("Number of items: {}", items.len());
|
||||||
|
items.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.get("extracurricular")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(AttributeValue::S("".to_string()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(items) = response.items else {
|
||||||
|
return Err(ServerFnError::new("Failed to find student information."));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Join items together into a single object.
|
||||||
|
let mut data = items.into_iter()
|
||||||
|
.fold(HashMap::new(), |mut map, item| {
|
||||||
|
item.into_iter()
|
||||||
|
.for_each(|(k, v)| { map.insert(k, v); });
|
||||||
|
map
|
||||||
|
});
|
||||||
|
|
||||||
|
data.insert("work_experience".to_string(), work_exp);
|
||||||
|
data.insert("extracurricular".to_string(), extracurricular);
|
||||||
|
|
||||||
|
let json_value: serde_json::Value = serde_dynamo::from_item(data)?;
|
||||||
|
let json_string = serde_json::to_string(&json_value)?;
|
||||||
|
println!("{}", json_string);
|
||||||
|
|
||||||
|
Ok(json_string)
|
||||||
|
}
|
||||||
+11
-2
@@ -17,10 +17,19 @@ pub async fn fetch_jwks(user_pool_id: String, region: String) -> Result<JwkSet,
|
|||||||
|
|
||||||
/// Validates a JWT, and, if successful, decodes it into a series of user claims.
|
/// Validates a JWT, and, if successful, decodes it into a series of user claims.
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn validate_and_get_token_info(token: String) -> Result<UserClaims, ServerFnError> {
|
pub async fn validate_and_get_token_info(
|
||||||
|
token: String,
|
||||||
|
pool_id: impl Into<String>,
|
||||||
|
pool_region: impl Into<String>
|
||||||
|
) -> Result<UserClaims, ServerFnError> {
|
||||||
use jsonwebtoken::{Algorithm, Validation, decode};
|
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);
|
let validation = Validation::new(Algorithm::RS256);
|
||||||
// validation.set_audience(&["scholarships-rs"]);
|
// validation.set_audience(&["scholarships-rs"]);
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use crate::common::{
|
use crate::common::{ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType};
|
||||||
ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode,
|
|
||||||
SchemaType, ValueType,
|
|
||||||
};
|
|
||||||
use crate::components::{ActionButton, Banner, DataDisplay, Loading};
|
use crate::components::{ActionButton, Banner, DataDisplay, Loading};
|
||||||
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::{GetStudentFiles, get_all_input_files, get_student_data};
|
use crate::pages::api::students::{GetStudentFiles, provider_get_all_input_files, get_student_data, GetStudentPdf};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use leptos::ev::{Event, MouseEvent};
|
use leptos::ev::{Event, MouseEvent};
|
||||||
use leptos::html::Dialog;
|
use leptos::html::Dialog;
|
||||||
@@ -71,12 +68,12 @@ fn ApplicantsScholarshipList() -> impl IntoView {
|
|||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn get_provider_scholarships(
|
async fn get_provider_scholarships(
|
||||||
access_token: String,
|
access_token: String
|
||||||
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||||
use crate::pages::api::SCHOLARSHIPS_TABLE;
|
use crate::pages::api::SCHOLARSHIPS_TABLE;
|
||||||
use crate::pages::api::tokens::validate_and_get_token_info;
|
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.
|
// Get the information from the database.
|
||||||
let client = crate::utils::server::create_dynamo_client().await;
|
let client = crate::utils::server::create_dynamo_client().await;
|
||||||
@@ -110,8 +107,10 @@ fn ApplicantsScholarshipList() -> impl IntoView {
|
|||||||
let trigger = Trigger::new();
|
let trigger = Trigger::new();
|
||||||
let scholarships_res = Resource::new(
|
let scholarships_res = Resource::new(
|
||||||
move || (trigger.track(), access_token.get()),
|
move || (trigger.track(), access_token.get()),
|
||||||
async move |(_, access_token)| {
|
move |(_, access_token)| async move {
|
||||||
get_provider_scholarships(access_token.unwrap_or_default()).await
|
get_provider_scholarships(
|
||||||
|
access_token.unwrap_or_default()
|
||||||
|
).await
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -201,7 +200,7 @@ fn ApplicantsScholarshipEntry(
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn ApplicantsStudentList() -> impl IntoView {
|
pub fn ApplicantsStudentList() -> impl IntoView {
|
||||||
use crate::pages::api::get_scholarship_info;
|
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::hooks::use_params;
|
||||||
use leptos_router::params::Params;
|
use leptos_router::params::Params;
|
||||||
|
|
||||||
@@ -241,7 +240,7 @@ pub fn ApplicantsStudentList() -> impl IntoView {
|
|||||||
ServerFnError,
|
ServerFnError,
|
||||||
> {
|
> {
|
||||||
let (students, scholarships, requirements_list) = tokio::join!(
|
let (students, scholarships, requirements_list) = tokio::join!(
|
||||||
get_completed_students(token),
|
provider_get_completed_students(token),
|
||||||
get_scholarship_info(scholarship_id),
|
get_scholarship_info(scholarship_id),
|
||||||
get_comparison_info()
|
get_comparison_info()
|
||||||
);
|
);
|
||||||
@@ -374,7 +373,7 @@ fn ApplicantsStudentListView(
|
|||||||
return Ok(HashMap::new());
|
return Ok(HashMap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
get_all_input_files(
|
provider_get_all_input_files(
|
||||||
access_token,
|
access_token,
|
||||||
"financial_info".to_string(),
|
"financial_info".to_string(),
|
||||||
"fafsa".to_string(),
|
"fafsa".to_string(),
|
||||||
@@ -394,7 +393,7 @@ fn ApplicantsStudentListView(
|
|||||||
return Ok(HashMap::new());
|
return Ok(HashMap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
get_all_input_files(
|
provider_get_all_input_files(
|
||||||
access_token,
|
access_token,
|
||||||
"scholarship_essays".to_string(),
|
"scholarship_essays".to_string(),
|
||||||
scholarship.get().subject,
|
scholarship.get().subject,
|
||||||
@@ -479,9 +478,18 @@ fn ApplicantsStudentListView(
|
|||||||
if list.is_empty() {
|
if list.is_empty() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
list.iter().any(|requirement| {
|
||||||
|
let result = requirement.compare(&student).unwrap_or(false);
|
||||||
|
|
||||||
list.iter()
|
if !result {
|
||||||
.any(|requirement| requirement.compare(&student).unwrap_or(false))
|
debug_log!(
|
||||||
|
"Student with id {id} failed - requirement with id {} failed.",
|
||||||
|
requirement.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if result { Some((id, student)) } else { None }
|
if result { Some((id, student)) } else { None }
|
||||||
@@ -583,7 +591,7 @@ fn ApplicantsStudentListView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn StudentInformationDialog(
|
pub fn StudentInformationDialog(
|
||||||
#[prop(into)] scholarship_id: Signal<String>,
|
#[prop(into)] scholarship_id: Signal<String>,
|
||||||
#[prop(into)] student_id: Signal<Option<String>>,
|
#[prop(into)] student_id: Signal<Option<String>>,
|
||||||
#[prop(into)] visible: Signal<bool>,
|
#[prop(into)] visible: Signal<bool>,
|
||||||
@@ -1112,13 +1120,17 @@ fn StudentInformationDialog(
|
|||||||
// scholarship, meaning their essays and/or FAFSA by request.
|
// scholarship, meaning their essays and/or FAFSA by request.
|
||||||
|
|
||||||
let get_student_files = ServerAction::<GetStudentFiles>::new();
|
let get_student_files = ServerAction::<GetStudentFiles>::new();
|
||||||
let get_buttons_disabled = get_student_files.pending();
|
let get_student_pdf = ServerAction::<GetStudentPdf>::new();
|
||||||
|
let get_buttons_disabled = Memo::new(move |_| {
|
||||||
|
get_student_files.pending().get() && get_student_pdf.pending().get()
|
||||||
|
});
|
||||||
let on_click_fafsa = move |_| {
|
let on_click_fafsa = move |_| {
|
||||||
get_student_files.dispatch(GetStudentFiles {
|
get_student_files.dispatch(GetStudentFiles {
|
||||||
access_token: access_token.get().unwrap_or_default(),
|
access_token: access_token.get().unwrap_or_default(),
|
||||||
student_id: student_id.get().unwrap_or_default(),
|
student_id: student_id.get().unwrap_or_default(),
|
||||||
form_name: "financial_info".to_string(),
|
form_name: "financial_info".to_string(),
|
||||||
question_id: "fafsa".to_string(),
|
question_id: "fafsa".to_string(),
|
||||||
|
file_name_postfix: "FafsaInfo".to_string(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1129,26 +1141,45 @@ fn StudentInformationDialog(
|
|||||||
student_id: student_id.get().unwrap_or_default(),
|
student_id: student_id.get().unwrap_or_default(),
|
||||||
form_name: "scholarship_essays".to_string(),
|
form_name: "scholarship_essays".to_string(),
|
||||||
question_id: scholarship_id.get(),
|
question_id: scholarship_id.get(),
|
||||||
|
file_name_postfix: "Essays".to_string(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_click_pdf = move |_| {
|
||||||
|
debug_log!("Getting student PDF");
|
||||||
|
get_student_pdf.dispatch(GetStudentPdf {
|
||||||
|
student_id: student_id.get().unwrap_or_default(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
fn open_file(file_name: String, file: Vec<u8>) {
|
||||||
|
let base64 = base64::engine::general_purpose::STANDARD.encode(file);
|
||||||
|
let data_url = format!("data:application/zip;base64,{}", base64);
|
||||||
|
// Create a base64 link and open it.
|
||||||
|
let a: HtmlAnchorElement = document()
|
||||||
|
.create_element("a")
|
||||||
|
.expect("Failed to create anchor element")
|
||||||
|
.dyn_into()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
a.set_download(&*file_name);
|
||||||
|
a.set_href(&data_url);
|
||||||
|
document().body().unwrap().append_child(&a).unwrap();
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
Effect::new(move || {
|
Effect::new(move || {
|
||||||
if let Some(Ok(file)) = get_student_files.value().get() {
|
if let Some(Ok((file_name, file))) = get_student_files.value().get() {
|
||||||
let base64 = base64::engine::general_purpose::STANDARD.encode(file);
|
open_file(file_name, file);
|
||||||
let data_url = format!("data:application/zip;base64,{}", base64);
|
get_student_files.clear();
|
||||||
// Create a base64 link and open it.
|
}
|
||||||
let a: HtmlAnchorElement = document()
|
});
|
||||||
.create_element("a")
|
|
||||||
.expect("Failed to create anchor element")
|
|
||||||
.dyn_into()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
a.set_download("");
|
|
||||||
a.set_href(&data_url);
|
|
||||||
document().body().unwrap().append_child(&a).unwrap();
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
|
|
||||||
|
// Copy and paste is bad, but it's a little short on time.
|
||||||
|
Effect::new(move || {
|
||||||
|
if let Some(Ok((file_name, file))) = get_student_pdf.value().get() {
|
||||||
|
open_file(file_name, file);
|
||||||
get_student_files.clear();
|
get_student_files.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1182,6 +1213,9 @@ fn StudentInformationDialog(
|
|||||||
"Download Essay File(s)"
|
"Download Essay File(s)"
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Show>
|
</Show>
|
||||||
|
<ActionButton on:click=on_click_pdf disabled=get_buttons_disabled>
|
||||||
|
"Download Student Application"
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
{move || Suspend::new(async move {
|
{move || Suspend::new(async move {
|
||||||
get_student_data(
|
get_student_data(
|
||||||
@@ -1276,7 +1310,10 @@ fn StudentInformationDialog(
|
|||||||
})
|
})
|
||||||
})}
|
})}
|
||||||
{move || Suspend::new(async move {
|
{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
|
.await
|
||||||
.map(|student_info| {
|
.map(|student_info| {
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
Reference in new issue
Block a user