ticker-serve: switch from gotham+typed-html to axum+askama
This commit is contained in:
parent
1a55ead24d
commit
bff8b4bd8c
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,3 @@ members = [
|
|||
|
||||
[profile.release]
|
||||
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"
|
||||
|
||||
[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" }
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ h3 a {
|
|||
font-weight: 500;
|
||||
font-size: 85%;
|
||||
line-height: 1.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.recurrence {
|
||||
color: #E7E7E7;
|
||||
|
|
|
@ -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