This commit is contained in:
Dominic Grimm 2023-01-17 06:54:45 +01:00
parent f614e606f4
commit a177f2a5d4
No known key found for this signature in database
GPG key ID: 6F294212DEAAC530
22 changed files with 1339 additions and 667 deletions

View file

@ -10,3 +10,6 @@ BACKEND_UNTIS_CLIENT_NAME="bvplan"
BACKEND_UNTIS_SCHOOL= BACKEND_UNTIS_SCHOOL=
BACKEND_UNTIS_USERNAME= BACKEND_UNTIS_USERNAME=
BACKEND_UNTIS_PASSWORD= BACKEND_UNTIS_PASSWORD=
BACKEND_UNTIS_VPLAN_URL=
BACKEND_UNTIS_VPLAN_USERNAME=
BACKEND_UNTIS_VPLAN_PASSWORD=

View file

@ -1,9 +1,6 @@
.PHONY: all dev prod .PHONY: all build
all: prod all: build
dev: build:
docker-compose build --build-arg BUILD_ENV=development BUILDKIT_PROGRESS=plain docker compose build
prod:
docker-compose build

View file

@ -7,3 +7,27 @@ insert_final_newline = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.sql]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.sql]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.html]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

689
backend/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,9 @@ edition = "2021"
[[bin]] [[bin]]
name = "api" name = "api"
[[bin]]
name = "worker"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1
lto = "fat" lto = "fat"
@ -19,13 +22,17 @@ anyhow = { version = "1.0.66", features = ["backtrace"] }
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"] }
env_logger = "0.10.0" env_logger = "0.9.3"
envconfig = "0.10.0" envconfig = "0.10.0"
juniper = { version = "0.15.10", features = ["scalar-naivetime"] } juniper = { version = "0.15.10", features = ["scalar-naivetime"] }
juniper_actix = "0.4.0" juniper_actix = "0.4.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
log = "0.4.17" log = "0.4.17"
r2d2_redis = "0.14.0"
reqwest = "0.11.13"
scraper = "0.14.0"
serde = "1.0.148" serde = "1.0.148"
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" }
url = "2.3.1" url = "2.3.1"

View file

@ -1,26 +1,27 @@
FROM lukemathwalker/cargo-chef:latest-rust-1.65.0 as chef FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.65.0 as chef
WORKDIR /usr/src/backend
FROM chef as diesel
RUN cargo install diesel_cli --no-default-features --features postgres
FROM chef as planner FROM chef as planner
WORKDIR /usr/src/backend
RUN mkdir src && touch src/main.rs RUN mkdir src && touch src/main.rs
COPY ./Cargo.toml ./Cargo.lock ./ COPY ./Cargo.toml ./Cargo.lock ./
RUN cargo chef prepare --recipe-path recipe.json RUN cargo chef prepare --recipe-path recipe.json
FROM chef as builder FROM chef as builder
WORKDIR /usr/src/backend
COPY --from=planner /usr/src/backend/recipe.json . COPY --from=planner /usr/src/backend/recipe.json .
RUN cargo chef cook --release --recipe-path recipe.json RUN cargo chef cook --release --recipe-path recipe.json
COPY ./src ./src COPY ./src ./src
RUN cargo build --release RUN cargo build --release
FROM chef as diesel FROM docker.io/debian:bullseye-slim as runner
RUN cargo install diesel_cli --no-default-features --features postgres
FROM debian:buster-slim as runner
RUN apt update RUN apt update
RUN apt install -y libpq5 RUN apt install -y libpq5
RUN apt install -y ca-certificates RUN apt install -y ca-certificates
RUN apt-get clean RUN apt-get clean
RUN apt-get autoremove --yes RUN apt-get autoremove -y
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
WORKDIR /usr/local/bin WORKDIR /usr/local/bin
COPY --from=diesel /usr/local/cargo/bin/diesel . COPY --from=diesel /usr/local/cargo/bin/diesel .

View file

@ -12,6 +12,8 @@ DROP TYPE substitution_type;
DROP TABLE substitution_queries; DROP TABLE substitution_queries;
DROP TYPE week_type;
DROP TABLE timegrid_time_unit; DROP TABLE timegrid_time_unit;
DROP TABLE timegrid_days; DROP TABLE timegrid_days;
@ -34,4 +36,4 @@ DROP INDEX tenants_active;
DROP TABLE tenants; DROP TABLE tenants;
DROP TABLE schoolyears; DROP TABLE schoolyears;

View file

