Compare commits

...
17 Commits
Author SHA1 Message Date
Lucas Vas f9d5b25436 Merge pull request #72 from R15-Software-Dev/fix/filtering-bugs
Check requirement return value on Ok variant
2026-03-23 12:05:22 -04:00
alphafetus 891e324186 fix: check requirement return value on Ok variant 2026-03-23 11:47:06 -04:00
Lucas Vas b502c2f96d Merge pull request #71 from R15-Software-Dev/fix/comparison-filtering
Fix comparison logic for filtering
2026-03-19 13:49:06 -04:00
alphafetus 681da8c326 fix: use correct comparison logic
Was originally short-circuiting on empty lists of requirements.
2026-03-19 13:37:11 -04:00
Lucas Vas 628b607074 Merge pull request #70 from R15-Software-Dev/fix/api-errors
Significantly improve logging for api requests
2026-03-18 12:30:19 -04:00
alphafetus 97be249826 fix: import ProvideErrorMetadata correctly 2026-03-18 12:16:44 -04:00
alphafetus e1bd472a36 feat: significantly improve logging for api requests 2026-03-18 12:08:41 -04:00
Lucas Vas dd19631e33 Merge pull request #69 from R15-Software-Dev/fix/comparison-changes
Small comparison changes
2026-03-18 10:38:26 -04:00
alphafetus a0b206a554 fix: do not display scholarships that are not completed 2026-03-12 10:41:32 -04:00
alphafetus e9b0452d38 feat: improve error messages for client side comparison check 2026-03-11 14:24:25 -04:00
Lucas Vas 448e8bbb38 Merge pull request #68 from R15-Software-Dev/fix/comparison-changes
Comparison and sports changes
2026-03-11 12:49:53 -04:00
alphafetus ae7a17ad92 chore: add volleyball option 2026-03-11 11:54:34 -04:00
alphafetus 750059e9ad fix: attended_bas checks correct data member 2026-03-11 11:53:50 -04:00
alphafetus 5a371ae574 chore: temporarily reopen provider forms (for real this time) 2026-03-11 11:53:05 -04:00
alphafetus d7f44463c6 chore: temporarily reopen provider forms 2026-03-11 11:33:30 -04:00
alphafetus 5872002538 feat: improve error messages in files api 2026-03-11 11:28:52 -04:00
alphafetus 957fc0c5eb chore: change title of form 2026-03-09 12:16:41 -04:00
11 changed files with 330 additions and 226 deletions

No files matched your search

