diff --git a/bvplan/Cargo.lock b/bvplan/Cargo.lock index f565d9b..69f5656 100644 --- a/bvplan/Cargo.lock +++ b/bvplan/Cargo.lock @@ -183,6 +183,18 @@ dependencies = [ "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]] name = "addr2line" version = "0.19.0" @@ -316,6 +328,17 @@ dependencies = [ "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]] name = "askama_derive" version = "0.11.2" @@ -603,8 +626,10 @@ name = "bvplan" version = "0.1.0" dependencies = [ "actix-web", + "actix-web-static-files", "anyhow", "askama", + "askama_actix", "celery", "chrono", "diesel", @@ -620,6 +645,7 @@ dependencies = [ "scraper", "serde", "serde_json", + "static-files", "stdext", "tikv-jemallocator", "tokio", @@ -711,6 +737,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "chrono" version = "0.4.23" @@ -1366,6 +1402,12 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "globset" version = "0.4.10" @@ -1381,9 +1423,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" dependencies = [ "bytes", "fnv", @@ -2037,6 +2079,21 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "percent-encoding" version = "2.2.0" @@ -2836,6 +2893,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "stdext" version = "0.3.1" diff --git a/bvplan/Cargo.toml b/bvplan/Cargo.toml index 3da28c2..505522f 100644 --- a/bvplan/Cargo.toml +++ b/bvplan/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [[bin]] name = "web" +build = "build.rs" [[bin]] name = "worker" @@ -17,8 +18,10 @@ panic = "abort" [dependencies] actix-web = "4.2.1" +actix-web-static-files = "4.0.1" anyhow = { version = "1.0.66", features = ["backtrace"] } 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" } chrono = "0.4.23" 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" serde = "1.0.148" serde_json = "1.0.93" +static-files = "0.2.3" stdext = "0.3.1" tokio = { version = "1.22.0", features = ["full"] } 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] tikv-jemallocator = "0.5" + +[build-dependencies] +static-files = "0.2.3" diff --git a/bvplan/Dockerfile b/bvplan/Dockerfile index 741ff79..d88cb15 100644 --- a/bvplan/Dockerfile +++ b/bvplan/Dockerfile @@ -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 chef as diesel @@ -13,6 +25,9 @@ FROM chef as builder WORKDIR /usr/src/backend COPY --from=planner /usr/src/backend/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 ./src ./src RUN cargo build diff --git a/bvplan/build.rs b/bvplan/build.rs new file mode 100644 index 0000000..acc2dcb --- /dev/null +++ b/bvplan/build.rs @@ -0,0 +1,5 @@ +use static_files::resource_dir; + +fn main() -> std::io::Result<()> { + resource_dir("./static").build() +} diff --git a/bvplan/scss/bvplan.scss b/bvplan/scss/bvplan.scss new file mode 100644 index 0000000..0662226 --- /dev/null +++ b/bvplan/scss/bvplan.scss @@ -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; + } +} diff --git a/bvplan/src/bin/web.rs b/bvplan/src/bin/web.rs index e3f46ba..dac6453 100644 --- a/bvplan/src/bin/web.rs +++ b/bvplan/src/bin/web.rs @@ -5,14 +5,28 @@ use tikv_jemallocator::Jemalloc; #[global_allocator] 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 std::ops::DerefMut; 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("/")] -async fn index(redis_pool: Data) -> HttpResponse { +async fn index(redis_pool: web::Data) -> HttpResponse { let redis_conn = &mut match redis_pool.get() { Ok(x) => x, Err(_) => return HttpResponse::InternalServerError().finish(), @@ -37,12 +51,19 @@ async fn index(redis_pool: Data) -> HttpResponse { async fn main() -> std::io::Result<()> { env_logger::init(); - let server = HttpServer::new(move || { + HttpServer::new(move || { + let generated = static_dir::generate(); + App::new() - .app_data(Data::new(cache::pool().unwrap())) + .app_data(web::Data::new(cache::pool().unwrap())) .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) .service(index) - }); - server.bind("0.0.0.0:80").unwrap().run().await + .service(ResourceFiles::new("/static", generated)) + .default_service(web::route().to(not_found)) + }) + .bind("0.0.0.0:80") + .unwrap() + .run() + .await } diff --git a/bvplan/src/cache.rs b/bvplan/src/cache.rs index 42693ec..9eec57e 100644 --- a/bvplan/src/cache.rs +++ b/bvplan/src/cache.rs @@ -37,16 +37,16 @@ pub mod keys { #[derive(Deserialize, Serialize, Debug)] pub struct Substitution { - #[serde(rename = "tm")] - pub time: (usize, Option), + #[serde(rename = "p")] + pub period: usize, #[serde(rename = "cl")] pub classes: Vec, #[serde(rename = "ps")] - pub prev_subject: String, + pub prev_subject: Option, #[serde(rename = "s")] pub subject: Option, #[serde(rename = "t")] - pub teachers: Vec, + pub teachers: Option>, #[serde(rename = "pr")] pub prev_room: Option, #[serde(rename = "r")] diff --git a/bvplan/src/templates.rs b/bvplan/src/templates.rs index b8d7e66..da47eb2 100644 --- a/bvplan/src/templates.rs +++ b/bvplan/src/templates.rs @@ -1,9 +1,51 @@ +use actix_web::{body::BoxBody, http, HttpResponse, HttpResponseBuilder, ResponseError}; use askama::Template; +use std::fmt; use crate::cache; +struct ActixError(askama::Error); + +impl fmt::Debug for ActixError { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(&self.0, f) + } +} + +impl fmt::Display for ActixError { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(&self.0, f) + } +} + +impl ResponseError for ActixError {} + +pub trait TemplateToResponseWithStatusCode { + fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse; +} + +impl TemplateToResponseWithStatusCode for T { + fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse { + 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)] #[template(path = "bvplan.html")] pub struct BVplan { pub data: cache::keys::substitutions::SubstitutionQuery, } + +#[derive(Template)] +#[template(path = "status_code.html")] +pub struct StatusCode { + pub status_code: http::StatusCode, + pub message: Option, +} diff --git a/bvplan/src/worker/get_substitutions.rs b/bvplan/src/worker/get_substitutions.rs index f51ed2f..18743a8 100644 --- a/bvplan/src/worker/get_substitutions.rs +++ b/bvplan/src/worker/get_substitutions.rs @@ -250,6 +250,7 @@ fn get_period(times: &Vec, start: bool, time: NaiveTime) -> Option fn cache_substitutions( db_conn: &mut PgConnection, redis_conn: &mut cache::Connection, + schoolyear_id: i32, substitution_query_id: i64, last_import_time: NaiveDateTime, ) -> Result<()> { @@ -287,6 +288,147 @@ fn cache_substitutions( .order(db::schema::timegrid_time_unit::start_time.asc()) .load::(db_conn)?; + let mut substitutions = db::schema::substitutions::table + .filter(db::schema::substitutions::substitution_query_id.eq(substitution_query_id)) + .load::(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_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::(db_conn)?; + let classes = db::schema::classes::table + .select(db::schema::classes::name) + .filter(db::schema::classes::id.eq_any(subst_class_ids)) + .load::(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_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_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::(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::(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::(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::(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::>(), + ), + ) + .load::(db_conn)?; + + if x.is_empty() { + None + } else { + Some(x) + } + } + }, + prev_room, + room, + text: s.text, + })) + } else { + Ok(None) + } + }) + .collect::>>() + .unwrap() + .into_iter() + .filter_map(|s| s) + .collect::>(); + substitutions.sort_by_key(|x| { + ( + x.classes.get(0).and_then(|x| { + x.split_once(' ') + .and_then(|y| y.0.parse::().map_or(None, |s| Some(s))) + }), + x.period, + ) + }); + let query = cache::keys::substitutions::SubstitutionQuery { date: format!( "{} {}", @@ -304,123 +446,15 @@ fn cache_substitutions( week_type, queried_at: queried_at.format("%d.%m.%Y %R").to_string(), last_import_time: last_import_time.format("%d.%m.%Y %R").to_string(), - schoolyear: "schoolyear".to_string(), - tenant: "OHG Furtwangen".to_string(), - substitutions: db::schema::substitutions::table - .filter(db::schema::substitutions::substitution_query_id.eq(substitution_query_id)) - .load::(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_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::(db_conn)?; - let classes = db::schema::classes::table - .select(db::schema::classes::name) - .filter(db::schema::classes::id.eq_any(subst_class_ids)) - .load::(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_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_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::(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::(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::(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::(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::>(), - ), - ) - .load::(db_conn)?, - }, - prev_room, - room, - text: s.text, - })) - } else { - Ok(None) - } - }) - .collect::>>() - .unwrap() - .into_iter() - .filter_map(|s| s) - .collect::>(), + schoolyear: db::schema::schoolyears::table + .select(db::schema::schoolyears::name) + .filter(db::schema::schoolyears::id.eq(schoolyear_id)) + .first::(db_conn)?, + tenant: db::schema::tenants::table + .select(db::schema::tenants::name) + .filter(db::schema::tenants::active) + .first::(db_conn)?, + substitutions, }; fs::write( @@ -497,9 +531,13 @@ pub async fn get_substitutions() -> TaskResult<()> { } // thread::sleep(dur); - if let Err(e) = - cache_substitutions(db_conn, redis_conn, substitution_query_id, last_import_time) - { + if let Err(e) = cache_substitutions( + db_conn, + redis_conn, + schoolyear_id, + substitution_query_id, + last_import_time, + ) { return Err(TaskError::UnexpectedError(format!("{:?}", e))); } diff --git a/bvplan/static/.keep b/bvplan/static/.keep new file mode 100644 index 0000000..e69de29 diff --git a/bvplan/templates/bvplan.html b/bvplan/templates/bvplan.html index 0766be9..beb2529 100644 --- a/bvplan/templates/bvplan.html +++ b/bvplan/templates/bvplan.html @@ -4,9 +4,10 @@ + BVplan | dergrimm.net - + @@ -16,114 +17,6 @@ crossorigin="anonymous" referrerpolicy="no-referrer" > - -
@@ -173,16 +66,16 @@ {% for subst in data.substitutions %} + {{ subst.period }} + {{ subst.classes|join(", ") }} - {% match subst.time.1 %} + {% match subst.prev_subject %} {% when Some with (x) %} - {{ subst.time.0 }} - {{ x }} + {{ x }} {% when None %} - {{ subst.time.0 }} + --- {% endmatch %} - {{ subst.classes|join(", ") }} - {{ subst.prev_subject }} {% match subst.subject %} {% when Some with (x) %} @@ -191,7 +84,14 @@ --- {% endmatch %} - {{ subst.teachers|join(", ") }} + + {% match subst.teachers %} + {% when Some with (x) %} + {{ x|join(", ") }} + {% when None %} + --- + {% endmatch %} + {% match subst.prev_room %} {% when Some with (x) %} @@ -222,7 +122,7 @@
-
+

BVplan - der bessre Vertretungsplan sogar mit UTF-8 Support! Wow @@ -234,12 +134,12 @@


-

+

Powered by Dominic Grimm <dominic@dergrimm.net> + >>, Untis sucks

@@ -284,7 +184,7 @@ } window.scrollTo(0, 0); - setTimeout($(window).height() > $(window).height() ? scrollDown : reload, waitDelay); + setTimeout($(document).height() > $(window).height() ? scrollDown : reload, waitDelay); diff --git a/bvplan/templates/status_code.html b/bvplan/templates/status_code.html new file mode 100644 index 0000000..3da247f --- /dev/null +++ b/bvplan/templates/status_code.html @@ -0,0 +1,25 @@ + + + + + + + + BVplan | dergrimm.net + + + + +

{{ status_code }}

+ {% match message %} + {% when Some with (x) %} +
+

{{ x }}

+ {% when None %} + {% endmatch %} + +