Implement support for inserting new articles

This commit is contained in:
Magnus Hoff 2017-09-21 11:38:52 +02:00
parent 0a3cb53a66
commit ad4addfc8c
2 changed files with 123 additions and 13 deletions

View file

@ -2,9 +2,12 @@ use futures::{self, Future};
use hyper; use hyper;
use hyper::header::ContentType; use hyper::header::ContentType;
use hyper::server::*; use hyper::server::*;
use serde_json;
use serde_urlencoded;
use assets::{StyleCss, ScriptJs}; use assets::{StyleCss, ScriptJs};
use mimes::*; use mimes::*;
use rendering::render_markdown;
use site::Layout; use site::Layout;
use state::State; use state::State;
use web::{Resource, ResponseFuture}; use web::{Resource, ResponseFuture};
@ -83,7 +86,64 @@ impl Resource for NewArticleResource {
})) }))
} }
fn put(self: Box<Self>, _body: hyper::Body) -> ResponseFuture { fn put(self: Box<Self>, body: hyper::Body) -> ResponseFuture {
unimplemented!() // 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"))
)
})
)
} }
} }

View file

@ -8,6 +8,7 @@ use r2d2::Pool;
use r2d2_diesel::ConnectionManager; use r2d2_diesel::ConnectionManager;
use models; use models;
use schema::*;
#[derive(Clone)] #[derive(Clone)]
pub struct State { pub struct State {
@ -26,6 +27,17 @@ pub enum SlugLookup {
Redirect(String), 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> { fn decide_slug(conn: &SqliteConnection, article_id: i32, prev_title: &str, title: &str, prev_slug: &str) -> Result<String, Error> {
if prev_slug == "" { if prev_slug == "" {
// Never give a non-empty slug to the front page // 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)?; 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( diesel::update(
article_revisions::table article_revisions::table
.filter(article_revisions::article_id.eq(article_id)) .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)?
)
})
})
}
} }