This commit is contained in:
Dominic Grimm 2023-03-03 17:13:19 +01:00
parent 73fdcb4bd2
commit e00f809d00
No known key found for this signature in database
GPG key ID: 6F294212DEAAC530
13 changed files with 416 additions and 237 deletions

49
bvplan/Cargo.lock generated
View file

@ -21,15 +21,15 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.3.0" version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0070905b2c4a98d184c4e81025253cb192aa8a73827553f38e9410801ceb35bb" checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"ahash", "ahash 0.8.3",
"base64", "base64",
"bitflags", "bitflags",
"brotli", "brotli",
@ -145,7 +145,7 @@ dependencies = [
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"actix-web-codegen", "actix-web-codegen",
"ahash", "ahash 0.7.6",
"bytes", "bytes",
"bytestring", "bytestring",
"cfg-if", "cfg-if",
@ -221,6 +221,18 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom 0.2.8",
"once_cell",
"version_check",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.20" version = "0.7.20"
@ -450,12 +462,11 @@ dependencies = [
[[package]] [[package]]
name = "async-lock" name = "async-lock"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
dependencies = [ dependencies = [
"event-listener", "event-listener",
"futures-lite",
] ]
[[package]] [[package]]
@ -636,7 +647,6 @@ dependencies = [
"env_logger", "env_logger",
"envconfig", "envconfig",
"fancy-regex", "fancy-regex",
"flate2",
"lazy_static", "lazy_static",
"log", "log",
"minify-html", "minify-html",
@ -644,7 +654,6 @@ dependencies = [
"reqwest", "reqwest",
"scraper", "scraper",
"serde", "serde",
"serde_json",
"static-files", "static-files",
"stdext", "stdext",
"tikv-jemallocator", "tikv-jemallocator",
@ -694,8 +703,8 @@ dependencies = [
[[package]] [[package]]
name = "celery" name = "celery"
version = "0.5.2" version = "0.5.3"
source = "git+https://github.com/rusty-celery/rusty-celery.git?branch=main#6a71f338dc6decdb647ebfa48628444e1cfc6582" source = "git+https://github.com/rusty-celery/rusty-celery.git?branch=main#32cb1c97d3bf8b57a1f4a501ec50024fbb0cea7f"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
@ -723,8 +732,8 @@ dependencies = [
[[package]] [[package]]
name = "celery-codegen" name = "celery-codegen"
version = "0.5.2" version = "0.5.3"
source = "git+https://github.com/rusty-celery/rusty-celery.git?branch=main#6a71f338dc6decdb647ebfa48628444e1cfc6582" source = "git+https://github.com/rusty-celery/rusty-celery.git?branch=main#32cb1c97d3bf8b57a1f4a501ec50024fbb0cea7f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -876,9 +885,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.14" version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -1674,9 +1683,9 @@ checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.25" version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -3099,9 +3108,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.25.0" version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -3114,7 +3123,7 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.42.0", "windows-sys 0.45.0",
] ]
[[package]] [[package]]

View file

@ -28,7 +28,6 @@ diesel = { version = "2.0.2", features = ["i-implement-a-third-party-backend-and
env_logger = "0.9.3" env_logger = "0.9.3"
envconfig = "0.10.0" envconfig = "0.10.0"
fancy-regex = "0.11.0" fancy-regex = "0.11.0"
flate2 = "1.0.25"
lazy_static = "1.4.0" lazy_static = "1.4.0"
log = "0.4.17" log = "0.4.17"
minify-html = "0.10.8" minify-html = "0.10.8"
@ -36,7 +35,6 @@ r2d2_redis = "0.14.0"
reqwest = "0.11.13" 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"
static-files = "0.2.3" 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"] }

View file

@ -38,7 +38,3 @@ function scrollDown() {
} }
window.scrollTo(0, 0); window.scrollTo(0, 0);
setTimeout(
$(document).height() > $(window).height() ? scrollDown : reload,
waitDelay
);

View file

@ -7,10 +7,13 @@ body {
body { body {
min-height: 100%; min-height: 100%;
margin: 0 5vw; margin: 0 5vw;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
&.display {
overflow: hidden;
}
} }
#info { #info {
@ -84,7 +87,8 @@ body {
} }
#footer { #footer {
margin: auto 0 2vh; margin-top: auto;
padding-bottom: 2vh;
text-align: center; text-align: center;
p { p {

View file

@ -38,7 +38,45 @@ async fn index(redis_pool: web::Data<cache::RedisPool>) -> HttpResponse {
}; };
if let Some(html) = match redis::cmd("GET") if let Some(html) = match redis::cmd("GET")
.arg(cache::keys::SUBSTITUTIONS_HTML) .arg(cache::keys::HTML)
.query::<Option<String>>(redis_conn.deref_mut())
{
Ok(x) => x,
Err(_) => {
return templates::StatusCode {
status_code: http::StatusCode::INTERNAL_SERVER_ERROR,
message: None,
}
.to_response()
}
} {
HttpResponse::Accepted()
.content_type("text/html; charset=utf-8")
.body(html)
} else {
templates::StatusCode {
status_code: http::StatusCode::INTERNAL_SERVER_ERROR,
message: None,
}
.to_response()
}
}
#[get("/display")]
async fn display(redis_pool: web::Data<cache::RedisPool>) -> HttpResponse {
let redis_conn = &mut match redis_pool.get() {
Ok(x) => x,
Err(_) => {
return templates::StatusCode {
status_code: http::StatusCode::INTERNAL_SERVER_ERROR,
message: None,
}
.to_response()
}
};
if let Some(html) = match redis::cmd("GET")
.arg(cache::keys::DISPLAY_HTML)
.query::<Option<String>>(redis_conn.deref_mut()) .query::<Option<String>>(redis_conn.deref_mut())
{ {
Ok(x) => x, Ok(x) => x,
@ -74,6 +112,7 @@ async fn main() -> std::io::Result<()> {
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(index) .service(index)
.service(display)
.service(ResourceFiles::new("/static", generated)) .service(ResourceFiles::new("/static", generated))
.default_service(web::route().to(not_found)) .default_service(web::route().to(not_found))
}) })

View file

@ -4,7 +4,6 @@ use lazy_static::lazy_static;
use r2d2_redis::{r2d2, redis, RedisConnectionManager}; use r2d2_redis::{r2d2, redis, RedisConnectionManager};
use crate::config; use crate::config;
use crate::db;
pub type RedisPool = r2d2::Pool<RedisConnectionManager>; pub type RedisPool = r2d2::Pool<RedisConnectionManager>;
pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>; pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>;
@ -25,52 +24,7 @@ lazy_static! {
} }
pub mod keys { pub mod keys {
use super::*;
// pub const SUBSTITUTIONS: &str = "substs"; pub const HTML: &str = "html";
pub const SUBSTITUTIONS_HTML: &str = "substs_html"; pub const DISPLAY_HTML: &str = "disp_html";
pub mod substitutions {
use serde::{Deserialize, Serialize};
use super::*;
#[derive(Deserialize, Serialize, Debug)]
pub struct Substitution {
#[serde(rename = "p")]
pub period: usize,
#[serde(rename = "cl")]
pub classes: Vec<String>,
#[serde(rename = "ps")]
pub prev_subject: Option<String>,
#[serde(rename = "s")]
pub subject: Option<String>,
#[serde(rename = "t")]
pub teachers: Option<Vec<String>>,
#[serde(rename = "pr")]
pub prev_rooms: Vec<String>,
#[serde(rename = "r")]
pub rooms: Vec<String>,
#[serde(rename = "tx")]
pub text: Option<String>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct SubstitutionQuery {
#[serde(rename = "d")]
pub date: String,
#[serde(rename = "w")]
pub week_type: db::models::WeekType,
#[serde(rename = "q")]
pub queried_at: String,
#[serde(rename = "i")]
pub last_import_time: String,
#[serde(rename = "y")]
pub schoolyear: String,
#[serde(rename = "t")]
pub tenant: String,
#[serde(rename = "s")]
pub substitutions: Vec<Substitution>,
}
}
} }

View file

@ -2,8 +2,6 @@ use actix_web::{body::BoxBody, http, HttpResponse, HttpResponseBuilder, Response
use askama::Template; use askama::Template;
use std::fmt; use std::fmt;
use crate::cache;
struct ActixError(askama::Error); struct ActixError(askama::Error);
impl fmt::Debug for ActixError { impl fmt::Debug for ActixError {
@ -37,10 +35,43 @@ impl<T: askama_actix::Template> TemplateToResponseWithStatusCode for T {
} }
} }
pub mod substitutions {
use crate::db;
#[derive(Clone, Debug)]
pub struct Substitution {
pub period: usize,
pub classes: Vec<String>,
pub prev_subject: Option<String>,
pub subject: Option<String>,
pub teachers: Option<Vec<String>>,
pub prev_rooms: Vec<String>,
pub rooms: Vec<String>,
pub text: Option<String>,
}
#[derive(Clone, Debug)]
pub struct SubstitutionQuery {
pub date: String,
pub week_type: db::models::WeekType,
pub queried_at: String,
pub last_import_time: String,
pub schoolyear: String,
pub tenant: String,
pub substitutions: Vec<Substitution>,
}
}
#[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: substitutions::SubstitutionQuery,
}
#[derive(Template)]
#[template(path = "bvplan_display.html")]
pub struct BVplanDisplay {
pub data: substitutions::SubstitutionQuery,
} }
#[derive(Template)] #[derive(Template)]

View file

@ -7,9 +7,6 @@ use diesel::prelude::*;
use fancy_regex::Regex; use fancy_regex::Regex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use r2d2_redis::redis; use r2d2_redis::redis;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::vec; use std::vec;
@ -371,7 +368,7 @@ fn cache_substitutions(
} }
}; };
Ok(Some(cache::keys::substitutions::Substitution { Ok(Some(templates::substitutions::Substitution {
period: get_period(&times, true, s.start_time) period: get_period(&times, true, s.start_time)
.context("Could not find period from start time")?, .context("Could not find period from start time")?,
classes, classes,
@ -427,7 +424,7 @@ fn cache_substitutions(
) )
}); });
let query = cache::keys::substitutions::SubstitutionQuery { let query = templates::substitutions::SubstitutionQuery {
date: format!( date: format!(
"{} {}", "{} {}",
date.format("%d.%m.%Y"), date.format("%d.%m.%Y"),
@ -455,22 +452,30 @@ fn cache_substitutions(
substitutions, substitutions,
}; };
fs::write(
Path::new("/backups").join(format!(
"{}_{}_{}.json.gz",
date, queried_at, substitution_query_id
)),
{
let mut e = flate2::write::ZlibEncoder::new(vec![], flate2::Compression::best());
e.write_all(serde_json::to_string(&query)?.as_bytes())?;
e.finish()?
},
)?;
redis::cmd("SET") redis::cmd("SET")
.arg(cache::keys::SUBSTITUTIONS_HTML) .arg(cache::keys::HTML)
.arg(minify_html::minify( .arg(minify_html::minify(
templates::BVplan { data: query }.render()?.as_bytes(), templates::BVplan {
data: query.to_owned(),
}
.render()?
.as_bytes(),
&minify_html::Cfg {
do_not_minify_doctype: false,
ensure_spec_compliant_unquoted_attribute_values: false,
keep_spaces_between_attributes: false,
minify_css: true,
minify_js: true,
..Default::default()
},
))
.query::<()>(redis_conn)?;
redis::cmd("SET")
.arg(cache::keys::DISPLAY_HTML)
.arg(minify_html::minify(
templates::BVplanDisplay { data: query }
.render()?
.as_bytes(),
&minify_html::Cfg { &minify_html::Cfg {
do_not_minify_doctype: false, do_not_minify_doctype: false,
ensure_spec_compliant_unquoted_attribute_values: false, ensure_spec_compliant_unquoted_attribute_values: false,

View file

@ -25,7 +25,7 @@ pub async fn init() {
tasks = [ tasks = [
get_substitutions::get_substitutions, get_substitutions::get_substitutions,
update_meta::update_meta, update_meta::update_meta,
cleanup::cleanup, // cleanup::cleanup,
], ],
task_routes = [ task_routes = [
"*" => QUEUE_NAME, "*" => QUEUE_NAME,
@ -57,11 +57,11 @@ pub fn beat() -> impl std::future::Future<
schedule = DeltaSchedule::new(Duration::from_hours(6)), schedule = DeltaSchedule::new(Duration::from_hours(6)),
args = (), args = (),
}, },
"cleanup" => { // "cleanup" => {
cleanup::cleanup, // cleanup::cleanup,
schedule = DeltaSchedule::new(Duration::from_hours(1)), // schedule = DeltaSchedule::new(Duration::from_hours(1)),
args = (), // args = (),
} // }
], ],
task_routes = [ task_routes = [
"*" => QUEUE_NAME, "*" => QUEUE_NAME,

View file

@ -15,6 +15,13 @@
{% block head %}{% endblock %} {% block head %}{% endblock %}
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"
integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ+jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<!-- Matomo --> <!-- Matomo -->
<script> <script>
var _paq = window._paq = window._paq || []; var _paq = window._paq = window._paq || [];
@ -31,7 +38,5 @@
</script> </script>
<!-- End Matomo Code --> <!-- End Matomo Code -->
</head> </head>
<body>
{% block body %}{% endblock %} {% block body %}{% endblock %}
</body>
</html> </html>

View file

@ -6,16 +6,10 @@
<link rel="stylesheet" type="text/css" href="/static/css/bvplan.css" /> <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>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.3/jquery.min.js"
integrity="sha512-STof4xm1wgkfm7heWqFJVn58Hm3EtS31XFaagaa8VMReCXAkQnJZ+jEy8PCC/iT18dFy95WcExNHFTqLyp72eQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<body>
<header id="info"> <header id="info">
<div id="title-wrapper" class="column"> <div id="title-wrapper" class="column">
<h1 id="title">BVplan</h1> <h1 id="title">BVplan</h1>
@ -140,4 +134,8 @@
</footer> </footer>
<script src="/static/js/bvplan.js"></script> <script src="/static/js/bvplan.js"></script>
<script>
setTimeout(reload, waitDelay);
</script>
</body>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,144 @@
{% extends "base.html" %}
{% block title %}BVplan{% endblock %}
{% block head %}
<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>
{% endblock %}
{% block body %}
<body class="display">
<header id="info">
<div id="title-wrapper" class="column">
<h1 id="title">BVplan</h1>
<span id="subtitle">dergrimm.net</span>
</div>
<div id="tenant-info" class="column">
<span>{{ data.tenant }}, {{ data.schoolyear }}</span>
<br />
<span>Stand: {{ data.queried_at }}</span>
<br />
<span>Vertretungsplan (offline): {{ data.last_import_time }}</span>
</div>
</header>
<main>
<table id="plan">
<caption>
{{ data.date }}, Woche {{ data.week_type }}
</caption>
<colgroup>
<col />
<col />
<col />
<col style="width: 30%" />
<col />
<col />
<col />
<col style="width: 20%" />
</colgroup>
<thead>
<tr>
<th>Stunde</th>
<th>Klasse(n)</th>
<th>(Fach)</th>
<th>Fach</th>
<th>Vertreter</th>
<th>(Raum)</th>
<th>Raum</th>
<th>Text</th>
</tr>
</thead>
<tbody>
{% for subst in data.substitutions %}
<tr>
<td>{{ subst.period }}</td>
<td>{{ subst.classes|join(", ") }}</td>
<td>
{% match subst.prev_subject %}
{% when Some with (x) %}
{{ x }}
{% when None %}
---
{% endmatch %}
</td>
<td>
{% match subst.subject %}
{% when Some with (x) %}
{{ x }}
{% when None %}
---
{% endmatch %}
</td>
<td>
{% match subst.teachers %}
{% when Some with (x) %}
{{ x|join(", ") }}
{% when None %}
---
{% endmatch %}
</td>
<td>
{% if subst.prev_rooms.is_empty() %}
---
{% else %}
{{ subst.prev_rooms|join(", ") }}
{% endif %}
</td>
<td>
{% if subst.rooms.is_empty() %}
---
{% else %}
{{ subst.rooms|join(", ") }}
{% endif %}
</td>
<td>
{% match subst.text %}
{% when Some with (x) %}
{{ x }}
{% when None %}
{% endmatch %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>
<footer id="footer">
<div class="element">
<p>
BVplan - der bessere Vertretungsplan sogar mit UTF-8 Support!
Wow
</p>
<p>
<a href="https://git.dergrimm.net/dergrimm/bvplan">
https://git.dergrimm.net/dergrimm/bvplan
</a>
</p>
</div>
<hr />
<p class="element">
<code>
Powered by Dominic Grimm &lt;<a
href="mailto:dominic@dergrimm.net"
>dominic@dergrimm.net</a
>&gt;, Untis sucks
</code>
</p>
</footer>
<script src="/static/js/bvplan.js"></script>
<script>
setTimeout(
$(document).height() > $(window).height() ? scrollDown : reload,
waitDelay
);
</script>
</body>
{% endblock %}

View file

@ -24,6 +24,9 @@ x-bvplan:
BVPLAN_UNTIS_VPLAN_URL: ${BVPLAN_UNTIS_VPLAN_URL} BVPLAN_UNTIS_VPLAN_URL: ${BVPLAN_UNTIS_VPLAN_URL}
BVPLAN_UNTIS_VPLAN_USERNAME: ${BVPLAN_UNTIS_VPLAN_USERNAME} BVPLAN_UNTIS_VPLAN_USERNAME: ${BVPLAN_UNTIS_VPLAN_USERNAME}
BVPLAN_UNTIS_VPLAN_PASSWORD: ${BVPLAN_UNTIS_VPLAN_PASSWORD} BVPLAN_UNTIS_VPLAN_PASSWORD: ${BVPLAN_UNTIS_VPLAN_PASSWORD}
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
services: services:
nginx: nginx:
@ -82,10 +85,6 @@ services:
worker: worker:
<<: *bvplan <<: *bvplan
command: worker command: worker
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- ./data/backups:/backups
web: web:
<<: *bvplan <<: *bvplan
@ -95,9 +94,6 @@ services:
- rabbitmq - rabbitmq
- worker - worker
- redis - redis
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
volumes: volumes:
db: db: