Merge branch 'main' into feat/applicants-page

# Conflicts:
#	src/app.rs
#	src/pages/student/eligibility_page.rs
This commit is contained in:
alphafetus committed 2026-03-31 07:14:13 -04:00
commit 8a44513a04
11 files changed
+287 -184

No files matched your search

+7 -7
View File
@@ -3,9 +3,9 @@ use crate::components::login::ProviderLoginContext;
use crate::pages::{ use crate::pages::{
AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage, AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminShell, AdminUtilsPage,
ApplicantsPageFallback, AuthCallbackPage, LoanerBorrowForm, LoanerFallback, LoanerReturnForm, ApplicantsPageFallback, AuthCallbackPage, LoanerBorrowForm, LoanerFallback, LoanerReturnForm,
LoanerShell, ProviderContactPage, ProviderPortal, ScholarshipInfoPage, LoanerShell, ProviderContactPage, ProviderPortal,
}; };
use crate::pages::{ApplicantsPageShell, student}; use crate::pages::{FormClosedPage, 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};
use leptos_oidc::AuthSignal; use leptos_oidc::AuthSignal;
@@ -91,11 +91,11 @@ fn ProviderRoutes() -> impl MatchNestedRoutes + Clone {
</ParentRoute> </ParentRoute>
<Route path=path!("/callback") view=AuthCallbackPage /> <Route path=path!("/callback") view=AuthCallbackPage />
<Route path=path!("/profile") view=ProviderContactPage /> <Route path=path!("/profile") view=ProviderContactPage />
// <Route path=path!("/scholarships") view=FormClosedPage /> <Route path=path!("/scholarships") view=FormClosedPage />
<ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage> // <ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
<Route path=path!(":id") view=ScholarshipInfoPage /> // <Route path=path!(":id") view=ScholarshipInfoPage />
<Route path=path!("") view=ScholarshipInfoPage /> // <Route path=path!("") view=ScholarshipInfoPage />
</ParentRoute> // </ParentRoute>
</ParentRoute> </ParentRoute>
} }
.into_inner() .into_inner()
+16 -4
View File
@@ -1,6 +1,6 @@
use crate::pages::api::files::{DeleteFile, upload_file}; use crate::pages::api::files::{DeleteFile, upload_file};
use leptos::html::{Input, Label}; use leptos::html::{Input, Label};
use leptos::logging::debug_log; use leptos::logging::{debug_error, debug_log};
use leptos::prelude::*; use leptos::prelude::*;
use leptos::web_sys::{File, FormData}; use leptos::web_sys::{File, FormData};
use leptos_icons::Icon; use leptos_icons::Icon;
@@ -28,6 +28,7 @@ pub fn FileDrop(
let file_name_list = RwSignal::new(Vec::new()); let file_name_list = RwSignal::new(Vec::new());
let hovering = RwSignal::new(false); let hovering = RwSignal::new(false);
let error_msg = RwSignal::new(String::new());
let name = StoredValue::new(name); let name = StoredValue::new(name);
let form_id = StoredValue::new(form_id); let form_id = StoredValue::new(form_id);
let auth = expect_context::<AuthSignal>(); let auth = expect_context::<AuthSignal>();
@@ -172,10 +173,18 @@ pub fn FileDrop(
existing_files existing_files
.get() .get()
.map(|result| { .map(|result| {
if let Ok(files) = result { match result {
file_name_list.set(files); Ok(files) => {
error_msg.set("".to_string());
file_name_list.set(files);
}
Err(err) => {
let msg = err.to_string();
debug_error!("Error while getting files: {}", msg);
error_msg.set(msg);
}
} }
}) });
}} }}
<label <label
node_ref=zone_ref node_ref=zone_ref
@@ -194,6 +203,9 @@ pub fn FileDrop(
<div class="text-sm text-gray-400">"Or click to select a file"</div> <div class="text-sm text-gray-400">"Or click to select a file"</div>
<input node_ref=input_ref type="file" class="hidden" on:change=on_input_change /> <input node_ref=input_ref type="file" class="hidden" on:change=on_input_change />
</label> </label>
<Show when=move || !error_msg.get().is_empty()>
<div class="flex-1">{error_msg}</div>
</Show>
<For <For
each=move || file_name_list.get() each=move || file_name_list.get()
key=|file| file.clone() key=|file| file.clone()
+6 -2
View File
@@ -435,6 +435,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
.await .await
{ {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("An unknown error occurred");
leptos::logging::error!("{}", msg);
return Err(ServerFnError::new(msg)); return Err(ServerFnError::new(msg));
} }
} }
@@ -453,7 +454,6 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
match client.scan().table_name(COMPARISONS_TABLE).send().await { match client.scan().table_name(COMPARISONS_TABLE).send().await {
Ok(output) => { Ok(output) => {
if let Some(items) = output.items { if let Some(items) = output.items {
log!("Found comparisons from API: {:?}", items);
Ok(serde_dynamo::from_items(items)?) Ok(serde_dynamo::from_items(items)?)
} else { } else {
Ok(vec![]) Ok(vec![])
@@ -461,6 +461,7 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
} }
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("An unknown error occurred");
leptos::logging::error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
@@ -493,11 +494,14 @@ pub async fn get_comparisons_categorized()
Ok(categorized) Ok(categorized)
} else { } else {
Err(ServerFnError::new("Couldn't find any comparisons.")) let msg = "Couldn't find any comparisons.";
leptos::logging::error!("{}", msg);
Err(ServerFnError::new(msg))
} }
} }
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("An unknown error occurred");
leptos::logging::error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
+25 -25
View File
@@ -1,22 +1,19 @@
use std::collections::HashMap;
use leptos::logging::log;
use leptos::prelude::*;
use crate::common::DateInfo; use crate::common::DateInfo;
use leptos::logging::{error, log};
use leptos::prelude::*;
use std::collections::HashMap;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use super::DATES_TABLE; use super::DATES_TABLE;
#[cfg(feature = "ssr")]
use aws_sdk_dynamodb::{
types::{WriteRequest, PutRequest},
Client as DynamoClient
};
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use crate::utils::server::create_dynamo_client; use crate::utils::server::create_dynamo_client;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use std::sync::Arc; use aws_sdk_dynamodb::{
Client as DynamoClient,
error::ProvideErrorMetadata,
types::{PutRequest, WriteRequest},
};
#[server] #[server]
pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> { pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> {
@@ -27,16 +24,12 @@ pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> {
let requests = dates let requests = dates
.into_iter() .into_iter()
.map(|date| { .filter_map(|date| {
let item = serde_dynamo::to_item(&date) let item = serde_dynamo::to_item(&date).unwrap_or_default();
.unwrap_or_default();
let put_request = PutRequest::builder() let put_request = PutRequest::builder().set_item(Some(item)).build().ok()?;
.set_item(Some(item))
.build()
.expect("Couldn't build request, item is not present");
WriteRequest::builder().put_request(put_request).build() Some(WriteRequest::builder().put_request(put_request).build())
}) })
.collect::<Vec<WriteRequest>>(); .collect::<Vec<WriteRequest>>();
@@ -49,11 +42,17 @@ pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> {
.send() .send()
.await .await
.map(|_| ()) .map(|_| ())
.map_err(ServerFnError::from) .map_err(|err| {
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
ServerFnError::new(msg)
})
} }
#[server] #[server]
pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> { pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
log!("Getting important dates list");
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
client client
@@ -65,14 +64,15 @@ pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
if let Some(items) = output.items { if let Some(items) = output.items {
items items
.into_iter() .into_iter()
.filter_map(|item| { .filter_map(|item| serde_dynamo::from_item::<_, DateInfo>(item).ok())
serde_dynamo::from_item::<_, DateInfo>(item)
.ok()
})
.collect::<Vec<DateInfo>>() .collect::<Vec<DateInfo>>()
} else { } else {
Vec::new() Vec::new()
} }
}) })
.map_err(ServerFnError::from) .map_err(|err| {
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
ServerFnError::new(msg)
})
} }
-1
View File
@@ -231,7 +231,6 @@ pub async fn get_scholarship_csv() -> Result<Vec<u8>, ServerFnError> {
writer.flush()?; writer.flush()?;
let result = writer.into_inner()?; let result = writer.into_inner()?;
debug_log!("{:?}", String::from_utf8(result.clone()));
Ok(result) Ok(result)
} }
+41 -23
View File
@@ -1,4 +1,5 @@
use leptos::logging::debug_log; use leptos::leptos_dom::error;
use leptos::logging::debug_log;
use leptos::prelude::*; use leptos::prelude::*;
use server_fn::codec::{MultipartData, MultipartFormData}; use server_fn::codec::{MultipartData, MultipartFormData};
@@ -83,10 +84,12 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|e| { .map_err(|e| {
ServerFnError::new(format!( let msg = format!(
"Couldn't put file to S3: {}", "Couldn't put file to S3: {}",
e.message().unwrap_or_default() e.message().unwrap_or("Unknown error occurred")
)) );
error!("{}", msg);
ServerFnError::new(msg)
})?; })?;
// Besides uploading the file to S3, we also want to store a FILE entry in Dynamo. // Besides uploading the file to S3, we also want to store a FILE entry in Dynamo.
@@ -113,11 +116,13 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
.item("file_key".to_string(), AttributeValue::S(key.clone())) .item("file_key".to_string(), AttributeValue::S(key.clone()))
.send() .send()
.await .await
.map_err(|e| { .map_err(|err| {
ServerFnError::new(format!( let msg = format!(
"Couldn't put file to Dynamo, file rolled back: {}", "Couldn't put file to Dynamo, file rolled back: {}",
e.message().unwrap_or_default() err.message().unwrap_or("Unknown error occurred")
)) );
error!("{}", msg);
ServerFnError::new(msg)
}); });
// If Dynamo fails, we want to handle the error by rolling back the file. Then, we return a failure. // If Dynamo fails, we want to handle the error by rolling back the file. Then, we return a failure.
@@ -128,11 +133,13 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
.key(&key) .key(&key)
.send() .send()
.await .await
.map_err(|e| { .map_err(|err| {
ServerFnError::new(format!( let msg = format!(
"Couldn't roll back S3 operation: {}", "Failed to rollback Dynamo entry: {}",
e.message().unwrap_or_default() err.message().unwrap_or("Unknown error occurred")
)) );
error!("{}", msg);
ServerFnError::new(msg)
})?; })?;
return Err(err); return Err(err);
@@ -179,7 +186,14 @@ pub async fn delete_file(
.key(key) .key(key)
.send() .send()
.await .await
.map_err(ServerFnError::from); .map_err(|err| {
let msg = format!(
"Failed to put file into S3: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
});
if let Err(err) = s3_result { if let Err(err) = s3_result {
// Add the key back to Dynamo and return an error. // Add the key back to Dynamo and return an error.
@@ -190,10 +204,12 @@ pub async fn delete_file(
.send() .send()
.await .await
.map_err(|err| { .map_err(|err| {
ServerFnError::new(format!( let msg = format!(
"Couldn't roll back Dynamo operation: {}", "Failed to rollback Dynamo entry: {}",
err.message().unwrap_or_default() err.message().unwrap_or("Unknown error occurred")
)) );
error!("{}", msg);
ServerFnError::new(msg)
})?; })?;
return Err(err); return Err(err);
@@ -242,10 +258,12 @@ pub async fn list_files(
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
}) })
.map_err(|e| { .map_err(|err| {
ServerFnError::new(format!( let msg = format!(
"Couldn't list files: {}", "Failed to get files: {}",
e.message().unwrap_or_default() err.message().unwrap_or("Unknown error occurred")
)) );
error!("{}", msg);
ServerFnError::new(msg)
}) })
} }
+22 -11
View File
@@ -1,8 +1,8 @@
use leptos::prelude::*; use crate::common::ValueType;
use leptos::logging::debug_log; use leptos::logging::{debug_log, error};
use std::collections::HashMap; use leptos::prelude::*;
use leptos::server_fn::codec::Json; use leptos::server_fn::codec::Json;
use crate::common::ValueType; use std::collections::HashMap;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use crate::utils::server::{create_dynamo_client, into_attr_map}; use crate::utils::server::{create_dynamo_client, into_attr_map};
@@ -12,8 +12,8 @@ static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
#[server] #[server]
pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> { pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
use aws_sdk_dynamodb::types::AttributeValue;
use aws_sdk_dynamodb::error::ProvideErrorMetadata; use aws_sdk_dynamodb::error::ProvideErrorMetadata;
use aws_sdk_dynamodb::types::AttributeValue;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
@@ -31,29 +31,39 @@ pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueTyp
return Ok(HashMap::new()); return Ok(HashMap::new());
}; };
let map = item.iter().map(|(key, val)| { let map = item
let val_type = ValueType::from(val); .iter()
.map(|(key, val)| {
let val_type = ValueType::from(val);
(key.clone(), val_type) (key.clone(), val_type)
}).collect::<HashMap<String, ValueType>>(); })
.collect::<HashMap<String, ValueType>>();
Ok(map) Ok(map)
} }
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred."); let msg = err.message().unwrap_or("An unknown error occurred.");
error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
} }
#[server(input = Json)] #[server(input = Json)]
pub async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> { pub async fn put_provider_contact(
id: String,
contact_info: HashMap<String, ValueType>,
) -> Result<(), ServerFnError> {
use aws_sdk_dynamodb::error::ProvideErrorMetadata; use aws_sdk_dynamodb::error::ProvideErrorMetadata;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
let mut item = into_attr_map(contact_info); let mut item = into_attr_map(contact_info);
item.insert("subject".to_owned(), aws_sdk_dynamodb::types::AttributeValue::S(id)); item.insert(
"subject".to_owned(),
aws_sdk_dynamodb::types::AttributeValue::S(id),
);
match client match client
.put_item() .put_item()
@@ -65,6 +75,7 @@ pub async fn put_provider_contact(id: String, contact_info: HashMap<String, Valu
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred."); let msg = err.message().unwrap_or("An unknown error occurred.");
error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
+76 -42
View File
@@ -1,15 +1,11 @@
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
mod imports { mod imports {
pub use leptos::logging::debug_log;
pub use aws_sdk_dynamodb::{
types::AttributeValue,
error::ProvideErrorMetadata,
};
pub use leptos::leptos_dom::log;
pub use uuid::Uuid;
pub use crate::utils::server::{create_dynamo_client};
pub use crate::common::ValueType;
pub use super::super::SCHOLARSHIPS_TABLE; pub use super::super::SCHOLARSHIPS_TABLE;
pub use crate::common::ValueType;
pub use crate::utils::server::create_dynamo_client;
pub use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue};
pub use leptos::logging::{debug_log, error, log};
pub use uuid::Uuid;
} }
use crate::common::ExpandableInfo; use crate::common::ExpandableInfo;
@@ -39,11 +35,14 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
Ok(serde_dynamo::from_item(item)?) Ok(serde_dynamo::from_item(item)?)
} else { } else {
log!("Couldn't find scholarship with ID {:?}", id); log!("Couldn't find scholarship with ID {:?}", id);
Err(ServerFnError::new("Couldn't find scholarship with given ID.")) Err(ServerFnError::new(
"Couldn't find scholarship with given ID.",
))
} }
} }
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("An unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
@@ -51,12 +50,15 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
#[server(CreateScholarshipInfo, input = Json)] #[server(CreateScholarshipInfo, input = Json)]
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> { pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
use imports::*;
use aws_sdk_dynamodb::error::ProvideErrorMetadata; use aws_sdk_dynamodb::error::ProvideErrorMetadata;
use imports::*;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
log!("Creating or updating scholarship with ID {:?}", info.subject); log!(
"Creating or updating scholarship with ID {:?}",
info.subject
);
match client match client
.put_item() .put_item()
@@ -68,6 +70,7 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("An unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
@@ -79,21 +82,17 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
debug_log!("Getting all scholarship info"); debug_log!("Getting all scholarship info");
match client match client.scan().table_name(SCHOLARSHIPS_TABLE).send().await {
.scan()
.table_name(SCHOLARSHIPS_TABLE)
.send()
.await
{
Ok(output) => { Ok(output) => {
if let Some(items) = output.items { if let Some(items) = output.items {
Ok(serde_dynamo::from_items(items)?) Ok(serde_dynamo::from_items(items)?)
} else { } else {
Ok(Vec::new()) Ok(Vec::new())
} }
}, }
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
@@ -101,17 +100,25 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
/// Gets all scholarships that are associated with the given scholarship provider's ID. /// Gets all scholarships that are associated with the given scholarship provider's ID.
#[server(GetProviderScholarships)] #[server(GetProviderScholarships)]
pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<ExpandableInfo>, ServerFnError> { pub async fn get_provider_scholarships(
provider_id: String,
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
use imports::*; use imports::*;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
debug_log!("Getting provider scholarships for provider with ID {:?}", provider_id); debug_log!(
"Getting provider scholarships for provider with ID {:?}",
provider_id
);
match client match client
.scan() .scan()
.table_name(SCHOLARSHIPS_TABLE) .table_name(SCHOLARSHIPS_TABLE)
.expression_attribute_values(":id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?) .expression_attribute_values(
":id",
serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?,
)
.filter_expression("provider_id = :id") .filter_expression("provider_id = :id")
.send() .send()
.await .await
@@ -124,7 +131,8 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
} }
} }
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
@@ -137,11 +145,17 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
debug_log!("Creating scholarship for provider with ID {:?}", provider_id); debug_log!(
"Creating scholarship for provider with ID {:?}",
provider_id
);
let mut current_uuid = Uuid::new_v4().to_string(); let mut current_uuid = Uuid::new_v4().to_string();
let mut item = ExpandableInfo::new(current_uuid.clone()); let mut item = ExpandableInfo::new(current_uuid.clone());
item.data.insert("provider_id".to_string(), ValueType::String(Some(provider_id))); item.data.insert(
"provider_id".to_string(),
ValueType::String(Some(provider_id)),
);
loop { loop {
let ser_item = serde_dynamo::to_item(&item)?; let ser_item = serde_dynamo::to_item(&item)?;
match client match client
@@ -165,10 +179,12 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
log!("Current error: {:?}", err); log!("Current error: {:?}", err);
current_uuid = Uuid::new_v4().to_string(); current_uuid = Uuid::new_v4().to_string();
item.subject = current_uuid.clone(); item.subject = current_uuid.clone();
continue continue;
} }
_ => { _ => {
return Err(ServerFnError::new(err.message().unwrap_or("An unknown error occurred"))); return Err(ServerFnError::new(
err.message().unwrap_or("Unknown error occurred"),
));
} }
} }
} }
@@ -178,12 +194,19 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
/// Deletes a provider's scholarship given their provider ID and scholarship ID. /// Deletes a provider's scholarship given their provider ID and scholarship ID.
#[server(DeleteProviderScholarship)] #[server(DeleteProviderScholarship)]
pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: String) -> Result<(), ServerFnError> { pub async fn delete_provider_scholarship(
provider_id: String,
scholarship_id: String,
) -> Result<(), ServerFnError> {
use imports::*; use imports::*;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
debug_log!("Deleting scholarship with ID {:?} for provider with ID {:?}", scholarship_id, provider_id); debug_log!(
"Deleting scholarship with ID {:?} for provider with ID {:?}",
scholarship_id,
provider_id
);
// When we delete a scholarship, we need to ensure that the provider's ID matches the scholarship, // When we delete a scholarship, we need to ensure that the provider's ID matches the scholarship,
// otherwise everyone can delete anyone else's scholarships. // otherwise everyone can delete anyone else's scholarships.
@@ -191,23 +214,26 @@ pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: St
.delete_item() .delete_item()
.table_name(SCHOLARSHIPS_TABLE) .table_name(SCHOLARSHIPS_TABLE)
.key("subject", AttributeValue::S(scholarship_id)) .key("subject", AttributeValue::S(scholarship_id))
.expression_attribute_values(":provider_id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?) .expression_attribute_values(
":provider_id",
serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?,
)
.condition_expression("provider_id = :provider_id") .condition_expression("provider_id = :provider_id")
.send() .send()
.await .await
{ {
Ok(_) => { Ok(_) => Ok(()),
Ok(())
}
Err(err) => { Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred"); let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg)) Err(ServerFnError::new(msg))
} }
} }
} }
#[server] #[server]
pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> { pub async fn get_all_scholarships()
-> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> {
use imports::*; use imports::*;
use std::collections::HashMap; use std::collections::HashMap;
@@ -223,11 +249,19 @@ pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<Stri
return Vec::new(); return Vec::new();
}; };
items.iter().map(|item| { items
// Each item should be simply converted from an AttributeValue to a ValueType. .iter()
item.iter().map(|(k, v)| (k.clone(), ValueType::from(v))) .map(|item| {
.collect::<HashMap<String, ValueType>>() // Each item should be simply converted from an AttributeValue to a ValueType.
}).collect::<Vec<_>>() item.iter()
.map(|(k, v)| (k.clone(), ValueType::from(v)))
.collect::<HashMap<String, ValueType>>()
})
.collect::<Vec<_>>()
})
.map_err(|err| {
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
ServerFnError::new(msg)
}) })
.map_err(ServerFnError::from)
} }
+41 -22
View File
@@ -3,19 +3,20 @@ use leptos::server_fn::codec::Json;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
mod imports { mod imports {
pub use crate::utils::server::*;
pub use aws_sdk_dynamodb::types::AttributeValue;
pub use super::super::MAIN_TABLE_NAME; pub use super::super::MAIN_TABLE_NAME;
pub use crate::common::ValueType; pub use crate::common::ValueType;
pub use crate::utils::server::*;
pub use aws_sdk_dynamodb::error::ProvideErrorMetadata;
pub use aws_sdk_dynamodb::types::AttributeValue;
pub use leptos::logging::{debug_log, error};
pub use std::collections::HashMap; pub use std::collections::HashMap;
pub use leptos::logging::debug_log;
} }
#[server(input = Json)] #[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 imports::*; use imports::*;
@@ -24,12 +25,9 @@ pub async fn put_student_data(
let mut data_map_attr = into_attr_map(data_map); let mut data_map_attr = into_attr_map(data_map);
data_map_attr.insert( data_map_attr.insert(
"HK".into(), "HK".into(),
AttributeValue::S(format!("STUDENT#{}", subject)) AttributeValue::S(format!("STUDENT#{}", subject)),
);
data_map_attr.insert(
"SK".into(),
AttributeValue::S(data_type.to_uppercase())
); );
data_map_attr.insert("SK".into(), AttributeValue::S(data_type.to_uppercase()));
debug_log!("Inserting this item: {:?}", data_map_attr); debug_log!("Inserting this item: {:?}", data_map_attr);
@@ -40,7 +38,14 @@ pub async fn put_student_data(
.send() .send()
.await .await
.map(|_| ()) .map(|_| ())
.map_err(ServerFnError::from) .map_err(|err| {
let msg = format!(
"Couldn't put item into Dynamo: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
} }
#[server] #[server]
@@ -68,16 +73,20 @@ pub async fn get_student_data(
let mut map = HashMap::new(); let mut map = HashMap::new();
let _ = list let _ = list.into_iter().flatten().for_each(|(k, v)| {
.into_iter() map.insert(k, ValueType::from(&v));
.flatten() });
.for_each(|(k, v)| {
map.insert(k, ValueType::from(&v));
});
map map
}) })
.map_err(ServerFnError::from) .map_err(|err| {
let msg = format!(
"Couldn't get item from Dynamo: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
} }
/// Gets and flattens all of a student's information from the database, regardless of their defined /// Gets and flattens all of a student's information from the database, regardless of their defined
@@ -89,13 +98,14 @@ pub async fn get_student_data(
/// specific order. /// specific order.
#[server] #[server]
pub async fn get_all_student_data( pub async fn get_all_student_data(
subject: String subject: String,
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> { ) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
use imports::*; use imports::*;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
client.query() client
.query()
.table_name(MAIN_TABLE_NAME) .table_name(MAIN_TABLE_NAME)
.key_condition_expression("HK = :hk") .key_condition_expression("HK = :hk")
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject))) .expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
@@ -106,9 +116,18 @@ pub async fn get_all_student_data(
return HashMap::new(); return HashMap::new();
}; };
items.iter().flatten() items
.map(|(|k, v)| (k.clone(), ValueType::from(v))) .iter()
.flatten()
.map(|(k, v)| (k.clone(), ValueType::from(v)))
.collect::<HashMap<String, ValueType>>() .collect::<HashMap<String, ValueType>>()
}) })
.map_err(ServerFnError::from) .map_err(|err| {
let msg = format!(
"Couldn't get data from Dynamo: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
} }
+31 -26
View File
@@ -4,7 +4,7 @@ use crate::pages::api::files::list_files;
use crate::pages::api::students::get_all_student_data; use crate::pages::api::students::get_all_student_data;
use crate::pages::api::{get_all_scholarship_info, get_comparison_info}; use crate::pages::api::{get_all_scholarship_info, get_comparison_info};
use crate::utils::get_user_claims; use crate::utils::get_user_claims;
use leptos::logging::debug_log; use leptos::logging::{debug_error, debug_log};
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::AuthSignal; use leptos_oidc::AuthSignal;
@@ -23,6 +23,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
let auth = expect_context::<AuthSignal>(); let auth = expect_context::<AuthSignal>();
let user_claims = get_user_claims(); let user_claims = get_user_claims();
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone())); let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
let access_token = Memo::new(move |_| auth.get().authenticated().map(|a| a.access_token()));
let form_id = StoredValue::new("scholarship_essays".to_string()); let form_id = StoredValue::new("scholarship_essays".to_string());
@@ -63,8 +64,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
.get("name") .get("name")
.unwrap_or(&ValueType::String(None)) .unwrap_or(&ValueType::String(None))
.as_string() .as_string()
.ok() .ok()?
.flatten()
.unwrap_or_default(); .unwrap_or_default();
debug_log!("Checking scholarship: {}", scholarship_name); debug_log!("Checking scholarship: {}", scholarship_name);
let requirements_map = scholarship let requirements_map = scholarship
@@ -72,8 +72,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
.get("requirements") .get("requirements")
.unwrap_or(&ValueType::Map(None)) .unwrap_or(&ValueType::Map(None))
.as_map() .as_map()
.ok() .ok()?
.flatten()
.unwrap_or_default() .unwrap_or_default()
.values() .values()
.cloned() .cloned()
@@ -108,38 +107,44 @@ pub fn StudentEligibilityPage() -> impl IntoView {
&& resolved_requirements && resolved_requirements
.iter() .iter()
.all(|list| { .all(|list| {
list.iter() debug_log!("Checking list: {:?}", list);
.fold( if list.is_empty() {
false, true
|prev, requirement| { } else {
let result = requirement list.iter()
.compare(&student_info) .any(|requirement| {
.unwrap_or_else(|err| { let result = requirement.compare(&student_info);
debug_log!("Requirement failed: {err}"); match result.as_ref() {
Ok(status) => {
debug_log!(
"Requirement with id {} is Ok({})", requirement.id, status
);
status.clone()
}
Err(e) => {
debug_error!(
"Requirement with id {} failed: {}", requirement.id, e
);
false false
}); }
if prev { prev } else { result } }
}, })
) }
}); });
if valid { if valid {
debug_log!("Scholarship is valid."); debug_log!("Scholarship is valid.");
let scholarship_id = StoredValue::new(scholarship.subject.clone()); let scholarship_id = StoredValue::new(scholarship.subject.clone());
let resource = Resource::new( let resource = Resource::new(
move || ( move || (
auth access_token.get(),
.try_with(|a| a.authenticated().map(|a| a.access_token()))
.flatten(),
form_id.get_value(), form_id.get_value(),
scholarship_id.get_value(), scholarship_id.get_value(),
), ),
async move |(access_token, form_id, scholarship_id)| { async move |(access_token, form_id, scholarship_id)| {
list_files( let Some(access_token) = access_token else {
access_token.unwrap_or_default(), return Err(ServerFnError::new("Couldn't find access token"));
form_id, };
scholarship_id, list_files(access_token, form_id, scholarship_id).await
)
.await
}, },
); );
Some((scholarship.clone(), resource)) Some((scholarship.clone(), resource))
+22 -21
View File
@@ -1,10 +1,10 @@
use std::collections::HashMap;
use leptos::prelude::*;
use uuid::Uuid;
use crate::common::ValueType; use crate::common::ValueType;
use crate::components::{Toast, ToastContext}; use crate::components::{Toast, ToastContext};
use crate::pages::api::students::{get_student_data, PutStudentData}; use crate::pages::api::students::{PutStudentData, get_student_data};
use crate::utils::get_user_claims; use crate::utils::get_user_claims;
use leptos::prelude::*;
use std::collections::HashMap;
use uuid::Uuid;
pub struct StudentFormInfo { pub struct StudentFormInfo {
pub data_map: RwSignal<HashMap<String, ValueType>>, pub data_map: RwSignal<HashMap<String, ValueType>>,
@@ -22,20 +22,19 @@ pub fn use_student_form(
let form_type = form_type.into(); let form_type = form_type.into();
let user_claims = get_user_claims(); let user_claims = get_user_claims();
let user_id = Memo::new(move |_| { let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
user_claims.get()
.map(|info| info.claims.subject.clone())
});
let data_map = RwSignal::new(HashMap::new()); let data_map = RwSignal::new(HashMap::new());
let refresh_trigger = Trigger::new(); let refresh_trigger = Trigger::new();
let data_resource = Resource::new( let data_resource = Resource::new(
move || (user_id.get().unwrap_or_default(), form_type.get(), refresh_trigger.track()), move || (user_id.get(), form_type.get(), refresh_trigger.track()),
async move |(id, form_type, _)| get_student_data( async move |(id, form_type, _)| {
id, let Some(id) = id else {
form_type, return Ok(HashMap::new());
).await };
get_student_data(id, form_type).await
},
); );
Effect::new(move || { Effect::new(move || {
@@ -50,11 +49,13 @@ pub fn use_student_form(
let submit_action = ServerAction::<PutStudentData>::new(); let submit_action = ServerAction::<PutStudentData>::new();
let on_submit = move || { let on_submit = move || {
submit_action.dispatch(PutStudentData { if let Some(id) = user_id.get() {
subject: user_id.get().unwrap_or_default(), submit_action.dispatch(PutStudentData {
data_type: form_type.get(), subject: id,
data_map: data_map.get() data_type: form_type.get(),
}); data_map: data_map.get(),
});
}
}; };
if enable_toasts { if enable_toasts {
@@ -74,13 +75,13 @@ pub fn use_student_form(
Err(e) => Toast::new() Err(e) => Toast::new()
.id(Uuid::new_v4()) .id(Uuid::new_v4())
.header("Submission Failed") .header("Submission Failed")
.msg(e.to_string()) .msg(e.to_string()),
}; };
toasts.toast(toast); toasts.toast(toast);
untrack(move || submit_action.clear()); untrack(move || submit_action.clear());
}, },
false false,
); );
} }
@@ -90,6 +91,6 @@ pub fn use_student_form(
data_resource, data_resource,
user_id, user_id,
submit_action: on_submit.into(), submit_action: on_submit.into(),
submit_pending: submit_action.pending() submit_pending: submit_action.pending(),
} }
} }