Init
This commit is contained in:
commit
2b65b6a1c5
26 changed files with 5911 additions and 0 deletions
25
.drone.yml
Normal file
25
.drone.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: ameba
|
||||||
|
image: veelenga/ameba
|
||||||
|
commands:
|
||||||
|
- cd bvplan
|
||||||
|
- ameba src
|
||||||
|
- name: build
|
||||||
|
image: tmaier/docker-compose
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- docker-compose build --build-arg BUILD_ENV=development bvplan
|
||||||
|
depends_on:
|
||||||
|
- ameba
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
11
.env.example
Normal file
11
.env.example
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
POSTGRES_USER="bvplan"
|
||||||
|
POSTGRES_PASSWORD="bvplan"
|
||||||
|
|
||||||
|
RABBITMQ_USER="bvplan"
|
||||||
|
RABBITMQ_PASSWORD="bvplan"
|
||||||
|
|
||||||
|
BACKEND_UNTIS_API_URL="https://mese.webuntis.com/WebUntis/api/"
|
||||||
|
BACKEND_UNTIS_RPC_URL="https://mese.webuntis.com/WebUntis/jsonrpc.do"
|
||||||
|
BACKEND_UNTIS_SCHOOL=
|
||||||
|
BACKEND_UNTIS_USERNAME=
|
||||||
|
BACKEND_UNTIS_PASSWORD=
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*.env
|
9
Makefile
Normal file
9
Makefile
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.PHONY: all dev prod
|
||||||
|
|
||||||
|
all: prod
|
||||||
|
|
||||||
|
dev:
|
||||||
|
docker-compose build --build-arg BUILD_ENV=development
|
||||||
|
|
||||||
|
prod:
|
||||||
|
docker-compose build
|
21
backend/.dockerignore
Normal file
21
backend/.dockerignore
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
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
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
dist/
|
||||||
|
Dockerfile
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
vendor/
|
||||||
|
.editorconfig
|
9
backend/.editorconfig
Normal file
9
backend/.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
3337
backend/Cargo.lock
generated
Normal file
3337
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
40
backend/Cargo.toml
Normal file
40
backend/Cargo.toml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
[package]
|
||||||
|
name = "backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "api"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "worker"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = "fat"
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-cors = "0.6.4"
|
||||||
|
actix-web = "4.2.1"
|
||||||
|
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
||||||
|
celery = { git = "https://github.com/rusty-celery/rusty-celery.git", branch = "main" }
|
||||||
|
chrono = { version = "0.4.23", features = ["serde"] }
|
||||||
|
cookie = "0.16.1"
|
||||||
|
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"
|
||||||
|
envconfig = "0.10.0"
|
||||||
|
juniper = "0.15.10"
|
||||||
|
juniper_actix = "0.4.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
log = "0.4.17"
|
||||||
|
now = "0.1.3"
|
||||||
|
reqwest = { version = "0.11.13", features = ["json"] }
|
||||||
|
serde = "1.0.148"
|
||||||
|
serde_json = "1.0.89"
|
||||||
|
tokio = { version = "1.22.0", features = ["full"] }
|
||||||
|
url = "2.3.1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||||
|
tikv-jemallocator = "0.5"
|
31
backend/Dockerfile
Normal file
31
backend/Dockerfile
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
FROM lukemathwalker/cargo-chef:latest-rust-1.65.0 as chef
|
||||||
|
WORKDIR /usr/src/backend
|
||||||
|
|
||||||
|
FROM chef as planner
|
||||||
|
RUN mkdir src && touch src/main.rs
|
||||||
|
COPY ./Cargo.toml .
|
||||||
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
|
FROM chef as builder
|
||||||
|
RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
COPY --from=planner /usr/src/backend/recipe.json .
|
||||||
|
RUN cargo chef cook --release --recipe-path recipe.json
|
||||||
|
COPY ./src ./src
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM debian:buster-slim as runner
|
||||||
|
RUN apt update
|
||||||
|
RUN apt install -y libpq5
|
||||||
|
RUN apt install -y ca-certificates
|
||||||
|
RUN apt-get clean
|
||||||
|
RUN apt-get autoremove --yes
|
||||||
|
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
|
||||||
|
WORKDIR /usr/local/bin
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/diesel .
|
||||||
|
WORKDIR /usr/src/backend
|
||||||
|
COPY ./run.sh .
|
||||||
|
RUN chmod +x ./run.sh
|
||||||
|
COPY ./migrations ./migrations
|
||||||
|
COPY --from=builder /usr/src/backend/target/release/api /usr/src/backend/target/release/worker ./bin/
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT [ "./run.sh" ]
|
39
backend/migrations/2022-12-03-124501_init/down.sql
Normal file
39
backend/migrations/2022-12-03-124501_init/down.sql
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
DROP TABLE substitution_query_results;
|
||||||
|
|
||||||
|
DROP TABLE substitution_queries;
|
||||||
|
|
||||||
|
DROP TABLE substitution_planned_queries;
|
||||||
|
|
||||||
|
DROP TABLE substitution_room;
|
||||||
|
|
||||||
|
DROP TABLE substitution_subject;
|
||||||
|
|
||||||
|
DROP TABLE substitution_teacher;
|
||||||
|
|
||||||
|
DROP TABLE substitution_class;
|
||||||
|
|
||||||
|
DROP TABLE substitutions;
|
||||||
|
|
||||||
|
DROP TYPE substitution_type;
|
||||||
|
|
||||||
|
DROP TABLE timegrid_time_unit;
|
||||||
|
|
||||||
|
DROP TABLE timegrid_days;
|
||||||
|
|
||||||
|
DROP TABLE timegrids;
|
||||||
|
|
||||||
|
DROP TABLE holidays;
|
||||||
|
|
||||||
|
DROP TABLE departments;
|
||||||
|
|
||||||
|
DROP TABLE rooms;
|
||||||
|
|
||||||
|
DROP TABLE subjects;
|
||||||
|
|
||||||
|
DROP TABLE classes;
|
||||||
|
|
||||||
|
DROP TABLE teachers;
|
||||||
|
|
||||||
|
DROP TABLE tenants;
|
||||||
|
|
||||||
|
DROP TABLE schoolyears;
|
199
backend/migrations/2022-12-03-124501_init/up.sql
Normal file
199
backend/migrations/2022-12-03-124501_init/up.sql
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
CREATE TABLE schoolyears(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tenants(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX ON tenants(active)
|
||||||
|
WHERE
|
||||||
|
active;
|
||||||
|
|
||||||
|
CREATE TABLE teachers(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
forename VARCHAR,
|
||||||
|
display_name VARCHAR NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE classes(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
long_name VARCHAR NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE subjects(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
long_name VARCHAR NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE rooms(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
long_name VARCHAR NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE departments(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
long_name VARCHAR NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE holidays(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
untis_id INTEGER NOT NULL UNIQUE,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
long_name VARCHAR NOT NULL,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE timegrids(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
schoolyear_id INTEGER NOT NULL REFERENCES schoolyears(id),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE timegrid_days(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
timegrid_id INTEGER NOT NULL REFERENCES timegrids(id),
|
||||||
|
day_index SMALLINT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE timegrid_time_unit(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
timegrid_day_id INTEGER NOT NULL REFERENCES timegrid_days(id),
|
||||||
|
start_time TIME NOT NULL,
|
||||||
|
end_time TIME NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE substitution_type AS ENUM (
|
||||||
|
'cancel',
|
||||||
|
'subst',
|
||||||
|
'add',
|
||||||
|
'shift',
|
||||||
|
'rmchg',
|
||||||
|
'rmlk',
|
||||||
|
'bs',
|
||||||
|
'oh',
|
||||||
|
'sb',
|
||||||
|
'other',
|
||||||
|
'free',
|
||||||
|
'exam',
|
||||||
|
'ac',
|
||||||
|
'holi',
|
||||||
|
'stxt'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE substitutions(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
subst_type substitution_type NOT NULL,
|
||||||
|
lesson_id INTEGER NOT NULL,
|
||||||
|
start_time TIME NOT NULL,
|
||||||
|
end_time TIME NOT NULL,
|
||||||
|
text VARCHAR,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE substitution_classes(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
substitution_id INTEGER NOT NULL REFERENCES substitutions(id),
|
||||||
|
class_id INTEGER NOT NULL REFERENCES classes(id),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE substitution_teachers(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
substitution_id INTEGER NOT NULL REFERENCES substitutions(id),
|
||||||
|
teacher_id INTEGER NOT NULL REFERENCES teachers(id),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE substitution_subjects(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
substitution_id INTEGER NOT NULL REFERENCES substitutions(id),
|
||||||
|
subject_id INTEGER NOT NULL REFERENCES subjects(id),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE substitution_rooms(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
substitution_id INTEGER NOT NULL REFERENCES substitutions(id),
|
||||||
|
room_id INTEGER NOT NULL REFERENCES rooms(id),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
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,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
6
backend/run.sh
Normal file
6
backend/run.sh
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DATABASE_URL="$BACKEND_DB_URL" diesel setup \
|
||||||
|
--migration-dir ./migrations \
|
||||||
|
--locked-schema &&
|
||||||
|
RUST_BACKTRACE=full "./bin/$1"
|
55
backend/src/bin/api.rs
Normal file
55
backend/src/bin/api.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#[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
|
||||||
|
}
|
37
backend/src/bin/worker.rs
Normal file
37
backend/src/bin/worker.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use std::thread;
|
||||||
|
#[cfg(not(target_env = "msvc"))]
|
||||||
|
use tikv_jemallocator::Jemalloc;
|
||||||
|
|
||||||
|
#[cfg(not(target_env = "msvc"))]
|
||||||
|
#[global_allocator]
|
||||||
|
static GLOBAL: Jemalloc = Jemalloc;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
std::env::set_var("RUST_LOG", "info");
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
println!("Starting worker!");
|
||||||
|
|
||||||
|
backend::worker::init_blocking();
|
||||||
|
|
||||||
|
thread::spawn(|| {
|
||||||
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
|
backend::worker::beat()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.start()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("asdnkjndfkjlewnflj");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = backend::worker::APP.lock().unwrap();
|
||||||
|
let app = app.as_ref().unwrap();
|
||||||
|
|
||||||
|
app.display_pretty().await;
|
||||||
|
app.consume_from(&[backend::worker::QUEUE_NAME])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
30
backend/src/config.rs
Normal file
30
backend/src/config.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
use envconfig::Envconfig;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
#[derive(Envconfig, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
#[envconfig(from = "BACKEND_DB_URL")]
|
||||||
|
pub db_url: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "BACKEND_AMQP_URL")]
|
||||||
|
pub amqp_url: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "BACKEND_UNTIS_API_URL")]
|
||||||
|
pub untis_api_url: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "BACKEND_UNTIS_RPC_URL")]
|
||||||
|
pub untis_rpc_url: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "BACKEND_UNTIS_SCHOOL")]
|
||||||
|
pub untis_school: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "BACKEND_UNTIS_USERNAME")]
|
||||||
|
pub untis_username: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "BACKEND_UNTIS_PASSWORD")]
|
||||||
|
pub untis_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref CONFIG: Config = Config::init_from_env().unwrap();
|
||||||
|
}
|
28
backend/src/db/mod.rs
Normal file
28
backend/src/db/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use diesel::pg::PgConnection;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
|
||||||
|
pub mod models;
|
||||||
|
pub mod schema;
|
||||||
|
|
||||||
|
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
|
pub fn establish_connection() -> ConnectionResult<PgConnection> {
|
||||||
|
PgConnection::establish(&config::CONFIG.db_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pool() -> Result<DbPool> {
|
||||||
|
Ok(
|
||||||
|
Pool::builder().build(ConnectionManager::<PgConnection>::new(
|
||||||
|
&config::CONFIG.db_url,
|
||||||
|
))?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref POOL: DbPool = pool().unwrap();
|
||||||
|
}
|
446
backend/src/db/models.rs
Normal file
446
backend/src/db/models.rs
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crate::db::schema;
|
||||||
|
use crate::untis;
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Debug)]
|
||||||
|
#[diesel(table_name = schema::schoolyears)]
|
||||||
|
pub struct Schoolyear {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub start_date: NaiveDate,
|
||||||
|
pub end_date: NaiveDate,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::schoolyears)]
|
||||||
|
pub struct NewSchoolyear<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub start_date: NaiveDate,
|
||||||
|
pub end_date: NaiveDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::tenants)]
|
||||||
|
#[diesel(belongs_to(Schoolyear))]
|
||||||
|
pub struct Tenant {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub active: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::tenants)]
|
||||||
|
pub struct NewTenant<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::teachers)]
|
||||||
|
#[diesel(belongs_to(Schoolyear))]
|
||||||
|
pub struct Teacher {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub forename: Option<String>,
|
||||||
|
pub display_name: String,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::teachers)]
|
||||||
|
pub struct NewTeacher<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub forename: Option<&'a str>,
|
||||||
|
pub display_name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::classes)]
|
||||||
|
#[diesel(belongs_to(Schoolyear))]
|
||||||
|
pub struct Class {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub long_name: String,
|
||||||
|
pub active: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::classes)]
|
||||||
|
pub struct NewClass<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub long_name: &'a str,
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::subjects)]
|
||||||
|
#[diesel(belongs_to(Schoolyear))]
|
||||||
|
pub struct Subject {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub long_name: String,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::subjects)]
|
||||||
|
pub struct NewSubject<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub long_name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::rooms)]
|
||||||
|
#[diesel(belongs_to(Schoolyear))]
|
||||||
|
pub struct Room {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub long_name: String,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::rooms)]
|
||||||
|
pub struct NewRoom<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub long_name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::departments)]
|
||||||
|
#[diesel(belongs_to(Schoolyear))]
|
||||||
|
pub struct Department {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub long_name: String,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::departments)]
|
||||||
|
pub struct NewDepartment<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub long_name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::holidays)]
|
||||||
|
#[diesel(belongs_to(Schoolyear))]
|
||||||
|
pub struct Holiday {
|
||||||
|
pub id: i32,
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub long_name: String,
|
||||||
|
pub start_date: NaiveDate,
|
||||||
|
pub end_date: NaiveDate,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::holidays)]
|
||||||
|
pub struct NewHoliday<'a> {
|
||||||
|
pub untis_id: i32,
|
||||||
|
pub schoolyear_id: i32,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub long_name: &'a str,
|
||||||
|
pub start_date: NaiveDate,
|
||||||
|
pub end_date: NaiveDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(diesel::FromSqlRow, diesel::AsExpression, PartialEq, Eq, Debug)]
|
||||||
|
#[diesel(sql_type = schema::sql_types::SubstitutionType)]
|
||||||
|
pub enum SubstitutionType {
|
||||||
|
Cancel,
|
||||||
|
Substitution,
|
||||||
|
Additional,
|
||||||
|
Shifted,
|
||||||
|
RoomChange,
|
||||||
|
Locked,
|
||||||
|
BreakSupervision,
|
||||||
|
OfficeHour,
|
||||||
|
Standby,
|
||||||
|
Other,
|
||||||
|
Free,
|
||||||
|
Exam,
|
||||||
|
Activity,
|
||||||
|
Holiday,
|
||||||
|
Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl diesel::serialize::ToSql<schema::sql_types::SubstitutionType, diesel::pg::Pg>
|
||||||
|
for SubstitutionType
|
||||||
|
{
|
||||||
|
fn to_sql<'b>(
|
||||||
|
&'b self,
|
||||||
|
out: &mut diesel::serialize::Output<'b, '_, diesel::pg::Pg>,
|
||||||
|
) -> diesel::serialize::Result {
|
||||||
|
match *self {
|
||||||
|
Self::Cancel => out.write_all(b"cancel")?,
|
||||||
|
Self::Substitution => out.write_all(b"subst")?,
|
||||||
|
Self::Additional => out.write_all(b"add")?,
|
||||||
|
Self::Shifted => out.write_all(b"shift")?,
|
||||||
|
Self::RoomChange => out.write_all(b"rmchg")?,
|
||||||
|
Self::Locked => out.write_all(b"rmlk")?,
|
||||||
|
Self::BreakSupervision => out.write_all(b"bs")?,
|
||||||
|
Self::OfficeHour => out.write_all(b"oh")?,
|
||||||
|
Self::Standby => out.write_all(b"sb")?,
|
||||||
|
Self::Other => out.write_all(b"other")?,
|
||||||
|
Self::Free => out.write_all(b"free")?,
|
||||||
|
Self::Exam => out.write_all(b"exam")?,
|
||||||
|
Self::Activity => out.write_all(b"ac")?,
|
||||||
|
Self::Holiday => out.write_all(b"holi")?,
|
||||||
|
Self::Text => out.write_all(b"stxt")?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(diesel::serialize::IsNull::No)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl diesel::deserialize::FromSql<schema::sql_types::SubstitutionType, diesel::pg::Pg>
|
||||||
|
for SubstitutionType
|
||||||
|
{
|
||||||
|
fn from_sql(
|
||||||
|
bytes: diesel::backend::RawValue<'_, diesel::pg::Pg>,
|
||||||
|
) -> diesel::deserialize::Result<Self> {
|
||||||
|
match bytes.as_bytes() {
|
||||||
|
b"cancel" => Ok(Self::Cancel),
|
||||||
|
b"subst" => Ok(Self::Substitution),
|
||||||
|
b"add" => Ok(Self::Additional),
|
||||||
|
b"shift" => Ok(Self::Shifted),
|
||||||
|
b"rmchg" => Ok(Self::RoomChange),
|
||||||
|
b"rmlk" => Ok(Self::Locked),
|
||||||
|
b"bs" => Ok(Self::BreakSupervision),
|
||||||
|
b"oh" => Ok(Self::OfficeHour),
|
||||||
|
b"sb" => Ok(Self::Standby),
|
||||||
|
b"other" => Ok(Self::Other),
|
||||||
|
b"free" => Ok(Self::Free),
|
||||||
|
b"exam" => Ok(Self::Exam),
|
||||||
|
b"ac" => Ok(Self::Activity),
|
||||||
|
b"holi" => Ok(Self::Holiday),
|
||||||
|
b"stxt" => Ok(Self::Text),
|
||||||
|
_ => Err("Unrecognized enum variant".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<untis::RpcSubstitionType> for SubstitutionType {
|
||||||
|
fn from(x: untis::RpcSubstitionType) -> Self {
|
||||||
|
match x {
|
||||||
|
untis::RpcSubstitionType::Cancel => Self::Cancel,
|
||||||
|
untis::RpcSubstitionType::Substitution => Self::Substitution,
|
||||||
|
untis::RpcSubstitionType::Additional => Self::Additional,
|
||||||
|
untis::RpcSubstitionType::Shifted => Self::Shifted,
|
||||||
|
untis::RpcSubstitionType::RoomChange => Self::RoomChange,
|
||||||
|
untis::RpcSubstitionType::Locked => Self::Locked,
|
||||||
|
untis::RpcSubstitionType::BreakSupervision => Self::BreakSupervision,
|
||||||
|
untis::RpcSubstitionType::OfficeHour => Self::OfficeHour,
|
||||||
|
untis::RpcSubstitionType::Standby => Self::Standby,
|
||||||
|
untis::RpcSubstitionType::Other => Self::Other,
|
||||||
|
untis::RpcSubstitionType::Free => Self::Free,
|
||||||
|
untis::RpcSubstitionType::Exam => Self::Exam,
|
||||||
|
untis::RpcSubstitionType::Activity => Self::Activity,
|
||||||
|
untis::RpcSubstitionType::Holiday => Self::Holiday,
|
||||||
|
untis::RpcSubstitionType::Text => Self::Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitutions)]
|
||||||
|
pub struct Substitution {
|
||||||
|
pub id: i32,
|
||||||
|
pub substitution_query_id: i32,
|
||||||
|
pub subst_type: SubstitutionType,
|
||||||
|
pub lesson_id: i32,
|
||||||
|
pub start_time: NaiveTime,
|
||||||
|
pub end_time: NaiveTime,
|
||||||
|
pub text: Option<String>,
|
||||||
|
pub active: bool,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitutions)]
|
||||||
|
pub struct NewSubstitution<'a> {
|
||||||
|
pub subst_type: SubstitutionType,
|
||||||
|
pub lesson_id: i32,
|
||||||
|
pub start_time: NaiveTime,
|
||||||
|
pub end_time: NaiveTime,
|
||||||
|
pub text: Option<&'a str>,
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_classes)]
|
||||||
|
#[diesel(belongs_to(Substitution))]
|
||||||
|
#[diesel(belongs_to(Class))]
|
||||||
|
pub struct SubstitutionClass {
|
||||||
|
pub id: i32,
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub class_id: i32,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_classes)]
|
||||||
|
pub struct NewSubstitutionClass {
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub class_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_teachers)]
|
||||||
|
#[diesel(belongs_to(Substitution))]
|
||||||
|
#[diesel(belongs_to(Teacher))]
|
||||||
|
pub struct SubstitutionTeacher {
|
||||||
|
pub id: i32,
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub teacher_id: i32,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_teachers)]
|
||||||
|
pub struct NewSubstitutionTeacher {
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub teacher_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_subjects)]
|
||||||
|
#[diesel(belongs_to(Substitution))]
|
||||||
|
#[diesel(belongs_to(Subject))]
|
||||||
|
pub struct SubstitutionSubject {
|
||||||
|
pub id: i32,
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub subject_id: i32,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_subjects)]
|
||||||
|
pub struct NewSubstitutionSubject {
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub subject_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_rooms)]
|
||||||
|
#[diesel(belongs_to(Substitution))]
|
||||||
|
#[diesel(belongs_to(Room))]
|
||||||
|
pub struct SubstitutionRoom {
|
||||||
|
pub id: i32,
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub room_id: i32,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = schema::substitution_rooms)]
|
||||||
|
pub struct NewSubstitutionRoom {
|
||||||
|
pub substitution_id: i32,
|
||||||
|
pub room_id: 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,
|
||||||
|
}
|
225
backend/src/db/schema.rs
Normal file
225
backend/src/db/schema.rs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
pub mod sql_types {
|
||||||
|
#[derive(diesel::sql_types::SqlType)]
|
||||||
|
#[diesel(postgres_type(name = "substitution_type"))]
|
||||||
|
pub struct SubstitutionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
schoolyears {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
start_date -> Date,
|
||||||
|
end_date -> Date,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
tenants {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
active -> Bool,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
teachers {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
forename -> Nullable<VarChar>,
|
||||||
|
display_name -> VarChar,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
classes {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
long_name -> VarChar,
|
||||||
|
active -> Bool,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
subjects {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
long_name -> VarChar,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
rooms {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
long_name -> VarChar,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
departments {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
long_name -> VarChar,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
holidays {
|
||||||
|
id -> Integer,
|
||||||
|
untis_id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
name -> VarChar,
|
||||||
|
long_name -> VarChar,
|
||||||
|
start_date -> Date,
|
||||||
|
end_date -> Date,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
timegrids {
|
||||||
|
id -> Integer,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
timegrid_days {
|
||||||
|
id -> Integer,
|
||||||
|
timegrid_id -> Integer,
|
||||||
|
#[sql_name = "day_index"]
|
||||||
|
day -> SmallInt,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
timegrid_time_unit {
|
||||||
|
id -> Integer,
|
||||||
|
timegrid_id -> Integer,
|
||||||
|
start_time -> Time,
|
||||||
|
end_time -> Time,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
|
||||||
|
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,
|
||||||
|
schoolyear_id -> Integer,
|
||||||
|
date -> Date,
|
||||||
|
active -> Bool,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
substitution_queries {
|
||||||
|
id -> Integer,
|
||||||
|
substitution_planned_query_id -> Integer,
|
||||||
|
queried_at -> Timestamp,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
substitution_query_results {
|
||||||
|
id -> Integer,
|
||||||
|
substitution_query_id -> Integer,
|
||||||
|
substitution_id -> Integer,
|
||||||
|
created_at -> Timestamp,
|
||||||
|
updated_at -> Nullable<Timestamp>,
|
||||||
|
}
|
||||||
|
}
|
75
backend/src/graphql.rs
Normal file
75
backend/src/graphql.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use juniper::{graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode};
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
use crate::db;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Context {
|
||||||
|
pub pool: db::DbPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl juniper::Context for Context {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
|
||||||
|
pub struct Config;
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl Config {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"BVplan"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn school(&self) -> &str {
|
||||||
|
&config::CONFIG.untis_school
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version(&self) -> &str {
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Teacher {
|
||||||
|
pub id: i32,
|
||||||
|
pub display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl Teacher {
|
||||||
|
fn id(&self) -> i32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> &str {
|
||||||
|
&self.display_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Query;
|
||||||
|
|
||||||
|
#[graphql_object(context = Context)]
|
||||||
|
impl Query {
|
||||||
|
fn ping() -> &str {
|
||||||
|
"pong"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config() -> Config {
|
||||||
|
Config
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn teachers() -> Vec<Teacher> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Schema = RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>;
|
||||||
|
|
||||||
|
pub fn schema() -> Schema {
|
||||||
|
Schema::new(
|
||||||
|
Query,
|
||||||
|
EmptyMutation::<Context>::new(),
|
||||||
|
EmptySubscription::<Context>::new(),
|
||||||
|
)
|
||||||
|
}
|
5
backend/src/lib.rs
Normal file
5
backend/src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod config;
|
||||||
|
pub mod db;
|
||||||
|
pub mod graphql;
|
||||||
|
pub mod untis;
|
||||||
|
pub mod worker;
|
717
backend/src/untis.rs
Normal file
717
backend/src/untis.rs
Normal file
|
@ -0,0 +1,717 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
76
backend/src/worker/mod.rs
Normal file
76
backend/src/worker/mod.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use celery::beat::DeltaSchedule;
|
||||||
|
use celery::broker::AMQPBroker;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::time;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
|
||||||
|
pub mod update_info;
|
||||||
|
|
||||||
|
pub const QUEUE_NAME: &str = "celery";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref APP: Mutex<Option<Arc<celery::Celery<AMQPBroker>>>> = Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn init() {
|
||||||
|
*APP.lock().unwrap() = Some(
|
||||||
|
celery::app!(
|
||||||
|
broker = AMQPBroker { &config::CONFIG.amqp_url },
|
||||||
|
tasks = [
|
||||||
|
update_info::update_info,
|
||||||
|
],
|
||||||
|
task_routes = [
|
||||||
|
"*" => QUEUE_NAME,
|
||||||
|
],
|
||||||
|
prefetch_count = 2,
|
||||||
|
heartbeat = Some(10)
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_blocking() {
|
||||||
|
thread::spawn(|| {
|
||||||
|
tokio::runtime::Runtime::new().unwrap().spawn_blocking(init);
|
||||||
|
});
|
||||||
|
|
||||||
|
let dur = time::Duration::from_secs(1);
|
||||||
|
let mut i = 0;
|
||||||
|
loop {
|
||||||
|
if APP.lock().unwrap().is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(dur);
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
if i >= 10 {
|
||||||
|
panic!("Worker not initialized after 10 seconds!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn beat() -> impl std::future::Future<
|
||||||
|
Output = Result<
|
||||||
|
celery::beat::Beat<AMQPBroker, celery::beat::LocalSchedulerBackend>,
|
||||||
|
celery::error::BeatError,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
celery::beat!(
|
||||||
|
broker = AMQPBroker { &config::CONFIG.amqp_url },
|
||||||
|
tasks = [
|
||||||
|
"update_info_" => {
|
||||||
|
update_info::update_info,
|
||||||
|
schedule = DeltaSchedule::new(time::Duration::from_secs(60)),
|
||||||
|
args = (),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
task_routes = [
|
||||||
|
"*" => QUEUE_NAME,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
356
backend/src/worker/update_info.rs
Normal file
356
backend/src/worker/update_info.rs
Normal file
|
@ -0,0 +1,356 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use celery::error::TaskError;
|
||||||
|
use celery::task::TaskResult;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use now::DateTimeNow;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
use crate::db;
|
||||||
|
use crate::untis;
|
||||||
|
|
||||||
|
async fn fetch_schoolyears(client: &untis::Client, conn: &mut PgConnection) -> Result<i32> {
|
||||||
|
let existing_schoolyears = db::schema::schoolyears::table
|
||||||
|
.select(db::schema::schoolyears::untis_id)
|
||||||
|
.load::<i32>(conn)?;
|
||||||
|
diesel::insert_into(db::schema::schoolyears::table)
|
||||||
|
.values(
|
||||||
|
&client
|
||||||
|
.schoolyears()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|y| !existing_schoolyears.contains(&y.id))
|
||||||
|
.map(|y| db::models::NewSchoolyear {
|
||||||
|
untis_id: y.id,
|
||||||
|
name: &y.name,
|
||||||
|
start_date: y.start_date,
|
||||||
|
end_date: y.end_date,
|
||||||
|
})
|
||||||
|
.collect::<Vec<db::models::NewSchoolyear>>(),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(db::schema::schoolyears::table
|
||||||
|
.filter(db::schema::schoolyears::untis_id.eq(client.current_schoolyear().await?.id))
|
||||||
|
.select(db::schema::schoolyears::id)
|
||||||
|
.first(conn)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_current_tenant(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let tenant = client.current_tenant().await?;
|
||||||
|
if diesel::select(diesel::dsl::not(diesel::expression::exists::exists(
|
||||||
|
db::schema::tenants::table.filter(db::schema::tenants::untis_id.eq(tenant.id)),
|
||||||
|
)))
|
||||||
|
.get_result::<bool>(conn)?
|
||||||
|
{
|
||||||
|
diesel::update(db::schema::tenants::table)
|
||||||
|
.filter(db::schema::tenants::active)
|
||||||
|
.set(db::schema::tenants::active.eq(false))
|
||||||
|
.execute(conn)?;
|
||||||
|
diesel::insert_into(db::schema::tenants::table)
|
||||||
|
.values(db::models::NewTenant {
|
||||||
|
untis_id: tenant.id,
|
||||||
|
schoolyear_id,
|
||||||
|
name: &tenant.display_name,
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
.execute(conn)?;
|
||||||
|
} else if diesel::select(diesel::expression::exists::exists(
|
||||||
|
db::schema::tenants::table
|
||||||
|
.filter(db::schema::tenants::untis_id.eq(tenant.id))
|
||||||
|
.filter(db::schema::tenants::active.eq(false)),
|
||||||
|
))
|
||||||
|
.get_result::<bool>(conn)?
|
||||||
|
{
|
||||||
|
diesel::update(db::schema::tenants::table)
|
||||||
|
.filter(db::schema::tenants::active)
|
||||||
|
.set((
|
||||||
|
db::schema::tenants::active.eq(false),
|
||||||
|
db::schema::tenants::updated_at.eq(diesel::dsl::now),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
diesel::update(db::schema::tenants::table)
|
||||||
|
.filter(db::schema::tenants::untis_id.eq(tenant.id))
|
||||||
|
.set((
|
||||||
|
db::schema::tenants::active.eq(true),
|
||||||
|
db::schema::tenants::updated_at.eq(diesel::dsl::now),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_teachers(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing_teachers = db::schema::teachers::table
|
||||||
|
.select(db::schema::teachers::untis_id)
|
||||||
|
.filter(db::schema::teachers::schoolyear_id.eq(schoolyear_id))
|
||||||
|
.load::<i32>(conn)?;
|
||||||
|
diesel::insert_into(db::schema::teachers::table)
|
||||||
|
.values(
|
||||||
|
&client
|
||||||
|
.teachers()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !existing_teachers.contains(&t.id))
|
||||||
|
.map(|t| db::models::NewTeacher {
|
||||||
|
untis_id: t.id,
|
||||||
|
schoolyear_id,
|
||||||
|
name: &t.name,
|
||||||
|
forename: if t.forename.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&t.forename)
|
||||||
|
},
|
||||||
|
display_name: &t.display_name,
|
||||||
|
})
|
||||||
|
.collect::<Vec<db::models::NewTeacher>>(),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_classes(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing_classes = db::schema::classes::table
|
||||||
|
.select(db::schema::classes::untis_id)
|
||||||
|
.filter(db::schema::classes::schoolyear_id.eq(schoolyear_id))
|
||||||
|
.load::<i32>(conn)?;
|
||||||
|
diesel::insert_into(db::schema::classes::table)
|
||||||
|
.values(
|
||||||
|
&client
|
||||||
|
.classes()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|c| !existing_classes.contains(&c.id))
|
||||||
|
.map(|c| db::models::NewClass {
|
||||||
|
untis_id: c.id,
|
||||||
|
schoolyear_id,
|
||||||
|
name: &c.name,
|
||||||
|
long_name: &c.long_name,
|
||||||
|
active: c.active,
|
||||||
|
})
|
||||||
|
.collect::<Vec<db::models::NewClass>>(),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_subjects(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing_classes = db::schema::subjects::table
|
||||||
|
.select(db::schema::subjects::untis_id)
|
||||||
|
.filter(db::schema::subjects::schoolyear_id.eq(schoolyear_id))
|
||||||
|
.load::<i32>(conn)?;
|
||||||
|
diesel::insert_into(db::schema::subjects::table)
|
||||||
|
.values(
|
||||||
|
&client
|
||||||
|
.subjects()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|c| !existing_classes.contains(&c.id))
|
||||||
|
.map(|c| db::models::NewSubject {
|
||||||
|
untis_id: c.id,
|
||||||
|
schoolyear_id,
|
||||||
|
name: &c.name,
|
||||||
|
long_name: &c.long_name,
|
||||||
|
})
|
||||||
|
.collect::<Vec<db::models::NewSubject>>(),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_rooms(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing_classes = db::schema::rooms::table
|
||||||
|
.select(db::schema::rooms::untis_id)
|
||||||
|
.filter(db::schema::rooms::schoolyear_id.eq(schoolyear_id))
|
||||||
|
.load::<i32>(conn)?;
|
||||||
|
diesel::insert_into(db::schema::rooms::table)
|
||||||
|
.values(
|
||||||
|
&client
|
||||||
|
.rooms()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|c| !existing_classes.contains(&c.id))
|
||||||
|
.map(|c| db::models::NewRoom {
|
||||||
|
untis_id: c.id,
|
||||||
|
schoolyear_id,
|
||||||
|
name: &c.name,
|
||||||
|
long_name: &c.long_name,
|
||||||
|
})
|
||||||
|
.collect::<Vec<db::models::NewRoom>>(),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_departments(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing_classes = db::schema::departments::table
|
||||||
|
.select(db::schema::departments::untis_id)
|
||||||
|
.filter(db::schema::departments::schoolyear_id.eq(schoolyear_id))
|
||||||
|
.load::<i32>(conn)?;
|
||||||
|
diesel::insert_into(db::schema::departments::table)
|
||||||
|
.values(
|
||||||
|
&client
|
||||||
|
.departments()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|c| !existing_classes.contains(&c.id))
|
||||||
|
.map(|c| db::models::NewDepartment {
|
||||||
|
untis_id: c.id,
|
||||||
|
schoolyear_id,
|
||||||
|
name: &c.name,
|
||||||
|
long_name: &c.long_name,
|
||||||
|
})
|
||||||
|
.collect::<Vec<db::models::NewDepartment>>(),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_holidays(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let existing_classes = db::schema::holidays::table
|
||||||
|
.select(db::schema::holidays::untis_id)
|
||||||
|
.filter(db::schema::holidays::schoolyear_id.eq(schoolyear_id))
|
||||||
|
.load::<i32>(conn)?;
|
||||||
|
diesel::insert_into(db::schema::holidays::table)
|
||||||
|
.values(
|
||||||
|
&client
|
||||||
|
.holidays()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.filter(|c| !existing_classes.contains(&c.id))
|
||||||
|
.map(|c| db::models::NewHoliday {
|
||||||
|
untis_id: c.id,
|
||||||
|
schoolyear_id,
|
||||||
|
name: &c.name,
|
||||||
|
long_name: &c.long_name,
|
||||||
|
start_date: c.start_date,
|
||||||
|
end_date: c.end_date,
|
||||||
|
})
|
||||||
|
.collect::<Vec<db::models::NewHoliday>>(),
|
||||||
|
)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_substitutions(
|
||||||
|
client: &untis::Client,
|
||||||
|
conn: &mut PgConnection,
|
||||||
|
schoolyear_id: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let beginning_of_week = now.beginning_of_week().date_naive();
|
||||||
|
let end_of_week = now.end_of_week().date_naive();
|
||||||
|
// if diesel::select(diesel::dsl::not(diesel::expression::exists::exists(
|
||||||
|
// db::schema::substitution_queries::table
|
||||||
|
// .filter(db::schema::substitution_queries::active)
|
||||||
|
// .filter(db::schema::substitution_queries::end_date.eq(end_of_week)),
|
||||||
|
// )))
|
||||||
|
// .get_result::<bool>(conn)?
|
||||||
|
// {}
|
||||||
|
|
||||||
|
// for query in db::schema::substitution_queries::table
|
||||||
|
// .filter(db::schema::substitution_queries::active)
|
||||||
|
// .load::<db::models::SubstitutionQuery>(conn)?
|
||||||
|
// {
|
||||||
|
// dbg!(&query);
|
||||||
|
// }
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[celery::task]
|
||||||
|
pub async fn update_info() -> TaskResult<()> {
|
||||||
|
let mut client = untis::Client {
|
||||||
|
api_url: match url::Url::parse(&config::CONFIG.untis_api_url) {
|
||||||
|
Ok(x) => x,
|
||||||
|
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 {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = &mut match db::POOL.get() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let schoolyear_id = match fetch_schoolyears(&client, conn).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Err(TaskError::UnexpectedError(e.to_string())),
|
||||||
|
};
|
||||||
|
if let Err(e) = fetch_current_tenant(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
if let Err(e) = fetch_teachers(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
if let Err(e) = fetch_classes(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
if let Err(e) = fetch_subjects(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
if let Err(e) = fetch_rooms(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
if let Err(e) = fetch_departments(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
if let Err(e) = fetch_holidays(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
if let Err(e) = fetch_substitutions(&client, conn, schoolyear_id).await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = client.logout().await {
|
||||||
|
return Err(TaskError::UnexpectedError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
51
config/nginx/nginx.conf
Normal file
51
config/nginx/nginx.conf
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server_tokens off;
|
||||||
|
more_clear_headers Server;
|
||||||
|
|
||||||
|
server {
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://frontend/;
|
||||||
|
# }
|
||||||
|
|
||||||
|
location /graphql {
|
||||||
|
proxy_pass http://api/;
|
||||||
|
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 /adminer {
|
||||||
|
proxy_pass http://adminer:8080/;
|
||||||
|
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/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
docker-compose.yml
Normal file
81
docker-compose.yml
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: docker.io/byjg/nginx-extras
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
depends_on:
|
||||||
|
- adminer
|
||||||
|
- rabbitmq
|
||||||
|
- api
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: docker.io/postgres:alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: docker.io/adminer:standalone
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: docker.io/rabbitmq:3-management-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
|
||||||
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- rabbitmq:/var/lib/rabbitmq
|
||||||
|
|
||||||
|
worker:
|
||||||
|
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_SCHOOL: ${BACKEND_UNTIS_SCHOOL}
|
||||||
|
BACKEND_UNTIS_USERNAME: ${BACKEND_UNTIS_USERNAME}
|
||||||
|
BACKEND_UNTIS_PASSWORD: ${BACKEND_UNTIS_PASSWORD}
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: git.dergrimm.net/dergrimm/bvplan_backend:latest
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
restart: always
|
||||||
|
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:
|
||||||
|
postgres:
|
||||||
|
rabbitmq:
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue