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;
|
||||||
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"))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
72
src/state.rs
72
src/state.rs
|
@ -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)?
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue