This commit is contained in:
transatoshi
2025-01-24 11:27:38 -08:00
parent 14bbc9086b
commit c8f4a4cfc4
29 changed files with 9729 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
[package]
name = "grinminer-form"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = { git = "https://github.com/rwf2/Rocket" }
[dependencies.rocket_dyn_templates]
version = "0.1.0"
features = ["handlebars", "tera", "minijinja"]

View File

@@ -0,0 +1,151 @@
** start of undefined **
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<header class="header">
<h1 id="title" class="text-center">Grinminer.net application</h1>
<p id="description" class="description text-center">
Apply for a free or paid node/stratum depending on support level
</p>
</header>
<form id="survey-form">
<div class="form-group">
<label id="name-label" for="name">Name</label>
<input
type="text"
name="name"
id="name"
class="form-control"
placeholder="Enter your name"
required
/>
</div>
<div class="form-group">
<label id="email-label" for="email">Email</label>
<input
type="email"
name="email"
id="email"
class="form-control"
placeholder="Enter your Email"
required
/>
</div>
<div class="form-group">
<label id="number-label" for="number">Number of nodes<span class="clue">(max 4)</span></label>
<input
type="number"
name="nodes"
id="number"
min="1"
max="4"
class="form-control"
placeholder="node"
/>
</div>
<div class="form-group">
<p>Select an option</p>
<select id="dropdown" name="stratum" class="form-control" required>
<option disabled selected value>Select features</option>
<option value="testnet">Testnet node</option>
<option value="testnet">Testnet node+mwixnet</option>
<option value="pruned">Pruned node</option>
<option value="node+stratum">Pruned node+stratum</option>
<option value="fullnode">Full node (10ツ)</option>
<option value="fullnode+stratum">Full node+stratum(10ツ)</option>
</select>
</div>
<div class="form-group">
<p>Do you need setup help and ongoing support your node(s)?</p>
<label>
<input
name="setup"
value="no"
type="radio"
class="input-radio"
checked
/>No</label>
<label>
<input
name="setup"
value="yes"
type="radio"
class="input-radio"
checked
/>Yes (25ツ)</label>
</div>
<div class="form-group">
<p>Do you agree to only use this VPS for Grin?</p>
<select id="agreement" name="agreement" class="form-control" required>
<option disabled selected value>Select an option</option>
<option value="challenges">Yes</option>
<option value="projects">No</option>
</select>
</div>
<div class="form-group">
<p>What future features are you interested in?<span class="clue">(Check all that apply)</span></p>
<label>
<input
name="prefer"
value="firmware-update"
type="checkbox"
class="input-checkbox"
/>Updated firmware</label>
<label>
<input
name="prefer"
value="firmware-overclock"
type="checkbox"
class="input-checkbox"
/>Overclocked firmware</label
>
<label
><input
name="prefer"
value="stratumv2"
type="checkbox"
class="input-checkbox"
/>Stratum V2</label
>
<label
><input
name="prefer"
value="controlpanel"
type="checkbox"
class="input-checkbox"
/>Mining control panel</label>
</div>
<div class="form-group">
<p>Comments or suggestions?</p>
<textarea
id="comments"
class="input-textarea"
name="comment"
placeholder="Enter your comments here..."></textarea>
</div>
<div class="form-group">
<button type="submit" id="submit" class="submit-button">
Submit
</button>
</div>
</form>
</div>
</body>
</html>
** end of undefined **
** start of undefined **
** end of undefined **

View File