@ -1,196 +1,190 @@
CREATE TABLE schoolyears( CREATE TABLE schoolyears (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
start_date DATE NOT NULL, start_date DATE NOT NULL,
end_date DATE NOT NULL, end_date DATE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE tenants( CREATE TABLE tenants (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
active BOOLEAN NOT NULL DEFAULT FALSE, active BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE UNIQUE INDEX tenants_active ON tenants(active) CREATE UNIQUE INDEX tenants_active ON tenants (active)
WHERE WHERE
active; active;
CREATE TABLE teachers( CREATE TABLE teachers (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
forename VARCHAR, forename VARCHAR,
display_name VARCHAR NOT NULL, display_name VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE classes( CREATE TABLE classes (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
long_name VARCHAR NOT NULL, long_name VARCHAR NOT NULL,
active BOOLEAN NOT NULL, active BOOLEAN NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE subjects( CREATE TABLE subjects (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
long_name VARCHAR NOT NULL, long_name VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE rooms( CREATE TABLE rooms (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
long_name VARCHAR NOT NULL, long_name VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE departments( CREATE TABLE departments (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
long_name VARCHAR NOT NULL, long_name VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE holidays( CREATE TABLE holidays (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
untis_id INTEGER NOT NULL UNIQUE, untis_id INTEGER NOT NULL UNIQUE,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
long_name VARCHAR NOT NULL, long_name VARCHAR NOT NULL,
start_date DATE NOT NULL, start_date DATE NOT NULL,
end_date DATE NOT NULL, end_date DATE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE timegrids( CREATE TABLE timegrids (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id), schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE timegrid_days( CREATE TABLE timegrid_days (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
timegrid_id INTEGER NOT NULL REFERENCES timegrids(id), timegrid_id INTEGER NOT NULL REFERENCES timegrids (id),
day_index SMALLINT NOT NULL, day_index SMALLINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE timegrid_time_unit( CREATE TABLE timegrid_time_unit (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
timegrid_day_id INTEGER NOT NULL REFERENCES timegrid_days(id), timegrid_day_id INTEGER NOT NULL REFERENCES timegrid_days (id),
start_time TIME NOT NULL, start_time TIME NOT NULL,
end_time TIME NOT NULL, end_time TIME NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE substitution_queries( CREATE TYPE week_type AS ENUM ('a', 'b');
id SERIAL PRIMARY KEY,
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
date DATE NOT NULL UNIQUE,
active BOOLEAN NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX substitution_queries_active ON substitution_queries(active); CREATE TABLE substitution_queries (
id SERIAL PRIMARY KEY,
CREATE TABLE substitution_query_results( schoolyear_id INTEGER NOT NULL REFERENCES schoolyears (id),
id SERIAL PRIMARY KEY, date DATE NOT NULL,
substitution_query_id INTEGER NOT NULL REFERENCES substitution_queries(id), week_type week_type NOT NULL,
queried_at TIMESTAMP NOT NULL, queried_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TYPE substitution_type AS ENUM ( CREATE TYPE substitution_type AS ENUM (
'cancel', 'cancel',
'subst', 'subst',
'add', 'add',
'shift', 'shift',
'rmchg', 'rmchg',
'rmlk', 'rmlk',
'bs', 'bs',
'oh', 'oh',
'sb', 'sb',
'other', 'other',
'free', 'free',
'exam', 'exam',
'ac', 'ac',
'holi', 'holi',
'stxt' 'stxt'
); );
CREATE TABLE substitutions( CREATE TABLE substitutions(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
substitution_query_result_id INTEGER NOT NULL REFERENCES substitution_query_results(id), substitution_query_id INTEGER NOT NULL REFERENCES substitution_queries (id),
subst_type substitution_type NOT NULL, subst_type substitution_type NOT NULL,
lesson_id INTEGER NOT NULL, lesson_id INTEGER NOT NULL,
start_time TIME NOT NULL, start_time TIME NOT NULL,
end_time TIME NOT NULL, end_time TIME NOT NULL,
text VARCHAR, text VARCHAR,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE substitution_classes( CREATE TABLE substitution_classes (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
substitution_id INTEGER NOT NULL REFERENCES substitutions(id), substitution_id INTEGER NOT NULL REFERENCES substitutions (id),
position SMALLINT NOT NULL, position SMALLINT NOT NULL,
class_id INTEGER NOT NULL REFERENCES classes(id), class_id INTEGER NOT NULL REFERENCES classes (id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE substitution_teachers( CREATE TABLE substitution_teachers (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
substitution_id INTEGER NOT NULL REFERENCES substitutions(id), substitution_id INTEGER NOT NULL REFERENCES substitutions (id),
position SMALLINT NOT NULL, position SMALLINT NOT NULL,
teacher_id INTEGER REFERENCES teachers(id), teacher_id INTEGER REFERENCES teachers (id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE substitution_subjects( CREATE TABLE substitution_subjects (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
substitution_id INTEGER NOT NULL REFERENCES substitutions(id), substitution_id INTEGER NOT NULL REFERENCES substitutions (id),
position SMALLINT NOT NULL, position SMALLINT NOT NULL,
subject_id INTEGER NOT NULL REFERENCES subjects(id), subject_id INTEGER NOT NULL REFERENCES subjects (id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE substitution_rooms( CREATE TABLE substitution_rooms (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
substitution_id INTEGER NOT NULL REFERENCES substitutions(id), substitution_id INTEGER NOT NULL REFERENCES substitutions (id),
position SMALLINT NOT NULL, position SMALLINT NOT NULL,
room_id INTEGER REFERENCES rooms(id), room_id INTEGER REFERENCES rooms (id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, original_room_id INTEGER REFERENCES rooms (id),
updated_at TIMESTAMP created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
); updated_at TIMESTAMP
);

View file

@ -1,4 +1,5 @@
#!/bin/bash #!/usr/bin/env bash
# -*- coding: utf-8 -*-
DATABASE_URL="$BACKEND_DB_URL" diesel setup \ DATABASE_URL="$BACKEND_DB_URL" diesel setup \
--migration-dir ./migrations \ --migration-dir ./migrations \

View file

@ -1,55 +0,0 @@
// #[cfg(not(target_env = "msvc"))]
// use tikv_jemallocator::Jemalloc;
// #[cfg(not(target_env = "msvc"))]
// #[global_allocator]
// static GLOBAL: Jemalloc = Jemalloc;
// async fn graphql_route(
// req: actix_web::HttpRequest,
// payload: actix_web::web::Payload,
// schema: actix_web::web::Data<backend::graphql::Schema>,
// ) -> Result<actix_web::HttpResponse, actix_web::Error> {
// juniper_actix::graphql_handler(
// &schema,
// &backend::graphql::Context {
// pool: backend::db::POOL.clone(),
// },
// req,
// payload,
// )
// .await
// }
// #[actix_web::main]
// async fn main() -> std::io::Result<()> {
// std::env::set_var("RUST_LOG", "info");
// env_logger::init();
// let server = actix_web::HttpServer::new(move || {
// actix_web::App::new()
// .app_data(actix_web::web::Data::new(backend::graphql::schema()))
// .wrap(
// actix_cors::Cors::default()
// .allow_any_origin()
// .allowed_methods(vec!["POST", "GET"])
// .allowed_headers(vec![
// actix_web::http::header::AUTHORIZATION,
// actix_web::http::header::ACCEPT,
// ])
// .allowed_header(actix_web::http::header::CONTENT_TYPE)
// .supports_credentials()
// .max_age(3600),
// )
// .wrap(actix_web::middleware::Compress::default())
// .wrap(actix_web::middleware::Logger::default())
// .service(
// actix_web::web::resource("/")
// .route(actix_web::web::post().to(graphql_route))
// .route(actix_web::web::get().to(graphql_route)),
// )
// });
// println!("Starting server on port 80!");
// server.bind("0.0.0.0:80")?.run().await
// }

View file

@ -1,3 +1,10 @@
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::{ use actix_web::{
http::header, http::header,

View file

@ -1,4 +1,3 @@
use std::thread;
#[cfg(not(target_env = "msvc"))] #[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc; use tikv_jemallocator::Jemalloc;
@ -6,6 +5,8 @@ use tikv_jemallocator::Jemalloc;
#[global_allocator] #[global_allocator]
static GLOBAL: Jemalloc = Jemalloc; static GLOBAL: Jemalloc = Jemalloc;
use std::thread;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
std::env::set_var("RUST_LOG", "info"); std::env::set_var("RUST_LOG", "info");

26
backend/src/cache.rs Normal file
View file

@ -0,0 +1,26 @@
use anyhow::Result;
use lazy_static::lazy_static;
use r2d2_redis::{r2d2, RedisConnectionManager};
use crate::config;
pub type CachePool = r2d2::Pool<RedisConnectionManager>;
pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>;
pub fn establish_connection() -> Result<RedisConnectionManager> {
Ok(RedisConnectionManager::new(
config::CONFIG.redis_url.as_str(),
)?)
}
pub fn pool() -> Result<CachePool> {
Ok(r2d2::Pool::builder().build(establish_connection()?)?)
}
lazy_static! {
pub static ref POOL: CachePool = pool().unwrap();
}
pub mod keys {
pub const LAST_SUBSTITUTION_QUERY_ID: &str = "last_subst_query_id";
}

View file

@ -11,6 +11,9 @@ pub struct Config {
#[envconfig(from = "BACKEND_AMQP_URL")] #[envconfig(from = "BACKEND_AMQP_URL")]
pub amqp_url: String, pub amqp_url: String,
#[envconfig(from = "BACKEND_REDIS_URL")]
pub redis_url: String,
#[envconfig(from = "BACKEND_UNTIS_API_URL")] #[envconfig(from = "BACKEND_UNTIS_API_URL")]
pub untis_api_url: String, pub untis_api_url: String,
@ -28,6 +31,15 @@ pub struct Config {
#[envconfig(from = "BACKEND_UNTIS_PASSWORD")] #[envconfig(from = "BACKEND_UNTIS_PASSWORD")]
pub untis_password: String, pub untis_password: String,
#[envconfig(from = "BACKEND_UNTIS_VPLAN_URL")]
pub untis_vplan_url: String,
#[envconfig(from = "BACKEND_UNTIS_VPLAN_USERNAME")]
pub untis_vplan_username: String,
#[envconfig(from = "BACKEND_UNTIS_VPLAN_PASSWORD")]
pub untis_vplan_password: String,
} }
lazy_static! { lazy_static! {

View file

@ -1,5 +1,6 @@
use chrono::prelude::*; use chrono::prelude::*;
use diesel::prelude::*; use diesel::prelude::*;
use juniper::GraphQLEnum;
use std::io::Write; use std::io::Write;
use crate::db::schema; use crate::db::schema;
@ -187,14 +188,48 @@ pub struct NewHoliday<'a> {
pub end_date: NaiveDate, pub end_date: NaiveDate,
} }
#[derive(Identifiable, Queryable, Associations, Debug)] #[derive(GraphQLEnum, diesel::FromSqlRow, diesel::AsExpression, PartialEq, Eq, Clone, Debug)]
#[diesel(sql_type = schema::sql_types::WeekType)]
pub enum WeekType {
A,
B,
}
impl diesel::serialize::ToSql<schema::sql_types::WeekType, diesel::pg::Pg> for WeekType {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, diesel::pg::Pg>,
) -> diesel::serialize::Result {
match *self {
Self::A => out.write_all(b"a")?,
Self::B => out.write_all(b"b")?,
}
Ok(diesel::serialize::IsNull::No)
}
}
impl diesel::deserialize::FromSql<schema::sql_types::WeekType, diesel::pg::Pg> for WeekType {
fn from_sql(
bytes: diesel::backend::RawValue<'_, diesel::pg::Pg>,
) -> diesel::deserialize::Result<Self> {
match bytes.as_bytes() {
b"a" => Ok(Self::A),
b"b" => Ok(Self::B),
_ => Err("Unrecognized enum variant".into()),
}
}
}
#[derive(Identifiable, Queryable, Associations, Clone, Debug)]
#[diesel(table_name = schema::substitution_queries)] #[diesel(table_name = schema::substitution_queries)]
#[diesel(belongs_to(Schoolyear))] #[diesel(belongs_to(Schoolyear))]
pub struct SubstitutionQuery { pub struct SubstitutionQuery {
pub id: i32, pub id: i32,
pub schoolyear_id: i32, pub schoolyear_id: i32,
pub date: NaiveDate, pub date: NaiveDate,
pub active: bool, pub week_type: WeekType,
pub queried_at: NaiveDateTime,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
} }
@ -204,27 +239,11 @@ pub struct SubstitutionQuery {
pub struct NewSubstitutionQuery { pub struct NewSubstitutionQuery {
pub schoolyear_id: i32, pub schoolyear_id: i32,
pub date: NaiveDate, pub date: NaiveDate,
pub active: bool, pub week_type: WeekType,
}
#[derive(Identifiable, Queryable, Associations, Debug)]
#[diesel(table_name = schema::substitution_query_results)]
#[diesel(belongs_to(SubstitutionQuery))]
pub struct SubstitutionQueryResult {
pub id: i32,
pub substitution_query_id: i32,
pub queried_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = schema::substitution_query_results)]
pub struct NewSubstitutionQueryResult {
pub substitution_query_id: i32,
pub queried_at: NaiveDateTime, pub queried_at: NaiveDateTime,
} }
#[derive(diesel::FromSqlRow, diesel::AsExpression, PartialEq, Eq, Debug)]
#[derive(GraphQLEnum, diesel::FromSqlRow, diesel::AsExpression, PartialEq, Eq, Clone, Debug)]
#[diesel(sql_type = schema::sql_types::SubstitutionType)] #[diesel(sql_type = schema::sql_types::SubstitutionType)]
pub enum SubstitutionType { pub enum SubstitutionType {
Cancel, Cancel,
@ -324,10 +343,10 @@ impl From<untis::RpcSubstitionType> for SubstitutionType {
#[derive(Identifiable, Queryable, Associations, Debug)] #[derive(Identifiable, Queryable, Associations, Debug)]
#[diesel(table_name = schema::substitutions)] #[diesel(table_name = schema::substitutions)]
#[diesel(belongs_to(SubstitutionQueryResult))] #[diesel(belongs_to(SubstitutionQuery))]
pub struct Substitution { pub struct Substitution {
pub id: i32, pub id: i32,
pub substitution_query_result_id: i32, pub substitution_query_id: i32,
pub subst_type: SubstitutionType, pub subst_type: SubstitutionType,
pub lesson_id: i32, pub lesson_id: i32,
pub start_time: NaiveTime, pub start_time: NaiveTime,
@ -340,7 +359,7 @@ pub struct Substitution {
#[derive(Insertable, Debug)] #[derive(Insertable, Debug)]
#[diesel(table_name = schema::substitutions)] #[diesel(table_name = schema::substitutions)]
pub struct NewSubstitution<'a> { pub struct NewSubstitution<'a> {
pub substitution_query_result_id: i32, pub substitution_query_id: i32,
pub subst_type: SubstitutionType, pub subst_type: SubstitutionType,
pub lesson_id: i32, pub lesson_id: i32,
pub start_time: NaiveTime, pub start_time: NaiveTime,
@ -421,6 +440,7 @@ pub struct SubstitutionRoom {
pub position: i16, pub position: i16,
pub index: i16, pub index: i16,
pub room_id: Option<i32>, pub room_id: Option<i32>,
pub original_room_id: Option<i32>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
} }
@ -431,4 +451,5 @@ pub struct NewSubstitutionRoom {
pub substitution_id: i32, pub substitution_id: i32,
pub position: i16, pub position: i16,
pub room_id: Option<i32>, pub room_id: Option<i32>,
pub original_room_id: Option<i32>,
} }

View file

@ -2,6 +2,10 @@ pub mod sql_types {
#[derive(diesel::sql_types::SqlType)] #[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "substitution_type"))] #[diesel(postgres_type(name = "substitution_type"))]
pub struct SubstitutionType; pub struct SubstitutionType;
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "week_type"))]
pub struct WeekType;
} }
diesel::table! { diesel::table! {
@ -136,20 +140,15 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
use diesel::sql_types::*;
use super::sql_types::WeekType;
substitution_queries { substitution_queries {
id -> Integer, id -> Integer,
schoolyear_id -> Integer, schoolyear_id -> Integer,
date -> Date, date -> Date,
active -> Bool, week_type -> WeekType,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_query_results {
id -> Integer,
substitution_query_id -> Integer,
queried_at -> Timestamp, queried_at -> Timestamp,
created_at -> Timestamp, created_at -> Timestamp,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
@ -163,7 +162,7 @@ diesel::table! {
substitutions { substitutions {
id -> Integer, id -> Integer,
substitution_query_result_id -> Integer, substitution_query_id -> Integer,
subst_type -> SubstitutionType, subst_type -> SubstitutionType,
lesson_id -> Integer, lesson_id -> Integer,
start_time -> Time, start_time -> Time,
@ -213,6 +212,7 @@ diesel::table! {
substitution_id -> Integer, substitution_id -> Integer,
position -> SmallInt, position -> SmallInt,
room_id -> Nullable<Integer>, room_id -> Nullable<Integer>,
original_room_id -> Nullable<Integer>,
created_at -> Timestamp, created_at -> Timestamp,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
} }

View file

@ -1,10 +1,286 @@
use juniper::{graphql_object, EmptyMutation, EmptySubscription, GraphQLObject, RootNode}; use diesel::prelude::*;
use juniper::{graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode};
use r2d2_redis::redis;
use std::ops::DerefMut;
use crate::{cache, db};
#[derive(Clone)] #[derive(Clone)]
pub struct Context; pub struct Context;
impl juniper::Context for Context {} impl juniper::Context for Context {}
pub struct Schoolyear {
pub model: db::models::Schoolyear,
}
#[graphql_object]
impl Schoolyear {
fn id(&self) -> i32 {
self.model.id
}
fn untis_id(&self) -> i32 {
self.model.untis_id
}
fn name(&self) -> &str {
self.model.name.as_str()
}
fn start_date(&self) -> chrono::NaiveDate {
self.model.start_date
}
fn end_date(&self) -> chrono::NaiveDate {
self.model.end_date
}
}
pub struct Teacher {
pub model: db::models::Teacher,
}
#[graphql_object]
impl Teacher {
fn id(&self) -> i32 {
self.model.id
}
fn untis_id(&self) -> i32 {
self.model.untis_id
}
fn schoolyear(&self) -> FieldResult<Schoolyear> {
let db_conn = &mut db::POOL.get()?;
Ok(Schoolyear {
model: db::schema::schoolyears::table
.filter(db::schema::schoolyears::id.eq(self.model.schoolyear_id))
.first::<db::models::Schoolyear>(db_conn)?,
})
}
fn name(&self) -> &str {
&self.model.name
}
fn forename(&self) -> Option<&str> {
self.model.forename.as_deref()
}
fn display_name(&self) -> &str {
&self.model.display_name
}
}
pub struct Class {
pub model: db::models::Class,
}
#[graphql_object]
impl Class {
fn id(&self) -> i32 {
self.model.id
}
fn untis_id(&self) -> i32 {
self.model.untis_id
}
fn schoolyear(&self) -> FieldResult<Schoolyear> {
let db_conn = &mut db::POOL.get()?;
Ok(Schoolyear {
model: db::schema::schoolyears::table
.filter(db::schema::schoolyears::id.eq(self.model.schoolyear_id))
.first::<db::models::Schoolyear>(db_conn)?,
})
}
fn name(&self) -> &str {
&self.model.name
}
fn long_name(&self) -> &str {
&self.model.long_name
}
fn active(&self) -> bool {
self.model.active
}
}
pub struct SubstitutionClass {
pub model: db::models::SubstitutionClass,
}
#[graphql_object]
impl SubstitutionClass {
fn id(&self) -> i32 {
self.model.id
}
fn substitution(&self) -> FieldResult<Substitution> {
let db_conn = &mut db::POOL.get()?;
Ok(Substitution {
model: db::schema::substitutions::table
.filter(db::schema::substitutions::id.eq(self.model.substitution_id))
.first::<db::models::Substitution>(db_conn)?,
})
}
fn position(&self) -> i32 {
self.model.position as i32
}
fn class(&self) -> FieldResult<Class> {
let db_conn = &mut db::POOL.get()?;
Ok(Class {
model: db::schema::classes::table
.filter(db::schema::classes::id.eq(self.model.class_id))
.first::<db::models::Class>(db_conn)?,
})
}
}
pub struct SubstitutionTeacher {
pub model: db::models::SubstitutionTeacher,
}
#[graphql_object]
impl SubstitutionTeacher {
fn id(&self) -> i32 {
self.model.id
}
fn substitution(&self) -> FieldResult<Substitution> {
let db_conn = &mut db::POOL.get()?;
Ok(Substitution {
model: db::schema::substitutions::table
.filter(db::schema::substitutions::id.eq(self.model.substitution_id))
.first::<db::models::Substitution>(db_conn)?,
})
}
fn position(&self) -> i32 {
self.model.position as i32
}
fn teacher(&self) -> FieldResult<Option<Teacher>> {
if let Some(teacher_id) = self.model.teacher_id {
let db_conn = &mut db::POOL.get()?;
Ok(Some(Teacher {
model: db::schema::teachers::table
.filter(db::schema::teachers::id.eq(teacher_id))
.first::<db::models::Teacher>(db_conn)?,
}))
} else {
Ok(None)
}
}
}
pub struct Substitution {
pub model: db::models::Substitution,
}
#[graphql_object]
impl Substitution {
fn id(&self) -> i32 {
self.model.id
}
fn substitution_query(&self) -> FieldResult<SubstitutionQuery> {
let db_conn = &mut db::POOL.get()?;
Ok(SubstitutionQuery {
model: db::schema::substitution_queries::table
.filter(db::schema::substitution_queries::id.eq(self.model.substitution_query_id))
.first::<db::models::SubstitutionQuery>(db_conn)?,
})
}
fn subst_type(&self) -> db::models::SubstitutionType {
self.model.subst_type.to_owned()
}
fn lesson_id(&self) -> i32 {
self.model.lesson_id
}
fn start_time(&self) -> chrono::NaiveTime {
self.model.start_time
}
fn end_time(&self) -> chrono::NaiveTime {
self.model.end_time
}
fn text(&self) -> Option<&str> {
self.model.text.as_deref()
}
fn classes(&self) -> FieldResult<Vec<SubstitutionClass>> {
let db_conn = &mut db::POOL.get()?;
Ok(db::schema::substitution_classes::table
.filter(db::schema::substitution_classes::substitution_id.eq(self.model.id))
.load::<db::models::SubstitutionClass>(db_conn)?
.into_iter()
.map(|x| SubstitutionClass { model: x })
.collect())
}
}
pub struct SubstitutionQuery {
pub model: db::models::SubstitutionQuery,
}
#[graphql_object]
impl SubstitutionQuery {
fn id(&self) -> i32 {
self.model.id
}
fn schoolyear(&self) -> FieldResult<Schoolyear> {
let db_conn = &mut db::POOL.get()?;
Ok(Schoolyear {
model: db::schema::schoolyears::table
.filter(db::schema::schoolyears::id.eq(self.model.schoolyear_id))
.first::<db::models::Schoolyear>(db_conn)?,
})
}
fn date(&self) -> chrono::NaiveDate {
self.model.date
}
fn week_type(&self) -> db::models::WeekType {
self.model.week_type.to_owned()
}
fn queried_at(&self) -> chrono::NaiveDateTime {
self.model.queried_at
}
fn substitutions(&self) -> FieldResult<Vec<Substitution>> {
let db_conn = &mut db::POOL.get()?;
Ok(db::schema::substitutions::table
.filter(db::schema::substitutions::substitution_query_id.eq(self.model.id))
.load::<db::models::Substitution>(db_conn)?
.into_iter()
.map(|x| Substitution { model: x })
.collect())
}
}
pub struct Query; pub struct Query;
#[graphql_object(context = Context)] #[graphql_object(context = Context)]
@ -12,6 +288,29 @@ impl Query {
fn ping() -> &'static str { fn ping() -> &'static str {
"pong" "pong"
} }
fn last_substitution_query() -> FieldResult<SubstitutionQuery> {
let db_conn = &mut db::POOL.get()?;
let redis_conn = &mut cache::POOL.get()?;
let id = redis::cmd("GET")
.arg(cache::keys::LAST_SUBSTITUTION_QUERY_ID)
.query::<Option<i32>>(redis_conn.deref_mut())?
.map_or_else(
|| {
db::schema::substitution_queries::table
.select(db::schema::substitution_queries::id)
.order(db::schema::substitution_queries::queried_at.desc())
.first::<i32>(db_conn)
},
|x| Ok(x),
)?;
let last_query = db::schema::substitution_queries::table
.filter(db::schema::substitution_queries::id.eq(id))
.first::<db::models::SubstitutionQuery>(db_conn)?;
Ok(SubstitutionQuery { model: last_query })
}
} }
pub type Schema = RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>; pub type Schema = RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>;

View file

@ -1,3 +1,4 @@
pub mod cache;
pub mod config; pub mod config;
pub mod db; pub mod db;
pub mod graphql; pub mod graphql;

View file

@ -4,6 +4,7 @@ use lazy_static::lazy_static;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time; use std::time;
use stdext::duration::DurationExt;
use crate::config; use crate::config;
@ -63,9 +64,9 @@ pub fn beat() -> impl std::future::Future<
celery::beat!( celery::beat!(
broker = AMQPBroker { &config::CONFIG.amqp_url }, broker = AMQPBroker { &config::CONFIG.amqp_url },
tasks = [ tasks = [
"update_info_" => { "update_info" => {
update_info::update_info, update_info::update_info,
schedule = DeltaSchedule::new(time::Duration::from_secs(5 * 60)), schedule = DeltaSchedule::new(time::Duration::from_minutes(5)),
args = (), args = (),
} }
], ],

View file

@ -1,17 +1,20 @@
use anyhow::Result; use anyhow::{bail, Context, Result};
use celery::error::TaskError; use celery::error::TaskError;
use celery::task::TaskResult; use celery::task::TaskResult;
use chrono::prelude::*; use chrono::prelude::*;
use diesel::prelude::*; use diesel::prelude::*;
use lazy_static::lazy_static;
use r2d2_redis::redis;
use std::ops::DerefMut;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use crate::{config, db}; use crate::{cache, config, db};
async fn fetch_schoolyears(client: &untis::Client, conn: &mut PgConnection) -> Result<i32> { async fn fetch_schoolyears(client: &untis::Client, db_conn: &mut PgConnection) -> Result<i32> {
let existing_schoolyears = db::schema::schoolyears::table let existing_schoolyears = db::schema::schoolyears::table
.select(db::schema::schoolyears::untis_id) .select(db::schema::schoolyears::untis_id)
.load::<i32>(conn)?; .load::<i32>(db_conn)?;
diesel::insert_into(db::schema::schoolyears::table) diesel::insert_into(db::schema::schoolyears::table)
.values( .values(
&client &client
@ -27,29 +30,29 @@ async fn fetch_schoolyears(client: &untis::Client, conn: &mut PgConnection) -> R
}) })
.collect::<Vec<db::models::NewSchoolyear>>(), .collect::<Vec<db::models::NewSchoolyear>>(),
) )
.execute(conn)?; .execute(db_conn)?;
Ok(db::schema::schoolyears::table Ok(db::schema::schoolyears::table
.filter(db::schema::schoolyears::untis_id.eq(client.current_schoolyear().await?.id)) .filter(db::schema::schoolyears::untis_id.eq(client.current_schoolyear().await?.id))
.select(db::schema::schoolyears::id) .select(db::schema::schoolyears::id)
.first(conn)?) .first(db_conn)?)
} }
async fn fetch_current_tenant( async fn fetch_current_tenant(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let tenant = client.current_tenant().await?; let tenant = client.current_tenant().await?;
if diesel::select(diesel::dsl::not(diesel::dsl::exists( if diesel::select(diesel::dsl::not(diesel::dsl::exists(
db::schema::tenants::table.filter(db::schema::tenants::untis_id.eq(tenant.id)), db::schema::tenants::table.filter(db::schema::tenants::untis_id.eq(tenant.id)),
))) )))
.get_result::<bool>(conn)? .get_result::<bool>(db_conn)?
{ {
diesel::update(db::schema::tenants::table) diesel::update(db::schema::tenants::table)
.filter(db::schema::tenants::active) .filter(db::schema::tenants::active)
.set(db::schema::tenants::active.eq(false)) .set(db::schema::tenants::active.eq(false))
.execute(conn)?; .execute(db_conn)?;
diesel::insert_into(db::schema::tenants::table) diesel::insert_into(db::schema::tenants::table)
.values(db::models::NewTenant { .values(db::models::NewTenant {
untis_id: tenant.id, untis_id: tenant.id,
@ -57,13 +60,13 @@ async fn fetch_current_tenant(
name: &tenant.display_name, name: &tenant.display_name,
active: true, active: true,
}) })
.execute(conn)?; .execute(db_conn)?;
} else if diesel::select(diesel::dsl::exists( } else if diesel::select(diesel::dsl::exists(
db::schema::tenants::table db::schema::tenants::table
.filter(db::schema::tenants::untis_id.eq(tenant.id)) .filter(db::schema::tenants::untis_id.eq(tenant.id))
.filter(db::schema::tenants::active.eq(false)), .filter(db::schema::tenants::active.eq(false)),
)) ))
.get_result::<bool>(conn)? .get_result::<bool>(db_conn)?
{ {
diesel::update(db::schema::tenants::table) diesel::update(db::schema::tenants::table)
.filter(db::schema::tenants::active) .filter(db::schema::tenants::active)
@ -71,14 +74,14 @@ async fn fetch_current_tenant(
db::schema::tenants::active.eq(false), db::schema::tenants::active.eq(false),
db::schema::tenants::updated_at.eq(diesel::dsl::now), db::schema::tenants::updated_at.eq(diesel::dsl::now),
)) ))
.execute(conn)?; .execute(db_conn)?;
diesel::update(db::schema::tenants::table) diesel::update(db::schema::tenants::table)
.filter(db::schema::tenants::untis_id.eq(tenant.id)) .filter(db::schema::tenants::untis_id.eq(tenant.id))
.set(( .set((
db::schema::tenants::active.eq(true), db::schema::tenants::active.eq(true),
db::schema::tenants::updated_at.eq(diesel::dsl::now), db::schema::tenants::updated_at.eq(diesel::dsl::now),
)) ))
.execute(conn)?; .execute(db_conn)?;
} }
Ok(()) Ok(())
@ -86,13 +89,13 @@ async fn fetch_current_tenant(
async fn fetch_teachers( async fn fetch_teachers(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let existing_teachers = db::schema::teachers::table let existing_teachers = db::schema::teachers::table
.select(db::schema::teachers::untis_id) .select(db::schema::teachers::untis_id)
.filter(db::schema::teachers::schoolyear_id.eq(schoolyear_id)) .filter(db::schema::teachers::schoolyear_id.eq(schoolyear_id))
.load::<i32>(conn)?; .load::<i32>(db_conn)?;
diesel::insert_into(db::schema::teachers::table) diesel::insert_into(db::schema::teachers::table)
.values( .values(
&client &client
@ -113,20 +116,20 @@ async fn fetch_teachers(
}) })
.collect::<Vec<db::models::NewTeacher>>(), .collect::<Vec<db::models::NewTeacher>>(),
) )
.execute(conn)?; .execute(db_conn)?;
Ok(()) Ok(())
} }
async fn fetch_classes( async fn fetch_classes(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let existing_classes = db::schema::classes::table let existing_classes = db::schema::classes::table
.select(db::schema::classes::untis_id) .select(db::schema::classes::untis_id)
.filter(db::schema::classes::schoolyear_id.eq(schoolyear_id)) .filter(db::schema::classes::schoolyear_id.eq(schoolyear_id))
.load::<i32>(conn)?; .load::<i32>(db_conn)?;
diesel::insert_into(db::schema::classes::table) diesel::insert_into(db::schema::classes::table)
.values( .values(
&client &client
@ -143,20 +146,20 @@ async fn fetch_classes(
}) })
.collect::<Vec<db::models::NewClass>>(), .collect::<Vec<db::models::NewClass>>(),
) )
.execute(conn)?; .execute(db_conn)?;
Ok(()) Ok(())
} }
async fn fetch_subjects( async fn fetch_subjects(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let existing_classes = db::schema::subjects::table let existing_classes = db::schema::subjects::table
.select(db::schema::subjects::untis_id) .select(db::schema::subjects::untis_id)
.filter(db::schema::subjects::schoolyear_id.eq(schoolyear_id)) .filter(db::schema::subjects::schoolyear_id.eq(schoolyear_id))
.load::<i32>(conn)?; .load::<i32>(db_conn)?;
diesel::insert_into(db::schema::subjects::table) diesel::insert_into(db::schema::subjects::table)
.values( .values(
&client &client
@ -172,20 +175,20 @@ async fn fetch_subjects(
}) })
.collect::<Vec<db::models::NewSubject>>(), .collect::<Vec<db::models::NewSubject>>(),
) )
.execute(conn)?; .execute(db_conn)?;
Ok(()) Ok(())
} }
async fn fetch_rooms( async fn fetch_rooms(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let existing_classes = db::schema::rooms::table let existing_classes = db::schema::rooms::table
.select(db::schema::rooms::untis_id) .select(db::schema::rooms::untis_id)
.filter(db::schema::rooms::schoolyear_id.eq(schoolyear_id)) .filter(db::schema::rooms::schoolyear_id.eq(schoolyear_id))
.load::<i32>(conn)?; .load::<i32>(db_conn)?;
diesel::insert_into(db::schema::rooms::table) diesel::insert_into(db::schema::rooms::table)
.values( .values(
&client &client
@ -201,20 +204,20 @@ async fn fetch_rooms(
}) })
.collect::<Vec<db::models::NewRoom>>(), .collect::<Vec<db::models::NewRoom>>(),
) )
.execute(conn)?; .execute(db_conn)?;
Ok(()) Ok(())
} }
async fn fetch_departments( async fn fetch_departments(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let existing_classes = db::schema::departments::table let existing_classes = db::schema::departments::table
.select(db::schema::departments::untis_id) .select(db::schema::departments::untis_id)
.filter(db::schema::departments::schoolyear_id.eq(schoolyear_id)) .filter(db::schema::departments::schoolyear_id.eq(schoolyear_id))
.load::<i32>(conn)?; .load::<i32>(db_conn)?;
diesel::insert_into(db::schema::departments::table) diesel::insert_into(db::schema::departments::table)
.values( .values(
&client &client
@ -230,20 +233,20 @@ async fn fetch_departments(
}) })
.collect::<Vec<db::models::NewDepartment>>(), .collect::<Vec<db::models::NewDepartment>>(),
) )
.execute(conn)?; .execute(db_conn)?;
Ok(()) Ok(())
} }
async fn fetch_holidays( async fn fetch_holidays(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let existing_classes = db::schema::holidays::table let existing_classes = db::schema::holidays::table
.select(db::schema::holidays::untis_id) .select(db::schema::holidays::untis_id)
.filter(db::schema::holidays::schoolyear_id.eq(schoolyear_id)) .filter(db::schema::holidays::schoolyear_id.eq(schoolyear_id))
.load::<i32>(conn)?; .load::<i32>(db_conn)?;
diesel::insert_into(db::schema::holidays::table) diesel::insert_into(db::schema::holidays::table)
.values( .values(
&client &client
@ -261,147 +264,198 @@ async fn fetch_holidays(
}) })
.collect::<Vec<db::models::NewHoliday>>(), .collect::<Vec<db::models::NewHoliday>>(),
) )
.execute(conn)?; .execute(db_conn)?;
Ok(()) Ok(())
} }
lazy_static! {
static ref TITLE_SELECTOR: scraper::Selector = scraper::Selector::parse(".mon_title").unwrap();
}
async fn fetch_substitutions( async fn fetch_substitutions(
client: &untis::Client, client: &untis::Client,
conn: &mut PgConnection, db_conn: &mut PgConnection,
redis_conn: &mut cache::ConnectionPool,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let today = Utc::now().date_naive(); let (date, week_type) = {
if diesel::select(diesel::dsl::not(diesel::dsl::exists( let html = reqwest::Client::new()
db::schema::substitution_queries::table .get(&config::CONFIG.untis_vplan_url)
.filter(db::schema::substitution_queries::date.eq(today)), .header(
))) reqwest::header::USER_AGENT,
.get_result::<bool>(conn)? &config::CONFIG.untis_client_name,
{ )
diesel::insert_into(db::schema::substitution_queries::table) .header(reqwest::header::ACCEPT, "text/html")
.values(db::models::NewSubstitutionQuery { .basic_auth(
schoolyear_id, &config::CONFIG.untis_vplan_username,
date: today, Some(&config::CONFIG.untis_vplan_password),
active: true, )
.send()
.await?
.text()
.await?;
let document = scraper::Html::parse_document(&html);
let title = document
.select(&TITLE_SELECTOR)
.next()
.context("No element in vplan html which is selectable class \"mon_title\"")?
.text()
.next()
.context("\"mon_title\" element is empty")?
.split_once(',')
.context("Could not split \"mon_title\" string")?;
(
chrono::NaiveDate::parse_from_str(
title
.0
.split_whitespace()
.next()
.context("Could not find date")?,
"%d.%m.%Y",
)?,
match title
.1
.split_whitespace()
.last()
.context("Could not find week type indicator")?
{
"A" => db::models::WeekType::A,
"B" => db::models::WeekType::B,
x => bail!("Invalid week type: {:?}", x),
},
)
};
let substitution_query_id = diesel::insert_into(db::schema::substitution_queries::table)
.values(db::models::NewSubstitutionQuery {
schoolyear_id,
date,
week_type,
queried_at: Utc::now().naive_utc(),
})
.returning(db::schema::substitution_queries::id)
.get_result::<i32>(db_conn)?;
redis::cmd("SET")
.arg(cache::keys::LAST_SUBSTITUTION_QUERY_ID)
.arg(substitution_query_id)
.query(redis_conn.deref_mut())?;
for substitution in client.substitutions(&date, &date, None).await? {
let substitution_id = diesel::insert_into(db::schema::substitutions::table)
.values(db::models::NewSubstitution {
substitution_query_id,
subst_type: substitution.subst_type.into(),
lesson_id: substitution.lesson_id,
start_time: substitution.start_time,
end_time: substitution.end_time,
text: substitution.text.as_deref(),
}) })
.execute(conn)?; .returning(db::schema::substitutions::id)
} .get_result::<i32>(db_conn)?;
for query in db::schema::substitution_queries::table diesel::insert_into(db::schema::substitution_classes::table)
.filter(db::schema::substitution_queries::active) .values(
.load::<db::models::SubstitutionQuery>(conn)? &substitution
{ .classes
let query_result_id = diesel::insert_into(db::schema::substitution_query_results::table) .iter()
.values(db::models::NewSubstitutionQueryResult { .enumerate()
substitution_query_id: query.id, .map(|(i, c)| {
queried_at: Utc::now().naive_utc(), Ok(db::models::NewSubstitutionClass {
}) substitution_id,
.returning(db::schema::substitution_query_results::id) position: i as i16,
.get_result::<i32>(conn)?; class_id: db::schema::classes::table
.filter(db::schema::classes::untis_id.eq(c.id))
.select(db::schema::classes::id)
.get_result::<i32>(db_conn)?,
})
})
.collect::<Result<Vec<db::models::NewSubstitutionClass>>>()?,
)
.execute(db_conn)?;
let substs = client.substitutions(&query.date, &query.date, None).await?; diesel::insert_into(db::schema::substitution_teachers::table)
for substitution in substs { .values(
let substitution_id = diesel::insert_into(db::schema::substitutions::table) &substitution
.values(db::models::NewSubstitution { .teachers
substitution_query_result_id: query_result_id, .iter()
subst_type: substitution.subst_type.into(), .enumerate()
lesson_id: substitution.lesson_id, .map(|(i, t)| {
start_time: substitution.start_time, Ok(db::models::NewSubstitutionTeacher {
end_time: substitution.end_time, substitution_id,
text: substitution.text.as_deref(), position: i as i16,
}) teacher_id: if t.id == 0 {
.returning(db::schema::substitutions::id) None
.get_result::<i32>(conn)?; } else {
Some(
db::schema::teachers::table
.filter(db::schema::teachers::untis_id.eq(t.id))
.select(db::schema::teachers::id)
.get_result::<i32>(db_conn)?,
)
},
})
})
.collect::<Result<Vec<db::models::NewSubstitutionTeacher>>>()?,
)
.execute(db_conn)?;
diesel::insert_into(db::schema::substitution_classes::table) diesel::insert_into(db::schema::substitution_subjects::table)
.values( .values(
&substitution &substitution
.classes .subjects
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, c)| { .map(|(i, s)| {
Ok(db::models::NewSubstitutionClass { Ok(db::models::NewSubstitutionSubject {
substitution_id, substitution_id,
position: i as i16, position: i as i16,
class_id: db::schema::classes::table subject_id: db::schema::subjects::table
.filter(db::schema::classes::untis_id.eq(c.id)) .filter(db::schema::subjects::untis_id.eq(s.id))
.select(db::schema::classes::id) .select(db::schema::subjects::id)
.get_result::<i32>(conn)?, .get_result::<i32>(db_conn)?,
})
}) })
.collect::<Result<Vec<db::models::NewSubstitutionClass>>>()?, })
) .collect::<Result<Vec<db::models::NewSubstitutionSubject>>>()?,
.execute(conn)?; )
diesel::insert_into(db::schema::substitution_teachers::table) .execute(db_conn)?;
.values(
&substitution diesel::insert_into(db::schema::substitution_rooms::table)
.teachers .values(
.iter() &substitution
.enumerate() .rooms
.map(|(i, t)| { .iter()
Ok(db::models::NewSubstitutionTeacher { .enumerate()
substitution_id, .map(|(i, r)| {
position: i as i16, Ok(db::models::NewSubstitutionRoom {
teacher_id: if t.id == 0 { substitution_id,
None position: i as i16,
} else { room_id: if r.id == 0 {
Some( None
db::schema::teachers::table } else {
.filter(db::schema::teachers::untis_id.eq(t.id)) Some(
.select(db::schema::teachers::id) db::schema::rooms::table
.get_result::<i32>(conn)?, .filter(db::schema::rooms::untis_id.eq(r.id))
) .select(db::schema::rooms::id)
}, .get_result::<i32>(db_conn)?,
}) )
},
original_room_id: if let Some(original_id) = r.original_id {
Some(
db::schema::rooms::table
.filter(db::schema::rooms::untis_id.eq(original_id))
.select(db::schema::rooms::id)
.get_result::<i32>(db_conn)?,
)
} else {
None
},
}) })
.collect::<Result<Vec<db::models::NewSubstitutionTeacher>>>()?, })
) .collect::<Result<Vec<db::models::NewSubstitutionRoom>>>()?,
.execute(conn)?; )
diesel::insert_into(db::schema::substitution_subjects::table) .execute(db_conn)?;
.values(
&substitution
.subjects
.iter()
.enumerate()
.map(|(i, s)| {
Ok(db::models::NewSubstitutionSubject {
substitution_id,
position: i as i16,
subject_id: db::schema::subjects::table
.filter(db::schema::subjects::untis_id.eq(s.id))
.select(db::schema::subjects::id)
.get_result::<i32>(conn)?,
})
})
.collect::<Result<Vec<db::models::NewSubstitutionSubject>>>()?,
)
.execute(conn)?;
diesel::insert_into(db::schema::substitution_rooms::table)
.values(
&substitution
.rooms
.iter()
.enumerate()
.map(|(i, r)| {
Ok(db::models::NewSubstitutionRoom {
substitution_id,
position: i as i16,
room_id: if r.id == 0 {
None
} else {
Some(
db::schema::rooms::table
.filter(db::schema::rooms::untis_id.eq(r.id))
.select(db::schema::rooms::id)
.get_result::<i32>(conn)?,
)
},
})
})
.collect::<Result<Vec<db::models::NewSubstitutionRoom>>>()?,
)
.execute(conn)?;
}
} }
Ok(()) Ok(())
@ -409,10 +463,9 @@ async fn fetch_substitutions(
#[celery::task] #[celery::task]
pub async fn update_info() -> TaskResult<()> { pub async fn update_info() -> TaskResult<()> {
let dur = Duration::from_secs(10); let dur = Duration::from_secs(2);
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!");
let mut client = match config::untis_from_env() { let mut client = match config::untis_from_env() {
Ok(x) => x, Ok(x) => x,
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())), Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
@ -421,65 +474,58 @@ pub async fn update_info() -> TaskResult<()> {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!");
let conn = &mut match db::POOL.get() { let db_conn = &mut match db::POOL.get() {
Ok(x) => x,
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
};
let redis_conn = &mut match cache::POOL.get() {
Ok(x) => x, Ok(x) => x,
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())), Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
}; };
let schoolyear_id = match fetch_schoolyears(&client, conn).await { let schoolyear_id = match fetch_schoolyears(&client, db_conn).await {
Ok(x) => x, Ok(x) => x,
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())), Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
}; };
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_current_tenant(&client, db_conn, schoolyear_id).await {
if let Err(e) = fetch_current_tenant(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_teachers(&client, db_conn, schoolyear_id).await {
if let Err(e) = fetch_teachers(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_classes(&client, db_conn, schoolyear_id).await {
if let Err(e) = fetch_classes(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_subjects(&client, db_conn, schoolyear_id).await {
if let Err(e) = fetch_subjects(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_rooms(&client, db_conn, schoolyear_id).await {
if let Err(e) = fetch_rooms(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_departments(&client, db_conn, schoolyear_id).await {
if let Err(e) = fetch_departments(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_holidays(&client, db_conn, schoolyear_id).await {
if let Err(e) = fetch_holidays(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!"); if let Err(e) = fetch_substitutions(&client, db_conn, redis_conn, schoolyear_id).await {
if let Err(e) = fetch_substitutions(&client, conn, schoolyear_id).await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!");
if let Err(e) = client.logout().await { if let Err(e) = client.logout().await {
return Err(TaskError::UnexpectedError(e.to_string())); return Err(TaskError::UnexpectedError(e.to_string()));
} }
thread::sleep(dur); thread::sleep(dur);
dbg!("DONE!");
Ok(()) Ok(())
} }

View file

@ -32,24 +32,5 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location ~* /rabbitmq/api/(.*?)/(.*) {
proxy_pass http://rabbitmq:15672/api/$1/%2F/$2?$query_string;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~* /rabbitmq/(.*) {
rewrite ^/rabbitmq/(.*)$ /$1 break;
proxy_pass http://rabbitmq:15672;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
} }
} }

View file

@ -10,15 +10,20 @@ x-backend:
depends_on: depends_on:
- postgres - postgres
- rabbitmq - rabbitmq
- redis
environment: environment:
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_USER} BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_USER}
BACKEND_AMQP_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672 BACKEND_AMQP_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
BACKEND_REDIS_URL: redis://redis:6379
BACKEND_UNTIS_API_URL: ${BACKEND_UNTIS_API_URL} BACKEND_UNTIS_API_URL: ${BACKEND_UNTIS_API_URL}
BACKEND_UNTIS_RPC_URL: ${BACKEND_UNTIS_RPC_URL} BACKEND_UNTIS_RPC_URL: ${BACKEND_UNTIS_RPC_URL}
BACKEND_UNTIS_CLIENT_NAME: ${BACKEND_UNTIS_CLIENT_NAME} BACKEND_UNTIS_CLIENT_NAME: ${BACKEND_UNTIS_CLIENT_NAME}
BACKEND_UNTIS_SCHOOL: ${BACKEND_UNTIS_SCHOOL} BACKEND_UNTIS_SCHOOL: ${BACKEND_UNTIS_SCHOOL}
BACKEND_UNTIS_USERNAME: ${BACKEND_UNTIS_USERNAME} BACKEND_UNTIS_USERNAME: ${BACKEND_UNTIS_USERNAME}
BACKEND_UNTIS_PASSWORD: ${BACKEND_UNTIS_PASSWORD} BACKEND_UNTIS_PASSWORD: ${BACKEND_UNTIS_PASSWORD}
BACKEND_UNTIS_VPLAN_URL: ${BACKEND_UNTIS_VPLAN_URL}
BACKEND_UNTIS_VPLAN_USERNAME: ${BACKEND_UNTIS_VPLAN_USERNAME}
BACKEND_UNTIS_VPLAN_PASSWORD: ${BACKEND_UNTIS_VPLAN_PASSWORD}
services: services:
nginx: nginx:
@ -49,7 +54,7 @@ services:
- postgres - postgres
rabbitmq: rabbitmq:
image: docker.io/rabbitmq:3-management-alpine image: docker.io/rabbitmq:3-alpine
restart: always restart: always
environment: environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
@ -57,6 +62,12 @@ services:
volumes: volumes:
- rabbitmq:/var/lib/rabbitmq - rabbitmq:/var/lib/rabbitmq
redis:
image: docker.io/redis:alpine
restart: always
volumes:
- redis:/data
worker: worker:
<<: *backend <<: *backend
command: worker command: worker
@ -72,5 +83,6 @@ services:
volumes: volumes:
postgres: postgres:
rabbitmq: rabbitmq:
redis: