Better big form handling (#430)
* Allow customizing max form size from env vars * Add error page for unprocessable entities And change default http port to 7878 * Improve char counter: under the editor, more discrete, and give it a default value
This commit is contained in:
parent
06d6bd361a
commit
e77e4d86e8
@ -1,16 +1,17 @@
|
|||||||
|
#![recursion_limit="128"]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate stdweb;
|
extern crate stdweb;
|
||||||
|
|
||||||
use stdweb::{unstable::TryFrom, web::{*, event::*}};
|
use stdweb::{unstable::{TryFrom, TryInto}, web::{*, event::*}};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
auto_expand();
|
editor_loop();
|
||||||
menu();
|
menu();
|
||||||
search();
|
search();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto expands the editor when adding text
|
/// Auto expands the editor when adding text and count chars
|
||||||
fn auto_expand() {
|
fn editor_loop() {
|
||||||
match document().query_selector("#plume-editor") {
|
match document().query_selector("#plume-editor") {
|
||||||
Ok(Some(x)) => HtmlElement::try_from(x).map(|article_content| {
|
Ok(Some(x)) => HtmlElement::try_from(x).map(|article_content| {
|
||||||
let offset = article_content.offset_height() - (article_content.get_bounding_client_rect().get_height() as i32);
|
let offset = article_content.offset_height() - (article_content.get_bounding_client_rect().get_height() as i32);
|
||||||
@ -19,7 +20,33 @@ fn auto_expand() {
|
|||||||
js! {
|
js! {
|
||||||
@{&article_content}.style.height = "auto";
|
@{&article_content}.style.height = "auto";
|
||||||
@{&article_content}.style.height = @{&article_content}.scrollHeight - @{offset} + "px";
|
@{&article_content}.style.height = @{&article_content}.scrollHeight - @{offset} + "px";
|
||||||
|
};
|
||||||
|
window().set_timeout(|| {match document().query_selector("#post-form") {
|
||||||
|
Ok(Some(form)) => HtmlElement::try_from(form).map(|form| {
|
||||||
|
if let Some(len) = form.get_attribute("content-size").and_then(|s| s.parse::<i32>().ok()) {
|
||||||
|
let consumed: i32 = js!{
|
||||||
|
var len = - 1;
|
||||||
|
for(var i = 0; i < @{&form}.length; i++) {
|
||||||
|
if(@{&form}[i].name != "") {
|
||||||
|
len += @{&form}[i].name.length + encodeURIComponent(@{&form}[i].value)
|
||||||
|
.replace(/%20/g, "+")
|
||||||
|
.replace(/%0A/g, "%0D%0A")
|
||||||
|
.replace(new RegExp("[!'*()]", "g"), "XXX") //replace exceptions of encodeURIComponent with placeholder
|
||||||
|
.length + 2;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}.try_into().unwrap_or_default();
|
||||||
|
match document().query_selector("#editor-left") {
|
||||||
|
Ok(Some(e)) => HtmlElement::try_from(e).map(|e| {
|
||||||
|
js!{@{e}.innerText = (@{len-consumed})};
|
||||||
|
}).ok(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}).ok(),
|
||||||
|
_ => None,
|
||||||
|
};}, 0);
|
||||||
});
|
});
|
||||||
}).ok(),
|
}).ok(),
|
||||||
_ => None
|
_ => None
|
||||||
|
19
src/main.rs
19
src/main.rs
@ -36,7 +36,10 @@ extern crate validator_derive;
|
|||||||
extern crate webfinger;
|
extern crate webfinger;
|
||||||
|
|
||||||
use diesel::r2d2::ConnectionManager;
|
use diesel::r2d2::ConnectionManager;
|
||||||
use rocket::State;
|
use rocket::{
|
||||||
|
Config, State,
|
||||||
|
config::Limits
|
||||||
|
};
|
||||||
use rocket_csrf::CsrfFairingBuilder;
|
use rocket_csrf::CsrfFairingBuilder;
|
||||||
use plume_models::{
|
use plume_models::{
|
||||||
DATABASE_URL, Connection, Error,
|
DATABASE_URL, Connection, Error,
|
||||||
@ -44,6 +47,7 @@ use plume_models::{
|
|||||||
search::{Searcher as UnmanagedSearcher, SearcherError},
|
search::{Searcher as UnmanagedSearcher, SearcherError},
|
||||||
};
|
};
|
||||||
use scheduled_thread_pool::ScheduledThreadPool;
|
use scheduled_thread_pool::ScheduledThreadPool;
|
||||||
|
use std::env;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@ -95,7 +99,17 @@ Then try to restart Plume.
|
|||||||
exit(0);
|
exit(0);
|
||||||
}).expect("Error setting Ctrl-c handler");
|
}).expect("Error setting Ctrl-c handler");
|
||||||
|
|
||||||
rocket::ignite()
|
let mut config = Config::active().unwrap();
|
||||||
|
config.set_address(env::var("ROCKET_ADDRESS").unwrap_or_else(|_| "localhost".to_owned())).unwrap();
|
||||||
|
config.set_port(env::var("ROCKET_PORT").ok().map(|s| s.parse::<u16>().unwrap()).unwrap_or(7878));
|
||||||
|
let _ = env::var("ROCKET_SECRET_KEY").map(|k| config.set_secret_key(k).unwrap());
|
||||||
|
let form_size = &env::var("FORM_SIZE").unwrap_or_else(|_| "32".to_owned()).parse::<u64>().unwrap();
|
||||||
|
let activity_size = &env::var("ACTIVITY_SIZE").unwrap_or_else(|_| "1024".to_owned()).parse::<u64>().unwrap();
|
||||||
|
config.set_limits(Limits::new()
|
||||||
|
.limit("forms", form_size * 1024)
|
||||||
|
.limit("json", activity_size * 1024));
|
||||||
|
|
||||||
|
rocket::custom(config)
|
||||||
.mount("/", routes![
|
.mount("/", routes![
|
||||||
routes::blogs::details,
|
routes::blogs::details,
|
||||||
routes::blogs::activity_details,
|
routes::blogs::activity_details,
|
||||||
@ -196,6 +210,7 @@ Then try to restart Plume.
|
|||||||
])
|
])
|
||||||
.register(catchers![
|
.register(catchers![
|
||||||
routes::errors::not_found,
|
routes::errors::not_found,
|
||||||
|
routes::errors::unprocessable_entity,
|
||||||
routes::errors::server_error
|
routes::errors::server_error
|
||||||
])
|
])
|
||||||
.manage(dbpool)
|
.manage(dbpool)
|
||||||
|
@ -47,6 +47,16 @@ pub fn not_found(req: &Request) -> Ructe {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[catch(422)]
|
||||||
|
pub fn unprocessable_entity(req: &Request) -> Ructe {
|
||||||
|
let conn = req.guard::<DbConn>().succeeded();
|
||||||
|
let intl = req.guard::<I18n>().succeeded();
|
||||||
|
let user = User::from_request(req).succeeded();
|
||||||
|
render!(errors::unprocessable_entity(
|
||||||
|
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[catch(500)]
|
#[catch(500)]
|
||||||
pub fn server_error(req: &Request) -> Ructe {
|
pub fn server_error(req: &Request) -> Ructe {
|
||||||
let conn = req.guard::<DbConn>().succeeded();
|
let conn = req.guard::<DbConn>().succeeded();
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
use atom_syndication::{ContentBuilder, Entry, EntryBuilder, LinkBuilder, Person, PersonBuilder};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
http::{RawStr, uri::{FromUriParam, Query}},
|
http::{RawStr, Status, uri::{FromUriParam, Query}},
|
||||||
request::FromFormValue,
|
Outcome,
|
||||||
|
request::{self, FromFormValue, FromRequest, Request},
|
||||||
response::NamedFile,
|
response::NamedFile,
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -46,6 +47,20 @@ impl Page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ContentLen(pub u64);
|
||||||
|
|
||||||
|
impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||||
|
match r.limits().get("forms") {
|
||||||
|
Some(l) => Outcome::Success(ContentLen(l)),
|
||||||
|
None => Outcome::Failure((Status::InternalServerError, ())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Default for Page {
|
impl Default for Page {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Page(1)
|
Page(1)
|
||||||
|
@ -24,7 +24,7 @@ use plume_models::{
|
|||||||
tags::*,
|
tags::*,
|
||||||
users::User
|
users::User
|
||||||
};
|
};
|
||||||
use routes::{errors::ErrorPage, comments::NewCommentForm};
|
use routes::{errors::ErrorPage, comments::NewCommentForm, ContentLen};
|
||||||
use template_utils::Ructe;
|
use template_utils::Ructe;
|
||||||
use Worker;
|
use Worker;
|
||||||
use Searcher;
|
use Searcher;
|
||||||
@ -103,7 +103,7 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<blog>/new", rank = 1)]
|
#[get("/~/<blog>/new", rank = 1)]
|
||||||
pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn new(blog: String, user: User, cl: ContentLen, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||||
|
|
||||||
if !user.is_author_in(&*conn, &b)? {
|
if !user.is_author_in(&*conn, &b)? {
|
||||||
@ -125,13 +125,14 @@ pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe,
|
|||||||
true,
|
true,
|
||||||
None,
|
None,
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
medias
|
medias,
|
||||||
|
cl.0
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/~/<blog>/<slug>/edit")]
|
#[get("/~/<blog>/<slug>/edit")]
|
||||||
pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
pub fn edit(blog: String, slug: String, user: User, cl: ContentLen, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
|
||||||
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
let b = Blog::find_by_fqn(&*conn, &blog)?;
|
||||||
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
|
||||||
|
|
||||||
@ -168,13 +169,14 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) ->
|
|||||||
!post.published,
|
!post.published,
|
||||||
Some(post),
|
Some(post),
|
||||||
ValidationErrors::default(),
|
ValidationErrors::default(),
|
||||||
medias
|
medias,
|
||||||
|
cl.0
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog>/<slug>/edit", data = "<form>")]
|
#[post("/~/<blog>/<slug>/edit", data = "<form>")]
|
||||||
pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: LenientForm<NewPostForm>, worker: Worker, intl: I18n, searcher: Searcher)
|
pub fn update(blog: String, slug: String, user: User, cl: ContentLen, form: LenientForm<NewPostForm>, worker: Worker, conn: DbConn, intl: I18n, searcher: Searcher)
|
||||||
-> Result<Redirect, Ructe> {
|
-> Result<Redirect, Ructe> {
|
||||||
let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error");
|
let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error");
|
||||||
let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
|
let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
|
||||||
@ -261,7 +263,8 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
|
|||||||
form.draft.clone(),
|
form.draft.clone(),
|
||||||
Some(post),
|
Some(post),
|
||||||
errors.clone(),
|
errors.clone(),
|
||||||
medias.clone()
|
medias.clone(),
|
||||||
|
cl.0
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -290,7 +293,7 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/~/<blog_name>/new", data = "<form>")]
|
#[post("/~/<blog_name>/new", data = "<form>")]
|
||||||
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Result<Ructe, ErrorPage>> {
|
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, cl: ContentLen, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Result<Ructe, ErrorPage>> {
|
||||||
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
|
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
|
||||||
let slug = form.title.to_string().to_kebab_case();
|
let slug = form.title.to_string().to_kebab_case();
|
||||||
|
|
||||||
@ -384,7 +387,8 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
|
|||||||
form.draft,
|
form.draft,
|
||||||
None,
|
None,
|
||||||
errors.clone(),
|
errors.clone(),
|
||||||
medias
|
medias,
|
||||||
|
cl.0
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
templates/errors/unprocessable_entity.rs.html
Normal file
10
templates/errors/unprocessable_entity.rs.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@use templates::errors::base;
|
||||||
|
@use template_utils::*;
|
||||||
|
|
||||||
|
@(ctx: BaseContext)
|
||||||
|
|
||||||
|
@:base(ctx, "Unprocessable entity", {
|
||||||
|
<h1>@i18n!(ctx.1, "The content you sent can't be processed.")</h1>
|
||||||
|
<p>@i18n!(ctx.1, "Maybe it was too long.")</p>
|
||||||
|
})
|
||||||
|
|
@ -8,7 +8,7 @@
|
|||||||
@use routes::posts::NewPostForm;
|
@use routes::posts::NewPostForm;
|
||||||
@use routes::*;
|
@use routes::*;
|
||||||
|
|
||||||
@(ctx: BaseContext, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>)
|
@(ctx: BaseContext, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>, content_len: u64)
|
||||||
|
|
||||||
@:base(ctx, &i18n!(ctx.1, if editing { "Edit {0}" } else { "New post" }; &form.title), {}, {}, {
|
@:base(ctx, &i18n!(ctx.1, if editing { "Edit {0}" } else { "New post" }; &form.title), {}, {}, {
|
||||||
<h1>
|
<h1>
|
||||||
@ -19,9 +19,9 @@
|
|||||||
}
|
}
|
||||||
</h1>
|
</h1>
|
||||||
@if let Some(article) = article {
|
@if let Some(article) = article {
|
||||||
<form class="new-post" method="post" action="@uri!(posts::update: blog = blog.actor_id, slug = &article.slug)">
|
<form id="post-form" class="new-post" method="post" action="@uri!(posts::update: blog = blog.actor_id, slug = &article.slug)" content-size="@content_len">
|
||||||
} else {
|
} else {
|
||||||
<form class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)">
|
<form id="post-form" class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)" content-size="@content_len">
|
||||||
}
|
}
|
||||||
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required")
|
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required")
|
||||||
@input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "")
|
@input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "")
|
||||||
@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
<label for="plume-editor">@i18n!(ctx.1, "Content")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
<label for="plume-editor">@i18n!(ctx.1, "Content")<small>@i18n!(ctx.1, "Markdown syntax is supported")</small></label>
|
||||||
<textarea id="plume-editor" name="content" rows="20">@form.content</textarea>
|
<textarea id="plume-editor" name="content" rows="20">@form.content</textarea>
|
||||||
|
<small id="editor-left">@content_len</small>
|
||||||
|
|
||||||
@input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "")
|
@input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user