Support editing articles without JavaScript
This commit is contained in:
parent
db2d8f5d76
commit
5b37a41632
6 changed files with 97 additions and 28 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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()))
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue