Add view of historical article revisions

This commit is contained in:
Magnus Hoff 2017-10-24 10:30:12 +02:00
parent db0c2ef7f7
commit ffccc5722c
11 changed files with 244 additions and 37 deletions

View file

@ -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;

View file

@ -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,

View 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: &timestamp_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()))
))
}
}

View file

@ -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;

View file

@ -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,

View file

@ -24,7 +24,7 @@ struct PaginationStruct<T> {
before: Option<T>,
}
#[derive(Clone)]
#[derive(Copy, Clone)]
pub enum Pagination<T> {
After(T),
Before(T),

View file

@ -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();

View file

@ -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
View 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>

View file

@ -1,40 +1,17 @@
<script src="_assets/script-{{script_js_checksum}}.js" defer></script>
<div class="container">
<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>
<div class="notice">
<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>
You are viewing an historical version of <a href="{{link_current}}">this article</a>,
authored at {{{timestamp_and_author}}}.
</p>
</article>
<div class="editor-controls">
{{#cancel_url}}
<a class="cancel" href="{{.}}">Cancel</a>
{{/cancel_url}}
<button type=submit>Save</button>
</div>
</form>
<div class="rendered">
{{>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>