@@ -0,0 +1,97 @@
#[macro_use] extern crate rocket;
use rocket::time::Date;
use rocket::http::{Status, ContentType};
use rocket::form::{Form, Contextual, FromForm, FromFormField, Context};
use rocket::fs::{FileServer, TempFile, relative};
use rocket_dyn_templates::Template;
#[derive(Debug, FromForm)]
struct Password<'v> {
#[field(validate = len(6..))]
#[field(validate = eq(self.second))]
#[allow(unused)]
first: &'v str,
#[allow(unused)]
#[field(validate = eq(self.first))]
second: &'v str,
}
#[derive(Debug, FromFormField)]
enum Rights {
Public,
Reserved,
Exclusive,
}
#[derive(Debug, FromFormField)]
enum Category {
Biology,
Chemistry,
Physics,
#[field(value = "CS")]
ComputerScience,
}
#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Submission<'v> {
#[field(validate = len(1..))]
title: &'v str,
date: Date,
#[field(validate = len(1..=250))]
r#abstract: &'v str,
#[field(validate = ext(ContentType::PDF))]
file: TempFile<'v>,
#[field(validate = len(1..))]
category: Vec<Category>,
rights: Rights,
ready: bool,
}
#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Account<'v> {
#[field(validate = len(1..))]
name: &'v str,
password: Password<'v>,
#[field(validate = contains('@').or_else(msg!("invalid email address")))]
email: &'v str,
}
#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Submit<'v> {
account: Account<'v>,
submission: Submission<'v>,
}
#[get("/")]
fn index() -> Template {
Template::render("index", &Context::default())
}
// NOTE: We use `Contextual` here because we want to collect all submitted form
// fields to re-render forms with submitted values on error. If you have no such
// need, do not use `Contextual`. Use the equivalent of `Form<Submit<'_>>`.
#[post("/", data = "<form>")]
fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) {
let template = match form.value {
Some(ref submission) => {
println!("submission: {:#?}", submission);
Template::render("success", &form.context)
}
None => Template::render("index", &form.context),
};
(form.context.status(), template)
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![index, submit])
.attach(Template::fairing())
.mount("/", FileServer::from(relative!("/static")))
}

View File

