This commit is contained in:
Dominic Grimm 2022-12-14 19:13:32 +01:00
parent 2b65b6a1c5
commit f13c9c905f
No known key found for this signature in database
GPG key ID: 6F294212DEAAC530
15 changed files with 322 additions and 981 deletions

View file

@ -6,6 +6,7 @@ RABBITMQ_PASSWORD="bvplan"
BACKEND_UNTIS_API_URL="https://mese.webuntis.com/WebUntis/api/" BACKEND_UNTIS_API_URL="https://mese.webuntis.com/WebUntis/api/"
BACKEND_UNTIS_RPC_URL="https://mese.webuntis.com/WebUntis/jsonrpc.do" BACKEND_UNTIS_RPC_URL="https://mese.webuntis.com/WebUntis/jsonrpc.do"
BACKEND_UNTIS_CLIENT_NAME="bvplan"
BACKEND_UNTIS_SCHOOL= BACKEND_UNTIS_SCHOOL=
BACKEND_UNTIS_USERNAME= BACKEND_UNTIS_USERNAME=
BACKEND_UNTIS_PASSWORD= BACKEND_UNTIS_PASSWORD=

View file

@ -3,17 +3,12 @@
debug/ debug/
target/ target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information # MSVC Windows builds of rustc generate these, which store debugging information
*.pdb *.pdb
dist/
Dockerfile Dockerfile
.gitignore .gitignore
.dockerignore .dockerignore

11
backend/.gitignore vendored
View file

@ -1 +1,10 @@
/target # Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

59
backend/Cargo.lock generated
View file

@ -549,12 +549,12 @@ dependencies = [
"juniper_actix", "juniper_actix",
"lazy_static", "lazy_static",
"log", "log",
"now",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tikv-jemallocator", "tikv-jemallocator",
"tokio", "tokio",
"untis",
"url", "url",
] ]
@ -1592,9 +1592,9 @@ dependencies = [
[[package]] [[package]]
name = "ipnet" name = "ipnet"
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 = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
@ -1742,9 +1742,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]] [[package]]
name = "local-channel" name = "local-channel"
@ -1865,15 +1865,6 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "now"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89e9874397a1f0a52fc1f197a8effd9735223cb2390e9dcc83ac6cd02923d0"
dependencies = [
"chrono",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -1994,7 +1985,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [ dependencies = [
"instant", "instant",
"lock_api", "lock_api",
"parking_lot_core 0.8.5", "parking_lot_core 0.8.6",
] ]
[[package]] [[package]]
@ -2009,9 +2000,9 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"instant", "instant",
@ -2036,9 +2027,9 @@ dependencies = [
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.9" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" checksum = "cf1c2c742266c2f1041c914ba65355a83ae8747b05f208319784083583494b4b"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
@ -2098,9 +2089,9 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]] [[package]]
name = "polling" name = "polling"
version = "2.5.1" version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cfg-if", "cfg-if",
@ -2271,9 +2262,9 @@ dependencies = [
[[package]] [[package]]
name = "redis" name = "redis"
version = "0.21.6" version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "571c252c68d09a2ad3e49edd14e9ee48932f3e0f27b06b4ea4c9b2a706d31103" checksum = "152f3863635cbb76b73bc247845781098302c6c9ad2060e1a9a7de56840346b6"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@ -2530,18 +2521,18 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2988,6 +2979,20 @@ dependencies = [
"void", "void",
] ]
[[package]]
name = "untis"
version = "0.1.0"
source = "git+https://git.dergrimm.net/dergrimm/untis.rs.git?branch=main#a6bc2bd2e947d73f18c7a118ba4836c4ea7e6531"
dependencies = [
"anyhow",
"chrono",
"cookie",
"reqwest",
"serde",
"serde_json",
"url",
]
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"

View file

@ -29,11 +29,11 @@ juniper = "0.15.10"
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"
now = "0.1.3"
reqwest = { version = "0.11.13", features = ["json"] } reqwest = { version = "0.11.13", features = ["json"] }
serde = "1.0.148" serde = "1.0.148"
serde_json = "1.0.89" serde_json = "1.0.89"
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" }
url = "2.3.1" url = "2.3.1"
[target.'cfg(not(target_env = "msvc"))'.dependencies] [target.'cfg(not(target_env = "msvc"))'.dependencies]

View file

@ -3,7 +3,7 @@ WORKDIR /usr/src/backend
FROM chef as planner FROM chef as planner
RUN mkdir src && touch src/main.rs RUN mkdir src && touch src/main.rs
COPY ./Cargo.toml . 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

View file

@ -1,9 +1,3 @@
DROP TABLE substitution_query_results;
DROP TABLE substitution_queries;
DROP TABLE substitution_planned_queries;
DROP TABLE substitution_room; DROP TABLE substitution_room;
DROP TABLE substitution_subject; DROP TABLE substitution_subject;
@ -16,6 +10,8 @@ DROP TABLE substitutions;
DROP TYPE substitution_type; DROP TYPE substitution_type;
DROP TABLE substitution_queries;
DROP TABLE timegrid_time_unit; DROP TABLE timegrid_time_unit;
DROP TABLE timegrid_days; DROP TABLE timegrid_days;
@ -34,6 +30,8 @@ DROP TABLE classes;
DROP TABLE teachers; DROP TABLE teachers;
DROP INDEX tenants_active;
DROP TABLE tenants; DROP TABLE tenants;
DROP TABLE schoolyears; DROP TABLE schoolyears;

View file

@ -18,7 +18,7 @@ CREATE TABLE tenants(
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE UNIQUE INDEX ON tenants(active) CREATE UNIQUE INDEX tenants_active ON tenants(active)
WHERE WHERE
active; active;
@ -110,6 +110,17 @@ CREATE TABLE timegrid_time_unit(
updated_at TIMESTAMP updated_at TIMESTAMP
); );
CREATE TABLE substitution_queries(
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 TYPE substitution_type AS ENUM ( CREATE TYPE substitution_type AS ENUM (
'cancel', 'cancel',
'subst', 'subst',
@ -130,6 +141,7 @@ CREATE TYPE substitution_type AS ENUM (
CREATE TABLE substitutions( CREATE TABLE substitutions(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
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,
@ -142,6 +154,7 @@ CREATE TABLE substitutions(
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,
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
@ -150,7 +163,8 @@ CREATE TABLE substitution_classes(
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),
teacher_id INTEGER NOT NULL REFERENCES teachers(id), position SMALLINT NOT NULL,
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
); );
@ -158,6 +172,7 @@ CREATE TABLE substitution_teachers(
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,
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
@ -166,34 +181,8 @@ CREATE TABLE substitution_subjects(
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),
room_id INTEGER NOT NULL REFERENCES rooms(id), position SMALLINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, room_id INTEGER REFERENCES rooms(id),
updated_at TIMESTAMP
);
CREATE TABLE substitution_planned_queries(
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 ON substitution_planned_queries(active);
CREATE TABLE substitution_queries(
id SERIAL PRIMARY KEY,
substitution_planned_query_id INTEGER NOT NULL REFERENCES substitution_planned_queries(id),
queried_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE substitution_query_results(
id SERIAL PRIMARY KEY,
substitution_query_id INTEGER NOT NULL REFERENCES substitution_queries(id),
substitution_id INTEGER NOT NULL REFERENCES substitutions(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP updated_at TIMESTAMP
); );

View file

@ -1,5 +1,7 @@
use anyhow::Result;
use envconfig::Envconfig; use envconfig::Envconfig;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use url::Url;
#[derive(Envconfig, Debug)] #[derive(Envconfig, Debug)]
pub struct Config { pub struct Config {
@ -15,6 +17,9 @@ pub struct Config {
#[envconfig(from = "BACKEND_UNTIS_RPC_URL")] #[envconfig(from = "BACKEND_UNTIS_RPC_URL")]
pub untis_rpc_url: String, pub untis_rpc_url: String,
#[envconfig(from = "BACKEND_UNTIS_CLIENT_NAME")]
pub untis_client_name: String,
#[envconfig(from = "BACKEND_UNTIS_SCHOOL")] #[envconfig(from = "BACKEND_UNTIS_SCHOOL")]
pub untis_school: String, pub untis_school: String,
@ -28,3 +33,18 @@ pub struct Config {
lazy_static! { lazy_static! {
pub static ref CONFIG: Config = Config::init_from_env().unwrap(); pub static ref CONFIG: Config = Config::init_from_env().unwrap();
} }
pub fn untis_from_env() -> Result<untis::Client> {
Ok(untis::Client {
api_url: Url::parse(&CONFIG.untis_api_url)?,
rpc_url: untis::Client::gen_rpc_url(
Url::parse(&CONFIG.untis_rpc_url)?,
&CONFIG.untis_school,
),
client_name: CONFIG.untis_client_name.to_owned(),
username: CONFIG.untis_username.to_owned(),
password: CONFIG.untis_password.to_owned(),
session: None,
authorization: None,
})
}

View file

@ -3,7 +3,6 @@ use diesel::prelude::*;
use std::io::Write; use std::io::Write;
use crate::db::schema; use crate::db::schema;
use crate::untis;
#[derive(Identifiable, Queryable, Debug)] #[derive(Identifiable, Queryable, Debug)]
#[diesel(table_name = schema::schoolyears)] #[diesel(table_name = schema::schoolyears)]
@ -286,8 +285,29 @@ impl From<untis::RpcSubstitionType> for SubstitutionType {
} }
} }
#[derive(Identifiable, Queryable, Debug)] #[derive(Identifiable, Queryable, Associations, Debug)]
#[diesel(table_name = schema::substitution_queries)]
#[diesel(belongs_to(Schoolyear))]
pub struct SubstitutionQuery {
pub id: i32,
pub schoolyear_id: i32,
pub date: NaiveDate,
pub active: bool,
pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = schema::substitution_queries)]
pub struct NewSubstitutionQuery {
pub schoolyear_id: i32,
pub date: NaiveDate,
pub active: bool,
}
#[derive(Identifiable, Queryable, Associations, Debug)]
#[diesel(table_name = schema::substitutions)] #[diesel(table_name = schema::substitutions)]
#[diesel(belongs_to(SubstitutionQuery))]
pub struct Substitution { pub struct Substitution {
pub id: i32, pub id: i32,
pub substitution_query_id: i32, pub substitution_query_id: i32,
@ -304,12 +324,12 @@ 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_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,
pub end_time: NaiveTime, pub end_time: NaiveTime,
pub text: Option<&'a str>, pub text: Option<&'a str>,
pub active: bool,
} }
#[derive(Identifiable, Queryable, Associations, Debug)] #[derive(Identifiable, Queryable, Associations, Debug)]
@ -319,6 +339,7 @@ pub struct NewSubstitution<'a> {
pub struct SubstitutionClass { pub struct SubstitutionClass {
pub id: i32, pub id: i32,
pub substitution_id: i32, pub substitution_id: i32,
pub position: i16,
pub class_id: i32, pub class_id: i32,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
@ -328,6 +349,7 @@ pub struct SubstitutionClass {
#[diesel(table_name = schema::substitution_classes)] #[diesel(table_name = schema::substitution_classes)]
pub struct NewSubstitutionClass { pub struct NewSubstitutionClass {
pub substitution_id: i32, pub substitution_id: i32,
pub position: i16,
pub class_id: i32, pub class_id: i32,
} }
@ -338,7 +360,8 @@ pub struct NewSubstitutionClass {
pub struct SubstitutionTeacher { pub struct SubstitutionTeacher {
pub id: i32, pub id: i32,
pub substitution_id: i32, pub substitution_id: i32,
pub teacher_id: i32, pub position: i16,
pub teacher_id: Option<i32>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
} }
@ -347,7 +370,8 @@ pub struct SubstitutionTeacher {
#[diesel(table_name = schema::substitution_teachers)] #[diesel(table_name = schema::substitution_teachers)]
pub struct NewSubstitutionTeacher { pub struct NewSubstitutionTeacher {
pub substitution_id: i32, pub substitution_id: i32,
pub teacher_id: i32, pub position: i16,
pub teacher_id: Option<i32>,
} }
#[derive(Identifiable, Queryable, Associations, Debug)] #[derive(Identifiable, Queryable, Associations, Debug)]
@ -357,6 +381,7 @@ pub struct NewSubstitutionTeacher {
pub struct SubstitutionSubject { pub struct SubstitutionSubject {
pub id: i32, pub id: i32,
pub substitution_id: i32, pub substitution_id: i32,
pub position: i16,
pub subject_id: i32, pub subject_id: i32,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
@ -366,6 +391,7 @@ pub struct SubstitutionSubject {
#[diesel(table_name = schema::substitution_subjects)] #[diesel(table_name = schema::substitution_subjects)]
pub struct NewSubstitutionSubject { pub struct NewSubstitutionSubject {
pub substitution_id: i32, pub substitution_id: i32,
pub position: i16,
pub subject_id: i32, pub subject_id: i32,
} }
@ -376,7 +402,9 @@ pub struct NewSubstitutionSubject {
pub struct SubstitutionRoom { pub struct SubstitutionRoom {
pub id: i32, pub id: i32,
pub substitution_id: i32, pub substitution_id: i32,
pub room_id: i32, pub position: i16,
pub index: i16,
pub room_id: Option<i32>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
} }
@ -385,62 +413,6 @@ pub struct SubstitutionRoom {
#[diesel(table_name = schema::substitution_rooms)] #[diesel(table_name = schema::substitution_rooms)]
pub struct NewSubstitutionRoom { pub struct NewSubstitutionRoom {
pub substitution_id: i32, pub substitution_id: i32,
pub room_id: i32, pub position: i16,
} pub room_id: Option<i32>,
#[derive(Identifiable, Queryable, Associations, Debug)]
#[diesel(table_name = schema::substitution_planned_queries)]
#[diesel(belongs_to(Schoolyear))]
pub struct SubstitutionPlannedQuery {
pub id: i32,
pub schoolyear_id: i32,
pub date: NaiveDate,
pub active: bool,
pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = schema::substitution_planned_queries)]
pub struct NewSubstitutionPlannedQuery {
pub schoolyear_id: i32,
pub date: NaiveDate,
pub active: bool,
}
#[derive(Identifiable, Queryable, Associations, Debug)]
#[diesel(table_name = schema::substitution_queries)]
#[diesel(belongs_to(SubstitutionPlannedQuery))]
pub struct SubstitutionQuery {
pub id: i32,
pub substitution_planned_query_id: i32,
pub queried_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = schema::substitution_queries)]
pub struct NewSubstitutionQuery {
pub substitution_planned_query_id: i32,
pub queried_at: NaiveDateTime,
}
#[derive(Identifiable, Queryable, Associations, Debug)]
#[diesel(table_name = schema::substitution_query_results)]
#[diesel(belongs_to(SubstitutionQuery))]
#[diesel(belongs_to(Substitution))]
pub struct SubstitutionQueryResult {
pub id: i32,
pub substitution_query_id: i32,
pub substitution_id: i32,
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 substitution_id: i32,
} }

View file

@ -136,65 +136,7 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
use diesel::sql_types::*; substitution_queries {
use super::sql_types::SubstitutionType;
substitutions {
id -> Integer,
subst_type -> SubstitutionType,
lesson_id -> Integer,
start_time -> Time,
end_time -> Time,
text -> Nullable<VarChar>,
active -> Bool,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_classes {
id -> Integer,
substitution_id -> Integer,
class_id -> Integer,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_teachers {
id -> Integer,
substitution_id -> Integer,
teacher_id -> Integer,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_subjects {
id -> Integer,
substitution_id -> Integer,
subject_id -> Integer,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_rooms {
id -> Integer,
substitution_id -> Integer,
room_id -> Integer,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_planned_queries {
id -> Integer, id -> Integer,
schoolyear_id -> Integer, schoolyear_id -> Integer,
date -> Date, date -> Date,
@ -205,20 +147,62 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
substitution_queries { use diesel::sql_types::*;
use super::sql_types::SubstitutionType;
substitutions {
id -> Integer, id -> Integer,
substitution_planned_query_id -> Integer, substitution_query_id -> Integer,
queried_at -> Timestamp, subst_type -> SubstitutionType,
lesson_id -> Integer,
start_time -> Time,
end_time -> Time,
text -> Nullable<VarChar>,
created_at -> Timestamp, created_at -> Timestamp,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
} }
} }
diesel::table! { diesel::table! {
substitution_query_results { substitution_classes {
id -> Integer, id -> Integer,
substitution_query_id -> Integer,
substitution_id -> Integer, substitution_id -> Integer,
position -> SmallInt,
class_id -> Integer,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_teachers {
id -> Integer,
substitution_id -> Integer,
position -> SmallInt,
teacher_id -> Nullable<Integer>,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_subjects {
id -> Integer,
substitution_id -> Integer,
position -> SmallInt,
subject_id -> Integer,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::table! {
substitution_rooms {
id -> Integer,
substitution_id -> Integer,
position -> SmallInt,
room_id -> Nullable<Integer>,
created_at -> Timestamp, created_at -> Timestamp,
updated_at -> Nullable<Timestamp>, updated_at -> Nullable<Timestamp>,
} }

View file

@ -1,5 +1,4 @@
pub mod config; pub mod config;
pub mod db; pub mod db;
pub mod graphql; pub mod graphql;
pub mod untis;
pub mod worker; pub mod worker;

View file

@ -1,717 +0,0 @@
use anyhow::{bail, Context, Result};
use chrono::Datelike;
use cookie::Cookie;
use serde::Deserialize;
use serde_json::json;
pub const CLIENT_NAME: &str = "BVplan@OHG-Furtwangen";
fn deserialize_date<'de, D>(deserializer: D) -> Result<chrono::NaiveDate, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = chrono::NaiveDate;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a naive date serialized as an unsigned integer")
}
fn visit_u64<E>(self, mut v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let y = (v / 10000) as i32;
v %= 10000;
let m = (v / 100) as u32;
v %= 100;
match chrono::NaiveDate::from_ymd_opt(y, m, v as u32) {
Some(x) => Ok(x),
None => Err(E::custom(format!("No such date: {}-{}-{}", y, m, v))),
}
}
}
deserializer.deserialize_u64(Visitor)
}
fn serialize_date(date: chrono::NaiveDate) -> u64 {
date.year() as u64 * 10000 + date.month() as u64 * 100 + date.day() as u64
}
fn deserialize_time<'de, D>(deserializer: D) -> Result<chrono::NaiveTime, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = chrono::NaiveTime;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a naive time serialized as an unsigned integer")
}
fn visit_u64<E>(self, mut v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let h = v / 100;
v %= 100;
match chrono::NaiveTime::from_hms_opt(h as u32, v as u32, 0) {
Some(x) => Ok(x),
None => Err(E::custom(format!("No such time: {}:{}", h, v))),
}
}
}
deserializer.deserialize_u64(Visitor)
}
#[derive(Deserialize, Debug)]
pub struct RpcError {
pub message: String,
pub code: i32,
}
#[derive(Deserialize, Debug)]
pub struct RpcResponse<T> {
pub jsonrpc: String,
pub id: String,
pub result: Option<T>,
pub error: Option<RpcError>,
}
#[derive(Deserialize, Debug)]
struct RpcEmptyResult;
#[derive(Deserialize, Debug)]
pub struct RpcLogin {
#[serde(rename = "sessionId")]
pub session_id: String,
#[serde(rename = "personType")]
pub person_type: i32,
#[serde(rename = "personId")]
pub person_id: i32,
#[serde(rename = "klasseId")]
pub class_id: i32,
}
#[derive(Deserialize, Debug)]
pub struct RpcClass {
pub id: i32,
pub name: String,
#[serde(rename = "longName")]
pub long_name: String,
pub active: bool,
}
#[derive(Deserialize, Debug)]
pub struct RpcSubject {
pub id: i32,
pub name: String,
#[serde(rename = "longName")]
pub long_name: String,
}
#[derive(Deserialize, Debug)]
pub struct RpcRoom {
pub id: i32,
pub name: String,
#[serde(rename = "longName")]
pub long_name: String,
}
#[derive(Deserialize, Debug)]
pub struct RpcDepartment {
pub id: i32,
pub name: String,
#[serde(rename = "longName")]
pub long_name: String,
}
#[derive(Deserialize, Debug)]
pub struct RpcHoliday {
pub id: i32,
pub name: String,
#[serde(rename = "longName")]
pub long_name: String,
#[serde(rename = "startDate", deserialize_with = "deserialize_date")]
pub start_date: chrono::NaiveDate,
#[serde(rename = "endDate", deserialize_with = "deserialize_date")]
pub end_date: chrono::NaiveDate,
}
#[derive(Deserialize, Debug)]
pub struct RpcTimegridDayTimeUnit {
#[serde(rename = "startTime", deserialize_with = "deserialize_time")]
pub start_time: chrono::NaiveTime,
#[serde(rename = "endTime", deserialize_with = "deserialize_time")]
pub end_time: chrono::NaiveTime,
}
#[derive(Deserialize, Debug)]
pub struct RpcTimegridDay {
pub day: u8,
#[serde(rename = "timeUnits")]
pub time_units: Vec<RpcTimegridDayTimeUnit>,
}
#[derive(Deserialize, Debug)]
pub struct RpcSchoolyear {
pub id: i32,
pub name: String,
#[serde(rename = "startDate", deserialize_with = "deserialize_date")]
pub start_date: chrono::NaiveDate,
#[serde(rename = "endDate", deserialize_with = "deserialize_date")]
pub end_date: chrono::NaiveDate,
}
#[derive(Deserialize, Clone, Copy, Debug)]
pub enum RpcSubstitionType {
#[serde(rename = "cancel")]
Cancel,
#[serde(rename = "subst")]
Substitution,
#[serde(rename = "add")]
Additional,
#[serde(rename = "shift")]
Shifted,
#[serde(rename = "rmchg")]
RoomChange,
#[serde(rename = "rmlk")]
Locked,
#[serde(rename = "bs")]
BreakSupervision,
#[serde(rename = "oh")]
OfficeHour,
#[serde(rename = "sb")]
Standby,
#[serde(rename = "other")]
Other,
#[serde(rename = "free")]
Free,
#[serde(rename = "exam")]
Exam,
#[serde(rename = "ac")]
Activity,
#[serde(rename = "holi")]
Holiday,
#[serde(rename = "stxt")]
Text,
}
#[derive(Deserialize, Debug)]
pub struct RpcSubstitutionId {
pub id: i32,
pub name: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct RpcSubstitutionReschedule {
#[serde(deserialize_with = "deserialize_date")]
pub date: chrono::NaiveDate,
#[serde(rename = "startTime", deserialize_with = "deserialize_time")]
pub start_time: chrono::NaiveTime,
#[serde(rename = "endTime", deserialize_with = "deserialize_time")]
pub end_time: chrono::NaiveTime,
}
#[derive(Deserialize, Debug)]
pub struct RpcSubstitution {
#[serde(rename = "type")]
pub subst_type: RpcSubstitionType,
#[serde(rename = "lsid")]
pub lesson_id: i32,
#[serde(rename = "startTime", deserialize_with = "deserialize_time")]
pub start_time: chrono::NaiveTime,
#[serde(rename = "endTime", deserialize_with = "deserialize_time")]
pub end_time: chrono::NaiveTime,
#[serde(rename = "txt")]
pub text: Option<String>,
#[serde(rename = "kl")]
pub classes: Vec<RpcSubstitutionId>,
#[serde(rename = "te")]
pub teachers: Vec<RpcSubstitutionId>,
#[serde(rename = "su")]
pub subjects: Vec<RpcSubstitutionId>,
#[serde(rename = "ro")]
pub rooms: Vec<RpcSubstitutionId>,
pub reschedule: Option<RpcSubstitutionReschedule>,
}
#[derive(Deserialize, Debug)]
pub struct ApiTenant {
pub id: i32,
#[serde(rename = "displayName")]
pub display_name: String,
}
#[derive(Deserialize, Debug)]
struct ApiDataResponse {
tenant: ApiTenant,
}
#[derive(Deserialize, Debug)]
pub struct ApiTeacher {
pub id: i32,
pub name: String,
pub forename: String,
#[serde(rename = "longName")]
pub long_name: String,
#[serde(rename = "displayname")]
pub display_name: String,
}
#[derive(Deserialize, Debug)]
struct ApiTeachersResponseData {
elements: Vec<ApiTeacher>,
}
#[derive(Deserialize, Debug)]
struct ApiTeachersResponse {
data: ApiTeachersResponseData,
}
#[derive(Debug)]
pub struct Client {
pub api_url: url::Url,
pub rpc_url: url::Url,
pub username: String,
pub password: String,
pub session: Option<String>,
pub authorization: Option<String>,
}
impl Client {
pub fn gen_rpc_url(mut endpoint: url::Url, school: &str) -> url::Url {
endpoint.query_pairs_mut().append_pair("school", school);
endpoint
}
pub async fn login_rpc(&mut self) -> Result<RpcLogin> {
let resp: RpcResponse<RpcLogin> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.json(&json!({
"id": "ID",
"method": "authenticate",
"params": {
"user": self.username,
"password": self.password,
"client": CLIENT_NAME,
},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
self.session = Some(
Cookie::new(
"JSESSIONID",
&resp
.result
.as_ref()
.context("Result null event though error not")?
.session_id,
)
.to_string(),
);
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn login_api(&mut self) -> Result<String> {
let jwt = reqwest::Client::new()
.get(self.api_url.join("token/new")?)
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.send()
.await?
.text()
.await?;
self.authorization = Some(jwt.to_string());
Ok(jwt)
}
pub async fn login(&mut self) -> Result<()> {
self.login_rpc().await?;
self.login_api().await?;
Ok(())
}
pub async fn logout(&mut self) -> Result<()> {
let resp: RpcResponse<RpcEmptyResult> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "logout",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
self.session = None;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
Ok(())
}
pub async fn classes(&self) -> Result<Vec<RpcClass>> {
let resp: RpcResponse<Vec<RpcClass>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getKlassen",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn subjects(&self) -> Result<Vec<RpcSubject>> {
let resp: RpcResponse<Vec<RpcSubject>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getSubjects",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn rooms(&self) -> Result<Vec<RpcRoom>> {
let resp: RpcResponse<Vec<RpcRoom>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getRooms",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn departments(&self) -> Result<Vec<RpcDepartment>> {
let resp: RpcResponse<Vec<RpcDepartment>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getDepartments",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn holidays(&self) -> Result<Vec<RpcHoliday>> {
let resp: RpcResponse<Vec<RpcHoliday>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getHolidays",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn timegrid(&self) -> Result<Vec<RpcTimegridDay>> {
let resp: RpcResponse<Vec<RpcTimegridDay>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getTimegridUnits",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn current_schoolyear(&self) -> Result<RpcSchoolyear> {
let resp: RpcResponse<RpcSchoolyear> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getCurrentSchoolyear",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn schoolyears(&self) -> Result<Vec<RpcSchoolyear>> {
let resp: RpcResponse<Vec<RpcSchoolyear>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getSchoolyears",
"params": {},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub async fn substitutions(
&self,
from: &chrono::NaiveDate,
to: &chrono::NaiveDate,
department: Option<i32>,
) -> Result<Vec<RpcSubstitution>> {
let resp: RpcResponse<Vec<RpcSubstitution>> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.header(reqwest::header::ACCEPT, "application/json-rpc")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.json(&json!({
"id": "ID",
"method": "getSubstitutions",
"params": {
"startDate": serialize_date(*from),
"endDate": serialize_date(*to),
"departmentId": department.map_or(0, |d| d)
},
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(x)
} else {
bail!("RPC result is null");
}
}
pub fn construct_bearer(auth: &str) -> String {
format!("Bearer {}", auth)
}
pub async fn current_tenant(&self) -> Result<ApiTenant> {
let resp: ApiDataResponse = reqwest::Client::new()
.get(self.api_url.join("rest/view/v1/app/data")?)
.header(reqwest::header::ACCEPT, "application/json")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.header(
reqwest::header::AUTHORIZATION,
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
)
.send()
.await?
.json()
.await?;
Ok(resp.tenant)
}
pub async fn teachers(&self) -> Result<Vec<ApiTeacher>> {
let mut url = self.api_url.join("public/timetable/weekly/pageconfig")?;
url.query_pairs_mut().append_pair("type", "2");
let resp: ApiTeachersResponse = reqwest::Client::new()
.get(url)
.header(reqwest::header::ACCEPT, "application/json")
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.header(
reqwest::header::AUTHORIZATION,
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
)
.send()
.await?
.json()
.await?;
Ok(resp.data.elements)
}
pub async fn exams(&self) -> Result<()> {
Ok(())
}
}

View file

@ -3,11 +3,8 @@ 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 now::DateTimeNow;
use crate::config; use crate::{config, db};
use crate::db;
use crate::untis;
async fn fetch_schoolyears(client: &untis::Client, conn: &mut PgConnection) -> Result<i32> { async fn fetch_schoolyears(client: &untis::Client, conn: &mut PgConnection) -> Result<i32> {
let existing_schoolyears = db::schema::schoolyears::table let existing_schoolyears = db::schema::schoolyears::table
@ -272,42 +269,140 @@ async fn fetch_substitutions(
conn: &mut PgConnection, conn: &mut PgConnection,
schoolyear_id: i32, schoolyear_id: i32,
) -> Result<()> { ) -> Result<()> {
let now = Utc::now(); let today = Utc::now().date_naive();
let beginning_of_week = now.beginning_of_week().date_naive(); if diesel::select(diesel::dsl::not(diesel::expression::exists::exists(
let end_of_week = now.end_of_week().date_naive(); db::schema::substitution_queries::table
// if diesel::select(diesel::dsl::not(diesel::expression::exists::exists( .filter(db::schema::substitution_queries::date.eq(today)),
// db::schema::substitution_queries::table )))
// .filter(db::schema::substitution_queries::active) .get_result::<bool>(conn)?
// .filter(db::schema::substitution_queries::end_date.eq(end_of_week)), {
// ))) diesel::insert_into(db::schema::substitution_queries::table)
// .get_result::<bool>(conn)? .values(db::models::NewSubstitutionQuery {
// {} schoolyear_id,
date: today,
active: true,
})
.execute(conn)?;
}
// for query in db::schema::substitution_queries::table for query in db::schema::substitution_queries::table
// .filter(db::schema::substitution_queries::active) .filter(db::schema::substitution_queries::active)
// .load::<db::models::SubstitutionQuery>(conn)? .load::<db::models::SubstitutionQuery>(conn)?
// { {
// dbg!(&query); let now = Utc::now().naive_utc();
// } let substs = client.substitutions(&query.date, &query.date, None).await?;
for substitution in substs {
let substitution_id = diesel::insert_into(db::schema::substitutions::table)
.values(db::models::NewSubstitution {
substitution_query_id: 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(),
})
.returning(db::schema::substitutions::id)
.get_result::<i32>(conn)?;
diesel::insert_into(db::schema::substitution_classes::table)
.values(
&substitution
.classes
.iter()
.enumerate()
.map(|(i, c)| {
Ok(db::models::NewSubstitutionClass {
substitution_id,
position: i as i16,
class_id: db::schema::classes::table
.filter(db::schema::classes::untis_id.eq(c.id))
.select(db::schema::classes::id)
.get_result::<i32>(conn)?,
})
})
.collect::<Result<Vec<db::models::NewSubstitutionClass>>>()?,
)
.execute(conn)?;
diesel::insert_into(db::schema::substitution_teachers::table)
.values(
&substitution
.teachers
.iter()
.enumerate()
.map(|(i, t)| {
Ok(db::models::NewSubstitutionTeacher {
substitution_id,
position: i as i16,
teacher_id: if t.id == 0 {
None
} else {
Some(
db::schema::teachers::table
.filter(db::schema::teachers::untis_id.eq(t.id))
.select(db::schema::teachers::id)
.get_result::<i32>(conn)?,
)
},
})
})
.collect::<Result<Vec<db::models::NewSubstitutionTeacher>>>()?,
)
.execute(conn)?;
diesel::insert_into(db::schema::substitution_subjects::table)
.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(())
} }
#[celery::task] #[celery::task]
pub async fn update_info() -> TaskResult<()> { pub async fn update_info() -> TaskResult<()> {
let mut client = untis::Client { let mut client = match config::untis_from_env() {
api_url: match url::Url::parse(&config::CONFIG.untis_api_url) {
Ok(x) => x, Ok(x) => x,
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())), Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
},
rpc_url: match url::Url::parse(&config::CONFIG.untis_rpc_url) {
Ok(x) => untis::Client::gen_rpc_url(x, &config::CONFIG.untis_school),
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
},
username: config::CONFIG.untis_username.to_owned(),
password: config::CONFIG.untis_password.to_owned(),
session: None,
authorization: None,
}; };
if let Err(e) = client.login().await { if let Err(e) = client.login().await {

View file

@ -1,5 +1,25 @@
version: "3" version: "3"
x-backend:
&backend
image: git.dergrimm.net/dergrimm/bvplan_backend:latest
build:
context: ./backend
restart: always
command: worker
depends_on:
- postgres
- rabbitmq
environment:
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_USER}
BACKEND_AMQP_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
BACKEND_UNTIS_API_URL: ${BACKEND_UNTIS_API_URL}
BACKEND_UNTIS_RPC_URL: ${BACKEND_UNTIS_RPC_URL}
BACKEND_UNTIS_CLIENT_NAME: ${BACKEND_UNTIS_CLIENT_NAME}
BACKEND_UNTIS_SCHOOL: ${BACKEND_UNTIS_SCHOOL}
BACKEND_UNTIS_USERNAME: ${BACKEND_UNTIS_USERNAME}
BACKEND_UNTIS_PASSWORD: ${BACKEND_UNTIS_PASSWORD}
services: services:
nginx: nginx:
image: docker.io/byjg/nginx-extras image: docker.io/byjg/nginx-extras
@ -38,41 +58,12 @@ services:
- rabbitmq:/var/lib/rabbitmq - rabbitmq:/var/lib/rabbitmq
worker: worker:
image: git.dergrimm.net/dergrimm/bvplan_backend:latest <<: *backend
build:
context: ./backend
restart: always
command: worker command: worker
depends_on:
- postgres
- rabbitmq
environment:
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_USER}
BACKEND_AMQP_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
BACKEND_UNTIS_API_URL: ${BACKEND_UNTIS_API_URL}
BACKEND_UNTIS_RPC_URL: ${BACKEND_UNTIS_RPC_URL}
BACKEND_UNTIS_SCHOOL: ${BACKEND_UNTIS_SCHOOL}
BACKEND_UNTIS_USERNAME: ${BACKEND_UNTIS_USERNAME}
BACKEND_UNTIS_PASSWORD: ${BACKEND_UNTIS_PASSWORD}
api: api:
image: git.dergrimm.net/dergrimm/bvplan_backend:latest <<: *backend
build:
context: ./backend
restart: always
command: api command: api
depends_on:
- postgres
- rabbitmq
- worker
environment:
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_USER}
BACKEND_AMQP_URL: amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672
BACKEND_UNTIS_API_URL: ${BACKEND_UNTIS_API_URL}
BACKEND_UNTIS_RPC_URL: ${BACKEND_UNTIS_RPC_URL}
BACKEND_UNTIS_SCHOOL: ${BACKEND_UNTIS_SCHOOL}
BACKEND_UNTIS_USERNAME: ${BACKEND_UNTIS_USERNAME}
BACKEND_UNTIS_PASSWORD: ${BACKEND_UNTIS_PASSWORD}
volumes: volumes:
postgres: postgres: