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]
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"
[dependencies]
gotham = "0.7"
gotham_derive = "0.7"
http = "0.2"
mime = "0.3"
typed-html = "0.2"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.4", features = ["fs"] }
axum = "0.6"
askama = "0.12"
diesel = { version = "1", features = ["postgres", "chrono"] }
chrono = "0.4"
libticker = { path = "../libticker" }

View File

@ -1,38 +1,38 @@
use std::convert::TryInto;
use gotham::{
helpers::http::response::create_response,
hyper::{Body, Response},
state::{FromState, State},
use askama::Template;
use axum::{
response::{IntoResponse, Response, Html},
http::StatusCode,
Extension,
};
use http::status::StatusCode;
use mime::TEXT_HTML;
use typed_html::{html, text, dom::DOMTree, types::{Class, SpacedSet}};
use diesel::prelude::*;
use chrono::{offset::Local, Datelike, Duration, NaiveDate, TimeZone};
use libticker::{
schema::{self, events::dsl::events},
model::Event,
model::Event, config::Config,
};
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:") {
s.into()
s.to_owned()
} else {
format!("http://{}", s).into()
format!("http://{}", s)
}
}
struct DayEvents<'e> {
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
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 prev_date = None;
@ -40,9 +40,19 @@ fn group_by_day(es: &[Event]) -> Vec<DayEvents> {
for (i, event) in es.iter().enumerate() {
if prev_date.is_some() && prev_date != Some(event.dtstart.date()) {
if i > date_start {
let date = prev_date.unwrap();
results.push(DayEvents {
date: prev_date.unwrap().clone(),
events: &es[date_start..i],
date: date.clone(),
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;
}
@ -53,7 +63,13 @@ fn group_by_day(es: &[Event]) -> Vec<DayEvents> {
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 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() +
@ -69,93 +85,17 @@ fn render_index(app_state: &AppState) -> String {
.then_order_by(schema::events::dtend.desc())
.load::<Event>(&*db)
.unwrap();
let days = group_by_day(&es);
let config = &app_state.config;
let doc: DOMTree<String> = 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>
{ 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 days = group_by_day(config, &es);
let template = IndexTemplate {
days,
};
let res = create_response(&state, StatusCode::OK, TEXT_HTML, message);
(state, res)
match template.render() {
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"]
#[macro_use]
extern crate gotham_derive;
use std::sync::{Arc, Mutex};
use gotham::{
handler::FileOptions,
router::builder::{DefineSingleRoute, DrawRoutes},
middleware::state::StateMiddleware,
pipeline::{single_pipeline, single_middleware},
router::builder::*,
use std::{
sync::{Arc, Mutex},
net::SocketAddr,
str::FromStr,
};
use axum::{
Router,
routing::get, Extension,
};
use tower_http::services::ServeDir;
use diesel::{Connection, pg::PgConnection};
use libticker::config::Config;
mod index;
#[derive(Clone, StateData)]
#[derive(Clone)]
pub struct AppState {
pub db: Arc<Mutex<PgConnection>>,
pub config: Config,
}
fn main() {
#[tokio::main]
async fn main() {
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)
.expect("DB");
@ -32,21 +31,12 @@ fn main() {
db: Arc::new(Mutex::new(db)),
config,
};
let (chain, pipelines) = single_pipeline(
single_middleware(
StateMiddleware::new(state)
)
);
let router = build_router(chain, pipelines, |route| {
route.get("/").to(index::index);
route.get("static/*").to_dir(
FileOptions::new(&"static")
// TODO:
.with_cache_control("no-cache")
.with_gzip(true)
.build()
);
});
gotham::start(http_bind, router)
.unwrap()
let app = Router::new()
.route("/", get(index::index))
.layer(Extension(state))
.nest_service("/static", ServeDir::new("static"));
axum::Server::bind(&http_bind)
.serve(app.into_make_service())
.await
.unwrap();
}

View File

@ -85,6 +85,7 @@ h3 a {
font-weight: 500;
font-size: 85%;
line-height: 1.5rem;
white-space: nowrap;
}
.recurrence {
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>