Implement support for inserting new articles
This commit is contained in:
parent
0a3cb53a66
commit
ad4addfc8c
2 changed files with 123 additions and 13 deletions
|
@ -2,9 +2,12 @@ use futures::{self, Future};
|
|||
use hyper;
|
||||
use hyper::header::ContentType;
|
||||
use hyper::server::*;
|
||||
use serde_json;
|
||||
use serde_urlencoded;
|
||||
|
||||
use assets::{StyleCss, ScriptJs};
|
||||
use mimes::*;
|
||||
use rendering::render_markdown;
|
||||
use site::Layout;
|
||||
use state::State;
|
||||
use web::{Resource, ResponseFuture};
|
||||
|
@ -83,7 +86,64 @@ impl Resource for NewArticleResource {
|
|||
}))
|
||||
}
|
||||
|
||||
fn put(self: Box<Self>, _body: hyper::Body) -> ResponseFuture {
|
||||
unimplemented!()
|
||||
fn put(self: Box<Self>, body: hyper::Body) -> ResponseFuture {
|
||||
// TODO Check incoming Content-Type
|
||||
|
||||
use chrono::{TimeZone, Local};
|
||||
use futures::Stream;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateArticle {
|
||||
base_revision: String,
|
||||
title: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article_revision_contents.html"]
|
||||
struct Template<'a> {
|
||||
title: &'a str,
|
||||
rendered: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PutResponse<'a> {
|
||||
slug: &'a str,
|
||||
revision: i32,
|
||||
title: &'a str,
|
||||
rendered: &'a str,
|
||||
created: &'a str,
|
||||
}
|
||||
|
||||
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| {
|
||||
// TODO Check that update.base_revision == NDASH
|
||||
// ... which seems silly. But there should be a mechanism to indicate that
|
||||
// the client is actually trying to create a new article
|
||||
self.state.create_article(self.slug.clone(), arg.title, arg.body)
|
||||
})
|
||||
.and_then(|updated| {
|
||||
futures::finished(Response::new()
|
||||
.with_status(hyper::StatusCode::Ok)
|
||||
.with_header(ContentType(APPLICATION_JSON.clone()))
|
||||
.with_body(serde_json::to_string(&PutResponse {
|
||||
slug: &updated.slug,
|
||||
revision: updated.revision,
|
||||
title: &updated.title,
|
||||
rendered: &Template {
|
||||
title: &updated.title,
|
||||
rendered: render_markdown(&updated.body),
|
||||
}.to_string(),
|
||||
created: &Local.from_utc_datetime(&updated.created).to_string(),
|
||||
}).expect("Should never fail"))
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
72
src/state.rs
72
src/state.rs
|
@ -8,6 +8,7 @@ use r2d2::Pool;
|
|||
use r2d2_diesel::ConnectionManager;
|
||||
|
||||
use models;
|
||||
use schema::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct State {
|
||||
|
@ -26,6 +27,17 @@ pub enum SlugLookup {
|
|||
Redirect(String),
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name="article_revisions"]
|
||||
struct NewRevision<'a> {
|
||||
article_id: i32,
|
||||
revision: i32,
|
||||
slug: &'a str,
|
||||
title: &'a str,
|
||||
body: &'a str,
|
||||
latest: bool,
|
||||
}
|
||||
|
||||
fn decide_slug(conn: &SqliteConnection, article_id: i32, prev_title: &str, title: &str, prev_slug: &str) -> Result<String, Error> {
|
||||
if prev_slug == "" {
|
||||
// Never give a non-empty slug to the front page
|
||||
|
@ -160,17 +172,6 @@ impl State {
|
|||
|
||||
let slug = decide_slug(&*conn, article_id, &prev_title, &title, &prev_slug)?;
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name="article_revisions"]
|
||||
struct NewRevision<'a> {
|
||||
article_id: i32,
|
||||
revision: i32,
|
||||
slug: &'a str,
|
||||
title: &'a str,
|
||||
body: &'a str,
|
||||
latest: bool,
|
||||
}
|
||||
|
||||
diesel::update(
|
||||
article_revisions::table
|
||||
.filter(article_revisions::article_id.eq(article_id))
|
||||
|
@ -198,4 +199,53 @@ impl State {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_article(&self, target_slug: String, title: String, body: String)
|
||||
-> CpuFuture<models::ArticleRevision, Error>
|
||||
{
|
||||
let connection_pool = self.connection_pool.clone();
|
||||
|
||||
self.cpu_pool.spawn_fn(move || {
|
||||
let conn = connection_pool.get()?;
|
||||
|
||||
conn.transaction(|| {
|
||||
#[derive(Insertable)]
|
||||
#[table_name="articles"]
|
||||
struct NewArticle {
|
||||
id: Option<i32>
|
||||
}
|
||||
|
||||
let article_id = {
|
||||
use diesel::expression::sql_literal::sql;
|
||||
// Diesel and SQLite are a bit in disagreement for how this should look:
|
||||
sql::<(diesel::types::Integer)>("INSERT INTO articles VALUES (null)")
|
||||
.execute(&*conn)?;
|
||||
sql::<(diesel::types::Integer)>("SELECT LAST_INSERT_ROWID()")
|
||||
.load::<i32>(&*conn)?
|
||||
.pop().expect("Statement must evaluate to an integer")
|
||||
};
|
||||
|
||||
let slug = decide_slug(&*conn, article_id, "", &title, &target_slug)?;
|
||||
|
||||
let new_revision = 1;
|
||||
|
||||
diesel::insert(&NewRevision {
|
||||
article_id,
|
||||
revision: new_revision,
|
||||
slug: &slug,
|
||||
title: &title,
|
||||
body: &body,
|
||||
latest: true,
|
||||
})
|
||||
.into(article_revisions::table)
|
||||
.execute(&*conn)?;
|
||||
|
||||
Ok(article_revisions::table
|
||||
.filter(article_revisions::article_id.eq(article_id))
|
||||
.filter(article_revisions::revision.eq(new_revision))
|
||||
.first::<models::ArticleRevision>(&*conn)?
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue