Implement editing
This commit is contained in:
parent
60a87d1898
commit
1a5b39b3a1
7 changed files with 142 additions and 33 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -19,6 +19,7 @@ dependencies = [
|
|||
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-io 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -572,6 +573,17 @@ dependencies = [
|
|||
"synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.5.1"
|
||||
|
@ -893,6 +905,7 @@ dependencies = [
|
|||
"checksum serde 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f7726f29ddf9731b17ff113c461e362c381d9d69433f79de4f3dd572488823e9"
|
||||
"checksum serde_derive 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cf823e706be268e73e7747b147aa31c8f633ab4ba31f115efb57e5047c3a76dd"
|
||||
"checksum serde_derive_internals 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37aee4e0da52d801acfbc0cc219eb1eda7142112339726e427926a6f6ee65d3a"
|
||||
"checksum serde_json 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d243424e06f9f9c39e3cd36147470fd340db785825e367625f79298a6ac6b7ac"
|
||||
"checksum serde_urlencoded 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce0fd303af908732989354c6f02e05e2e6d597152870f2c6990efb0577137480"
|
||||
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
|
||||
"checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013"
|
||||
|
|
|
@ -12,6 +12,7 @@ tokio-service = "0.1"
|
|||
serde_derive = "1.0.0"
|
||||
serde = "1.0.0"
|
||||
serde_urlencoded = "0.5.0"
|
||||
serde_json = "1.0"
|
||||
r2d2 = "0.7"
|
||||
r2d2-diesel = "0.16"
|
||||
regex = "0.2"
|
||||
|
|
|
@ -11,6 +11,7 @@ extern crate hyper;
|
|||
extern crate pulldown_cmark;
|
||||
extern crate r2d2;
|
||||
extern crate r2d2_diesel;
|
||||
extern crate serde_json;
|
||||
extern crate serde_urlencoded;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
|
77
src/site.rs
77
src/site.rs
|
@ -5,6 +5,8 @@ use hyper;
|
|||
use hyper::header::ContentType;
|
||||
use hyper::mime;
|
||||
use hyper::server::*;
|
||||
use serde_json;
|
||||
use serde_urlencoded;
|
||||
|
||||
use models;
|
||||
use state::State;
|
||||
|
@ -47,6 +49,7 @@ fn render_markdown(src: &str) -> String {
|
|||
|
||||
lazy_static! {
|
||||
static ref TEXT_HTML: mime::Mime = "text/html;charset=utf-8".parse().unwrap();
|
||||
static ref APPLICATION_JSON: mime::Mime = "application/json".parse().unwrap();
|
||||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
|
@ -70,7 +73,7 @@ struct WikiLookup {
|
|||
|
||||
impl Lookup for WikiLookup {
|
||||
type Resource = ArticleResource;
|
||||
type Error = Box<::std::error::Error + Send>;
|
||||
type Error = Box<::std::error::Error + Send + Sync>;
|
||||
type Future = futures::future::FutureResult<Option<Self::Resource>, Self::Error>;
|
||||
|
||||
fn lookup(&self, path: &str, _query: Option<&str>, _fragment: Option<&str>) -> Self::Future {
|
||||
|
@ -85,7 +88,7 @@ impl Lookup for WikiLookup {
|
|||
if let Ok(article_id) = slug.parse() {
|
||||
match self.state.get_article_revision_by_id(article_id) {
|
||||
Ok(Some(article)) => {
|
||||
futures::finished(Some(ArticleResource::new(article)))
|
||||
futures::finished(Some(ArticleResource::new(self.state.clone(), article)))
|
||||
},
|
||||
Ok(None) => futures::finished(None),
|
||||
Err(err) => futures::failed(err),
|
||||
|
@ -97,14 +100,13 @@ impl Lookup for WikiLookup {
|
|||
}
|
||||
|
||||
struct ArticleResource {
|
||||
state: State,
|
||||
data: models::ArticleRevision,
|
||||
}
|
||||
|
||||
impl ArticleResource {
|
||||
fn new(data: models::ArticleRevision) -> Self {
|
||||
Self {
|
||||
data
|
||||
}
|
||||
fn new(state: State, data: models::ArticleRevision) -> Self {
|
||||
Self { state, data }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,14 +116,14 @@ impl Resource for ArticleResource {
|
|||
vec![Options, Head, Get, Put]
|
||||
}
|
||||
|
||||
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send>> {
|
||||
fn head(&self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
|
||||
futures::finished(Response::new()
|
||||
.with_status(hyper::StatusCode::Ok)
|
||||
.with_header(ContentType(TEXT_HTML.clone()))
|
||||
).boxed()
|
||||
}
|
||||
|
||||
fn get(self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send>> {
|
||||
fn get(self) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
|
||||
use chrono::{self, TimeZone, Local};
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
|
@ -152,8 +154,52 @@ impl Resource for ArticleResource {
|
|||
).boxed()
|
||||
}
|
||||
|
||||
fn put(self, body: &[u8]) -> futures::BoxFuture<Response, Box<::std::error::Error + Send>> {
|
||||
unimplemented!()
|
||||
fn put<S: 'static + futures::Stream<Item=hyper::Chunk, Error=hyper::Error> + Send + Sync>(self, body: S) -> futures::BoxFuture<Response, Box<::std::error::Error + Send + Sync>> {
|
||||
// TODO Check incoming Content-Type
|
||||
|
||||
use chrono::{TimeZone, Local};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateArticle {
|
||||
base_revision: i32,
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PutResponse<'a> {
|
||||
revision: i32,
|
||||
rendered: &'a str,
|
||||
created: &'a str,
|
||||
}
|
||||
|
||||
body
|
||||
.concat2()
|
||||
.map_err(|x| Box::new(x) as Box<::std::error::Error + Send + Sync>)
|
||||
.and_then(move |body| {
|
||||
let update: UpdateArticle = match serde_urlencoded::from_bytes(&body) {
|
||||
Ok(x) => x,
|
||||
Err(err) => return futures::finished(Response::new()
|
||||
.with_status(hyper::StatusCode::BadRequest)
|
||||
.with_body(format!("{:#?}", err))
|
||||
).boxed()
|
||||
};
|
||||
|
||||
let updated = match self.state.update_article(self.data.article_id, update.base_revision, &update.body) {
|
||||
Ok(x) => x,
|
||||
Err(x) => return futures::failed(x).boxed(),
|
||||
};
|
||||
|
||||
futures::finished(Response::new()
|
||||
.with_status(hyper::StatusCode::Ok)
|
||||
.with_header(ContentType(APPLICATION_JSON.clone()))
|
||||
.with_body(serde_json::to_string(&PutResponse {
|
||||
revision: updated.revision,
|
||||
rendered: &render_markdown(&updated.body),
|
||||
created: &Local.from_utc_datetime(&updated.created).to_string(),
|
||||
}).expect("Should never fail"))
|
||||
).boxed()
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +225,7 @@ impl Site {
|
|||
.with_status(hyper::StatusCode::NotFound)
|
||||
}
|
||||
|
||||
fn internal_server_error(err: Box<::std::error::Error + Send>) -> Response {
|
||||
fn internal_server_error(err: Box<::std::error::Error + Send + Sync>) -> Response {
|
||||
eprintln!("Internal Server Error:\n{:#?}", err);
|
||||
|
||||
Response::new()
|
||||
|
@ -210,14 +256,7 @@ impl Service for Site {
|
|||
Options => futures::finished(resource.options()).boxed(),
|
||||
Head => resource.head(),
|
||||
Get => resource.get(),
|
||||
Put => {
|
||||
use futures::Stream;
|
||||
body
|
||||
.concat2()
|
||||
.map_err(|x| Box::new(x) as Box<::std::error::Error + Send>)
|
||||
.and_then(move |body| resource.put(&body))
|
||||
.boxed()
|
||||
},
|
||||
Put => resource.put(body),
|
||||
_ => futures::finished(resource.method_not_allowed()).boxed()
|
||||
}
|
||||
},
|
||||
|
|
55
src/state.rs
55
src/state.rs
|
@ -1,6 +1,7 @@
|
|||
use std;
|
||||
|
||||
use chrono;
|
||||
use diesel;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use diesel::prelude::*;
|
||||
use r2d2::Pool;
|
||||
|
@ -13,12 +14,6 @@ pub struct State {
|
|||
connection_pool: Pool<ConnectionManager<SqliteConnection>>
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateArticle {
|
||||
base_revision: i32,
|
||||
body: String,
|
||||
}
|
||||
|
||||
pub type Error = Box<std::error::Error + Send + Sync>;
|
||||
|
||||
impl State {
|
||||
|
@ -46,4 +41,52 @@ impl State {
|
|||
.load::<models::ArticleRevision>(&*self.connection_pool.get()?)?
|
||||
.pop())
|
||||
}
|
||||
|
||||
pub fn update_article(&self, article_id: i32, base_revision: i32, body: &str) -> Result<models::ArticleRevision, Error> {
|
||||
let conn = self.connection_pool.get()?;
|
||||
conn.transaction(|| {
|
||||
use schema::article_revisions;
|
||||
|
||||
let (latest_revision, title) = article_revisions::table
|
||||
.filter(article_revisions::article_id.eq(article_id))
|
||||
.order(article_revisions::revision.desc())
|
||||
.limit(1)
|
||||
.select((article_revisions::revision, article_revisions::title))
|
||||
.load::<(i32, String)>(&*conn)?
|
||||
.pop()
|
||||
.unwrap_or_else(|| unimplemented!("TODO Missing an error type"));
|
||||
|
||||
if latest_revision != base_revision {
|
||||
// TODO: If it is the same edit repeated, just respond OK
|
||||
// TODO: If there is a conflict, transform the edit to work seamlessly
|
||||
unimplemented!("TODO Missing handling of revision conflicts");
|
||||
}
|
||||
let new_revision = base_revision + 1;
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name="article_revisions"]
|
||||
struct NewRevision<'a> {
|
||||
article_id: i32,
|
||||
revision: i32,
|
||||
title: &'a str,
|
||||
body: &'a str,
|
||||
}
|
||||
|
||||
diesel::insert(&NewRevision {
|
||||
article_id,
|
||||
revision: new_revision,
|
||||
title: &title,
|
||||
body
|
||||
})
|
||||
.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))
|
||||
.load::<models::ArticleRevision>(&*conn)?
|
||||
.pop()
|
||||
.expect("We just inserted this row!"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,13 @@ lazy_static! {
|
|||
static ref TEXT_PLAIN: mime::Mime = "text/plain;charset=utf-8".parse().unwrap();
|
||||
}
|
||||
|
||||
type Error = Box<std::error::Error + Send>;
|
||||
type Error = Box<std::error::Error + Send + Sync>;
|
||||
|
||||
pub trait Resource {
|
||||
fn allow(&self) -> Vec<hyper::Method>;
|
||||
fn head(&self) -> futures::BoxFuture<server::Response, Error>;
|
||||
fn get(self) -> futures::BoxFuture<server::Response, Error>;
|
||||
fn put(self, body: &[u8]) -> futures::BoxFuture<server::Response, Error>;
|
||||
fn put<S: 'static + futures::Stream<Item=hyper::Chunk, Error=hyper::Error> + Send + Sync>(self, body: S) -> futures::BoxFuture<server::Response, Error>;
|
||||
|
||||
fn options(&self) -> Response {
|
||||
Response::new()
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="editor">
|
||||
<form action="" method="POST">
|
||||
<input type=hidden name=baseRevision value="{{revision}}">
|
||||
<input type=hidden name=base_revision value="{{revision}}">
|
||||
<textarea autocomplete=off name=body>{{raw}}</textarea>
|
||||
<textarea autocomplete=off class="shadow-control"></textarea>
|
||||
<div class="editor-controls">
|
||||
|
@ -26,10 +26,10 @@
|
|||
<dd>{{article_id}}</dd>
|
||||
|
||||
<dt>Revision</dt>
|
||||
<dd>{{revision}}</dd>
|
||||
<dd class="revision">{{revision}}</dd>
|
||||
|
||||
<dt>Last updated</dt>
|
||||
<dd>{{created}}</dd>
|
||||
<dd class="last-updated">{{created}}</dd>
|
||||
</dl>
|
||||
</footer>
|
||||
|
||||
|
@ -49,22 +49,29 @@ function queryArgsFromForm(form) {
|
|||
return items.join('&');
|
||||
}
|
||||
|
||||
let hasBeenOpen = false;
|
||||
function openEditor() {
|
||||
const article = document.querySelector("article");
|
||||
const rendered = document.querySelector(".rendered");
|
||||
const editor = document.querySelector(".editor");
|
||||
const textarea = editor.querySelector('textarea[name="body"]');
|
||||
const shadow = editor.querySelector('textarea.shadow-control');
|
||||
const form = editor.querySelector("form");
|
||||
|
||||
textarea.style.height = rendered.clientHeight + "px";
|
||||
|
||||
article.classList.add('edit');
|
||||
|
||||
autosizeTextarea(textarea, shadow);
|
||||
|
||||
textarea.focus();
|
||||
|
||||
if (hasBeenOpen) return;
|
||||
hasBeenOpen = true;
|
||||
|
||||
textarea.addEventListener('input', () => autosizeTextarea(textarea, shadow));
|
||||
window.addEventListener('resize', () => autosizeTextarea(textarea, shadow));
|
||||
|
||||
const form = editor.querySelector("form");
|
||||
form.addEventListener("submit", function (ev) {
|
||||
(async function () {
|
||||
ev.preventDefault();
|
||||
|
@ -86,6 +93,13 @@ function openEditor() {
|
|||
|
||||
if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")");
|
||||
|
||||
const result = await response.json();
|
||||
form.elements.base_revision.value = result.revision;
|
||||
document.querySelector("footer .revision").textContent = result.revision;
|
||||
document.querySelector("footer .last-updated").textContent = result.created;
|
||||
rendered.innerHTML = result.rendered;
|
||||
article.classList.remove('edit');
|
||||
|
||||
textarea.disabled = false;
|
||||
}()
|
||||
.catch(err => {
|
||||
|
@ -94,8 +108,6 @@ function openEditor() {
|
|||
alert(err);
|
||||
}));
|
||||
});
|
||||
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
document
|
||||
|
|
Loading…
Reference in a new issue