Finished and mostly clean new character flow.
Lots of changes and reorganization to support the ability to create new characters.
This commit is contained in:
parent
e59e9a5ebf
commit
31a3fa2e46
|
@ -1404,6 +1404,27 @@ version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483"
|
checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2 1.0.24",
|
||||||
|
"quote 1.0.7",
|
||||||
|
"syn 1.0.53",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -1469,6 +1490,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"strum",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ rust-argon2 = "0.8"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
rocket = { version= "0.4.6", features = ["private-cookies"] }
|
rocket = { version= "0.4.6", features = ["private-cookies"] }
|
||||||
|
strum = { version = "0.20", features = ["derive"] }
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod characters;
|
pub mod characters;
|
||||||
|
pub mod convert;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
|
@ -3,7 +3,9 @@ use crate::models::proto::cofd::*;
|
||||||
use crate::models::users::User;
|
use crate::models::users::User;
|
||||||
use crate::schema::characters;
|
use crate::schema::characters;
|
||||||
use diesel_derive_enum::DbEnum;
|
use diesel_derive_enum::DbEnum;
|
||||||
|
use prost::bytes::BytesMut;
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
|
use strum::{EnumIter, EnumString};
|
||||||
|
|
||||||
/// Dynamic character data is an opaque container type that holds
|
/// Dynamic character data is an opaque container type that holds
|
||||||
/// successfully deserialized character data protobuf object of any
|
/// successfully deserialized character data protobuf object of any
|
||||||
|
@ -39,12 +41,36 @@ pub(crate) trait Visibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(DbEnum, Debug, Serialize, PartialEq, Clone, Copy)]
|
#[derive(DbEnum, Debug, Serialize, PartialEq, Clone, Copy, EnumIter, EnumString)]
|
||||||
pub enum CharacterDataType {
|
pub enum CharacterDataType {
|
||||||
ChroniclesOfDarknessV1,
|
ChroniclesOfDarknessV1,
|
||||||
ChangelingV1,
|
ChangelingV1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CharacterDataType {
|
||||||
|
pub fn create_data(&self) -> Result<BytesMut, Error> {
|
||||||
|
use prost::Message;
|
||||||
|
use CharacterDataType::*;
|
||||||
|
let data: BytesMut = match self {
|
||||||
|
ChroniclesOfDarknessV1 => {
|
||||||
|
let sheet = CofdSheet::default();
|
||||||
|
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
|
||||||
|
sheet.encode(&mut buf)?;
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
ChangelingV1 => {
|
||||||
|
let mut sheet = ChangelingSheet::default();
|
||||||
|
sheet.base = Some(CofdSheet::default());
|
||||||
|
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
|
||||||
|
sheet.encode(&mut buf)?;
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An entry that appears in a user's character list. Properties are
|
/// An entry that appears in a user's character list. Properties are
|
||||||
/// in order of table columns.
|
/// in order of table columns.
|
||||||
#[derive(Serialize, Debug, Queryable)]
|
#[derive(Serialize, Debug, Queryable)]
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
use super::characters::CharacterDataType;
|
||||||
|
use rocket::http::RawStr;
|
||||||
|
use rocket::request::FromFormValue;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Validation errors specific to the new character form.
|
||||||
|
#[derive(Serialize, Error, Debug, Clone)]
|
||||||
|
pub enum ValidationError {
|
||||||
|
#[error("bad UTF-8 encoding")]
|
||||||
|
BadEncoding,
|
||||||
|
|
||||||
|
#[error("invalid game system: {0}")]
|
||||||
|
InvalidGameSystem(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'v> FromFormValue<'v> for CharacterDataType {
|
||||||
|
type Error = ValidationError;
|
||||||
|
|
||||||
|
fn from_form_value(form_value: &'v RawStr) -> Result<CharacterDataType, ValidationError> {
|
||||||
|
let system = form_value
|
||||||
|
.url_decode()
|
||||||
|
.or(Err(ValidationError::BadEncoding))?;
|
||||||
|
|
||||||
|
CharacterDataType::from_str(&system)
|
||||||
|
.or_else(|_| Err(ValidationError::InvalidGameSystem(system.to_string())))
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ fn view_character_template(user: &User, character: Character) -> Result<Template
|
||||||
use CharacterDataType::*;
|
use CharacterDataType::*;
|
||||||
let template = match character.data_type {
|
let template = match character.data_type {
|
||||||
ChroniclesOfDarknessV1 => Template::render("characters/view_character", context),
|
ChroniclesOfDarknessV1 => Template::render("characters/view_character", context),
|
||||||
ChangelingV1 => Template::render("characters/view_character", context),
|
ChangelingV1 => Template::render("characters/view_changeling_character", context),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(template)
|
Ok(template)
|
||||||
|
|
|
@ -2,74 +2,74 @@ use crate::db::{Dao, TenebrousDbConn};
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
characters::{CharacterDataType, NewCharacter},
|
characters::{CharacterDataType, NewCharacter},
|
||||||
proto::cofd::*,
|
convert::ValidationError,
|
||||||
users::User,
|
users::User,
|
||||||
};
|
};
|
||||||
use prost::{bytes::BytesMut, Message};
|
use prost::bytes::BytesMut;
|
||||||
use rocket::http::RawStr;
|
use rocket::{request::Form, response::Redirect};
|
||||||
use rocket::request::{Form, FormError, FromFormValue};
|
|
||||||
use rocket::response::Redirect;
|
|
||||||
use rocket_contrib::templates::Template;
|
use rocket_contrib::templates::Template;
|
||||||
use std::collections::HashMap;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
/// Form submission for creating a new character.
|
||||||
#[derive(FromForm, Serialize)]
|
#[derive(FromForm, Serialize)]
|
||||||
pub(super) struct NewCharacterForm {
|
pub(super) struct NewCharacterForm {
|
||||||
name: String,
|
name: String,
|
||||||
system: CharacterDataType,
|
system: Result<CharacterDataType, ValidationError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Template context for the new character page. Serves as both a
|
||||||
|
/// means of populating form fields and reindering error messages.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct RawNewCharacterForm {
|
pub(super) struct NewCharacterContext {
|
||||||
name: String,
|
name: String,
|
||||||
system: String,
|
error_message: String,
|
||||||
|
selected_system: CharacterDataType,
|
||||||
|
systems: Vec<CharacterDataType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'v> FromFormValue<'v> for CharacterDataType {
|
impl NewCharacterContext {
|
||||||
type Error = &'v RawStr;
|
/// Default empty context, used when page first loads.
|
||||||
|
pub fn default() -> NewCharacterContext {
|
||||||
|
NewCharacterContext {
|
||||||
|
name: "".to_string(),
|
||||||
|
error_message: "".to_string(),
|
||||||
|
selected_system: CharacterDataType::ChroniclesOfDarknessV1,
|
||||||
|
systems: CharacterDataType::iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn from_form_value(form_value: &'v RawStr) -> Result<CharacterDataType, &'v RawStr> {
|
/// Create a context from a form submission. Used to repopulate
|
||||||
let system = form_value.url_decode().or(Err("bad input"))?;
|
/// fields in the form when an error is sent back.
|
||||||
match system.as_ref() {
|
pub fn from_form(form: &Form<NewCharacterForm>) -> NewCharacterContext {
|
||||||
"cofd" => Ok(CharacterDataType::ChroniclesOfDarknessV1),
|
let system: CharacterDataType = *form
|
||||||
"changeling" => Ok(CharacterDataType::ChangelingV1),
|
.system
|
||||||
_ => Err(form_value),
|
.as_ref()
|
||||||
|
.unwrap_or(&CharacterDataType::ChroniclesOfDarknessV1);
|
||||||
|
|
||||||
|
NewCharacterContext {
|
||||||
|
name: form.name.clone(),
|
||||||
|
error_message: "".to_string(),
|
||||||
|
selected_system: system,
|
||||||
|
systems: CharacterDataType::iter().collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_new_sheet(system: &CharacterDataType) -> Result<BytesMut, Error> {
|
/// Create and insert a new character into the database. The form is
|
||||||
use CharacterDataType::*;
|
/// assumed to be successfully validated.
|
||||||
let sheet: BytesMut = match system {
|
|
||||||
ChroniclesOfDarknessV1 => {
|
|
||||||
let sheet = CofdSheet::default();
|
|
||||||
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
|
|
||||||
sheet.encode(&mut buf)?;
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
ChangelingV1 => {
|
|
||||||
let mut sheet = ChangelingSheet::default();
|
|
||||||
sheet.base = Some(CofdSheet::default());
|
|
||||||
let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&sheet));
|
|
||||||
sheet.encode(&mut buf)?;
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(sheet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_new_character(
|
fn create_new_character(
|
||||||
form: Form<NewCharacterForm>,
|
form: &Form<NewCharacterForm>,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
conn: TenebrousDbConn,
|
conn: TenebrousDbConn,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let sheet = create_new_sheet(&form.system)?;
|
let system: CharacterDataType = *form.system.as_ref().unwrap();
|
||||||
|
let sheet: BytesMut = system.create_data()?;
|
||||||
|
|
||||||
let insert = NewCharacter {
|
let insert = NewCharacter {
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
viewable: true,
|
viewable: true,
|
||||||
character_name: &form.name,
|
character_name: &form.name,
|
||||||
data_type: form.system,
|
data_type: system,
|
||||||
data_version: 1,
|
data_version: 1,
|
||||||
data: &sheet,
|
data: &sheet,
|
||||||
};
|
};
|
||||||
|
@ -78,46 +78,35 @@ fn create_new_character(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/new")]
|
/// Render an error message on the new character page, repopulating
|
||||||
pub(super) fn new_character_page(
|
/// the form based on information from the passed-in form.
|
||||||
logged_in_user: &User,
|
fn render_error(form: &Form<NewCharacterForm>, error: String) -> Template {
|
||||||
conn: TenebrousDbConn,
|
let mut context = NewCharacterContext::from_form(form);
|
||||||
) -> Result<Template, Error> {
|
context.error_message = error;
|
||||||
let mut context = HashMap::new();
|
Template::render("characters/new_character", context)
|
||||||
let form = NewCharacterForm {
|
}
|
||||||
name: "".to_string(),
|
|
||||||
system: CharacterDataType::ChroniclesOfDarknessV1,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.insert("form", form);
|
#[get("/new")]
|
||||||
Ok(Template::render("characters/new_character", context))
|
pub(super) fn new_character_page(_logged_in_user: &User) -> Result<Template, Error> {
|
||||||
|
Ok(Template::render(
|
||||||
|
"characters/new_character",
|
||||||
|
NewCharacterContext::default(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/new", data = "<form>")]
|
#[post("/new", data = "<form>")]
|
||||||
pub(super) fn new_character_submit(
|
pub(super) fn new_character_submit(
|
||||||
form: Result<Form<NewCharacterForm>, FormError>,
|
form: Form<NewCharacterForm>,
|
||||||
logged_in_user: &User,
|
logged_in_user: &User,
|
||||||
conn: TenebrousDbConn,
|
conn: TenebrousDbConn,
|
||||||
) -> Result<Redirect, Template> {
|
) -> Result<Redirect, Template> {
|
||||||
//TODO redirect to character edit page
|
if let Err(e) = &form.system {
|
||||||
//TODO redirect back to new character page with an error and filled-out form if validation errors.
|
return Err(render_error(&form, e.to_string().clone()));
|
||||||
if let Err(e) = form {
|
|
||||||
//Not sure how to repopulate the form.
|
|
||||||
let mut context = HashMap::new();
|
|
||||||
let form = NewCharacterForm {
|
|
||||||
name: "".to_string(),
|
|
||||||
system: CharacterDataType::ChroniclesOfDarknessV1,
|
|
||||||
};
|
|
||||||
context.insert("form", form);
|
|
||||||
return Err(Template::render("characters/new_character", context));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match create_new_character(form.unwrap(), logged_in_user.id, conn) {
|
match create_new_character(&form, logged_in_user.id, conn) {
|
||||||
Ok(_) => Ok(crate::routes::common::redirect_to_index()),
|
Ok(_) => Ok(crate::routes::common::redirect_to_index()),
|
||||||
Err(e) => {
|
Err(e) => Err(render_error(&form, e.to_string().clone())),
|
||||||
let context = HashMap::<String, String>::new();
|
|
||||||
return Err(Template::render("characters/new_character", context));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,26 @@
|
||||||
<div>
|
<div>
|
||||||
New character page.
|
New character page.
|
||||||
|
|
||||||
|
{% if error_message %}
|
||||||
|
<p>Error: {{ error_message }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form action="/characters/new" method="post">
|
<form action="/characters/new" method="post">
|
||||||
<div>
|
<div>
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input id="name" name="name" type="text" value="{{ form.name }}" />
|
<input id="name" name="name" type="text" value="{{ name }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="system">System:</label>
|
<label for="system">System:</label>
|
||||||
<select id="system" name="system">
|
<select id="system" name="system">
|
||||||
<option value="cofd">Chronicles of Darkness</option>
|
{% for system in systems %}
|
||||||
<option value="changeling">Changeling</option>
|
{% if system == selected_system %}
|
||||||
|
<option value="{{ system }}" selected="true">{{ system }}</option>
|
||||||
|
{% else %}
|
||||||
|
<option value="{{ system }}">{{ system }}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Changeling Sheet</h1>
|
||||||
|
<div>
|
||||||
|
<h1>Character {{name}}</h1>
|
||||||
|
<h3>User: {{username}}</h3>
|
||||||
|
<p>System: {{data_type}}</h3>
|
||||||
|
<p>Strength: {{sheet.base.strength}}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h1>Core Sheet</h1>
|
||||||
<div>
|
<div>
|
||||||
<h1>Character {{name}}</h1>
|
<h1>Character {{name}}</h1>
|
||||||
<h3>User: {{username}}</h3>
|
<h3>User: {{username}}</h3>
|
||||||
|
|
Loading…
Reference in New Issue