@@ -0,0 +1,193 @@
use std::fmt;
use super::{rocket, FormInput, FormOption};
use rocket::local::blocking::Client;
use rocket::http::ContentType;
impl fmt::Display for FormOption {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
FormOption::A => write!(f, "a"),
FormOption::B => write!(f, "b"),
FormOption::C => write!(f, "c"),
}
}
}
macro_rules! assert_form_eq {
($client:expr, $form_str:expr, $expected:expr) => {{
let res = $client.post("/")
.header(ContentType::Form)
.body($form_str)
.dispatch();
assert_eq!(res.into_string(), Some($expected));
}};
}
macro_rules! assert_valid_form {
($client:expr, $input:expr) => {{
let f = format!("checkbox={}&number={}&type={}&password={}&textarea={}&select={}",
$input.checkbox, $input.number, $input.radio, $input.password,
$input.text_area, $input.select);
assert_form_eq!($client, &f, format!("{:?}", $input));
}};
}
macro_rules! assert_valid_raw_form {
($client:expr, $form_str:expr, $input:expr) => {{
assert_form_eq!($client, $form_str, format!("{:?}", $input));
}};
}
#[test]
fn test_good_forms() {
let client = Client::tracked(rocket()).unwrap();
let mut input = FormInput {
checkbox: true,
number: 310,
radio: FormOption::A,
password: "beep".into(),
text_area: "bop".to_string(),
select: FormOption::B
};
assert_valid_form!(&client, &input);
input.checkbox = false;
assert_valid_form!(&client, &input);
input.number = 0;
assert_valid_form!(&client, &input);
input.number = 120;
assert_valid_form!(&client, &input);
input.number = 133;
assert_valid_form!(&client, &input);
input.radio = FormOption::B;
assert_valid_form!(&client, &input);
input.radio = FormOption::C;
assert_valid_form!(&client, &input);
input.password = "".into();
assert_valid_form!(&client, &input);
input.password = "----90138490285u2o3hndslkv".into();
assert_valid_form!(&client, &input);
input.password = "hi".into();
assert_valid_form!(&client, &input);
input.text_area = "".to_string();
assert_valid_form!(&client, &input);
input.text_area = "----90138490285u2o3hndslkv".to_string();
assert_valid_form!(&client, &input);
input.text_area = "hey".to_string();
assert_valid_form!(&client, &input);
input.select = FormOption::A;
assert_valid_form!(&client, &input);
input.select = FormOption::C;
assert_valid_form!(&client, &input);
// checkbox need not be present; defaults to false; accepts 'on' and 'off'
assert_valid_raw_form!(&client,
"number=133&type=c&password=hi&textarea=hey&select=c",
&input);
assert_valid_raw_form!(&client,
"checkbox=off&number=133&type=c&password=hi&textarea=hey&select=c",
&input);
input.checkbox = true;
assert_valid_raw_form!(&client,
"checkbox=on&number=133&type=c&password=hi&textarea=hey&select=c",
&input);
}
macro_rules! assert_invalid_form {
($client:expr, $vals:expr) => {{
let vals = $vals;
let s = format!("checkbox={}&number={}&type={}&password={}&textarea={}&select={}",
vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]);
assert_form_eq!($client, &s, format!("Invalid form input: {}", s));
*vals = ["true", "1", "a", "hi", "hey", "b"];
}};
}
macro_rules! assert_invalid_raw_form {
($client:expr, $form_str:expr) => {{
assert_form_eq!($client, $form_str, format!("Invalid form input: {}", $form_str));
}};
}
#[test]
fn check_semantically_invalid_forms() {
let client = Client::tracked(rocket()).unwrap();
let mut form_vals = ["true", "1", "a", "hi", "hey", "b"];
form_vals[0] = "not true";
assert_invalid_form!(&client, &mut form_vals);
form_vals[0] = "bing";
assert_invalid_form!(&client, &mut form_vals);
form_vals[0] = "true0";
assert_invalid_form!(&client, &mut form_vals);
form_vals[0] = " false";
assert_invalid_form!(&client, &mut form_vals);
form_vals[1] = "-1";
assert_invalid_form!(&client, &mut form_vals);
form_vals[1] = "1e10";
assert_invalid_form!(&client, &mut form_vals);
form_vals[1] = "-1-1";
assert_invalid_form!(&client, &mut form_vals);
form_vals[1] = "NaN";
assert_invalid_form!(&client, &mut form_vals);
form_vals[2] = "A?";
assert_invalid_form!(&client, &mut form_vals);
form_vals[2] = " B";
assert_invalid_form!(&client, &mut form_vals);
form_vals[2] = "d";
assert_invalid_form!(&client, &mut form_vals);
form_vals[2] = "100";
assert_invalid_form!(&client, &mut form_vals);
form_vals[2] = "";
assert_invalid_form!(&client, &mut form_vals);
// password and textarea are always valid, so we skip them
form_vals[5] = "A.";
assert_invalid_form!(&client, &mut form_vals);
form_vals[5] = "b ";
assert_invalid_form!(&client, &mut form_vals);
form_vals[5] = "d";
assert_invalid_form!(&client, &mut form_vals);
form_vals[5] = "-a";
assert_invalid_form!(&client, &mut form_vals);
form_vals[5] = "";
assert_invalid_form!(&client, &mut form_vals);
// now forms with missing fields
assert_invalid_raw_form!(&client, "number=10&type=a&password=hi&textarea=hey");
assert_invalid_raw_form!(&client, "number=10&radio=a&password=hi&textarea=hey&select=b");
assert_invalid_raw_form!(&client, "number=10&password=hi&select=b");
assert_invalid_raw_form!(&client, "number=10&select=b");
assert_invalid_raw_form!(&client, "password=hi&select=b");
assert_invalid_raw_form!(&client, "password=hi");
assert_invalid_raw_form!(&client, "");
}
#[test]
fn check_structurally_invalid_forms() {
let client = Client::tracked(rocket()).unwrap();
assert_invalid_raw_form!(&client, "==&&&&&&==");
assert_invalid_raw_form!(&client, "a&=b");
assert_invalid_raw_form!(&client, "=");
}
#[test]
fn check_bad_utf8() {
let client = Client::tracked(rocket()).unwrap();
unsafe {
let bad_str = std::str::from_utf8_unchecked(b"a=\xff");
assert_form_eq!(&client, bad_str, "Form input was invalid UTF-8.".into());
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,148 @@
{% import "macros" as m %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Rocket Form Example</title>
<link rel="stylesheet" href="/chota.min.css">
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px 10px;
}
h1 {
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Form Example</h1>
{% if errors | length > 0 %}
<div class="row">
<div class="col">
<small class="text-error">
error: {{ errors | length }} field{{ errors | length | pluralize }}
failed to validate
</small>
</div>
</div>
{% endif %}
<form action="/" method="post" enctype="multipart/form-data">
<fieldset>
<legend>About You</legend>
<div class="row">
<div class="col">
{{ m::input(label="Name", type="text", name="account.name") }}
<!-- required -->
</div>
<div class="col">
{{ m::input(label="Email Address", type="text", name="account.email") }}
<!-- required pattern=".*@.*"/> -->
</div>
</div>
<div class="row">
<div class="col">
{{ m::input(label="Password", type="password", name="account.password.first") }}
<!-- required minlength="6" value="" /> -->
</div>
<div class="col">
{{
m::input(label="Confirm Password",
type="password",
name="account.password.second")
}}
<!-- required minlength="6" value="" /> -->
</div>
</div>
</fieldset>
<fieldset>
<legend>Metadata</legend>
<div class="row">
<div class="col">
{{ m::input(label="Title", type="text", name="submission.title") }}
<!-- required -->
</div>
</div>
<div class="row">
<div class="col">
{{ m::input(label="Publish Date", type="date", name="submission.date") }}
<!-- <input type="date" name="submission.date" id="date" value="2020&#45;12&#45;26"> -->
</div>
<div class="col">
{{
m::select(
label="Rights Assignment",
name="submission.rights",
options=["Public", "Reserved", "Exclusive"]
)
}}
</div>
</div>
<div class="row">
<div class="col">
<label>Applicable Categories</label>
<br />
{{ m::checkbox(name="submission.category", label="Biology", value="Biology") }}
<br />
{{ m::checkbox(name="submission.category", label="Chemistry", value="Chemistry") }}
<br />
{{ m::checkbox(name="submission.category", label="Physics", value="Physics") }}
<br />
{{ m::checkbox(name="submission.category", label="CS", value="CS") }}
</div>
</div>
</fieldset>
<fieldset>
<legend>Contents</legend>
{{
m::textarea(
label="Abstract",
name="submission.abstract",
placeholder="Your abstract, max 250 characters...",
max=250
)
}}
{{
m::input(
label="File to Upload (PDF, max 1MiB)",
type="file",
name="submission.file"
)
}}
<!-- <input type="file" name="submission.file" id="file" required accept=".pdf"> -->
<div class="row">
<div class="col">
{{ m::checkbox(name="submission.ready", label="Submission is ready for review.") }}
</div>
</div>
</fieldset>
<br />
<input type="submit" value="Submit" class="is-full-width" />
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,63 @@
{% macro value_for(name) %}
{%- if name in values -%}
{{- values | get(key=name) | first -}}
{%- endif -%}
{% endmacro value_for %}
{% macro errors_for(name) %}
{%- if name in errors -%}
{% set field_errors = errors | get(key=name) %}
{% for error in field_errors %}
<p class="text-error is-marginless">{{ error.msg }}</p>
{% endfor %}
{%- endif -%}
{% endmacro errors_for %}
{% macro input(type, label, name, value="") %}
<label for="{{ name }}">{{ label }}</label>
<input type="{{ type }}"
name="{{ name }}"
id="{{ name }}"
value='{{ self::value_for(name=name) }}'
{% if name in errors %} class="error" {% endif %}
/>
{{ self::errors_for(name=name) }}
{% endmacro input %}
{% macro checkbox(name, label, value="yes") %}
<label {% if name in errors %} class="bd-error" {% endif %}>
<input type="checkbox" name="{{ name }}" value={{ value }}
{% if name in values %}
{% set field_values = values | get(key=name) %}
{% if field_values is containing(value) %}
checked
{% endif %}
{% endif %}
>
{{ label }}
</label>
{% endmacro checkbox %}
{% macro textarea(label, name, placeholder="", max=250) %}
<label for="{{ name }}">{{ label }}</label>
<textarea placeholder="{{ placeholder }}"
name="{{ name }}" id="{{ name }}" rows="8" cols="40"
{% if name in errors %} class="error" {% endif %}
>
{{- self::value_for(name=name) -}}
</textarea>
{{ self::errors_for(name=name) }}
{% endmacro textarea %}
{% macro select(label, name, options) %}
<label for="{{ name }}">{{ label }}</label>
<select name="{{ name }}" id="{{ name }}">
{% for value in options %}
<option value="{{ value }}"
{% if self::value_for(name=name) == value %} selected {% endif %}
>{{ value }}</option>
{% endfor %}
</select>
{% endmacro select %}

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Rocket Form Example</title>
<link rel="stylesheet" href="/chota.min.css">
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>Success!</h1>
<h3>Submission Data</h3>
<ul>
{% for key, value in values %}
<li><strong>{{ key }}</strong> - {{ value }}</li>
{% endfor %}
</ul>
<a href="/">&lt; Submit Another</a>
</body>
</html>