Compare commits

..
Author SHA1 Message Date
alphafetus a9dfbb423f refactor: remove unused traits crate
This crate was created as an experimental way to make data stores reactive, but is no longer used or required.
2026-09-24 12:18:18 -04:00
Lucas Vas 2e0e47d72b Merge pull request #81 from R15-Software-Dev/fix/admin-file-errors
Insert trailing commas after lists of strings
2026-04-27 15:02:16 -04:00
alphafetus 287598bb79 fix: insert trailing commas after lists of strings
This was already present for lists of dictionaries, but lists that contained a single string also failed to parse properly.
2026-04-27 14:57:31 -04:00
Lucas Vas fff1854067 Merge pull request #80 from R15-Software-Dev/fix/admin-file-errors
Allow essays and fafsa to be requested by administrators
2026-04-27 14:24:20 -04:00
alphafetus 6d6862a680 fix: allow essays and fafsa to be requested by administrators 2026-04-27 14:08:14 -04:00
6 changed files with 58 additions and 140 deletions

No files matched your search

+40 -28
View File
@@ -13,10 +13,10 @@ mod imports {
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 zip::write::SimpleFileOptions;
pub use leptos::serde_json;
pub use std::collections::HashMap;
pub use std::process::Command;
pub use zip::write::SimpleFileOptions;
}
static PDF_TEMPLATE: &str = include_str!("../../../pdf_template.typ");
@@ -147,7 +147,8 @@ pub async fn provider_get_completed_students(
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
use imports::*;
let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
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",
@@ -169,7 +170,8 @@ pub async fn admin_get_completed_students(
}
#[server]
async fn get_completed_students() -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
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
@@ -251,7 +253,8 @@ pub async fn get_file_by_key(
) -> Result<Vec<u8>, ServerFnError> {
use imports::*;
let user_claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
let user_claims =
validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
// Check if the user's subject is contained in the file_key (since all files are keyed by the
// user's subject)
@@ -301,12 +304,19 @@ pub async fn get_student_files(
) -> Result<(String, Vec<u8>), ServerFnError> {
use imports::*;
let claims = validate_and_get_token_info(access_token, "us-east-1_Lfjuy5zaM", "us-east-1").await?;
// Check if the user has permission to get these files.
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
return Err(ServerFnError::new("Access denied: user is not a provider"));
}
match validate_and_get_token_info(access_token.clone(), "us-east-1_Lfjuy5zaM", "us-east-1")
.await
{
Ok(claims) => {
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
return Err(ServerFnError::new("Access denied: user is not a provider"));
}
}
Err(_) => {
let _ = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1")
.await?;
}
};
let client = create_dynamo_client().await;
@@ -510,7 +520,8 @@ pub async fn provider_get_all_input_files(
) -> 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?;
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"));
}
@@ -519,9 +530,7 @@ pub async fn provider_get_all_input_files(
}
#[server]
pub async fn get_student_pdf(
student_id: String,
) -> Result<(String, Vec<u8>), ServerFnError> {
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.
@@ -529,11 +538,13 @@ pub async fn get_student_pdf(
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")
let first_name = student_info
.get("first_name")
.map(|v| v.to_string())
.unwrap_or_default();
let last_name = student_info.get("last_name")
let last_name = student_info
.get("last_name")
.map(|v| v.to_string())
.unwrap_or_default();
@@ -543,7 +554,8 @@ pub async fn get_student_pdf(
.replace("[", "(")
.replace("]", ")")
// Ensures trailing commas for lists of dictionaries
.replace("))", "),)");
.replace("))", "),)")
.replace("\")", "\",)");
debug_log!("New string: {}", typst_string);
@@ -577,9 +589,7 @@ pub async fn get_student_pdf(
}
#[server]
pub async fn get_student_info_json(
student_id: String,
) -> Result<String, ServerFnError> {
pub async fn get_student_info_json(student_id: String) -> Result<String, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
@@ -613,7 +623,8 @@ pub async fn get_student_info_json(
.map(|output| {
let items = output.items.unwrap_or_default();
debug_log!("Number of items: {}", items.len());
items.into_iter()
items
.into_iter()
.next()
.unwrap_or_default()
.get("extracurricular")
@@ -637,7 +648,8 @@ pub async fn get_student_info_json(
.map(|output| {
let items = output.items.unwrap_or_default();
debug_log!("Number of items: {}", items.len());
items.into_iter()
items
.into_iter()
.next()
.unwrap_or_default()
.get("extracurricular")
@@ -650,12 +662,12 @@ pub async fn get_student_info_json(
};
// 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
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);
+18 -14
View File
@@ -1,8 +1,13 @@
use crate::common::{ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType};
use crate::common::{
ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode,
SchemaType, ValueType,
};
use crate::components::{ActionButton, Banner, DataDisplay, Loading};
use crate::pages::UnauthenticatedPage;
use crate::pages::api::get_comparison_info;
use crate::pages::api::students::{GetStudentFiles, provider_get_all_input_files, get_student_data, GetStudentPdf};
use crate::pages::api::students::{
GetStudentFiles, GetStudentPdf, get_student_data, provider_get_all_input_files,
};
use base64::Engine;
use leptos::ev::{Event, MouseEvent};
use leptos::html::Dialog;
@@ -68,12 +73,17 @@ fn ApplicantsScholarshipList() -> impl IntoView {
#[server]
async fn get_provider_scholarships(
access_token: String
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_Lfjuy5zaM".to_string(), "us-east-1".to_string()).await?;
let claims = validate_and_get_token_info(
access_token,
"us-east-1_Lfjuy5zaM".to_string(),
"us-east-1".to_string(),
)
.await?;
// Get the information from the database.
let client = crate::utils::server::create_dynamo_client().await;
@@ -108,9 +118,7 @@ fn ApplicantsScholarshipList() -> impl IntoView {
let scholarships_res = Resource::new(
move || (trigger.track(), access_token.get()),
move |(_, access_token)| async move {
get_provider_scholarships(
access_token.unwrap_or_default()
).await
get_provider_scholarships(access_token.unwrap_or_default()).await
},
);
@@ -1121,9 +1129,8 @@ pub fn StudentInformationDialog(
let get_student_files = ServerAction::<GetStudentFiles>::new();
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 get_buttons_disabled =
Memo::new(move |_| get_student_files.pending().get() && get_student_pdf.pending().get());
let on_click_fafsa = move |_| {
get_student_files.dispatch(GetStudentFiles {
access_token: access_token.get().unwrap_or_default(),
@@ -1310,10 +1317,7 @@ pub fn StudentInformationDialog(
})
})}
{move || Suspend::new(async move {
get_student_data(
student_id.get().unwrap_or_default(),
"family".to_string()
)
get_student_data(student_id.get().unwrap_or_default(), "family".to_string())
.await
.map(|student_info| {
view! {
-7
View File
@@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "traits"
version = "0.1.0"
-6
View File
@@ -1,6 +0,0 @@
[package]
name = "traits"
version = "0.1.0"
edition = "2024"
[dependencies]
-3
View File
@@ -1,3 +0,0 @@
pub mod reactive;
pub use reactive::*;
-82
View File
@@ -1,82 +0,0 @@
/// Trait that ensures a reactive struct can be created from a non-reactive struct.
///
/// For example:
///
/// ```rust
/// struct User {
/// id: i32,
/// name: String
/// }
///
/// struct UserReactive {
/// id: RwSignal<i32>,
/// name: RwSignal<String>
/// }
///
/// impl ReactiveCapture for UserReactive {
/// // implement - required for ReactiveType.
/// }
///
/// impl AsReactive for User {
/// type ReactiveType = UserReactive;
/// fn as_reactive(self) -> UserReactive {
/// UserReactive {
/// id: RwSignal::new(self.id),
/// name: RwSignal::new(self.name)
/// }
/// }
/// }
/// ```
pub trait AsReactive {
/// The type of the reactive struct. It must implement `ReactiveCapture`.
type ReactiveType: ReactiveCapture;
/// Wraps all fields of the struct into an `RwSignal`, returning a new
/// "reactive" struct.
///
/// Note that this method consumes the original struct and returns a new
/// reactive struct.
fn as_reactive(self) -> Self::ReactiveType;
}
/// Trait that ensures a reactive struct can be captured into a non-reactive struct.
/// The "captured" struct does not contain *any* reactive fields, meaning that it is not
/// wrapped in an `RwSignal`.
///
/// The captured struct should simply collect all the values from the reactive fields at
/// that point in time.
///
/// For example:
///
/// ```rust
/// struct User {
/// id: i32,
/// name: String
/// }
///
/// struct UserReactive {
/// id: RwSignal<i32>,
/// name: RwSignal<String>
/// }
///
/// impl AsReactive for User {
/// // implement - required for CaptureType.
/// }
///
/// impl ReactiveCapture for UserReactive {
/// type CaptureType = User;
/// fn capture(&self) -> User {
/// User {
/// id: self.id.get(),
/// name: self.name.get()
/// }
/// }
/// }
/// ```
pub trait ReactiveCapture {
/// The type of the captured struct. It must implement `AsReactive`.
type CaptureType: AsReactive;
/// Captures the current state of the reactive struct into a non-reactive struct.
fn capture(&self) -> Self::CaptureType;
}