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! {
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();
}

View file

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

View file

@ -1,7 +1,7 @@
use chrono::{TimeZone, DateTime, Local};
use futures::{self, Future};
use hyper;
use hyper::header::ContentType;
use hyper::header::{ContentType, Location};
use hyper::server::*;
use serde_json;
use serde_urlencoded;
@ -22,6 +22,13 @@ pub struct ArticleResource {
edit: bool,
}
#[derive(Deserialize)]
struct UpdateArticle {
base_revision: i32,
title: String,
body: String,
}
impl ArticleResource {
pub fn new(state: State, article_id: i32, revision: i32, edit: bool) -> Self {
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 {
fn allow(&self) -> Vec<hyper::Method> {
use hyper::Method::*;
vec![Options, Head, Get, Put]
vec![Options, Head, Get, Put, Post]
}
fn head(&self) -> ResponseFuture {
@ -114,13 +121,6 @@ impl Resource for ArticleResource {
use futures::Stream;
#[derive(Deserialize)]
struct UpdateArticle {
base_revision: i32,
title: String,
body: String,
}
#[derive(BartDisplay)]
#[template="templates/article_contents.html"]
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 hyper;
use hyper::header::ContentType;
use hyper::header::{ContentType, Location};
use hyper::server::*;
use serde_json;
use serde_urlencoded;
@ -29,6 +29,13 @@ pub struct NewArticleResource {
slug: Option<String>,
}
#[derive(Deserialize)]
struct CreateArticle {
base_revision: String,
title: String,
body: String,
}
impl NewArticleResource {
pub fn new(state: State, slug: Option<String>) -> Self {
Self { state, slug }
@ -98,13 +105,6 @@ impl Resource for NewArticleResource {
use chrono::{TimeZone, Local};
use futures::Stream;
#[derive(Deserialize)]
struct CreateArticle {
base_revision: String,
title: String,
body: String,
}
#[derive(BartDisplay)]
#[template="templates/article_contents.html"]
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(),
Get => resource.get(),
Put => resource.put(body, identity),
Post => resource.post(body, identity),
_ => Box::new(futures::finished(resource.method_not_allowed()))
}
},

View file

@ -1,4 +1,5 @@
use futures;
use futures::{Future, Stream};
use hyper::{self, header, mime, server};
use hyper::server::Response;
use std;
@ -24,18 +25,18 @@ pub trait Resource {
fn put(self: Box<Self>, body: hyper::Body, _identity: Option<String>) -> ResponseFuture
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
.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)
.and_then(move |_| futures::finished(self.method_not_allowed()))
)