+7 -7
View File
@@ -1,11 +1,11 @@
use crate::components::ToastList; use crate::components::ToastList;
use crate::components::login::ProviderLoginContext; use crate::components::login::ProviderLoginContext;
use crate::pages::student;
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, LoanerShell, ProviderContactPage, ProviderPortal, ScholarshipInfoPage,
}; };
use crate::pages::{FormClosedPage, 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;
@@ -89,11 +89,11 @@ fn ProviderRoutes() -> impl MatchNestedRoutes + Clone {
<Route path=path!("/applicants") view=ApplicantsPageFallback /> <Route path=path!("/applicants") view=ApplicantsPageFallback />
<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=FormClosedPage> <ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
// <Route path=path!(":id") view=FormClosedPage /> <Route path=path!(":id") view=ScholarshipInfoPage />
// <Route path=path!("") view=FormClosedPage /> <Route path=path!("") view=ScholarshipInfoPage />
// </ParentRoute> </ParentRoute>
</ParentRoute> </ParentRoute>
} }
.into_inner() .into_inner()
+37 -44
View File
@@ -14,10 +14,9 @@ use crate::common::{
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison, ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
TextComparison, TextListComparison, ValueType, TextComparison, TextListComparison, ValueType,
}; };
use std::collections::HashMap;
use leptos::prelude::ServerFnError; use leptos::prelude::ServerFnError;
use leptos::server; use leptos::server;
use std::collections::HashMap;
/// Helper function to create all sports comparisons. Only used to create initial /// Helper function to create all sports comparisons. Only used to create initial
/// comparison data, and should be removed upon completion of a comparison editor page. /// comparison data, and should be removed upon completion of a comparison editor page.
@@ -250,14 +249,18 @@ fn create_major_comparisons() -> Vec<ComparisonData> {
let ids = display_texts let ids = display_texts
.iter() .iter()
.map(|text| .map(|text| {
format!("{}_{}", "major", text.to_lowercase().replace(" ", "_").replace("/", "_")) format!(
) "{}_{}",
"major",
text.to_lowercase().replace(" ", "_").replace("/", "_")
)
})
.collect::<Vec<String>>(); .collect::<Vec<String>>();
ids.iter().zip(display_texts).map(|(id, display_text)| { ids.iter()
create_major_comparison(id, display_text) .zip(display_texts)
}) .map(|(id, display_text)| create_major_comparison(id, display_text))
.collect::<Vec<ComparisonData>>() .collect::<Vec<ComparisonData>>()
} }
@@ -280,13 +283,11 @@ fn make_comp_list() -> Vec<ComparisonData> {
"Academic Information", "Academic Information",
"Math SAT Score > 650", "Math SAT Score > 650",
); );
let service_hours_20 = ComparisonData::new( let service_hours_20 = ComparisonData::new(
"service_hours_20", "service_hours_20",
"service_hours", "service_hours",
ComparisonType::Number( ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
NumberComparison::GreaterThanOrEqual
),
ValueType::Number(Some(20.to_string())), ValueType::Number(Some(20.to_string())),
"Community Service", "Community Service",
"20+ service hours", "20+ service hours",
@@ -295,9 +296,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
let service_hours_25 = ComparisonData::new( let service_hours_25 = ComparisonData::new(
"service_hours_25", "service_hours_25",
"service_hours", "service_hours",
ComparisonType::Number( ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
NumberComparison::GreaterThanOrEqual
),
ValueType::Number(Some(25.to_string())), ValueType::Number(Some(25.to_string())),
"Community Service", "Community Service",
"25+ service hours", "25+ service hours",
@@ -306,9 +305,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
let service_hours_30 = ComparisonData::new( let service_hours_30 = ComparisonData::new(
"service_hours_30", "service_hours_30",
"service_hours", "service_hours",
ComparisonType::Number( ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
NumberComparison::GreaterThanOrEqual
),
ValueType::Number(Some(30.to_string())), ValueType::Number(Some(30.to_string())),
"Community Service", "Community Service",
"30+ service hours", "30+ service hours",
@@ -334,7 +331,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
let attended_bas = ComparisonData::new( let attended_bas = ComparisonData::new(
"attended_bas", "attended_bas",
"attended_bas", "attend_bas",
ComparisonType::Text(TextComparison::Matches), ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())), ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors", "Additional Eligibility Factors",
@@ -365,7 +362,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
ComparisonType::Text(TextComparison::Matches), ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())), ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors", "Additional Eligibility Factors",
"Participated in Pomperaug Youth Baseball" "Participated in Pomperaug Youth Baseball",
); );
let aquatic_club = ComparisonData::new( let aquatic_club = ComparisonData::new(
@@ -374,25 +371,25 @@ fn make_comp_list() -> Vec<ComparisonData> {
ComparisonType::Text(TextComparison::Matches), ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())), ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors", "Additional Eligibility Factors",
"Participated in Panthers Aquatic Club" "Participated in Panthers Aquatic Club",
); );
let pea_member = ComparisonData::new( let pea_member = ComparisonData::new(
"pea_member", "pea_member",
"pea_member", "pea_member",
ComparisonType::Text(TextComparison::Matches), ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())), ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors", "Additional Eligibility Factors",
"Has a Parent in the PEA" "Has a Parent in the PEA",
); );
let music_program = ComparisonData::new( let music_program = ComparisonData::new(
"music_program", "music_program",
"music_program", "music_program",
ComparisonType::Text(TextComparison::Matches), ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())), ValueType::String(Some("Yes".to_string())),
"Sports Participation", "Sports Participation",
"PHS Music Program" "PHS Music Program",
); );
let mut sports_comps = create_sports_comparisons(); let mut sports_comps = create_sports_comparisons();
@@ -412,7 +409,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
youth_baseball, youth_baseball,
aquatic_club, aquatic_club,
pea_member, pea_member,
music_program music_program,
]; ];
comp_list.append(&mut sports_comps); comp_list.append(&mut sports_comps);
@@ -438,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,15 +451,9 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
// Query the database for all comparisons. The client is only going to use the // Query the database for all comparisons. The client is only going to use the
// id and display text, but we'll return the whole thing. // id and display text, but we'll return the whole thing.
match client match client.scan().table_name(COMPARISONS_TABLE).send().await {
.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![])
@@ -469,22 +461,19 @@ 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))
} }
} }
} }
#[server] #[server]
pub async fn get_comparisons_categorized() -> Result<HashMap<String, Vec<ComparisonData>>, ServerFnError> { pub async fn get_comparisons_categorized()
-> Result<HashMap<String, Vec<ComparisonData>>, ServerFnError> {
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
log!("Getting all comparisons from the database, by category."); log!("Getting all comparisons from the database, by category.");
match client match client.scan().table_name(COMPARISONS_TABLE).send().await {
.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, categorizing."); log!("Found comparisons, categorizing.");
@@ -492,23 +481,27 @@ pub async fn get_comparisons_categorized() -> Result<HashMap<String, Vec<Compari
let items: Vec<ComparisonData> = serde_dynamo::from_items(items)?; let items: Vec<ComparisonData> = serde_dynamo::from_items(items)?;
let categorized = items.iter().fold( let categorized = items.iter().fold(
HashMap::<String, Vec<ComparisonData>>::new(), |mut map, comp| { HashMap::<String, Vec<ComparisonData>>::new(),
// Load each comparison into a map, then return that map. |mut map, comp| {
// Load each comparison into a map, then return that map.
map.entry(comp.category.clone()) map.entry(comp.category.clone())
.and_modify(|vec| vec.push(comp.clone())) .and_modify(|vec| vec.push(comp.clone()))
.or_insert(vec![comp.clone()]); .or_insert(vec![comp.clone()]);
map map
} },
); );
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))
} }
} }
+32 -32
View File
@@ -1,61 +1,60 @@
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> {
// Serialize and put dates into the database. // Serialize and put dates into the database.
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
log!("Creating dates: {:?}", dates); log!("Creating dates: {:?}", dates);
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().set_item(Some(item)).build().ok()?;
let put_request = PutRequest::builder()
.set_item(Some(item)) Some(WriteRequest::builder().put_request(put_request).build())
.build()
.expect("Couldn't build request, item is not present");
WriteRequest::builder().put_request(put_request).build()
}) })
.collect::<Vec<WriteRequest>>(); .collect::<Vec<WriteRequest>>();
let mut request_map = HashMap::new(); let mut request_map = HashMap::new();
request_map.insert(DATES_TABLE.to_string(), requests); request_map.insert(DATES_TABLE.to_string(), requests);
client client
.batch_write_item() .batch_write_item()
.set_request_items(Some(request_map)) .set_request_items(Some(request_map))
.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
.scan() .scan()
.table_name(DATES_TABLE) .table_name(DATES_TABLE)
@@ -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)
} }
+53 -18
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};
@@ -7,7 +8,11 @@ mod imports {
pub use super::super::{MAIN_TABLE_NAME, S3_BUCKET_NAME}; pub use super::super::{MAIN_TABLE_NAME, S3_BUCKET_NAME};
pub use crate::pages::api::tokens::validate_and_get_token_info; pub use crate::pages::api::tokens::validate_and_get_token_info;
pub use crate::utils::server::create_aws_config; pub use crate::utils::server::create_aws_config;
pub use aws_sdk_dynamodb::types::{AttributeValue, ReturnValue, Select}; pub use aws_sdk_dynamodb::{
error::ProvideErrorMetadata as _,
types::{AttributeValue, ReturnValue, Select},
};
pub use aws_sdk_s3::error::ProvideErrorMetadata as _;
} }
#[server(input = MultipartFormData)] #[server(input = MultipartFormData)]
@@ -57,6 +62,9 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
if input_name.is_empty() { if input_name.is_empty() {
return Err(ServerFnError::new("Missing input file")); return Err(ServerFnError::new("Missing input file"));
} }
if file_bytes.is_empty() {
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).await?;
let subject = user_claims.subject; let subject = user_claims.subject;
@@ -75,7 +83,14 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
.send() .send()
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|e| ServerFnError::new(format!("Couldn't put file to S3: {}", e.to_string())))?; .map_err(|e| {
let msg = format!(
"Couldn't put file to S3: {}",
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.
// This will allow the server to quickly return a list of all files that a specific user has // This will allow the server to quickly return a list of all files that a specific user has
@@ -101,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.to_string() 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.
@@ -116,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.to_string() err.message().unwrap_or("Unknown error occurred")
)) );
error!("{}", msg);
ServerFnError::new(msg)
})?; })?;
return Err(err); return Err(err);
@@ -167,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.
@@ -178,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.to_string() err.message().unwrap_or("Unknown error occurred")
)) );
error!("{}", msg);
ServerFnError::new(msg)
})?; })?;
return Err(err); return Err(err);
@@ -230,5 +258,12 @@ pub async fn list_files(
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
}) })
.map_err(|e| ServerFnError::new(format!("Couldn't list files: {}", e.to_string()))) .map_err(|err| {
let msg = format!(
"Failed to get files: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
} }
+23 -12
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,7 +75,8 @@ 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))
} }
} }
} }
+90 -56
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;
@@ -20,7 +16,7 @@ use leptos::server_fn::codec::Json;
#[server(GetScholarshipInfo)] #[server(GetScholarshipInfo)]
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> { pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
use imports::*; use imports::*;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
// Perform the operation - we just want to return all data that's contained in this entry, // Perform the operation - we just want to return all data that's contained in this entry,
@@ -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))
} }
} }
@@ -76,24 +79,20 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
#[server(GetAllScholarshipInfo)] #[server(GetAllScholarshipInfo)]
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> { pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
use imports::*; use imports::*;
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))
} }
} }
@@ -134,14 +142,20 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
#[server(RegisterScholarship)] #[server(RegisterScholarship)]
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> { pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
use imports::*; use imports::*;
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,41 +194,51 @@ 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.
match client match client
.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;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
client client
.scan() .scan()
.table_name(SCHOLARSHIPS_TABLE) .table_name(SCHOLARSHIPS_TABLE)
@@ -222,12 +248,20 @@ pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<Stri
let Some(items) = output.items else { let Some(items) = output.items else {
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)
} }
+54 -35
View File
@@ -3,36 +3,34 @@ 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::*;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
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( data_map_attr.insert("SK".into(), AttributeValue::S(data_type.to_uppercase()));
"SK".into(),
AttributeValue::S(data_type.to_uppercase())
);
debug_log!("Inserting this item: {:?}", data_map_attr); debug_log!("Inserting this item: {:?}", data_map_attr);
client client
.put_item() .put_item()
.table_name(MAIN_TABLE_NAME) .table_name(MAIN_TABLE_NAME)
@@ -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]
@@ -49,9 +54,9 @@ pub async fn get_student_data(
data_type: String, data_type: String,
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> { ) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
use imports::*; use imports::*;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
client client
.query() .query()
.table_name(MAIN_TABLE_NAME) .table_name(MAIN_TABLE_NAME)
@@ -65,37 +70,42 @@ pub async fn get_student_data(
let Some(list) = output.items else { let Some(list) = output.items else {
return HashMap::new(); return HashMap::new();
}; };
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
/// sort key. /// sort key.
/// ///
/// Note: if sorted data from the `put_student_data` function contains the same fields, this /// Note: if sorted data from the `put_student_data` function contains the same fields, this
/// function will have unexpected behavior. For example, if a `first_name` field appears in multiple /// function will have unexpected behavior. For example, if a `first_name` field appears in multiple
/// locations in the database, all for this single student, they will overwrite each other in no /// locations in the database, all for this single student, they will overwrite each other in no
/// 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)))
@@ -105,10 +115,19 @@ pub async fn get_all_student_data(
let Some(items) = output.items else { let Some(items) = output.items else {
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)
})
} }
+2 -1
View File
@@ -40,7 +40,8 @@ pub fn StudentAthleticsPage() -> impl IntoView {
"Lacrosse", "Lacrosse",
"Softball", "Softball",
"Tennis", "Tennis",
"Baseball" "Baseball",
"Volleyball"
] ]
), ),
input!( input!(
+31 -19
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_log, debug_error};
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::AuthSignal; use leptos_oidc::AuthSignal;
@@ -63,8 +63,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 +71,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()
@@ -104,20 +102,34 @@ pub fn StudentEligibilityPage() -> impl IntoView {
debug_log!( debug_log!(
"Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>() "Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>()
); );
let valid = resolved_requirements let valid = !scholarship_name.is_empty()
.iter() && resolved_requirements
.all(|list| { .iter()
list.iter() .all(|list| {
.fold( debug_log!("Checking list: {:?}", list);
false, if list.is_empty() {
|prev, requirement| { true
let result = requirement } else {
.compare(&student_info) list.iter()
.unwrap_or(false); .any(
if prev { prev } else { result } |requirement| {
}, let result = requirement
) .compare(&student_info);
});
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
}
}
},
)
}
});
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());
+1 -1
View File
@@ -12,7 +12,7 @@ pub fn StudentExtracurricularsPage() -> impl IntoView {
<div class="flex flex-1" /> <div class="flex flex-1" />
<div class="flex flex-col flex-2 mt-6"> <div class="flex flex-col flex-2 mt-6">
<ValidatedForm <ValidatedForm
title="Extracurricular Information" title="Extracurricular and Community Service Information"
description="This information will allow you to be eligible for any scholarships that have specific extracurricular requirements." description="This information will allow you to be eligible for any scholarships that have specific extracurricular requirements."
on_submit=controller.submit_action on_submit=controller.submit_action
disabled=controller.submit_pending disabled=controller.submit_pending