Some checks failed
CI / Check links (push) Failing after 31s
CI / Check typos (push) Successful in 52s
CI / Clippy (push) Successful in 1m11s
CI / Build and test (push) Failing after 1m12s
CI / Build container (push) Has been skipped
CI / Deploy on waypoint (push) Has been skipped
268 lines
5.7 KiB
Rust
268 lines
5.7 KiB
Rust
use std::{
|
|
pin::Pin,
|
|
sync::Arc,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use assetserver::Asset;
|
|
use chrono::{DateTime, TimeDelta, Utc};
|
|
use maud::{Markup, PreEscaped, html};
|
|
use page::{DeviceType, Page, RequestContext};
|
|
use parking_lot::Mutex;
|
|
use serde::Deserialize;
|
|
use tracing::{debug, warn};
|
|
|
|
use crate::{
|
|
components::{
|
|
md::{Markdown, meta_from_markdown},
|
|
misc::{Backlinks, FarLink},
|
|
},
|
|
routes::assets::Image_Icon,
|
|
};
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct HandoutEntry {
|
|
title: String,
|
|
group: String,
|
|
handout: String,
|
|
solutions: Option<String>,
|
|
}
|
|
|
|
struct CachedRequestInner<T> {
|
|
last_fetch: DateTime<Utc>,
|
|
last_value: Option<Arc<T>>,
|
|
}
|
|
|
|
pub struct CachedRequest<T> {
|
|
inner: Mutex<CachedRequestInner<T>>,
|
|
ttl: TimeDelta,
|
|
get: Box<dyn Fn() -> Pin<Box<dyn Future<Output = T> + Send + Sync>> + Send + Sync>,
|
|
}
|
|
|
|
impl<T> CachedRequest<T> {
|
|
pub fn new(
|
|
ttl: TimeDelta,
|
|
get: Box<dyn Fn() -> Pin<Box<dyn Future<Output = T> + Send + Sync>> + Send + Sync>,
|
|
) -> Arc<Self> {
|
|
Arc::new(Self {
|
|
get,
|
|
ttl,
|
|
inner: Mutex::new(CachedRequestInner {
|
|
last_fetch: Utc::now(),
|
|
last_value: None,
|
|
}),
|
|
})
|
|
}
|
|
|
|
pub async fn get(self: Arc<Self>) -> Arc<T> {
|
|
let now = Utc::now();
|
|
let expires = self.inner.lock().last_fetch + self.ttl;
|
|
|
|
if now < expires
|
|
&& let Some(last_value) = self.inner.lock().last_value.clone()
|
|
{
|
|
return last_value;
|
|
}
|
|
|
|
let res = Arc::new((self.get)().await);
|
|
|
|
let mut inner = self.inner.lock();
|
|
inner.last_fetch = now;
|
|
inner.last_value = Some(res.clone());
|
|
return res;
|
|
}
|
|
|
|
pub async fn autoget(self: Arc<Self>, interval: Duration) {
|
|
loop {
|
|
{
|
|
let now = Utc::now();
|
|
let res = Arc::new((self.get)().await);
|
|
|
|
let mut inner = self.inner.lock();
|
|
inner.last_fetch = now;
|
|
inner.last_value = Some(res.clone());
|
|
}
|
|
|
|
tokio::time::sleep(interval).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn get_index() -> Result<Vec<HandoutEntry>, reqwest::Error> {
|
|
let start = Instant::now();
|
|
let res = reqwest::get(
|
|
"https://git.betalupi.com/api/packages/Mark/generic/ormc-handouts/latest/index.json",
|
|
)
|
|
.await;
|
|
|
|
let res = match res {
|
|
Ok(x) => x,
|
|
Err(err) => {
|
|
warn!("Error while getting index: {err:?}");
|
|
return Err(err);
|
|
}
|
|
};
|
|
|
|
let mut res: Vec<HandoutEntry> = res.json().await?;
|
|
res.sort_by_key(|x| x.title.clone());
|
|
|
|
debug!(
|
|
message = "Fetched handout index",
|
|
n_handouts = res.len(),
|
|
time_ms = start.elapsed().as_millis()
|
|
);
|
|
return Ok(res);
|
|
}
|
|
|
|
fn build_list_for_group(
|
|
handouts: &[HandoutEntry],
|
|
group: &str,
|
|
req_ctx: &RequestContext,
|
|
) -> Markup {
|
|
let mobile = req_ctx.client_info.device_type == DeviceType::Mobile;
|
|
|
|
if mobile {
|
|
html! {
|
|
ul class="handout-ul" {
|
|
|
|
@for h in handouts {
|
|
@if h.group ==group {
|
|
li {
|
|
span class="handout-li-title" {
|
|
a href=(h.handout) class="underline-link" {
|
|
strong style="text-decoration: underline;text-underline-offset:1.5pt;color:var(--fgColor);" { (h.title) }
|
|
}
|
|
}
|
|
|
|
@if let Some(solutions) = &h.solutions {
|
|
" ["
|
|
a href=(solutions) { "sols" }
|
|
"]"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
html! {
|
|
ul class="handout-ul" {
|
|
|
|
@for h in handouts {
|
|
@if h.group ==group {
|
|
li {
|
|
span class="handout-li-title" {
|
|
strong { (h.title) }
|
|
}
|
|
" "
|
|
span class="handout-li-links" {
|
|
"[ "
|
|
|
|
@if let Some(solutions) = &h.solutions {
|
|
a href=(h.handout) {"handout"}
|
|
" | "
|
|
a href=(solutions) {"solutions"}
|
|
} @else {
|
|
a href=(h.handout) {"handout"}
|
|
}
|
|
" ]"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// MARK: page
|
|
//
|
|
|
|
pub fn handouts() -> Page {
|
|
let md = Markdown::parse(include_str!("handouts.md"));
|
|
|
|
let index = CachedRequest::new(
|
|
TimeDelta::minutes(30),
|
|
Box::new(|| Box::pin(async move { get_index().await })),
|
|
);
|
|
|
|
tokio::spawn(index.clone().autoget(Duration::from_secs(60 * 20)));
|
|
|
|
#[expect(clippy::unwrap_used)]
|
|
let mut meta = meta_from_markdown(&md).unwrap().unwrap();
|
|
|
|
if meta.image.is_none() {
|
|
meta.image = Some(Image_Icon::URL.to_owned());
|
|
}
|
|
|
|
let html = PreEscaped(md.render());
|
|
|
|
Page {
|
|
meta,
|
|
html_ttl: Some(TimeDelta::seconds(300)),
|
|
|
|
generate_html: Box::new(move |page, req_ctx| {
|
|
let html = html.clone(); // TODO: find a way to not clone here
|
|
let index = index.clone();
|
|
Box::pin(async move {
|
|
let handouts = index.get().await;
|
|
|
|
let fallback = html! {
|
|
span style="color:var(--yellow)" {
|
|
"Could not load handouts, something broke."
|
|
}
|
|
" "
|
|
(
|
|
FarLink(
|
|
"https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest",
|
|
"Try this direct link."
|
|
)
|
|
)
|
|
};
|
|
|
|
let warmups = match &*handouts {
|
|
Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", req_ctx),
|
|
Err(error) => {
|
|
warn!("Could not load handout index: {error:?}");
|
|
fallback.clone()
|
|
}
|
|
};
|
|
|
|
let advanced = match &*handouts {
|
|
Ok(handouts) => build_list_for_group(handouts, "Advanced", req_ctx),
|
|
Err(_) => fallback,
|
|
};
|
|
|
|
html! {
|
|
@if let Some(slug) = &page.meta.slug {
|
|
(Backlinks(&[("/", "home")], slug))
|
|
}
|
|
|
|
(html)
|
|
|
|
(Markdown(concat!(
|
|
"## Warm-Ups",
|
|
"\n\n",
|
|
"Students never show up on time. Some come early, some come late. Warm-ups ",
|
|
"are my solution to this problem: we hand these out as students walk in, ",
|
|
"giving them something to do until we can start the lesson.",
|
|
)))
|
|
(warmups)
|
|
br {}
|
|
|
|
(Markdown(concat!(
|
|
"## Advanced",
|
|
"\n\n",
|
|
"The highest level of the ORMC, and the group I spend most of my time with. ",
|
|
"Students in ORMC Advanced are in high school, which means ",
|
|
"they're ~14-18 years old.",
|
|
)))
|
|
(advanced)
|
|
br {}
|
|
}
|
|
})
|
|
}),
|
|
}
|
|
}
|