From ad4addfc8cc4f1f56a4ebbaad3dd9706812e74c7 Mon Sep 17 00:00:00 2001 From: Magnus Hoff Date: Thu, 21 Sep 2017 11:38:52 +0200 Subject: [PATCH] Implement support for inserting new articles --- src/new_article_resource.rs | 64 +++++++++++++++++++++++++++++++-- src/state.rs | 72 +++++++++++++++++++++++++++++++------ 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/src/new_article_resource.rs b/src/new_article_resource.rs index da667ea..770d9b5 100644 --- a/src/new_article_resource.rs +++ b/src/new_article_resource.rs @@ -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, _body: hyper::Body) -> ResponseFuture { - unimplemented!() + fn put(self: Box, 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")) + ) + }) + ) } } diff --git a/src/state.rs b/src/state.rs index 81be9cc..2d75b01 100644 --- a/src/state.rs +++ b/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 { 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 + { + 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 + } + + 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::(&*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::(&*conn)? + ) + }) + }) + } }