ticker-serve: switch from gotham+typed-html to axum+askama

This commit is contained in:
Astro 2023-03-19 23:21:30 +01:00
parent 1a55ead24d
commit bff8b4bd8c
7 changed files with 370 additions and 761 deletions

844
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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" }

View File

@ -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" }

View File

@ -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(),
}
} }

View File

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

View File

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

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