Update
This commit is contained in:
parent
d9be1b6783
commit
c9b11aa165
12 changed files with 474 additions and 251 deletions
72
bvplan/Cargo.lock
generated
72
bvplan/Cargo.lock
generated
|
@ -183,6 +183,18 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-web-static-files"
|
||||||
|
version = "4.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adf6d1ef6d7a60e084f9e0595e2a5234abda14e76c105ecf8e2d0e8800c41a1f"
|
||||||
|
dependencies = [
|
||||||
|
"actix-web",
|
||||||
|
"derive_more",
|
||||||
|
"futures-util",
|
||||||
|
"static-files",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
|
@ -316,6 +328,17 @@ dependencies = [
|
||||||
"askama_shared",
|
"askama_shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_actix"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c52f74f8382a142ecfc052100b21abc33f2c069e20fe345808e7ed914b179449"
|
||||||
|
dependencies = [
|
||||||
|
"actix-web",
|
||||||
|
"askama",
|
||||||
|
"askama_shared",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_derive"
|
name = "askama_derive"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
@ -603,8 +626,10 @@ name = "bvplan"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"actix-web-static-files",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"askama",
|
"askama",
|
||||||
|
"askama_actix",
|
||||||
"celery",
|
"celery",
|
||||||
"chrono",
|
"chrono",
|
||||||
"diesel",
|
"diesel",
|
||||||
|
@ -620,6 +645,7 @@ dependencies = [
|
||||||
"scraper",
|
"scraper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"static-files",
|
||||||
"stdext",
|
"stdext",
|
||||||
"tikv-jemallocator",
|
"tikv-jemallocator",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -711,6 +737,16 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "change-detection"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "159fa412eae48a1d94d0b9ecdb85c97ce56eb2a347c62394d3fdbf221adabc1a"
|
||||||
|
dependencies = [
|
||||||
|
"path-matchers",
|
||||||
|
"path-slash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.23"
|
version = "0.4.23"
|
||||||
|
@ -1366,6 +1402,12 @@ version = "0.27.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
|
checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "globset"
|
name = "globset"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
|
@ -1381,9 +1423,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.15"
|
version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
|
checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
|
@ -2037,6 +2079,21 @@ version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
|
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "path-matchers"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36cd9b72a47679ec193a5f0229d9ab686b7bd45e1fbc59ccf953c9f3d83f7b2b"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "path-slash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -2836,6 +2893,17 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static-files"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64712ea1e3e140010e1d9605872ba205afa2ab5bd38191cc6ebd248ae1f6a06b"
|
||||||
|
dependencies = [
|
||||||
|
"change-detection",
|
||||||
|
"mime_guess",
|
||||||
|
"path-slash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stdext"
|
name = "stdext"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
|
@ -5,6 +5,7 @@ edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "web"
|
name = "web"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "worker"
|
name = "worker"
|
||||||
|
@ -17,8 +18,10 @@ panic = "abort"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4.2.1"
|
actix-web = "4.2.1"
|
||||||
|
actix-web-static-files = "4.0.1"
|
||||||
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
||||||
askama = { version = "0.11.1", features = ["serde-json"] }
|
askama = { version = "0.11.1", features = ["serde-json"] }
|
||||||
|
askama_actix = "0.13.0"
|
||||||
celery = { git = "https://github.com/rusty-celery/rusty-celery.git", branch = "main" }
|
celery = { git = "https://github.com/rusty-celery/rusty-celery.git", branch = "main" }
|
||||||
chrono = "0.4.23"
|
chrono = "0.4.23"
|
||||||
diesel = { version = "2.0.2", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "chrono", "r2d2"] }
|
diesel = { version = "2.0.2", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "chrono", "r2d2"] }
|
||||||
|
@ -34,6 +37,7 @@ reqwest = "0.11.13"
|
||||||
scraper = "0.14.0"
|
scraper = "0.14.0"
|
||||||
serde = "1.0.148"
|
serde = "1.0.148"
|
||||||
serde_json = "1.0.93"
|
serde_json = "1.0.93"
|
||||||
|
static-files = "0.2.3"
|
||||||
stdext = "0.3.1"
|
stdext = "0.3.1"
|
||||||
tokio = { version = "1.22.0", features = ["full"] }
|
tokio = { version = "1.22.0", features = ["full"] }
|
||||||
untis = { git = "https://git.dergrimm.net/dergrimm/untis.rs.git", branch = "main" }
|
untis = { git = "https://git.dergrimm.net/dergrimm/untis.rs.git", branch = "main" }
|
||||||
|
@ -41,3 +45,6 @@ url = "2.3.1"
|
||||||
|
|
||||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||||
tikv-jemallocator = "0.5"
|
tikv-jemallocator = "0.5"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
static-files = "0.2.3"
|
||||||
|
|
|
@ -1,3 +1,15 @@
|
||||||
|
FROM codycraven/sassc:latest as css
|
||||||
|
WORKDIR /usr/src/scss
|
||||||
|
RUN mkdir dist
|
||||||
|
WORKDIR /usr/src/scss/src
|
||||||
|
COPY ./scss .
|
||||||
|
RUN find . -name "*.scss" -type f | xargs -I % sh -c 'sassc % > ../dist/$(basename -- "%" .scss).css'
|
||||||
|
|
||||||
|
FROM tdewolff/minify:latest as static
|
||||||
|
WORKDIR /usr/src/static
|
||||||
|
COPY --from=css /usr/src/scss/dist ./css
|
||||||
|
RUN minify . -r -o .
|
||||||
|
|
||||||
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.65.0 as chef
|
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.65.0 as chef
|
||||||
|
|
||||||
FROM chef as diesel
|
FROM chef as diesel
|
||||||
|
@ -13,6 +25,9 @@ FROM chef as builder
|
||||||
WORKDIR /usr/src/backend
|
WORKDIR /usr/src/backend
|
||||||
COPY --from=planner /usr/src/backend/recipe.json .
|
COPY --from=planner /usr/src/backend/recipe.json .
|
||||||
RUN cargo chef cook --recipe-path recipe.json
|
RUN cargo chef cook --recipe-path recipe.json
|
||||||
|
RUN rm -rf ./src
|
||||||
|
COPY ./build.rs .
|
||||||
|
COPY --from=static /usr/src/static ./static
|
||||||
COPY ./templates ./templates
|
COPY ./templates ./templates
|
||||||
COPY ./src ./src
|
COPY ./src ./src
|
||||||
RUN cargo build
|
RUN cargo build
|
||||||
|
|
5
bvplan/build.rs
Normal file
5
bvplan/build.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use static_files::resource_dir;
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
resource_dir("./static").build()
|
||||||
|
}
|
102
bvplan/scss/bvplan.scss
Normal file
102
bvplan/scss/bvplan.scss
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0 5vw;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info {
|
||||||
|
margin-top: 2vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > .column {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#title-wrapper {
|
||||||
|
color: #ee7f00;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tenant-info {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#plan {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
caption {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: thin solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
&:not(:nth-child(even)) {
|
||||||
|
background-color: #fad3a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: #fdecd9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
margin: auto 0 2vh;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: thin solid black;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,14 +5,28 @@ use tikv_jemallocator::Jemalloc;
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: Jemalloc = Jemalloc;
|
static GLOBAL: Jemalloc = Jemalloc;
|
||||||
|
|
||||||
use actix_web::{get, middleware, web::Data, App, HttpResponse, HttpServer};
|
use actix_web::{get, http, middleware, web, App, HttpResponse, HttpServer};
|
||||||
|
use actix_web_static_files::ResourceFiles;
|
||||||
use r2d2_redis::redis;
|
use r2d2_redis::redis;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
use bvplan::*;
|
use bvplan::*;
|
||||||
|
use templates::TemplateToResponseWithStatusCode;
|
||||||
|
|
||||||
|
pub mod static_dir {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn not_found() -> HttpResponse {
|
||||||
|
templates::StatusCode {
|
||||||
|
status_code: http::StatusCode::NOT_FOUND,
|
||||||
|
message: None,
|
||||||
|
}
|
||||||
|
.to_response_with_status_code(http::StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index(redis_pool: Data<cache::RedisPool>) -> HttpResponse {
|
async fn index(redis_pool: web::Data<cache::RedisPool>) -> HttpResponse {
|
||||||
let redis_conn = &mut match redis_pool.get() {
|
let redis_conn = &mut match redis_pool.get() {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||||
|
@ -37,12 +51,19 @@ async fn index(redis_pool: Data<cache::RedisPool>) -> HttpResponse {
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let server = HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
|
let generated = static_dir::generate();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(Data::new(cache::pool().unwrap()))
|
.app_data(web::Data::new(cache::pool().unwrap()))
|
||||||
.wrap(middleware::Compress::default())
|
.wrap(middleware::Compress::default())
|
||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
.service(index)
|
.service(index)
|
||||||
});
|
.service(ResourceFiles::new("/static", generated))
|
||||||
server.bind("0.0.0.0:80").unwrap().run().await
|
.default_service(web::route().to(not_found))
|
||||||
|
})
|
||||||
|
.bind("0.0.0.0:80")
|
||||||
|
.unwrap()
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,16 +37,16 @@ pub mod keys {
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
pub struct Substitution {
|
pub struct Substitution {
|
||||||
#[serde(rename = "tm")]
|
#[serde(rename = "p")]
|
||||||
pub time: (usize, Option<usize>),
|
pub period: usize,
|
||||||
#[serde(rename = "cl")]
|
#[serde(rename = "cl")]
|
||||||
pub classes: Vec<String>,
|
pub classes: Vec<String>,
|
||||||
#[serde(rename = "ps")]
|
#[serde(rename = "ps")]
|
||||||
pub prev_subject: String,
|
pub prev_subject: Option<String>,
|
||||||
#[serde(rename = "s")]
|
#[serde(rename = "s")]
|
||||||
pub subject: Option<String>,
|
pub subject: Option<String>,
|
||||||
#[serde(rename = "t")]
|
#[serde(rename = "t")]
|
||||||
pub teachers: Vec<String>,
|
pub teachers: Option<Vec<String>>,
|
||||||
#[serde(rename = "pr")]
|
#[serde(rename = "pr")]
|
||||||
pub prev_room: Option<String>,
|
pub prev_room: Option<String>,
|
||||||
#[serde(rename = "r")]
|
#[serde(rename = "r")]
|
||||||
|
|
|
@ -1,9 +1,51 @@
|
||||||
|
use actix_web::{body::BoxBody, http, HttpResponse, HttpResponseBuilder, ResponseError};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
use crate::cache;
|
use crate::cache;
|
||||||
|
|
||||||
|
struct ActixError(askama::Error);
|
||||||
|
|
||||||
|
impl fmt::Debug for ActixError {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
<askama::Error as fmt::Debug>::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ActixError {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
<askama::Error as fmt::Display>::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for ActixError {}
|
||||||
|
|
||||||
|
pub trait TemplateToResponseWithStatusCode {
|
||||||
|
fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse<BoxBody>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: askama_actix::Template> TemplateToResponseWithStatusCode for T {
|
||||||
|
fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse<BoxBody> {
|
||||||
|
match self.render() {
|
||||||
|
Ok(buffer) => HttpResponseBuilder::new(code)
|
||||||
|
.content_type(http::header::HeaderValue::from_static(T::MIME_TYPE))
|
||||||
|
.body(buffer),
|
||||||
|
Err(err) => HttpResponse::from_error(ActixError(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "bvplan.html")]
|
#[template(path = "bvplan.html")]
|
||||||
pub struct BVplan {
|
pub struct BVplan {
|
||||||
pub data: cache::keys::substitutions::SubstitutionQuery,
|
pub data: cache::keys::substitutions::SubstitutionQuery,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "status_code.html")]
|
||||||
|
pub struct StatusCode {
|
||||||
|
pub status_code: http::StatusCode,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
|
@ -250,6 +250,7 @@ fn get_period(times: &Vec<StartEndTime>, start: bool, time: NaiveTime) -> Option
|
||||||
fn cache_substitutions(
|
fn cache_substitutions(
|
||||||
db_conn: &mut PgConnection,
|
db_conn: &mut PgConnection,
|
||||||
redis_conn: &mut cache::Connection,
|
redis_conn: &mut cache::Connection,
|
||||||
|
schoolyear_id: i32,
|
||||||
substitution_query_id: i64,
|
substitution_query_id: i64,
|
||||||
last_import_time: NaiveDateTime,
|
last_import_time: NaiveDateTime,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
@ -287,6 +288,147 @@ fn cache_substitutions(
|
||||||
.order(db::schema::timegrid_time_unit::start_time.asc())
|
.order(db::schema::timegrid_time_unit::start_time.asc())
|
||||||
.load::<StartEndTime>(db_conn)?;
|
.load::<StartEndTime>(db_conn)?;
|
||||||
|
|
||||||
|
let mut substitutions = db::schema::substitutions::table
|
||||||
|
.filter(db::schema::substitutions::substitution_query_id.eq(substitution_query_id))
|
||||||
|
.load::<db::models::Substitution>(db_conn)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
if let Some(subst_subject) = db::schema::substitution_subjects::table
|
||||||
|
.filter(db::schema::substitution_subjects::substitution_id.eq(s.id))
|
||||||
|
.order(db::schema::substitution_subjects::position.asc())
|
||||||
|
.first::<db::models::SubstitutionSubject>(db_conn)
|
||||||
|
.optional()?
|
||||||
|
{
|
||||||
|
let subst_class_ids = db::schema::substitution_classes::table
|
||||||
|
.select(db::schema::substitution_classes::class_id)
|
||||||
|
.filter(db::schema::substitution_classes::substitution_id.eq(s.id))
|
||||||
|
.order(db::schema::substitution_classes::position.asc())
|
||||||
|
.load::<i32>(db_conn)?;
|
||||||
|
let classes = db::schema::classes::table
|
||||||
|
.select(db::schema::classes::name)
|
||||||
|
.filter(db::schema::classes::id.eq_any(subst_class_ids))
|
||||||
|
.load::<String>(db_conn)?;
|
||||||
|
|
||||||
|
let subst_teachers = db::schema::substitution_teachers::table
|
||||||
|
.filter(db::schema::substitution_teachers::substitution_id.eq(s.id))
|
||||||
|
.order(db::schema::substitution_teachers::position.asc())
|
||||||
|
.load::<db::models::SubstitutionTeacher>(db_conn)?;
|
||||||
|
|
||||||
|
let (prev_room, room) = if let Some(r) = db::schema::substitution_rooms::table
|
||||||
|
.filter(db::schema::substitution_rooms::substitution_id.eq(s.id))
|
||||||
|
.order(db::schema::substitution_rooms::position.asc())
|
||||||
|
.first::<db::models::SubstitutionRoom>(db_conn)
|
||||||
|
.optional()?
|
||||||
|
{
|
||||||
|
let name = if let Some(id) = r.room_id {
|
||||||
|
Some(
|
||||||
|
db::schema::rooms::table
|
||||||
|
.select(db::schema::rooms::name)
|
||||||
|
.filter(db::schema::rooms::id.eq(id))
|
||||||
|
.first::<String>(db_conn)?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match s.subst_type {
|
||||||
|
db::models::SubstitutionType::Cancel => (name, None),
|
||||||
|
_ => (
|
||||||
|
if let Some(id) = r.original_id {
|
||||||
|
Some(
|
||||||
|
db::schema::rooms::table
|
||||||
|
.select(db::schema::rooms::name)
|
||||||
|
.filter(db::schema::rooms::id.eq(id))
|
||||||
|
.first::<String>(db_conn)?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (prev_subject, subject) = {
|
||||||
|
let name = db::schema::subjects::table
|
||||||
|
.select(db::schema::subjects::name)
|
||||||
|
.filter(db::schema::subjects::id.eq(subst_subject.subject_id))
|
||||||
|
.first::<String>(db_conn)?;
|
||||||
|
|
||||||
|
match s.subst_type {
|
||||||
|
db::models::SubstitutionType::Cancel => (Some(name), None),
|
||||||
|
_ => {
|
||||||
|
if let Some(prev_id) = subst_subject.original_id {
|
||||||
|
(
|
||||||
|
Some(
|
||||||
|
db::schema::subjects::table
|
||||||
|
.select(db::schema::subjects::name)
|
||||||
|
.filter(db::schema::subjects::id.eq(prev_id))
|
||||||
|
.first::<String>(db_conn)?,
|
||||||
|
),
|
||||||
|
Some(name),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(Some(name.to_owned()), Some(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(cache::keys::substitutions::Substitution {
|
||||||
|
period: get_period(×, true, s.start_time)
|
||||||
|
.context("Could not find period from start time")?,
|
||||||
|
classes,
|
||||||
|
prev_subject,
|
||||||
|
subject,
|
||||||
|
teachers: match s.subst_type {
|
||||||
|
db::models::SubstitutionType::Cancel => None,
|
||||||
|
_ => {
|
||||||
|
let x = db::schema::teachers::table
|
||||||
|
.select(db::schema::teachers::display_name)
|
||||||
|
.filter(
|
||||||
|
db::schema::teachers::id.eq_any(
|
||||||
|
subst_teachers
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| t.teacher_id)
|
||||||
|
.collect::<Vec<i32>>(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.load::<String>(db_conn)?;
|
||||||
|
|
||||||
|
if x.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prev_room,
|
||||||
|
room,
|
||||||
|
text: s.text,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| s)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
substitutions.sort_by_key(|x| {
|
||||||
|
(
|
||||||
|
x.classes.get(0).and_then(|x| {
|
||||||
|
x.split_once(' ')
|
||||||
|
.and_then(|y| y.0.parse::<u32>().map_or(None, |s| Some(s)))
|
||||||
|
}),
|
||||||
|
x.period,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let query = cache::keys::substitutions::SubstitutionQuery {
|
let query = cache::keys::substitutions::SubstitutionQuery {
|
||||||
date: format!(
|
date: format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
|
@ -304,123 +446,15 @@ fn cache_substitutions(
|
||||||
week_type,
|
week_type,
|
||||||
queried_at: queried_at.format("%d.%m.%Y %R").to_string(),
|
queried_at: queried_at.format("%d.%m.%Y %R").to_string(),
|
||||||
last_import_time: last_import_time.format("%d.%m.%Y %R").to_string(),
|
last_import_time: last_import_time.format("%d.%m.%Y %R").to_string(),
|
||||||
schoolyear: "schoolyear".to_string(),
|
schoolyear: db::schema::schoolyears::table
|
||||||
tenant: "OHG Furtwangen".to_string(),
|
.select(db::schema::schoolyears::name)
|
||||||
substitutions: db::schema::substitutions::table
|
.filter(db::schema::schoolyears::id.eq(schoolyear_id))
|
||||||
.filter(db::schema::substitutions::substitution_query_id.eq(substitution_query_id))
|
.first::<String>(db_conn)?,
|
||||||
.load::<db::models::Substitution>(db_conn)
|
tenant: db::schema::tenants::table
|
||||||
.unwrap()
|
.select(db::schema::tenants::name)
|
||||||
.into_iter()
|
.filter(db::schema::tenants::active)
|
||||||
.map(|s| {
|
.first::<String>(db_conn)?,
|
||||||
if let Some(subst_subject) = db::schema::substitution_subjects::table
|
substitutions,
|
||||||
.filter(db::schema::substitution_subjects::substitution_id.eq(s.id))
|
|
||||||
.order(db::schema::substitution_subjects::position.asc())
|
|
||||||
.first::<db::models::SubstitutionSubject>(db_conn)
|
|
||||||
.optional()?
|
|
||||||
{
|
|
||||||
let subst_class_ids = db::schema::substitution_classes::table
|
|
||||||
.select(db::schema::substitution_classes::class_id)
|
|
||||||
.filter(db::schema::substitution_classes::substitution_id.eq(s.id))
|
|
||||||
.order(db::schema::substitution_classes::position.asc())
|
|
||||||
.load::<i32>(db_conn)?;
|
|
||||||
let classes = db::schema::classes::table
|
|
||||||
.select(db::schema::classes::name)
|
|
||||||
.filter(db::schema::classes::id.eq_any(subst_class_ids))
|
|
||||||
.load::<String>(db_conn)?;
|
|
||||||
|
|
||||||
let subst_teachers = db::schema::substitution_teachers::table
|
|
||||||
.filter(db::schema::substitution_teachers::substitution_id.eq(s.id))
|
|
||||||
.order(db::schema::substitution_teachers::position.asc())
|
|
||||||
.load::<db::models::SubstitutionTeacher>(db_conn)?;
|
|
||||||
|
|
||||||
let (prev_room, room) = if let Some(r) = db::schema::substitution_rooms::table
|
|
||||||
.filter(db::schema::substitution_rooms::substitution_id.eq(s.id))
|
|
||||||
.order(db::schema::substitution_rooms::position.asc())
|
|
||||||
.first::<db::models::SubstitutionRoom>(db_conn)
|
|
||||||
.optional()?
|
|
||||||
{
|
|
||||||
let name = if let Some(id) = r.room_id {
|
|
||||||
Some(
|
|
||||||
db::schema::rooms::table
|
|
||||||
.select(db::schema::rooms::name)
|
|
||||||
.filter(db::schema::rooms::id.eq(id))
|
|
||||||
.first::<String>(db_conn)?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
match s.subst_type {
|
|
||||||
db::models::SubstitutionType::Cancel => (name, None),
|
|
||||||
_ => (
|
|
||||||
if let Some(id) = r.original_id {
|
|
||||||
Some(
|
|
||||||
db::schema::rooms::table
|
|
||||||
.select(db::schema::rooms::name)
|
|
||||||
.filter(db::schema::rooms::id.eq(id))
|
|
||||||
.first::<String>(db_conn)?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
name,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let prev_subject = db::schema::subjects::table
|
|
||||||
.select(db::schema::subjects::name)
|
|
||||||
.filter(db::schema::subjects::id.eq(subst_subject.subject_id))
|
|
||||||
.first::<String>(db_conn)?;
|
|
||||||
|
|
||||||
let start_period = get_period(×, true, s.start_time)
|
|
||||||
.context("Could not find period from start time")?;
|
|
||||||
|
|
||||||
Ok(Some(cache::keys::substitutions::Substitution {
|
|
||||||
time: (start_period, None),
|
|
||||||
classes,
|
|
||||||
prev_subject: prev_subject.to_owned(),
|
|
||||||
subject: match s.subst_type {
|
|
||||||
db::models::SubstitutionType::Cancel => None,
|
|
||||||
_ => match subst_subject.original_id {
|
|
||||||
Some(id) => Some(
|
|
||||||
db::schema::subjects::table
|
|
||||||
.select(db::schema::subjects::name)
|
|
||||||
.filter(db::schema::subjects::id.eq(id))
|
|
||||||
.first::<String>(db_conn)?,
|
|
||||||
),
|
|
||||||
None => Some(prev_subject),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teachers: match s.subst_type {
|
|
||||||
db::models::SubstitutionType::Cancel => vec![],
|
|
||||||
_ => db::schema::teachers::table
|
|
||||||
.select(db::schema::teachers::display_name)
|
|
||||||
.filter(
|
|
||||||
db::schema::teachers::id.eq_any(
|
|
||||||
subst_teachers
|
|
||||||
.iter()
|
|
||||||
.filter_map(|t| t.teacher_id)
|
|
||||||
.collect::<Vec<i32>>(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.load::<String>(db_conn)?,
|
|
||||||
},
|
|
||||||
prev_room,
|
|
||||||
room,
|
|
||||||
text: s.text,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>>>()
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|s| s)
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::write(
|
fs::write(
|
||||||
|
@ -497,9 +531,13 @@ pub async fn get_substitutions() -> TaskResult<()> {
|
||||||
}
|
}
|
||||||
// thread::sleep(dur);
|
// thread::sleep(dur);
|
||||||
|
|
||||||
if let Err(e) =
|
if let Err(e) = cache_substitutions(
|
||||||
cache_substitutions(db_conn, redis_conn, substitution_query_id, last_import_time)
|
db_conn,
|
||||||
{
|
redis_conn,
|
||||||
|
schoolyear_id,
|
||||||
|
substitution_query_id,
|
||||||
|
last_import_time,
|
||||||
|
) {
|
||||||
return Err(TaskError::UnexpectedError(format!("{:?}", e)));
|
return Err(TaskError::UnexpectedError(format!("{:?}", e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
0
bvplan/static/.keep
Normal file
0
bvplan/static/.keep
Normal file
|
@ -4,9 +4,10 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="generator" content="BVplan" />
|
||||||
<title>BVplan | dergrimm.net</title>
|
<title>BVplan | dergrimm.net</title>
|
||||||
|
|
||||||
<meta name="generator" content="BVplan" />
|
<link rel="stylesheet" type="text/css" href="/static/css/bvplan.css" />
|
||||||
|
|
||||||
<script id="data-element-count" type="application/json">{{ data.substitutions.len()|json|safe }}</script>
|
<script id="data-element-count" type="application/json">{{ data.substitutions.len()|json|safe }}</script>
|
||||||
|
|
||||||
|
@ -16,114 +17,6 @@
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
></script>
|
></script>
|
||||||
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100%;
|
|
||||||
margin: 0 5vw;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#info {
|
|
||||||
margin-top: 1vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
#info > .column {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#title-wrapper {
|
|
||||||
color: #ee7f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
#title-wrapper > * {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tenant-info {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
text-align: center;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan thead,
|
|
||||||
#plan tbody {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan > caption {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan th,
|
|
||||||
#plan td {
|
|
||||||
border: thin solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan thead {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan th {
|
|
||||||
padding: 0.5rem 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan td {
|
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan tbody {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan tbody tr:not(:nth-child(even)) {
|
|
||||||
background-color: #fad3a6;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plan tbody tr:nth-child(even) {
|
|
||||||
background-color: #fdecd9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer {
|
|
||||||
margin: auto 0 1vh;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer p {
|
|
||||||
margin: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer > hr {
|
|
||||||
border: thin solid black;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer .message,
|
|
||||||
#footer .powered-by {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header id="info">
|
<header id="info">
|
||||||
|
@ -173,16 +66,16 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for subst in data.substitutions %}
|
{% for subst in data.substitutions %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>{{ subst.period }}</td>
|
||||||
|
<td>{{ subst.classes|join(", ") }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% match subst.time.1 %}
|
{% match subst.prev_subject %}
|
||||||
{% when Some with (x) %}
|
{% when Some with (x) %}
|
||||||
{{ subst.time.0 }} - {{ x }}
|
{{ x }}
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{{ subst.time.0 }}
|
---
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ subst.classes|join(", ") }}</td>
|
|
||||||
<td>{{ subst.prev_subject }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% match subst.subject %}
|
{% match subst.subject %}
|
||||||
{% when Some with (x) %}
|
{% when Some with (x) %}
|
||||||
|
@ -191,7 +84,14 @@
|
||||||
---
|
---
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ subst.teachers|join(", ") }}</td>
|
<td>
|
||||||
|
{% match subst.teachers %}
|
||||||
|
{% when Some with (x) %}
|
||||||
|
{{ x|join(", ") }}
|
||||||
|
{% when None %}
|
||||||
|
---
|
||||||
|
{% endmatch %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% match subst.prev_room %}
|
{% match subst.prev_room %}
|
||||||
{% when Some with (x) %}
|
{% when Some with (x) %}
|
||||||
|
@ -222,7 +122,7 @@
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer id="footer">
|
<footer id="footer">
|
||||||
<div class="message">
|
<div class="element">
|
||||||
<p>
|
<p>
|
||||||
BVplan - der bessre Vertretungsplan sogar mit UTF-8 Support!
|
BVplan - der bessre Vertretungsplan sogar mit UTF-8 Support!
|
||||||
Wow
|
Wow
|
||||||
|
@ -234,12 +134,12 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<p class="powered-by">
|
<p class="element">
|
||||||
<code>
|
<code>
|
||||||
Powered by Dominic Grimm <<a
|
Powered by Dominic Grimm <<a
|
||||||
href="mailto:dominic@dergrimm.net"
|
href="mailto:dominic@dergrimm.net"
|
||||||
>dominic@dergrimm.net</a
|
>dominic@dergrimm.net</a
|
||||||
>>
|
>>, Untis sucks
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -284,7 +184,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
setTimeout($(window).height() > $(window).height() ? scrollDown : reload, waitDelay);
|
setTimeout($(document).height() > $(window).height() ? scrollDown : reload, waitDelay);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
25
bvplan/templates/status_code.html
Normal file
25
bvplan/templates/status_code.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de-DE">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="generator" content="BVplan" />
|
||||||
|
<title>BVplan | dergrimm.net</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ status_code }}</h1>
|
||||||
|
{% match message %}
|
||||||
|
{% when Some with (x) %}
|
||||||
|
<hr />
|
||||||
|
<p>{{ x }}</p>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue