feat: implement comparison skeleton
This commit lays out the general idea of how the ComparisonData struct will work. It is expandable and I'm sure we will add more to it as time goes on.
This commit is contained in:
1 parent
431a8f244a
commit
83e46f4064
17 files changed
+1161
-89
No files matched your search
Binary file not shown.
@@ -0,0 +1,113 @@
|
||||
#set heading(numbering: "1.1")
|
||||
#set par(justify: true)
|
||||
|
||||
= Purpose
|
||||
|
||||
The purpose of this document is to define how the binding and filtering
|
||||
algorithm works within the Region 15 Scholarship Application. As such, it
|
||||
must define how the student and provider schema works and is editable by the
|
||||
application administrators.
|
||||
|
||||
= Student and Provider Schema
|
||||
|
||||
Administrators of the program shall be able to define a specific structure
|
||||
for the data that should be collected from the students and providers. This
|
||||
requires the ability to edit forms based on a series of options.
|
||||
|
||||
== Data Types
|
||||
|
||||
The application shall have a series of data types, so far limited to:
|
||||
- Text
|
||||
- Numbers
|
||||
- Lists (multiple text or number values)
|
||||
- Mapped values (larger structures comprised of multiple separate text, number, or list values)
|
||||
|
||||
== Input Types
|
||||
|
||||
The application has a series of components that have been or will be built
|
||||
to support this data collection:
|
||||
- `OutlinedTextField`: supports text and numbers
|
||||
- `Dropdown`: supports strictly defined text
|
||||
- `CheckboxList`: supports strictly defined text lists
|
||||
- `RadioList`: supports strictly defined text
|
||||
- `MultiEntry`: supports mapped values
|
||||
|
||||
Student information is defined within a specific form schema. The required values
|
||||
for each input type to work is well-defined, and shall be editable by the scholarship
|
||||
administrators. An interface shall be created that allows for editing with a simple
|
||||
preview.
|
||||
|
||||
= Student and Provider Binding
|
||||
|
||||
For the purposes of this document, binding is defined as the ability to relate data
|
||||
members defined in the student and provider data structures so that they may be
|
||||
procedurally or programmatically filtered through a database. For example, binding
|
||||
the `first_name` member from the student with the `scholarship_name` member from the
|
||||
provider means that the application administrators shall be able to perform comparisons
|
||||
from the `first_name` to the `scholarship_name`, or vice versa.
|
||||
|
||||
== Data Types in comparisons
|
||||
|
||||
Values that do not have the same type shall not be bindable. For example, a member that
|
||||
contains a text value can _only_ be bound with another member that also contains text.
|
||||
The same applies for numbers and lists.
|
||||
|
||||
Mapped values are slightly different, in that they contain a series of extra values.
|
||||
Since mapped values contain other data types, binding must happen on a more precise scale.
|
||||
The administrators shall be able to choose individual members from within the map and link
|
||||
them to other members of the same type. For example, a data map `example_map` contains a
|
||||
member `text_member`. Binding `text_member` is only possible with another member that
|
||||
contains text.
|
||||
|
||||
= Methodology
|
||||
|
||||
== Constraints
|
||||
|
||||
Administrators will create a series of comparisons dependent upon the types of the members
|
||||
that are being bound. For example, number values will present inequality options. Text
|
||||
values are a little more complex, in that they may have a specific set of options that
|
||||
are available (users are shown a `RadioList` or `Dropdown`). Depending on what these
|
||||
values are defined as, the available options for comparison need to match.
|
||||
|
||||
The full list of possible comparisons is as follows:
|
||||
- Numbers: inequality operators ($>$, $<$, $<=$, $>=$, $=$)
|
||||
- Text:
|
||||
- Single text values: direct matching based on input string
|
||||
- Selection text values: direct matching based on string options
|
||||
- Multiple text values:
|
||||
|
||||
== Appearance
|
||||
|
||||
Administrators will be able to select a specific schema - provider or student - and the
|
||||
members will be displayed in a list-like format. Depending on the type of each member
|
||||
shown, the available options for comparisons will follow the rules above. The
|
||||
administrators can choose and create multiple of these conditions, which will be
|
||||
displayed to the providers while they are choosing requirements.
|
||||
|
||||
Comparisons will have a member that contains a "display text." This text is displayed
|
||||
to the providers and acts as an abstraction for the actual comparison. For example,
|
||||
a GPA comparison may consist of the `Student.gpa` member, a $>$ condition, and a
|
||||
conditional value of `3.2`. The display text for this comparison may be "GPA $> 3.2$",
|
||||
or anything that the administrator may choose.
|
||||
|
||||
After being created, comparisons will be sortable within the list that the providers
|
||||
will see. The list may have multiple categories and multiple conditions may be selected
|
||||
by the providers.
|
||||
|
||||
Members with associated comparisons shall have a boolean value that indicates whether
|
||||
multiple conditions are selectable. For example, perhaps a provider wants to select
|
||||
multiple sports (`athletics` has several comparisons, one for each sport). The
|
||||
`athletics` member will be able to determine that multiple values are selectable,
|
||||
allowing the providers to select multiple types of sports.
|
||||
|
||||
*NOTE: The issue with this is that even though multiple options are selected, we do not
|
||||
currently have a method to determine if the options are joined with an AND or OR
|
||||
condition. This means that selecting multiple sports does not distinguish between
|
||||
a student playing football AND soccer, or a student playing football OR soccer.*
|
||||
|
||||
= Tasks left to do:
|
||||
|
||||
- finalize dashboard plan
|
||||
- create filtering algorithm (and more theory?)
|
||||
- file input component
|
||||
- pdf generation
|
||||
+15
-14
@@ -1,27 +1,27 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
||||
use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge, LoginLink};
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||
use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge};
|
||||
use leptos_router::{
|
||||
StaticSegment,
|
||||
components::{Route, Router, Routes},
|
||||
StaticSegment,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::pages::{AboutPage, HomePage, UnauthenticatedPage};
|
||||
use crate::pages::{AboutPage, ComparisonTestPage, HomePage};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
<HydrationScripts options />
|
||||
<MetaTags />
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
<App />
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
@@ -45,28 +45,29 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
let auth = Auth::signal();
|
||||
provide_context(auth); // allows use of this signal in lower areas of the tree without
|
||||
// explicitly passing it through the html tree
|
||||
// explicitly passing it through the html tree
|
||||
|
||||
let _ = Auth::init(parameters);
|
||||
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css"/>
|
||||
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css" />
|
||||
|
||||
// <AuthLoading><p>"Authentication is loading"</p></AuthLoading>
|
||||
// <AuthErrorContext><AuthErrorView/></AuthErrorContext>
|
||||
|
||||
// sets the document title
|
||||
<Title text="R15 Scholarship App DEV"/>
|
||||
<Title text="R15 Scholarship App DEV" />
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
<main>
|
||||
// TODO Create a 404 page
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=StaticSegment("") view=HomePage/>
|
||||
<Route path=StaticSegment("about") view=AboutPage/>
|
||||
<Route path=StaticSegment("") view=HomePage />
|
||||
<Route path=StaticSegment("about") view=AboutPage />
|
||||
<Route path=StaticSegment("comparison") view=ComparisonTestPage />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
use super::{
|
||||
Comparison, MapListComparison, NestedListComparison, NumberComparison, NumberListComparison,
|
||||
TextComparison, TextListComparison,
|
||||
};
|
||||
use crate::common::{ExpandableInfo, ValueType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ComparisonData {
|
||||
/// Each comparison will have a unique ID.
|
||||
pub id: String,
|
||||
/// The member from the Student structure to match against.
|
||||
pub member: String,
|
||||
/// The comparison to perform.
|
||||
pub comparison: ComparisonType,
|
||||
/// The value to match against.
|
||||
pub target_value: ValueType,
|
||||
/// The display category for this comparison. Affects where this comparison
|
||||
/// will be displayed in the provider side of the application.
|
||||
pub category: String,
|
||||
/// The text that will be displayed on the provider side.
|
||||
pub display_text: String,
|
||||
}
|
||||
|
||||
impl ComparisonData {
|
||||
pub fn compare(&self, student_data: &ExpandableInfo) -> Result<bool, String> {
|
||||
let value = student_data.data.get(&self.member).unwrap();
|
||||
self.comparison.evaluate(value, &self.target_value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ComparisonType {
|
||||
Number(NumberComparison),
|
||||
Text(TextComparison),
|
||||
TextList(TextListComparison),
|
||||
NumberList(NumberListComparison),
|
||||
MapList(MapListComparison),
|
||||
NestedList(NestedListComparison),
|
||||
}
|
||||
|
||||
impl Comparison for ComparisonType {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String> {
|
||||
match self {
|
||||
ComparisonType::Number(n) => n.evaluate(comp_value, target_value),
|
||||
ComparisonType::Text(t) => t.evaluate(comp_value, target_value),
|
||||
ComparisonType::NumberList(l) => l.evaluate(comp_value, target_value),
|
||||
ComparisonType::MapList(m) => m.evaluate(comp_value, target_value),
|
||||
ComparisonType::TextList(tl) => tl.evaluate(comp_value, target_value),
|
||||
ComparisonType::NestedList(nl) => nl.evaluate(comp_value, target_value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::comparison::test_utils::create_student_data;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_student_data() {
|
||||
let student_data = create_student_data();
|
||||
|
||||
// Should succeed every time.
|
||||
let text_comp_success = ComparisonData {
|
||||
id: "first_name_success".to_string(),
|
||||
member: "first_name".to_string(),
|
||||
comparison: ComparisonType::Text(TextComparison::Matches),
|
||||
target_value: ValueType::String(Some("John".to_string())),
|
||||
category: "Basic Checks".to_string(),
|
||||
display_text: "First Name is John".to_string(),
|
||||
};
|
||||
|
||||
// Uses an incorrect target_value type.
|
||||
let text_comp_fail = ComparisonData {
|
||||
id: "first_name_fail".to_string(),
|
||||
member: "first_name".to_string(),
|
||||
comparison: ComparisonType::Text(TextComparison::Matches),
|
||||
target_value: ValueType::Number(Some("Jane".to_string())),
|
||||
category: "Basic Checks".to_string(),
|
||||
display_text: "First Name is Jane".to_string(),
|
||||
};
|
||||
|
||||
let mut result = text_comp_success.compare(&student_data);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
|
||||
result = text_comp_fail.compare(&student_data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
use super::{Comparison, NumberComparison};
|
||||
use crate::common::ValueType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum NumberListComparison {
|
||||
/// Sums the values in the list and checks them using the given [`NumberComparison`].
|
||||
/// Requires the list to contain only numbers.
|
||||
Sum(Box<NumberComparison>),
|
||||
/// Checks if the list contains a specific value.
|
||||
Contains,
|
||||
/// Checks if the list does not contain a specific value.
|
||||
NotContains,
|
||||
}
|
||||
|
||||
impl Comparison for NumberListComparison {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String> {
|
||||
match self {
|
||||
NumberListComparison::Sum(num_comp) => {
|
||||
// Requires the list to contain only numbers. Get this list or throw an error.
|
||||
let student_list = match comp_value {
|
||||
ValueType::List(Some(list)) => list,
|
||||
_ => return Err("Expected a list".into()),
|
||||
};
|
||||
|
||||
let sum = student_list.iter().try_fold(0.0, |acc, item| match item {
|
||||
ValueType::Number(Some(num_str)) => {
|
||||
let num = num_str.parse::<f64>().unwrap();
|
||||
Ok(acc + num)
|
||||
}
|
||||
_ => Err("Expected a number".into()),
|
||||
});
|
||||
|
||||
// If the sum is not a number, return an error.
|
||||
let sum = match sum {
|
||||
Ok(sum) => sum,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
// Finally perform the comparison using this sum... we don't have a function for this yet.
|
||||
match num_comp.evaluate(&ValueType::Number(Some(sum.to_string())), target_value) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
NumberListComparison::Contains => {
|
||||
// Get list and check if it contains the target value.
|
||||
// This will check the type and the value, in that order. If the type does not match,
|
||||
// this will return false.
|
||||
let list = match comp_value {
|
||||
ValueType::List(Some(vec)) => vec,
|
||||
_ => return Err("Expected a list".into()),
|
||||
};
|
||||
|
||||
// Check if the list contains the target value.
|
||||
Ok(list.contains(target_value))
|
||||
}
|
||||
_ => Err("Not implemented".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all available comparisons for a text list. All comparisons are comparable to a
|
||||
/// single value.
|
||||
///
|
||||
/// For example, using `TextListComparison::Contains` checks if a specific value is contained
|
||||
/// in the list.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum TextListComparison {
|
||||
/// Checks if the list contains the target value.
|
||||
Contains,
|
||||
/// Checks if the list does not contain the target value.
|
||||
NotContains,
|
||||
/// Checks if the list is empty.
|
||||
IsEmpty,
|
||||
/// Checks if the list is not empty.
|
||||
IsNotEmpty,
|
||||
}
|
||||
|
||||
impl Comparison for TextListComparison {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String> {
|
||||
match self {
|
||||
TextListComparison::Contains => {
|
||||
let list_opt = match comp_value {
|
||||
ValueType::List(list) => list,
|
||||
_ => return Err("Expected a list".into()),
|
||||
};
|
||||
|
||||
// If the list exists, check if it contains the target value.
|
||||
// Otherwise, return false. Note that this is not a strongly typed comparison - if
|
||||
// the list contains multiple different types, no error is thrown.
|
||||
let check = match list_opt {
|
||||
Some(list) => list.contains(target_value),
|
||||
None => false,
|
||||
};
|
||||
|
||||
Ok(check)
|
||||
}
|
||||
TextListComparison::NotContains => {
|
||||
let list_opt = match comp_value {
|
||||
ValueType::List(list) => list,
|
||||
_ => return Err("Expected a list".into()),
|
||||
};
|
||||
|
||||
// If the list exists, check if it does not contain the target value.
|
||||
// Otherwise, return true.
|
||||
let check = match list_opt {
|
||||
Some(list) => !list.contains(target_value),
|
||||
None => true,
|
||||
};
|
||||
|
||||
Ok(check)
|
||||
}
|
||||
TextListComparison::IsEmpty => {
|
||||
let list_opt = match comp_value {
|
||||
ValueType::List(list) => list,
|
||||
_ => return Err("Expected a list".into()),
|
||||
};
|
||||
|
||||
// If the list exists, check if it is empty.
|
||||
// Otherwise, return false.
|
||||
let check = match list_opt {
|
||||
Some(list) => list.is_empty(),
|
||||
None => true,
|
||||
};
|
||||
|
||||
Ok(check)
|
||||
}
|
||||
TextListComparison::IsNotEmpty => {
|
||||
let list_opt = match comp_value {
|
||||
ValueType::List(list) => list,
|
||||
_ => return Err("Expected a list".into()),
|
||||
};
|
||||
|
||||
// If the list exists, check if it is not empty.
|
||||
// Otherwise, return true.
|
||||
let check = match list_opt {
|
||||
Some(list) => !list.is_empty(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
Ok(check)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For right now, nested lists should be flattened into a list of a single type before they
|
||||
/// are compared. The plan is to create a more robust framework that eventually be able to
|
||||
/// query specific lists and perform comparisons on them.
|
||||
///
|
||||
/// For example:
|
||||
/// ```
|
||||
/// NestedListComparison::FlattenToTextList(Box::new(TextListComparison::Contains));
|
||||
/// ```
|
||||
///
|
||||
/// This will flatten a nested list into a single list of `ValueType::String` enums and then
|
||||
/// will check if the list contains the indicated target value.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum NestedListComparison {
|
||||
/// Flattens a nested list into a single text list, then performs the given comparison.
|
||||
FlattenToTextList(Box<TextListComparison>),
|
||||
}
|
||||
|
||||
impl Comparison for NestedListComparison {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String> {
|
||||
match self {
|
||||
NestedListComparison::FlattenToTextList(t) => {
|
||||
// Check the comp_value for a series of lists. We'll then iterate over each list and flatten it to a text list.
|
||||
// The end type should be a ValueType::List(Option<Vec<T>>), where T is ValueType::String.
|
||||
let list = match comp_value {
|
||||
ValueType::List(Some(list)) => list,
|
||||
_ => return Err("Expected a list of values".to_owned()),
|
||||
};
|
||||
|
||||
let mut out: Vec<ValueType> = Vec::new();
|
||||
|
||||
for item in list {
|
||||
// Every item should be another list.
|
||||
let sub_list = match item {
|
||||
ValueType::List(Some(sub_list)) => sub_list,
|
||||
_ => return Err("Expected a list of lists".to_owned()),
|
||||
};
|
||||
|
||||
for sub_item in sub_list {
|
||||
match sub_item {
|
||||
s @ ValueType::String(_) => out.push(s.clone()),
|
||||
_ => return Err("Expected a list of strings".to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.evaluate(&ValueType::List(Some(out)), target_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists of maps must be flattened to a list of some type before they can be compared.
|
||||
/// Flattening maps simply extracts the values from a specific key from all maps in the list.
|
||||
/// This makes comparison much simpler to implement and understand.
|
||||
///
|
||||
/// All flattening operations also contain a `String` key (the map's member to flatten)
|
||||
/// and some `Comparison` enum that matches the type of the flattened values. Flattening will
|
||||
/// fail if any single map in the list does not contain the specified key or if the value
|
||||
/// is not of the expected type.
|
||||
///
|
||||
/// For example, to flatten a list of maps by using the `name` key, we would use the following
|
||||
/// structure:
|
||||
/// ```
|
||||
/// MapListComparison::FlattenToTextList("name".to_owned(), Box::new(TextListComparison::Equal));
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum MapListComparison {
|
||||
/// Flattens the map list to a text list and then performs the given comparison.
|
||||
FlattenToTextList(String, Box<TextListComparison>),
|
||||
/// Flattens the map list to a number list and then performs the given comparison.
|
||||
FlattenToNumberList(String, Box<NumberListComparison>),
|
||||
/// Flattens the map list to a nested list and then performs the given comparison.
|
||||
FlattenToNestedList(String, Box<NestedListComparison>),
|
||||
}
|
||||
|
||||
impl Comparison for MapListComparison {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String> {
|
||||
// Define base list - this must be present otherwise we cannot proceed.
|
||||
let list = match comp_value {
|
||||
ValueType::List(Some(list)) => list,
|
||||
ValueType::List(None) => &Vec::new(),
|
||||
_ => return Err("Expected a list".to_owned()),
|
||||
};
|
||||
|
||||
match self {
|
||||
MapListComparison::FlattenToTextList(key, comp) => {
|
||||
let mut out = Vec::new();
|
||||
|
||||
for item in list {
|
||||
let map = match item {
|
||||
ValueType::Map(Some(map)) => map,
|
||||
_ => return Err("Expected a map".to_string()),
|
||||
};
|
||||
|
||||
// Ignore any non-string values.
|
||||
if let Some(text @ ValueType::String(_)) = map.get(key) {
|
||||
out.push(text.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let flattened = ValueType::List(Some(out));
|
||||
comp.evaluate(&flattened, target_value)
|
||||
}
|
||||
MapListComparison::FlattenToNumberList(key, comp) => {
|
||||
let mut out = Vec::new();
|
||||
for item in list {
|
||||
let map = match item {
|
||||
ValueType::Map(Some(map)) => map,
|
||||
_ => return Err("Expected a map".to_string()),
|
||||
};
|
||||
|
||||
// Ignore any non-number values.
|
||||
if let Some(number @ ValueType::Number(_)) = map.get(key) {
|
||||
out.push(number.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let flattened = ValueType::List(Some(out));
|
||||
comp.evaluate(&flattened, target_value)
|
||||
}
|
||||
MapListComparison::FlattenToNestedList(key, comp) => {
|
||||
let mut out = Vec::new();
|
||||
for item in list {
|
||||
let map = match item {
|
||||
ValueType::Map(Some(map)) => map,
|
||||
_ => return Err("Expected a map".to_string()),
|
||||
};
|
||||
|
||||
// Ignore any non-list values.
|
||||
if let Some(list @ ValueType::List(_)) = map.get(key) {
|
||||
out.push(list.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let flattened = ValueType::List(Some(out));
|
||||
comp.evaluate(&flattened, target_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::test_utils::{create_map_list_helper};
|
||||
use crate::common::{ComparisonType};
|
||||
use crate::common::comparison::test_utils::{create_empty_lists, create_number_list, create_text_list};
|
||||
|
||||
#[test]
|
||||
fn map_list_flatten_to_text_list() {
|
||||
let map_list = create_map_list_helper();
|
||||
|
||||
// Define our comparison, paying close attention to the target values.
|
||||
let first_name_comp = ComparisonType::MapList(MapListComparison::FlattenToTextList(
|
||||
"first_name".to_string(),
|
||||
Box::new(TextListComparison::Contains),
|
||||
));
|
||||
|
||||
let last_name_comp = ComparisonType::MapList(MapListComparison::FlattenToTextList(
|
||||
"last_name".to_string(),
|
||||
Box::new(TextListComparison::Contains),
|
||||
));
|
||||
|
||||
let gender_comp = ComparisonType::MapList(MapListComparison::FlattenToTextList(
|
||||
"gender".to_string(),
|
||||
Box::new(TextListComparison::Contains),
|
||||
));
|
||||
|
||||
let first_name_result =
|
||||
first_name_comp.evaluate(&map_list, &ValueType::String(Some("Jane".to_string())));
|
||||
let last_name_result =
|
||||
last_name_comp.evaluate(&map_list, &ValueType::String(Some("Fails".to_string())));
|
||||
let gender_result =
|
||||
gender_comp.evaluate(&map_list, &ValueType::String(Some("Female".to_string())));
|
||||
|
||||
// We have one map that contains the target value (map_two.first_name)
|
||||
assert_eq!(first_name_result.unwrap(), true);
|
||||
assert_eq!(last_name_result.unwrap(), false);
|
||||
assert_eq!(gender_result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_list_flatten_to_number_list() {
|
||||
let map_list = create_map_list_helper();
|
||||
|
||||
let sum_comparison = ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"sat_score".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(NumberComparison::Equal))),
|
||||
));
|
||||
|
||||
let result = sum_comparison.evaluate(&map_list, &ValueType::Number(Some(2200.to_string())));
|
||||
let failed = sum_comparison.evaluate(&map_list, &ValueType::Number(Some(2201.to_string())));
|
||||
|
||||
assert_eq!(result.unwrap(), true);
|
||||
assert_eq!(failed.unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_list_flatten_to_nested_list() {
|
||||
let map_list = create_map_list_helper();
|
||||
|
||||
let comparison = ComparisonType::MapList(MapListComparison::FlattenToNestedList(
|
||||
"grades_participated".to_string(),
|
||||
Box::new(NestedListComparison::FlattenToTextList(Box::new(
|
||||
TextListComparison::Contains,
|
||||
))),
|
||||
));
|
||||
|
||||
let result = comparison.evaluate(&map_list, &ValueType::String(Some("9".to_string())));
|
||||
let failed = comparison.evaluate(&map_list, &ValueType::String(Some("13".to_string())));
|
||||
|
||||
assert_eq!(result.unwrap(), true);
|
||||
assert_eq!(failed.unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_list_sum_comparison() {
|
||||
let list = create_number_list();
|
||||
|
||||
let comparison = ComparisonType::NumberList(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::Equal,
|
||||
)));
|
||||
|
||||
let result = comparison.evaluate(&list, &ValueType::Number(Some(15.to_string())));
|
||||
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_list_contains_all_types() {
|
||||
let num_list = create_number_list();
|
||||
|
||||
let string_list = ValueType::List(Some(vec![
|
||||
ValueType::String(Some("a".to_string())),
|
||||
ValueType::String(Some("b".to_string())),
|
||||
ValueType::String(Some("c".to_string())),
|
||||
ValueType::String(Some("d".to_string())),
|
||||
ValueType::String(Some("e".to_string())),
|
||||
]));
|
||||
|
||||
let mixed_list = ValueType::List(Some(vec![
|
||||
ValueType::Number(Some(1.to_string())),
|
||||
ValueType::String(Some("b".to_string())),
|
||||
ValueType::Number(Some(3.to_string())),
|
||||
ValueType::String(Some("d".to_string())),
|
||||
ValueType::Number(Some(5.to_string())),
|
||||
]));
|
||||
|
||||
let comparison = ComparisonType::NumberList(NumberListComparison::Contains);
|
||||
|
||||
let num_result = comparison.evaluate(&num_list, &ValueType::Number(Some(3.to_string())));
|
||||
let string_result =
|
||||
comparison.evaluate(&string_list, &ValueType::String(Some("a".to_string())));
|
||||
let mixed_result =
|
||||
comparison.evaluate(&mixed_list, &ValueType::String(Some("b".to_string())));
|
||||
|
||||
let incorrect_type =
|
||||
comparison.evaluate(&string_list, &ValueType::Number(Some(1.to_string())));
|
||||
let incorrect_value =
|
||||
comparison.evaluate(&string_list, &ValueType::String(Some("f".to_string())));
|
||||
let mixed_incorrect_type =
|
||||
comparison.evaluate(&string_list, &ValueType::String(Some("1".to_string())));
|
||||
|
||||
assert_eq!(num_result.unwrap(), true);
|
||||
assert_eq!(string_result.unwrap(), true);
|
||||
assert_eq!(mixed_result.unwrap(), true);
|
||||
assert_eq!(incorrect_type.unwrap(), false);
|
||||
assert_eq!(incorrect_value.unwrap(), false);
|
||||
assert_eq!(mixed_incorrect_type.unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_list_contains() {
|
||||
let (none_list, empty_list) = create_empty_lists();
|
||||
let num_list = create_number_list();
|
||||
let text_list = create_text_list();
|
||||
|
||||
// Define comparisons here
|
||||
let comparison = ComparisonType::TextList(TextListComparison::Contains);
|
||||
let target_value = ValueType::String(Some("one".to_string()));
|
||||
|
||||
let none_result = comparison.evaluate(&none_list, &target_value);
|
||||
let empty_result = comparison.evaluate(&empty_list, &target_value);
|
||||
let num_result = comparison.evaluate(&num_list, &target_value);
|
||||
let text_list = comparison.evaluate(&text_list, &target_value);
|
||||
|
||||
assert_eq!(none_result.unwrap(), false);
|
||||
assert_eq!(empty_result.unwrap(), false);
|
||||
assert_eq!(num_result.unwrap(), false);
|
||||
assert_eq!(text_list.unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_list_empty() {
|
||||
let none_list = ValueType::List(None);
|
||||
let empty_list = ValueType::List(Some(vec![]));
|
||||
let long_list = ValueType::List(Some(vec![
|
||||
ValueType::String(None),
|
||||
]));
|
||||
|
||||
let empty_comp = ComparisonType::TextList(TextListComparison::IsEmpty);
|
||||
let not_empty_comp = ComparisonType::TextList(TextListComparison::IsNotEmpty);
|
||||
|
||||
// This comparison does not use a target value, as it should just check if the list
|
||||
// either doesn't exist or is empty.
|
||||
let mut result = empty_comp.evaluate(&empty_list, &ValueType::List(None));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
|
||||
result = empty_comp.evaluate(&none_list, &ValueType::List(None));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
|
||||
result = empty_comp.evaluate(&long_list, &ValueType::List(None));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), false);
|
||||
|
||||
result = not_empty_comp.evaluate(&empty_list, &ValueType::List(None));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), false);
|
||||
|
||||
result = not_empty_comp.evaluate(&none_list, &ValueType::List(None));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), false);
|
||||
|
||||
result = not_empty_comp.evaluate(&long_list, &ValueType::List(None));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
mod base;
|
||||
mod lists;
|
||||
mod number;
|
||||
mod text;
|
||||
mod traits;
|
||||
mod utils;
|
||||
|
||||
pub use self::{base::*, lists::*, number::*, text::*, traits::*};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils {
|
||||
use std::collections::HashMap;
|
||||
use crate::common::{ExpandableInfo, ValueType};
|
||||
|
||||
pub fn create_student_data() -> ExpandableInfo {
|
||||
let map = create_map_helper(
|
||||
vec![
|
||||
"first_name".to_string(),
|
||||
"last_name".to_string(),
|
||||
"gender".to_string(),
|
||||
"sat_score".to_string(),
|
||||
"athletic_participation".to_string(),
|
||||
"community_involvement".to_string(),
|
||||
],
|
||||
vec![
|
||||
ValueType::String(Some("John".to_string())),
|
||||
ValueType::String(Some("Doe".to_string())),
|
||||
ValueType::String(Some("Male".to_string())),
|
||||
ValueType::Number(Some(1190.to_string())),
|
||||
ValueType::Map(Some(create_map_helper(
|
||||
vec![
|
||||
"sport_name".to_string(),
|
||||
"grades_participated".to_string(),
|
||||
"special_achievement".to_string(),
|
||||
],
|
||||
vec![
|
||||
ValueType::String(Some("Football".to_string())),
|
||||
ValueType::List(Some(vec![
|
||||
ValueType::String(Some(9.to_string())),
|
||||
ValueType::String(Some(10.to_string())),
|
||||
])),
|
||||
ValueType::String(Some("N/A".to_string())),
|
||||
]
|
||||
))),
|
||||
ValueType::Map(Some(create_map_helper(
|
||||
vec![
|
||||
"activity_name".to_string(),
|
||||
"total_hours".to_string(),
|
||||
],
|
||||
vec![
|
||||
ValueType::String(Some("Raking".to_string())),
|
||||
ValueType::Number(Some(30.to_string())),
|
||||
]
|
||||
))),
|
||||
]
|
||||
);
|
||||
|
||||
let mut info = ExpandableInfo::new("test");
|
||||
info.data = map;
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub fn create_map_helper(keys: Vec<String>, values: Vec<ValueType>) -> HashMap<String, ValueType> {
|
||||
keys
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(values.iter().cloned())
|
||||
.collect::<HashMap<String, ValueType>>()
|
||||
}
|
||||
|
||||
pub fn create_map_list_helper() -> ValueType {
|
||||
let map_one = create_map_helper(
|
||||
vec![
|
||||
"first_name".to_string(),
|
||||
"last_name".to_string(),
|
||||
"gender".to_string(),
|
||||
"sat_score".to_string(),
|
||||
"grades_participated".to_string()
|
||||
],
|
||||
vec![
|
||||
ValueType::String(Some("John".to_string())),
|
||||
ValueType::String(Some("Doe".to_string())),
|
||||
ValueType::String(Some("Male".to_string())),
|
||||
ValueType::Number(Some(1200.to_string())),
|
||||
ValueType::List(Some(vec![
|
||||
ValueType::String(Some("9".to_string())),
|
||||
ValueType::String(Some("10".to_string()))
|
||||
]))
|
||||
],
|
||||
);
|
||||
|
||||
let map_two = create_map_helper(
|
||||
vec![
|
||||
"first_name".to_string(),
|
||||
"last_name".to_string(),
|
||||
"gender".to_string(),
|
||||
"sat_score".to_string(),
|
||||
"grades_participated".to_string()
|
||||
],
|
||||
vec![
|
||||
ValueType::String(Some("Jane".to_string())),
|
||||
ValueType::String(Some("Doe".to_string())),
|
||||
ValueType::String(Some("Female".to_string())),
|
||||
ValueType::Number(Some(1000.to_string())),
|
||||
ValueType::List(Some(vec![
|
||||
ValueType::String(Some("11".to_string())),
|
||||
ValueType::String(Some("12".to_string()))
|
||||
]))
|
||||
],
|
||||
);
|
||||
|
||||
ValueType::List(Some(vec![
|
||||
ValueType::Map(Some(map_one)),
|
||||
ValueType::Map(Some(map_two)),
|
||||
]))
|
||||
}
|
||||
|
||||
pub fn create_empty_lists() -> (ValueType, ValueType) {
|
||||
let none_list = ValueType::List(None);
|
||||
let empty_list = ValueType::List(Some(vec![]));
|
||||
|
||||
(none_list, empty_list)
|
||||
}
|
||||
|
||||
pub fn create_number_list() -> ValueType {
|
||||
ValueType::List(Some(vec![
|
||||
ValueType::Number(Some(1.to_string())),
|
||||
ValueType::Number(Some(2.to_string())),
|
||||
ValueType::Number(Some(3.to_string())),
|
||||
ValueType::Number(Some(4.to_string())),
|
||||
ValueType::Number(Some(5.to_string())),
|
||||
]))
|
||||
}
|
||||
|
||||
pub fn create_text_list() -> ValueType {
|
||||
ValueType::List(Some(vec![
|
||||
ValueType::String(Some("one".to_string())),
|
||||
ValueType::String(Some("two".to_string())),
|
||||
ValueType::String(Some("three".to_string())),
|
||||
ValueType::String(Some("four".to_string())),
|
||||
ValueType::String(Some("five".to_string())),
|
||||
]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use super::Comparison;
|
||||
use crate::common::ValueType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum NumberComparison {
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
Equal,
|
||||
NotEqual,
|
||||
GreaterThanOrEqual,
|
||||
LessThanOrEqual,
|
||||
}
|
||||
|
||||
impl Comparison for NumberComparison {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String> {
|
||||
// The prerequisites for all number comparisons is that the target value is Some(number) and the
|
||||
// comparison member points to Some(number) in the student_info.
|
||||
let student_value = match comp_value {
|
||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
||||
_ => {
|
||||
return Err("Student value is not a number".into());
|
||||
}
|
||||
};
|
||||
|
||||
let target = match target_value {
|
||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
||||
_ => {
|
||||
return Err("Target value is not a number".into());
|
||||
}
|
||||
};
|
||||
|
||||
let matched = match self {
|
||||
NumberComparison::GreaterThan => student_value > target,
|
||||
NumberComparison::LessThan => student_value < target,
|
||||
NumberComparison::Equal => student_value == target,
|
||||
NumberComparison::NotEqual => student_value != target,
|
||||
NumberComparison::GreaterThanOrEqual => student_value >= target,
|
||||
NumberComparison::LessThanOrEqual => student_value <= target,
|
||||
};
|
||||
|
||||
Ok(matched)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use super::Comparison;
|
||||
use crate::common::ValueType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum TextComparison {
|
||||
Matches,
|
||||
NotMatches,
|
||||
Contains,
|
||||
NotContains,
|
||||
}
|
||||
|
||||
impl Comparison for TextComparison {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String> {
|
||||
// All text comparisons require that the comp.member points to a Some(string) in the student_info,
|
||||
// and that the target_value is also a Some(string).
|
||||
let student_value = match comp_value {
|
||||
ValueType::String(Some(value)) => value,
|
||||
_ => return Err("Student value is not a string".into()),
|
||||
};
|
||||
|
||||
let target_value = match target_value {
|
||||
ValueType::String(Some(value)) => value,
|
||||
_ => return Err("Target value is not a string".into()),
|
||||
};
|
||||
|
||||
let matched = match self {
|
||||
TextComparison::Matches => student_value == target_value,
|
||||
TextComparison::NotMatches => student_value != target_value,
|
||||
TextComparison::Contains => student_value.contains(target_value),
|
||||
TextComparison::NotContains => !student_value.contains(target_value),
|
||||
};
|
||||
|
||||
Ok(matched)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use crate::common::ValueType;
|
||||
|
||||
pub trait Comparison {
|
||||
fn evaluate(&self, comp_value: &ValueType, target_value: &ValueType) -> Result<bool, String>;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::common::ValueType;
|
||||
|
||||
/// This function is a work in progress. It's designed to flatten a list of maps by using the
|
||||
/// given key and verifier function, however it currently doesn't work and just needs to be
|
||||
/// planned out a little more.
|
||||
fn flatten_map_helper<F>(
|
||||
list: &Vec<ValueType>,
|
||||
key: &String,
|
||||
verifier: F,
|
||||
) -> Result<Vec<ValueType>, String>
|
||||
where
|
||||
F: Fn(&ValueType) -> Option<&ValueType>,
|
||||
{
|
||||
let mut out = Vec::new();
|
||||
for item in list {
|
||||
let map = match item {
|
||||
ValueType::Map(Some(map)) => map,
|
||||
_ => return Err("Expected a map".to_owned()),
|
||||
};
|
||||
|
||||
// Get the key, and verify that the value is the correct type. If so, push it.
|
||||
if let Some(value) = map.get(key).and_then(|v| verifier(v)) {
|
||||
out.push(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
@@ -25,9 +25,9 @@ pub struct ExpandableInfo {
|
||||
}
|
||||
|
||||
impl ExpandableInfo {
|
||||
pub fn new(key: String) -> Self {
|
||||
pub fn new(key: impl Into<String>) -> Self {
|
||||
Self {
|
||||
subject: key,
|
||||
subject: key.into(),
|
||||
data: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
mod comparison;
|
||||
mod expandable_info;
|
||||
mod form_schema;
|
||||
mod user;
|
||||
pub mod value_type;
|
||||
mod value_type;
|
||||
|
||||
pub use self::{expandable_info::*, form_schema::*, user::*, value_type::*};
|
||||
pub use self::{comparison::*, expandable_info::*, form_schema::*, user::*, value_type::*};
|
||||
@@ -2,6 +2,7 @@ pub mod app;
|
||||
mod common;
|
||||
mod components;
|
||||
mod pages;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use super::home_page::get_submission;
|
||||
use super::UnauthenticatedPage;
|
||||
use crate::common::{
|
||||
ComparisonData, ComparisonType, ExpandableInfo, NumberComparison, UserClaims, ValueType,
|
||||
};
|
||||
use crate::components::{ActionButton, Loading, Panel, Row};
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
|
||||
|
||||
#[component]
|
||||
pub fn ComparisonTestPage() -> impl IntoView {
|
||||
let result_msg = RwSignal::new("Found submission, waiting for input.".to_string());
|
||||
let auth = use_context::<AuthSignal>().expect("Couldn't find user information.");
|
||||
let user_claims = Signal::derive(move || {
|
||||
auth.with(|auth| {
|
||||
auth.authenticated().and_then(|data| {
|
||||
data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// Note that the value passed in MUST be equatable.
|
||||
// We get/unwrap the value repeatedly until we get a simple string value, then clone it so that
|
||||
// we don't lose access to it in the future, should we need it again.
|
||||
let server_resource: Resource<ExpandableInfo> = Resource::new(
|
||||
move || user_claims.get().map(|claim| claim.claims.subject.clone()),
|
||||
async
|
||||
| opt_username
|
||||
| match opt_username {
|
||||
Some(subject) => get_submission(subject.clone()).await.unwrap_or_else(|e| {
|
||||
console_log(e.to_string().as_str());
|
||||
ExpandableInfo::new(subject)
|
||||
}),
|
||||
None => ExpandableInfo::new("".to_owned()),
|
||||
},
|
||||
);
|
||||
|
||||
view! {
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<Suspense fallback=Loading>
|
||||
{move || {
|
||||
server_resource
|
||||
.get()
|
||||
.map(|response| {
|
||||
view! {
|
||||
<Panel>
|
||||
<Row>
|
||||
<p>{result_msg}</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton on:click=move |_| {
|
||||
let comp = ComparisonData {
|
||||
id: "".into(),
|
||||
member: "math_sat".into(),
|
||||
target_value: ValueType::Number(Some("800".into())),
|
||||
comparison: ComparisonType::Number(
|
||||
NumberComparison::GreaterThan,
|
||||
),
|
||||
category: "".into(),
|
||||
display_text: "".into(),
|
||||
};
|
||||
let result = comp.compare(&response);
|
||||
match result {
|
||||
Ok(val) => {
|
||||
result_msg
|
||||
.set(
|
||||
format!(
|
||||
"member {} is greater than target value {}: {}",
|
||||
comp.member,
|
||||
comp.target_value,
|
||||
val,
|
||||
),
|
||||
)
|
||||
}
|
||||
Err(msg) => result_msg.set(msg),
|
||||
}
|
||||
}>"Test Comparison"</ActionButton>
|
||||
</Row>
|
||||
</Panel>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
}
|
||||
}
|
||||
+87
-70
@@ -107,19 +107,19 @@ pub async fn create_example_pdf() -> Result<Vec<u8>, ServerFnError> {
|
||||
This form will create a new student application. The PDF that's generated (like this one)
|
||||
is a template that will be used later. We want to preprocess these templates to insert the
|
||||
correct values from the API later on.
|
||||
|
||||
|
||||
= Second Header
|
||||
This area is going to do some math because I think that's fun. We'll typeset a simple sum:
|
||||
$ sum_(i=0)^(10)
|
||||
(n_i) $
|
||||
|
||||
|
||||
= Third Test
|
||||
This is just going to create a bulleted list inside a numbered list:
|
||||
+ Numbers
|
||||
+ Number again
|
||||
- Not a number.
|
||||
"#;
|
||||
|
||||
|
||||
// Create and run the typst command.
|
||||
let mut command = Command::new("typst");
|
||||
command
|
||||
@@ -137,7 +137,7 @@ pub async fn create_example_pdf() -> Result<Vec<u8>, ServerFnError> {
|
||||
|
||||
let output = process.wait_with_output()?;
|
||||
let pdf_bytes = output.stdout;
|
||||
|
||||
|
||||
Ok(pdf_bytes)
|
||||
}
|
||||
|
||||
@@ -182,8 +182,6 @@ pub fn HomePage() -> impl IntoView {
|
||||
let expandable_react = submission.as_reactive();
|
||||
let elements_disabled = RwSignal::new(false);
|
||||
let result_msg = Signal::derive(move || {
|
||||
// Eventually, this function will show and hide a loading symbol
|
||||
// and a success checkmark.
|
||||
if submit_action.pending().get() {
|
||||
elements_disabled.set(true);
|
||||
"Sending request...".to_owned()
|
||||
@@ -192,17 +190,16 @@ pub fn HomePage() -> impl IntoView {
|
||||
if let Some(result) = submit_action.value().get() {
|
||||
match result {
|
||||
Ok(_) => "Request sent successfully.".to_owned(),
|
||||
Err(e) => format!("Failed to send request: {:?}", e)
|
||||
Err(e) => format!("Failed to send request: {:?}", e),
|
||||
}
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<Row>
|
||||
<div class="flex flex-col flex-1"/>
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
@@ -220,15 +217,15 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member = "first_name"
|
||||
data_map = expandable_react.data
|
||||
data_member="first_name"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member = "last_name"
|
||||
data_map = expandable_react.data
|
||||
data_member="last_name"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -236,8 +233,8 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member = "contact_email"
|
||||
data_map = expandable_react.data
|
||||
data_member="contact_email"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -245,8 +242,8 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
data_member = "phone_number"
|
||||
data_map = expandable_react.data
|
||||
data_member="phone_number"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -254,8 +251,8 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
data_member = "address"
|
||||
data_map = expandable_react.data
|
||||
data_member="address"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -263,9 +260,9 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="Highest Math SAT Score:"
|
||||
placeholder="600"
|
||||
disabled=elements_disabled
|
||||
data_member = "math_sat"
|
||||
data_map = expandable_react.data
|
||||
input_type = "number"
|
||||
data_member="math_sat"
|
||||
data_map=expandable_react.data
|
||||
input_type="number"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -278,7 +275,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="town"
|
||||
data_map = expandable_react.data
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -311,13 +308,24 @@ pub fn HomePage() -> impl IntoView {
|
||||
<ChipsList
|
||||
data_member="athletic_requirements"
|
||||
data_map=expandable_react.data
|
||||
items=vec!(
|
||||
"Football", "Soccer", "Cross Country", "Cheerleading",
|
||||
"Swimming", "Wrestling", "Ski", "Basketball",
|
||||
"Lacrosse", "Softball", "Indoor/Outdoor Track",
|
||||
"Golf", "Tennis", "Volleyball"
|
||||
)
|
||||
.into_iter().map(|s| s.to_owned())
|
||||
items=vec![
|
||||
"Football",
|
||||
"Soccer",
|
||||
"Cross Country",
|
||||
"Cheerleading",
|
||||
"Swimming",
|
||||
"Wrestling",
|
||||
"Ski",
|
||||
"Basketball",
|
||||
"Lacrosse",
|
||||
"Softball",
|
||||
"Indoor/Outdoor Track",
|
||||
"Golf",
|
||||
"Tennis",
|
||||
"Volleyball",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
label="Sports/Athletic Requirements"
|
||||
@@ -327,11 +335,13 @@ pub fn HomePage() -> impl IntoView {
|
||||
<ChipsList
|
||||
data_member="community_involvement"
|
||||
data_map=expandable_react.data
|
||||
items=vec!(
|
||||
"Lion's Club", "Knights of Columbus",
|
||||
"Community Service > 20hrs"
|
||||
)
|
||||
.into_iter().map(|s| s.to_owned())
|
||||
items=vec![
|
||||
"Lion's Club",
|
||||
"Knights of Columbus",
|
||||
"Community Service > 20hrs",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
label="Required Community Involvement"
|
||||
@@ -339,14 +349,16 @@ pub fn HomePage() -> impl IntoView {
|
||||
</Row>
|
||||
<Row>
|
||||
<MultiEntry
|
||||
data_map = expandable_react.data
|
||||
data_member = "test"
|
||||
name_member = "first_name"
|
||||
schema = vec![
|
||||
data_map=expandable_react.data
|
||||
data_member="test"
|
||||
name_member="first_name"
|
||||
schema=vec![
|
||||
input!(Text, "first_name", "First Name:", "John"),
|
||||
input!(Text, "last_name", "Last Name:", "Smith"),
|
||||
input!(Select, "gender", "Gender:", ["Male", "Female"]),
|
||||
input!(Checkbox, "candy", "Favorite Candy:", ["Twizzlers", "Starburst"])
|
||||
input!(
|
||||
Checkbox, "candy", "Favorite Candy:", ["Twizzlers", "Starburst"]
|
||||
),
|
||||
]
|
||||
/>
|
||||
</Row>
|
||||
@@ -354,44 +366,49 @@ pub fn HomePage() -> impl IntoView {
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let captured_map = expandable_react.capture();
|
||||
console_log(format!("Map values: {:?}", captured_map).as_str());
|
||||
submit_action.dispatch(CreateSampleSubmission {
|
||||
student_info: captured_map,
|
||||
});
|
||||
console_log(
|
||||
format!("Map values: {:?}", captured_map).as_str(),
|
||||
);
|
||||
submit_action
|
||||
.dispatch(CreateSampleSubmission {
|
||||
student_info: captured_map,
|
||||
});
|
||||
}
|
||||
disabled=elements_disabled
|
||||
>"Submit"</ActionButton>
|
||||
>
|
||||
"Submit"
|
||||
</ActionButton>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
console_log("Attempting to get PDF from server endpoint");
|
||||
spawn_local(async move {
|
||||
let result = create_example_pdf().await;
|
||||
if let Ok(bytes) = result {
|
||||
let base64 = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||
let data_url = format!("data:application/pdf;base64,{}", base64);
|
||||
|
||||
console_log("Found document, opening in new tab...");
|
||||
|
||||
let link = document().create_element("a")
|
||||
.unwrap().dyn_into::<HtmlAnchorElement>()
|
||||
.unwrap();
|
||||
link.set_href(&*data_url);
|
||||
link.set_target("_blank");
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
>"Get PDF"</ActionButton>
|
||||
<ActionButton on:click=move |_| {
|
||||
console_log("Attempting to get PDF from server endpoint");
|
||||
spawn_local(async move {
|
||||
let result = create_example_pdf().await;
|
||||
if let Ok(bytes) = result {
|
||||
let base64 = base64::engine::general_purpose::STANDARD
|
||||
.encode(bytes);
|
||||
let data_url = format!(
|
||||
"data:application/pdf;base64,{}",
|
||||
base64,
|
||||
);
|
||||
console_log("Found document, opening in new tab...");
|
||||
let link = document()
|
||||
.create_element("a")
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlAnchorElement>()
|
||||
.unwrap();
|
||||
link.set_href(&*data_url);
|
||||
link.set_target("_blank");
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
}>"Get PDF"</ActionButton>
|
||||
</Row>
|
||||
<Row>
|
||||
<p>
|
||||
{result_msg}
|
||||
</p>
|
||||
<p>{result_msg}</p>
|
||||
</Row>
|
||||
</Panel>
|
||||
<div class="flex flex-col flex-1"/>
|
||||
<div class="flex flex-col flex-1" />
|
||||
</Row>
|
||||
}
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
mod about_page;
|
||||
mod comparison_test;
|
||||
mod home_page;
|
||||
mod unauthenticated_page;
|
||||
|
||||
pub use self::{about_page::*, home_page::*, unauthenticated_page::*};
|
||||
pub use self::{about_page::*, comparison_test::*, home_page::*, unauthenticated_page::*};
|
||||
@@ -0,0 +1,23 @@
|
||||
use crate::common::{ExpandableInfo, ValueType};
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
pub fn get_number_list(student_info: &ExpandableInfo, member: &String) -> Result<Vec<f64>, String> {
|
||||
match student_info.data.get(member) {
|
||||
Some(ValueType::List(Some(list))) => {
|
||||
let temp = list.iter().try_fold(vec![], |mut list_acc, item| {
|
||||
if let ValueType::Number(Some(number)) = item {
|
||||
list_acc.push(number.parse::<f64>().unwrap());
|
||||
ControlFlow::Continue(list_acc)
|
||||
} else {
|
||||
ControlFlow::Break(list_acc)
|
||||
}
|
||||
});
|
||||
|
||||
match temp {
|
||||
ControlFlow::Continue(list) => Ok(list),
|
||||
ControlFlow::Break(_) => return Err("List contains non-number values".to_owned()),
|
||||
}
|
||||
}
|
||||
_ => return Err("Couldn't find the list".to_owned()),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user