ticker-serve: switch from gotham+typed-html to axum+askama
This commit is contained in:
parent
1a55ead24d
commit
bff8b4bd8c
844
Cargo.lock
generated
844
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,3 @@ members = [
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
typed-html = { git = "https://github.com/astro/typed-html", branch = "microdata" }
|
|
||||||
|
|
|
@ -6,11 +6,10 @@ edition = "2018"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gotham = "0.7"
|
tokio = { version = "1", features = ["full"] }
|
||||||
gotham_derive = "0.7"
|
tower-http = { version = "0.4", features = ["fs"] }
|
||||||
http = "0.2"
|
axum = "0.6"
|
||||||
mime = "0.3"
|
askama = "0.12"
|
||||||
typed-html = "0.2"
|
|
||||||
diesel = { version = "1", features = ["postgres", "chrono"] }
|
diesel = { version = "1", features = ["postgres", "chrono"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
libticker = { path = "../libticker" }
|
libticker = { path = "../libticker" }
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
use std::convert::TryInto;
|
use askama::Template;
|
||||||
use gotham::{
|
use axum::{
|
||||||
helpers::http::response::create_response,
|
response::{IntoResponse, Response, Html},
|
||||||
hyper::{Body, Response},
|
http::StatusCode,
|
||||||
state::{FromState, State},
|
Extension,
|
||||||
};
|
};
|
||||||
use http::status::StatusCode;
|
|
||||||
use mime::TEXT_HTML;
|
|
||||||
|
|
||||||
use typed_html::{html, text, dom::DOMTree, types::{Class, SpacedSet}};
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use chrono::{offset::Local, Datelike, Duration, NaiveDate, TimeZone};
|
use chrono::{offset::Local, Datelike, Duration, NaiveDate, TimeZone};
|
||||||
|
|
||||||
use libticker::{
|
use libticker::{
|
||||||
schema::{self, events::dsl::events},
|
schema::{self, events::dsl::events},
|
||||||
model::Event,
|
model::Event, config::Config,
|
||||||
};
|
};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
|
|
||||||
fn fix_url(s: &str) -> std::borrow::Cow<str> {
|
fn fix_url(s: &str) -> String {
|
||||||
if s.starts_with("http:") || s.starts_with("https:") {
|
if s.starts_with("http:") || s.starts_with("https:") {
|
||||||
s.into()
|
s.to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("http://{}", s).into()
|
format!("http://{}", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DayEvents<'e> {
|
struct DayEvents<'e> {
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
events: &'e [Event],
|
month: &'e str,
|
||||||
|
weekday: &'e str,
|
||||||
|
/// (event, url, color)
|
||||||
|
events: Vec<(&'e Event, Option<String>, &'e str)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// assumes pre-sorted input
|
/// assumes pre-sorted input
|
||||||
fn group_by_day(es: &[Event]) -> Vec<DayEvents> {
|
fn group_by_day<'e>(config: &'e Config, es: &'e [Event]) -> Vec<DayEvents<'e>> {
|
||||||
let mut results = vec![];
|
let mut results = vec![];
|
||||||
|
|
||||||
let mut prev_date = None;
|
let mut prev_date = None;
|
||||||
|
@ -40,9 +40,19 @@ fn group_by_day(es: &[Event]) -> Vec<DayEvents> {
|
||||||
for (i, event) in es.iter().enumerate() {
|
for (i, event) in es.iter().enumerate() {
|
||||||
if prev_date.is_some() && prev_date != Some(event.dtstart.date()) {
|
if prev_date.is_some() && prev_date != Some(event.dtstart.date()) {
|
||||||
if i > date_start {
|
if i > date_start {
|
||||||
|
let date = prev_date.unwrap();
|
||||||
results.push(DayEvents {
|
results.push(DayEvents {
|
||||||
date: prev_date.unwrap().clone(),
|
date: date.clone(),
|
||||||
events: &es[date_start..i],
|
month: &config.months[date.month0() as usize],
|
||||||
|
weekday: &config.weekdays[date.weekday().num_days_from_monday() as usize],
|
||||||
|
events: es[date_start..i].iter()
|
||||||
|
.map(|event| {
|
||||||
|
let url = event.url.as_ref().map(|url| fix_url(url));
|
||||||
|
let color = config.calendars.get(&event.calendar)
|
||||||
|
.map(|calendar| &calendar.color[..])
|
||||||
|
.unwrap_or("white");
|
||||||
|
(event, url, color)
|
||||||
|
}).collect(),
|
||||||
});
|
});
|
||||||
date_start = i;
|
date_start = i;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +63,13 @@ fn group_by_day(es: &[Event]) -> Vec<DayEvents> {
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_index(app_state: &AppState) -> String {
|
#[derive(Template)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
struct IndexTemplate<'a> {
|
||||||
|
days: Vec<DayEvents<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn index(Extension(app_state): Extension<AppState>) -> Response {
|
||||||
let db = app_state.db.lock().unwrap();
|
let db = app_state.db.lock().unwrap();
|
||||||
let today = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap();
|
let today = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap();
|
||||||
let limit = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap() +
|
let limit = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap() +
|
||||||
|
@ -69,93 +85,17 @@ fn render_index(app_state: &AppState) -> String {
|
||||||
.then_order_by(schema::events::dtend.desc())
|
.then_order_by(schema::events::dtend.desc())
|
||||||
.load::<Event>(&*db)
|
.load::<Event>(&*db)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let days = group_by_day(&es);
|
|
||||||
|
|
||||||
let config = &app_state.config;
|
let config = &app_state.config;
|
||||||
|
let days = group_by_day(config, &es);
|
||||||
let doc: DOMTree<String> = html!(
|
let template = IndexTemplate {
|
||||||
<html>
|
days,
|
||||||
<head>
|
|
||||||
<title>"Ticker"</title>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width"/>
|
|
||||||
<link rel="stylesheet" title="Style" type="text/css" href="static/style.css"/>
|
|
||||||
<link rel="icon" type="image/png" href="static/favicon.png"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{ days.iter().map(|day| {
|
|
||||||
let mut day_class: SpacedSet<Class> = ["date"].try_into().unwrap();
|
|
||||||
day_class.add(&format!("wd{}", day.date.weekday().num_days_from_monday())[..]);
|
|
||||||
html!(
|
|
||||||
<div>
|
|
||||||
<h2>
|
|
||||||
<span class=day_class>
|
|
||||||
<span class="day">
|
|
||||||
{ text!("{}", day.date.day()) }
|
|
||||||
</span>
|
|
||||||
<span class="month">
|
|
||||||
{ text!("{}", &config.months[day.date.month0() as usize]) }
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="weekday">
|
|
||||||
{ text!("{}", &config.weekdays[day.date.weekday().num_days_from_monday() as usize]) }
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{ day.events.iter().map(|e| html!(
|
|
||||||
<article class="event"
|
|
||||||
itemprop="event" itemscope=true itemtype="https://schema.org/Event"
|
|
||||||
style=( format!("border-left: 1.5rem solid {}", &config.calendars.get(&e.calendar).map(|o| &o.color[..]).unwrap_or("white")) )>
|
|
||||||
<p class="time">
|
|
||||||
{ if e.recurrence {
|
|
||||||
html!(<span class="recurrence" title="Regelmässige Veranstaltung">"⭯"</span>)
|
|
||||||
} else {
|
|
||||||
html!(<span class="recurrence">" "</span>)
|
|
||||||
} }
|
|
||||||
<time class="dtstart" itemprop="startDate"
|
|
||||||
datetime=( format!("{}", Local.from_local_datetime(&e.dtstart).unwrap().format("%Y-%m-%dT%H:%M:%S%:z")) )>
|
|
||||||
{ text!("{}", &e.dtstart.format("%H:%M")) }
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
{ match &e.url {
|
|
||||||
None => html!(
|
|
||||||
<h3 itemprop="name">{ text!("{}", &e.summary) }</h3>
|
|
||||||
),
|
|
||||||
Some(url) => html!(
|
|
||||||
<h3 itemprop="name">
|
|
||||||
<a href=fix_url(url) itemprop="url">
|
|
||||||
{ text!("{}", &e.summary) }
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
),
|
|
||||||
} }
|
|
||||||
|
|
||||||
<p class="location" itemprop="location">
|
|
||||||
{ text!("{}", e.location.as_ref().unwrap_or(&"".to_owned())) }
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
)) }
|
|
||||||
</div>)
|
|
||||||
}) }
|
|
||||||
<footer>
|
|
||||||
<p>
|
|
||||||
"Ein Projekt des "
|
|
||||||
<a href="https://www.c3d2.de/">"C3D2"</a>
|
|
||||||
" - "
|
|
||||||
<a href="https://gitea.c3d2.de/astro/ticker">"Code"</a>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
format!("<!DOCTYPE html>\n{}", doc.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index(state: State) -> (State, Response<Body>) {
|
|
||||||
let message = {
|
|
||||||
let app_state = AppState::borrow_from(&state);
|
|
||||||
render_index(app_state)
|
|
||||||
};
|
};
|
||||||
let res = create_response(&state, StatusCode::OK, TEXT_HTML, message);
|
match template.render() {
|
||||||
(state, res)
|
Ok(rendered) =>
|
||||||
|
Html(rendered).into_response(),
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Failed to render template. Error: {}", e),
|
||||||
|
).into_response(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
#![recursion_limit="2048"]
|
use std::{
|
||||||
|
sync::{Arc, Mutex},
|
||||||
#[macro_use]
|
net::SocketAddr,
|
||||||
extern crate gotham_derive;
|
str::FromStr,
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use gotham::{
|
|
||||||
handler::FileOptions,
|
|
||||||
router::builder::{DefineSingleRoute, DrawRoutes},
|
|
||||||
middleware::state::StateMiddleware,
|
|
||||||
pipeline::{single_pipeline, single_middleware},
|
|
||||||
router::builder::*,
|
|
||||||
};
|
};
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::get, Extension,
|
||||||
|
};
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
use diesel::{Connection, pg::PgConnection};
|
use diesel::{Connection, pg::PgConnection};
|
||||||
|
|
||||||
use libticker::config::Config;
|
use libticker::config::Config;
|
||||||
mod index;
|
mod index;
|
||||||
|
|
||||||
#[derive(Clone, StateData)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: Arc<Mutex<PgConnection>>,
|
pub db: Arc<Mutex<PgConnection>>,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
let config = Config::read_yaml_file("config.yaml");
|
let config = Config::read_yaml_file("config.yaml");
|
||||||
let http_bind = config.http_bind.clone();
|
let http_bind = SocketAddr::from_str(&config.http_bind)
|
||||||
|
.expect("http_bind");
|
||||||
let db = PgConnection::establish(&config.db_url)
|
let db = PgConnection::establish(&config.db_url)
|
||||||
.expect("DB");
|
.expect("DB");
|
||||||
|
|
||||||
|
@ -32,21 +31,12 @@ fn main() {
|
||||||
db: Arc::new(Mutex::new(db)),
|
db: Arc::new(Mutex::new(db)),
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
let (chain, pipelines) = single_pipeline(
|
let app = Router::new()
|
||||||
single_middleware(
|
.route("/", get(index::index))
|
||||||
StateMiddleware::new(state)
|
.layer(Extension(state))
|
||||||
)
|
.nest_service("/static", ServeDir::new("static"));
|
||||||
);
|
axum::Server::bind(&http_bind)
|
||||||
let router = build_router(chain, pipelines, |route| {
|
.serve(app.into_make_service())
|
||||||
route.get("/").to(index::index);
|
.await
|
||||||
route.get("static/*").to_dir(
|
.unwrap();
|
||||||
FileOptions::new(&"static")
|
|
||||||
// TODO:
|
|
||||||
.with_cache_control("no-cache")
|
|
||||||
.with_gzip(true)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
gotham::start(http_bind, router)
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ h3 a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.recurrence {
|
.recurrence {
|
||||||
color: #E7E7E7;
|
color: #E7E7E7;
|
||||||
|
|
72
ticker-serve/templates/index.html
Normal file
72
ticker-serve/templates/index.html
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Ticker</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width"/>
|
||||||
|
<link rel="stylesheet" title="Style" type="text/css" href="static/style.css"/>
|
||||||
|
<link rel="icon" type="image/png" href="static/favicon.png"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% for day in days.iter() %}
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
<span class="date {{
|
||||||
|
format!("wd{}", day.date.weekday().num_days_from_monday())
|
||||||
|
}}">
|
||||||
|
<span class="day">
|
||||||
|
{{ day.date.day() }}
|
||||||
|
</span>
|
||||||
|
<span class="month">
|
||||||
|
{{ day.month }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="weekday">
|
||||||
|
{{ day.weekday }}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{% for (e, url, color) in day.events.iter() %}
|
||||||
|
<article class="event"
|
||||||
|
itemprop="event" itemscope=true itemtype="https://schema.org/Event"
|
||||||
|
style="border-left: 1.5rem solid {{ color }}">
|
||||||
|
<p class="time">
|
||||||
|
{% if e.recurrence %}
|
||||||
|
<span class="recurrence" title="Regelmässige Veranstaltung">⭯</span>
|
||||||
|
{% endif %}
|
||||||
|
<time class="dtstart" itemprop="startDate"
|
||||||
|
datetime="{{
|
||||||
|
Local.from_local_datetime(e.dtstart).unwrap().format("%Y-%m-%dT%H:%M:%S%:z")
|
||||||
|
}}">
|
||||||
|
{{ e.dtstart.format("%H:%M") }}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
{% if let Some(url) = url %}
|
||||||
|
<h3 itemprop="name">
|
||||||
|
<a href="{{ url }}" itemprop="url">
|
||||||
|
{{ e.summary }}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
{% else %}
|
||||||
|
<h3 itemprop="name">{{ e.summary }}</h3>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="location" itemprop="location">
|
||||||
|
{% if let Some(location) = e.location %}
|
||||||
|
{{ location }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
Ein Projekt des
|
||||||
|
<a href="https://www.c3d2.de/">"C3D2"</a>
|
||||||
|
-
|
||||||
|
<a href="https://gitea.c3d2.de/astro/ticker">Code</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user