Support editing articles without JavaScript

This commit is contained in:
Magnus Hoff 2017-10-30 11:01:37 +01:00
parent db2d8f5d76
commit 5b37a41632
6 changed files with 97 additions and 28 deletions

View file

@ -2,5 +2,6 @@ use hyper::mime;
lazy_static! { lazy_static! {
pub static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap(); pub static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap();
pub static ref TEXT_PLAIN: mime::Mime = "text/plain;charset=utf-8".parse().unwrap();
pub static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap(); pub static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap();
} }

View file

@ -17,6 +17,16 @@ pub struct ArticleRevision {
pub author: Option<String>, pub author: Option<String>,
} }
impl ArticleRevision {
pub fn link(&self) -> &str {
if self.slug.is_empty() {
"."
} else {
&self.slug
}
}
}
#[derive(Debug, Queryable)] #[derive(Debug, Queryable)]
pub struct ArticleRevisionStub { pub struct ArticleRevisionStub {
pub sequence_number: i32, pub sequence_number: i32,

View file

@ -1,7 +1,7 @@
use chrono::{TimeZone, DateTime, Local}; use chrono::{TimeZone, DateTime, Local};
use futures::{self, Future}; use futures::{self, Future};
use hyper; use hyper;
use hyper::header::ContentType; use hyper::header::{ContentType, Location};
use hyper::server::*; use hyper::server::*;
use serde_json; use serde_json;
use serde_urlencoded; use serde_urlencoded;
@ -22,6 +22,13 @@ pub struct ArticleResource {
edit: bool, edit: bool,
} }
#[derive(Deserialize)]
struct UpdateArticle {
base_revision: i32,
title: String,
body: String,
}
impl ArticleResource { impl ArticleResource {
pub fn new(state: State, article_id: i32, revision: i32, edit: bool) -> Self { pub fn new(state: State, article_id: i32, revision: i32, edit: bool) -> Self {
Self { state, article_id, revision, edit } Self { state, article_id, revision, edit }
@ -55,7 +62,7 @@ pub fn last_updated(article_id: i32, created: &DateTime<Local>, author: Option<&
impl Resource for ArticleResource { impl Resource for ArticleResource {
fn allow(&self) -> Vec<hyper::Method> { fn allow(&self) -> Vec<hyper::Method> {
use hyper::Method::*; use hyper::Method::*;
vec![Options, Head, Get, Put] vec![Options, Head, Get, Put, Post]
} }
fn head(&self) -> ResponseFuture { fn head(&self) -> ResponseFuture {
@ -114,13 +121,6 @@ impl Resource for ArticleResource {
use futures::Stream; use futures::Stream;
#[derive(Deserialize)]
struct UpdateArticle {
base_revision: i32,
title: String,
body: String,
}
#[derive(BartDisplay)] #[derive(BartDisplay)]
#[template="templates/article_contents.html"] #[template="templates/article_contents.html"]
struct Template<'a> { struct Template<'a> {
@ -169,4 +169,30 @@ impl Resource for ArticleResource {
}) })
) )
} }
fn post(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
// TODO Check incoming Content-Type
use futures::Stream;
Box::new(body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |update: UpdateArticle| {
self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity)
})
.and_then(|updated| {
futures::finished(Response::new()
.with_status(hyper::StatusCode::SeeOther)
.with_header(ContentType(TEXT_PLAIN.clone()))
.with_header(Location::new(updated.link().to_owned()))
.with_body("See other")
)
})
)
}
} }

View file

@ -1,6 +1,6 @@
use futures::{self, Future}; use futures::{self, Future};
use hyper; use hyper;
use hyper::header::ContentType; use hyper::header::{ContentType, Location};
use hyper::server::*; use hyper::server::*;
use serde_json; use serde_json;
use serde_urlencoded; use serde_urlencoded;
@ -29,6 +29,13 @@ pub struct NewArticleResource {
slug: Option<String>, slug: Option<String>,
} }
#[derive(Deserialize)]
struct CreateArticle {
base_revision: String,
title: String,
body: String,
}
impl NewArticleResource { impl NewArticleResource {
pub fn new(state: State, slug: Option<String>) -> Self { pub fn new(state: State, slug: Option<String>) -> Self {
Self { state, slug } Self { state, slug }
@ -98,13 +105,6 @@ impl Resource for NewArticleResource {
use chrono::{TimeZone, Local}; use chrono::{TimeZone, Local};
use futures::Stream; use futures::Stream;
#[derive(Deserialize)]
struct CreateArticle {
base_revision: String,
title: String,
body: String,
}
#[derive(BartDisplay)] #[derive(BartDisplay)]
#[template="templates/article_contents.html"] #[template="templates/article_contents.html"]
struct Template<'a> { struct Template<'a> {
@ -158,4 +158,34 @@ impl Resource for NewArticleResource {
}) })
) )
} }
fn post(self: Box<Self>, body: hyper::Body, identity: Option<String>) -> ResponseFuture {
// TODO Check incoming Content-Type
// TODO Refactor? Reduce duplication with ArticleResource::put?
use futures::Stream;
Box::new(body
.concat2()
.map_err(Into::into)
.and_then(|body| {
serde_urlencoded::from_bytes(&body)
.map_err(Into::into)
})
.and_then(move |arg: CreateArticle| {
if arg.base_revision != NEW {
unimplemented!("Version update conflict");
}
self.state.create_article(self.slug.clone(), arg.title, arg.body, identity)
})
.and_then(|updated| {
futures::finished(Response::new()
.with_status(hyper::StatusCode::SeeOther)
.with_header(ContentType(TEXT_PLAIN.clone()))
.with_header(Location::new(updated.link().to_owned()))
.with_body("See other")
)
})
)
}
} }

View file

@ -121,6 +121,7 @@ impl Service for Site {
Head => resource.head(), Head => resource.head(),
Get => resource.get(), Get => resource.get(),
Put => resource.put(body, identity), Put => resource.put(body, identity),
Post => resource.post(body, identity),
_ => Box::new(futures::finished(resource.method_not_allowed())) _ => Box::new(futures::finished(resource.method_not_allowed()))
} }
}, },

View file

@ -1,4 +1,5 @@
use futures; use futures;
use futures::{Future, Stream};
use hyper::{self, header, mime, server}; use hyper::{self, header, mime, server};
use hyper::server::Response; use hyper::server::Response;
use std; use std;
@ -24,18 +25,18 @@ pub trait Resource {
fn put(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture fn put(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
where Self: 'static where Self: 'static
{ {
use futures::{Future, Stream};
// TODO Cleanup by moving to the built in never type, !, when it stabilizes
enum Never {};
impl std::convert::From<Never> for hyper::Error {
fn from(_: Never) -> hyper::Error {
panic!()
}
}
Box::new(body Box::new(body
.fold((), |_, _| -> Result<(), Never> { Ok(()) }) .fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
.map_err(Into::into)
.and_then(move |_| futures::finished(self.method_not_allowed()))
)
}
fn post(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
where Self: 'static
{
Box::new(body
.fold((), |_, _| -> Result<(), hyper::Error> { Ok(()) })
.map_err(Into::into) .map_err(Into::into)
.and_then(move |_| futures::finished(self.method_not_allowed())) .and_then(move |_| futures::finished(self.method_not_allowed()))
) )