Add view of historical article revisions
This commit is contained in:
parent
db0c2ef7f7
commit
ffccc5722c
11 changed files with 244 additions and 37 deletions
|
@ -61,6 +61,17 @@ article>hr {
|
|||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.notice {
|
||||
background: lightyellow;
|
||||
padding: 32px 48px;
|
||||
|
||||
box-sizing: border-box;
|
||||
max-width: 616px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
header, article>* {
|
||||
box-sizing: border-box;
|
||||
max-width: 616px;
|
||||
|
|
|
@ -67,7 +67,7 @@ impl Resource for ArticleResource {
|
|||
|
||||
fn get(self: Box<Self>) -> ResponseFuture {
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article_revision.html"]
|
||||
#[template="templates/article.html"]
|
||||
struct Template<'a> {
|
||||
revision: i32,
|
||||
last_updated: Option<&'a str>,
|
||||
|
@ -123,7 +123,7 @@ impl Resource for ArticleResource {
|
|||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article_revision_contents.html"]
|
||||
#[template="templates/article_contents.html"]
|
||||
struct Template<'a> {
|
||||
title: &'a str,
|
||||
rendered: String,
|
||||
|
|
111
src/resources/article_revision_resource.rs
Normal file
111
src/resources/article_revision_resource.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use chrono::{TimeZone, DateTime, Local};
|
||||
use futures::{self, Future};
|
||||
use hyper;
|
||||
use hyper::header::ContentType;
|
||||
use hyper::server::*;
|
||||
|
||||
use assets::StyleCss;
|
||||
use mimes::*;
|
||||
use models;
|
||||
use rendering::render_markdown;
|
||||
use site::Layout;
|
||||
use web::{Resource, ResponseFuture};
|
||||
|
||||
use super::changes_resource::QueryParameters;
|
||||
use super::pagination::Pagination;
|
||||
|
||||
pub struct ArticleRevisionResource {
|
||||
data: models::ArticleRevision,
|
||||
}
|
||||
|
||||
impl ArticleRevisionResource {
|
||||
pub fn new(data: models::ArticleRevision) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp_and_author(sequence_number: i32, article_id: i32, created: &DateTime<Local>, author: Option<&str>) -> String {
|
||||
struct Author<'a> {
|
||||
author: &'a str,
|
||||
history: String,
|
||||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
#[template_string = "<a href=\"{{article_history}}\">{{created}}</a>{{#author}} by <a href=\"{{.history}}\">{{.author}}</a>{{/author}}"]
|
||||
struct Template<'a> {
|
||||
created: &'a str,
|
||||
article_history: &'a str,
|
||||
author: Option<Author<'a>>,
|
||||
}
|
||||
|
||||
let pagination = Pagination::Before(sequence_number + 1);
|
||||
|
||||
Template {
|
||||
created: &created.to_rfc2822(),
|
||||
article_history: &format!("_changes{}",
|
||||
QueryParameters::default()
|
||||
.pagination(pagination)
|
||||
.article_id(Some(article_id))
|
||||
.into_link()
|
||||
),
|
||||
author: author.map(|author| Author {
|
||||
author: &author,
|
||||
history: format!("_changes{}",
|
||||
QueryParameters::default()
|
||||
.pagination(pagination)
|
||||
.author(Some(author.to_owned()))
|
||||
.into_link()
|
||||
),
|
||||
}),
|
||||
}.to_string()
|
||||
}
|
||||
|
||||
impl Resource for ArticleRevisionResource {
|
||||
fn allow(&self) -> Vec<hyper::Method> {
|
||||
use hyper::Method::*;
|
||||
vec![Options, Head, Get, Put]
|
||||
}
|
||||
|
||||
fn head(&self) -> ResponseFuture {
|
||||
Box::new(futures::finished(Response::new()
|
||||
.with_status(hyper::StatusCode::Ok)
|
||||
.with_header(ContentType(TEXT_HTML.clone()))
|
||||
))
|
||||
}
|
||||
|
||||
fn get(self: Box<Self>) -> ResponseFuture {
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article_revision.html"]
|
||||
struct Template<'a> {
|
||||
link_current: &'a str,
|
||||
timestamp_and_author: &'a str,
|
||||
|
||||
title: &'a str,
|
||||
rendered: String,
|
||||
}
|
||||
|
||||
let head = self.head();
|
||||
let data = self.data;
|
||||
|
||||
Box::new(head
|
||||
.and_then(move |head|
|
||||
Ok(head
|
||||
.with_body(Layout {
|
||||
base: Some("../../"), // Hmm, should perhaps accept `base` as argument
|
||||
title: &data.title,
|
||||
body: &Template {
|
||||
link_current: &format!("_by_id/{}", data.article_id),
|
||||
timestamp_and_author: ×tamp_and_author(
|
||||
data.sequence_number,
|
||||
data.article_id,
|
||||
&Local.from_utc_datetime(&data.created),
|
||||
data.author.as_ref().map(|x| &**x)
|
||||
),
|
||||
title: &data.title,
|
||||
rendered: render_markdown(&data.body),
|
||||
},
|
||||
style_css_checksum: StyleCss::checksum(),
|
||||
}.to_string()))
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
pub mod pagination;
|
||||
|
||||
mod article_redirect_resource;
|
||||
mod article_revision_resource;
|
||||
mod article_resource;
|
||||
mod changes_resource;
|
||||
mod new_article_resource;
|
||||
|
@ -8,6 +9,7 @@ mod sitemap_resource;
|
|||
mod temporary_redirect_resource;
|
||||
|
||||
pub use self::article_redirect_resource::ArticleRedirectResource;
|
||||
pub use self::article_revision_resource::ArticleRevisionResource;
|
||||
pub use self::article_resource::ArticleResource;
|
||||
pub use self::changes_resource::{ChangesLookup, ChangesResource};
|
||||
pub use self::new_article_resource::NewArticleResource;
|
||||
|
|
|
@ -50,7 +50,7 @@ impl Resource for NewArticleResource {
|
|||
|
||||
fn get(self: Box<Self>) -> ResponseFuture {
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article_revision.html"]
|
||||
#[template="templates/article.html"]
|
||||
struct Template<'a> {
|
||||
revision: &'a str,
|
||||
last_updated: Option<&'a str>,
|
||||
|
@ -107,7 +107,7 @@ impl Resource for NewArticleResource {
|
|||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article_revision_contents.html"]
|
||||
#[template="templates/article_contents.html"]
|
||||
struct Template<'a> {
|
||||
title: &'a str,
|
||||
rendered: String,
|
||||
|
|
|
@ -24,7 +24,7 @@ struct PaginationStruct<T> {
|
|||
before: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Pagination<T> {
|
||||
After(T),
|
||||
Before(T),
|
||||
|
|
15
src/state.rs
15
src/state.rs
|
@ -87,6 +87,21 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_article_slug(&self, article_id: i32) -> CpuFuture<Option<String>, Error> {
|
||||
let connection_pool = self.connection_pool.clone();
|
||||
|
||||
self.cpu_pool.spawn_fn(move || {
|
||||
use schema::article_revisions;
|
||||
|
||||
Ok(article_revisions::table
|
||||
.filter(article_revisions::article_id.eq(article_id))
|
||||
.filter(article_revisions::latest.eq(true))
|
||||
.select((article_revisions::slug))
|
||||
.first::<String>(&*connection_pool.get()?)
|
||||
.optional()?)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_article_revision(&self, article_id: i32, revision: i32) -> CpuFuture<Option<models::ArticleRevision>, Error> {
|
||||
let connection_pool = self.connection_pool.clone();
|
||||
|
||||
|
|
|
@ -76,6 +76,53 @@ impl WikiLookup {
|
|||
WikiLookup { state, changes_lookup }
|
||||
}
|
||||
|
||||
fn revisions_lookup(&self, path: &str, _query: Option<&str>) -> <Self as Lookup>::Future {
|
||||
let (article_id, revision): (i32, i32) = match (|| -> Result<_, <Self as Lookup>::Error> {
|
||||
let (article_id, tail) = split_one(path)?;
|
||||
let (revision, tail) = split_one(tail.ok_or("Not found")?)?;
|
||||
if tail.is_some() {
|
||||
return Err("Not found".into());
|
||||
}
|
||||
|
||||
Ok((article_id.parse()?, revision.parse()?))
|
||||
})() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Box::new(finished(None)),
|
||||
};
|
||||
|
||||
Box::new(
|
||||
self.state.get_article_revision(article_id, revision)
|
||||
.and_then(|article_revision|
|
||||
Ok(article_revision.map(move |x| Box::new(
|
||||
ArticleRevisionResource::new(x)
|
||||
) as BoxResource))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn by_id_lookup(&self, path: &str, _query: Option<&str>) -> <Self as Lookup>::Future {
|
||||
let article_id: i32 = match (|| -> Result<_, <Self as Lookup>::Error> {
|
||||
let (article_id, tail) = split_one(path)?;
|
||||
if tail.is_some() {
|
||||
return Err("Not found".into());
|
||||
}
|
||||
|
||||
Ok(article_id.parse()?)
|
||||
})() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Box::new(finished(None)),
|
||||
};
|
||||
|
||||
Box::new(
|
||||
self.state.get_article_slug(article_id)
|
||||
.and_then(|slug|
|
||||
Ok(slug.map(|slug| Box::new(
|
||||
TemporaryRedirectResource::new(format!("../{}", slug))
|
||||
) as BoxResource))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn reserved_lookup(&self, path: &str, query: Option<&str>) -> <Self as Lookup>::Future {
|
||||
let (head, tail) = match split_one(path) {
|
||||
Ok(x) => x,
|
||||
|
@ -85,10 +132,14 @@ impl WikiLookup {
|
|||
match (head.as_ref(), tail) {
|
||||
("_assets", Some(asset)) =>
|
||||
Box::new(asset_lookup(asset)),
|
||||
("_by_id", Some(tail)) =>
|
||||
self.by_id_lookup(tail, query),
|
||||
("_changes", None) =>
|
||||
Box::new(self.changes_lookup.lookup(query)),
|
||||
("_new", None) =>
|
||||
Box::new(finished(Some(Box::new(NewArticleResource::new(self.state.clone(), None)) as BoxResource))),
|
||||
("_revisions", Some(tail)) =>
|
||||
self.revisions_lookup(tail, query),
|
||||
("_sitemap", None) =>
|
||||
Box::new(finished(Some(Box::new(SitemapResource::new(self.state.clone())) as BoxResource))),
|
||||
_ => Box::new(finished(None)),
|
||||
|
|
40
templates/article.html
Normal file
40
templates/article.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script src="_assets/script-{{script_js_checksum}}.js" defer></script>
|
||||
|
||||
<div class="container {{#edit?}}edit{{/edit}}">
|
||||
<div class="rendered">
|
||||
{{>article_contents.html}}
|
||||
</div>
|
||||
|
||||
<div class="editor">
|
||||
<form action="" method="POST">
|
||||
|
||||
<header>
|
||||
<h1><input autocomplete=off type=text name=title value="{{title}}" placeholder="Title"></h1>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
<p>
|
||||
<input autocomplete=off type=hidden name=base_revision value="{{revision}}">
|
||||
<textarea autocomplete=off name=body placeholder="Article goes here">{{raw}}</textarea>
|
||||
<textarea autocomplete=off class="shadow-control"></textarea>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<div class="editor-controls">
|
||||
{{#cancel_url}}
|
||||
<a class="cancel" href="{{.}}">Cancel</a>
|
||||
{{/cancel_url}}
|
||||
<button type=submit>Save</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<ul class="dense"
|
||||
><li class="last-updated {{^last_updated}}missing{{/last_updated}}">{{#last_updated}}{{{.}}}{{/last_updated}}</li
|
||||
><li><a id="openEditor" href="?edit">Edit</a></li
|
||||
></ul>
|
||||
{{>footer/items.html}}
|
||||
</footer>
|
|
@ -1,40 +1,17 @@
|
|||
<script src="_assets/script-{{script_js_checksum}}.js" defer></script>
|
||||
<div class="container">
|
||||
|
||||
<div class="notice">
|
||||
<p>
|
||||
You are viewing an historical version of <a href="{{link_current}}">this article</a>,
|
||||
authored at {{{timestamp_and_author}}}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="container {{#edit?}}edit{{/edit}}">
|
||||
<div class="rendered">
|
||||
{{>article_revision_contents.html}}
|
||||
</div>
|
||||
|
||||
<div class="editor">
|
||||
<form action="" method="POST">
|
||||
|
||||
<header>
|
||||
<h1><input autocomplete=off type=text name=title value="{{title}}" placeholder="Title"></h1>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
<p>
|
||||
<input autocomplete=off type=hidden name=base_revision value="{{revision}}">
|
||||
<textarea autocomplete=off name=body placeholder="Article goes here">{{raw}}</textarea>
|
||||
<textarea autocomplete=off class="shadow-control"></textarea>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<div class="editor-controls">
|
||||
{{#cancel_url}}
|
||||
<a class="cancel" href="{{.}}">Cancel</a>
|
||||
{{/cancel_url}}
|
||||
<button type=submit>Save</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{{>article_contents.html}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<ul class="dense"
|
||||
><li class="last-updated {{^last_updated}}missing{{/last_updated}}">{{#last_updated}}{{{.}}}{{/last_updated}}</li
|
||||
><li><a id="openEditor" href="?edit">Edit</a></li
|
||||
></ul>
|
||||
{{>footer/items.html}}
|
||||
</footer>
|
||||
|
|
Loading…
Reference in a new issue