Literally rewrote everything database related for clear ORM
This commit is contained in:
parent
f02151bd5d
commit
c75cad99f7
|
@ -37,6 +37,7 @@ BACKEND_SMTP_NAME=
|
||||||
BACKEND_SMTP_USERNAME=
|
BACKEND_SMTP_USERNAME=
|
||||||
BACKEND_SMTP_PASSWORD=
|
BACKEND_SMTP_PASSWORD=
|
||||||
# Backend - Db
|
# Backend - Db
|
||||||
|
BACKEND_DB_ALLOW_OLD_SCHEMA=false
|
||||||
# Backend - LDAP
|
# Backend - LDAP
|
||||||
BACKEND_LDAP_HOST="ldap.example.com"
|
BACKEND_LDAP_HOST="ldap.example.com"
|
||||||
BACKEND_LDAP_PORT=389
|
BACKEND_LDAP_PORT=389
|
||||||
|
@ -44,4 +45,4 @@ BACKEND_LDAP_BASE_DN="dc=ldap,dc=example,dc=com"
|
||||||
BACKEND_LDAP_BASE_USER_DN="ou=users,dc=ldap,dc=example,dc=com"
|
BACKEND_LDAP_BASE_USER_DN="ou=users,dc=ldap,dc=example,dc=com"
|
||||||
BACKEND_LDAP_BIND_DN="cn=admin,dc=ldap,dc=example,dc=com"
|
BACKEND_LDAP_BIND_DN="cn=admin,dc=ldap,dc=example,dc=com"
|
||||||
BACKEND_LDAP_BIND_PASSWORD=
|
BACKEND_LDAP_BIND_PASSWORD=
|
||||||
BACKEND_LDAP_CACHE_REFRESH_INTERVAL=60
|
BACKEND_LDAP_CACHE_REFRESH_INTERVAL=720
|
||||||
|
|
|
@ -88,6 +88,7 @@ services:
|
||||||
BACKEND_SMTP_USERNAME: ${BACKEND_SMTP_USERNAME}
|
BACKEND_SMTP_USERNAME: ${BACKEND_SMTP_USERNAME}
|
||||||
BACKEND_SMTP_PASSWORD: ${BACKEND_SMTP_PASSWORD}
|
BACKEND_SMTP_PASSWORD: ${BACKEND_SMTP_PASSWORD}
|
||||||
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
||||||
|
BACKEND_DB_ALLOW_OLD_SCHEMA: ${BACKEND_DB_ALLOW_OLD_SCHEMA}
|
||||||
BACKEND_REDIS_HOST: redis
|
BACKEND_REDIS_HOST: redis
|
||||||
BACKEND_REDIS_PORT: 6379
|
BACKEND_REDIS_PORT: 6379
|
||||||
BACKEND_LDAP_HOST: ${BACKEND_LDAP_HOST}
|
BACKEND_LDAP_HOST: ${BACKEND_LDAP_HOST}
|
||||||
|
|
|
@ -43,7 +43,6 @@ RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||||
|
|
||||||
FROM scratch as runner
|
FROM scratch as runner
|
||||||
COPY --from=builder /src/bin /bin
|
COPY --from=builder /src/bin /bin
|
||||||
COPY ./db /db
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENTRYPOINT [ "backend" ]
|
ENTRYPOINT [ "backend" ]
|
||||||
CMD [ "run" ]
|
CMD [ "run" ]
|
||||||
|
|
|
@ -22,7 +22,7 @@ dev:
|
||||||
shards build -Ddevelopment --static --verbose -s -p -t
|
shards build -Ddevelopment --static --verbose -s -p -t
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
shards build --static --release --no-debug --verbose -s -p -t
|
shards build --static --release --verbose -s -p -t
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
crystal docs --project-name "Mentorenwahl Backend" -D granite_docs
|
crystal docs --project-name "Mentorenwahl Backend" -D granite_docs
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
Copyright (C) 2022 Dominic Grimm
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
-- +micrate Up
|
|
||||||
-- SQL in section ' Up ' is executed when this migration is applied
|
|
||||||
CREATE TYPE user_role AS ENUM ('Teacher', 'Student');
|
|
||||||
|
|
||||||
CREATE TABLE users(
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
username TEXT UNIQUE NOT NULL,
|
|
||||||
role user_role NOT NULL,
|
|
||||||
admin BOOLEAN NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE teachers(
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
user_id BIGINT NOT NULL UNIQUE REFERENCES users(id),
|
|
||||||
max_students INT NOT NULL,
|
|
||||||
skif BOOLEAN NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE students(
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
user_id BIGINT NOT NULL UNIQUE REFERENCES users(id),
|
|
||||||
skif BOOLEAN NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE
|
|
||||||
users
|
|
||||||
ADD
|
|
||||||
COLUMN teacher_id BIGINT UNIQUE REFERENCES teachers(id);
|
|
||||||
|
|
||||||
ALTER TABLE
|
|
||||||
users
|
|
||||||
ADD
|
|
||||||
COLUMN student_id BIGINT UNIQUE REFERENCES students(id);
|
|
||||||
|
|
||||||
CREATE TABLE votes(
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
student_id BIGINT NOT NULL UNIQUE REFERENCES students(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE
|
|
||||||
students
|
|
||||||
ADD
|
|
||||||
COLUMN vote_id BIGINT UNIQUE REFERENCES votes(id);
|
|
||||||
|
|
||||||
CREATE TABLE teacher_votes(
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
vote_id BIGINT NOT NULL REFERENCES votes(id),
|
|
||||||
teacher_id BIGINT NOT NULL REFERENCES teachers(id),
|
|
||||||
priority INT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- +micrate Down
|
|
||||||
-- SQL section ' Down ' is executed when this migration is rolled back
|
|
||||||
DROP TABLE teacher_votes;
|
|
||||||
|
|
||||||
DROP TABLE votes;
|
|
||||||
|
|
||||||
DROP TABLE admins;
|
|
||||||
|
|
||||||
DROP TABLE teachers;
|
|
||||||
|
|
||||||
DROP TABLE students;
|
|
||||||
|
|
||||||
DROP TABLE users;
|
|
||||||
|
|
||||||
DROP TYPE user_roles;
|
|
|
@ -1,5 +1,9 @@
|
||||||
version: 2.0
|
version: 2.0
|
||||||
shards:
|
shards:
|
||||||
|
admiral:
|
||||||
|
git: https://github.com/jwaldrip/admiral.cr.git
|
||||||
|
version: 1.12.1
|
||||||
|
|
||||||
athena:
|
athena:
|
||||||
git: https://github.com/athena-framework/framework.git
|
git: https://github.com/athena-framework/framework.git
|
||||||
version: 0.16.0
|
version: 0.16.0
|
||||||
|
@ -40,13 +44,17 @@ shards:
|
||||||
git: https://github.com/spider-gazelle/bindata.git
|
git: https://github.com/spider-gazelle/bindata.git
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
|
|
||||||
|
clear:
|
||||||
|
git: https://github.com/vici37/clear.git
|
||||||
|
version: 0.9+git.commit.2139d151d966b1119fd75c97d3b4d40a269592b9
|
||||||
|
|
||||||
commander:
|
commander:
|
||||||
git: https://github.com/mrrooijen/commander.git
|
git: https://github.com/mrrooijen/commander.git
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
|
|
||||||
db:
|
db:
|
||||||
git: https://github.com/crystal-lang/crystal-db.git
|
git: https://github.com/crystal-lang/crystal-db.git
|
||||||
version: 0.10.1
|
version: 0.11.0
|
||||||
|
|
||||||
email:
|
email:
|
||||||
git: https://github.com/arcage/crystal-email.git
|
git: https://github.com/arcage/crystal-email.git
|
||||||
|
@ -56,18 +64,22 @@ shards:
|
||||||
git: https://github.com/repomaa/env_config.cr.git
|
git: https://github.com/repomaa/env_config.cr.git
|
||||||
version: 0.1.0+git.commit.a3ef5b955f27e2c65de2fe0ff41718e2eea7c06f
|
version: 0.1.0+git.commit.a3ef5b955f27e2c65de2fe0ff41718e2eea7c06f
|
||||||
|
|
||||||
granite:
|
generate:
|
||||||
git: https://github.com/amberframework/granite.git
|
git: https://github.com/anykeyh/generate.cr.git
|
||||||
version: 0.23.0
|
version: 0.1.0+git.commit.f5dafc934a70e0ee2f246dddf3df44686f844da2
|
||||||
|
|
||||||
graphql:
|
graphql:
|
||||||
git: https://github.com/graphql-crystal/graphql.git
|
git: https://github.com/graphql-crystal/graphql.git
|
||||||
version: 0.3.2+git.commit.f49615eb286e90cfa9041107706a50d2c95e988d
|
version: 0.4.0
|
||||||
|
|
||||||
habitat:
|
habitat:
|
||||||
git: https://github.com/luckyframework/habitat.git
|
git: https://github.com/luckyframework/habitat.git
|
||||||
version: 0.4.7
|
version: 0.4.7
|
||||||
|
|
||||||
|
inflector:
|
||||||
|
git: https://github.com/anykeyh/inflector.cr.git
|
||||||
|
version: 0.1.8+git.commit.dc5c898b0a834617d8b3ff73ac5a2239bd9fc019
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
git: https://github.com/crystal-community/jwt.git
|
git: https://github.com/crystal-community/jwt.git
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
|
@ -84,10 +96,6 @@ shards:
|
||||||
git: https://git.dergrimm.net/dergrimm/ldap_escape.git
|
git: https://git.dergrimm.net/dergrimm/ldap_escape.git
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
|
|
||||||
micrate:
|
|
||||||
git: https://github.com/juanedi/micrate.git
|
|
||||||
version: 0.12.0
|
|
||||||
|
|
||||||
mosquito:
|
mosquito:
|
||||||
git: https://github.com/mosquito-cr/mosquito.git
|
git: https://github.com/mosquito-cr/mosquito.git
|
||||||
version: 1.0.0.rc1+git.commit.afd53dd241447b60ece9232b6c71669b192baaa4
|
version: 1.0.0.rc1+git.commit.afd53dd241447b60ece9232b6c71669b192baaa4
|
||||||
|
@ -98,7 +106,7 @@ shards:
|
||||||
|
|
||||||
pg:
|
pg:
|
||||||
git: https://github.com/will/crystal-pg.git
|
git: https://github.com/will/crystal-pg.git
|
||||||
version: 0.25.0
|
version: 0.26.0
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
git: https://github.com/ysbaddaden/pool.git
|
git: https://github.com/ysbaddaden/pool.git
|
||||||
|
|
|
@ -25,25 +25,21 @@ license: GNU GPLv3
|
||||||
targets:
|
targets:
|
||||||
backend:
|
backend:
|
||||||
main: src/cli/backend.cr
|
main: src/cli/backend.cr
|
||||||
micrate:
|
clear:
|
||||||
main: src/cli/micrate.cr
|
main: src/cli/clear.cr
|
||||||
|
|
||||||
crystal: 1.3.2
|
crystal: 1.3.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
granite:
|
clear:
|
||||||
github: amberframework/granite
|
github: vici37/clear
|
||||||
pg:
|
branch: master
|
||||||
github: will/crystal-pg
|
|
||||||
graphql:
|
graphql:
|
||||||
github: graphql-crystal/graphql
|
github: graphql-crystal/graphql
|
||||||
branch: main
|
|
||||||
jwt:
|
jwt:
|
||||||
github: crystal-community/jwt
|
github: crystal-community/jwt
|
||||||
commander:
|
commander:
|
||||||
github: mrrooijen/commander
|
github: mrrooijen/commander
|
||||||
micrate:
|
|
||||||
github: juanedi/micrate
|
|
||||||
mosquito:
|
mosquito:
|
||||||
github: mosquito-cr/mosquito
|
github: mosquito-cr/mosquito
|
||||||
branch: master
|
branch: master
|
||||||
|
|
|
@ -89,13 +89,15 @@ module Backend
|
||||||
return false unless authenticated?
|
return false unless authenticated?
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
return true if @role == role && if external_check
|
return true if @role == role &&
|
||||||
role == case @external.not_nil!
|
if external_check
|
||||||
when Db::Teacher
|
role ==
|
||||||
Schema::UserRole::Teacher
|
case @external.not_nil!
|
||||||
when Db::Student
|
when Db::Teacher
|
||||||
Schema::UserRole::Student
|
Schema::UserRole::Teacher
|
||||||
end
|
when Db::Student
|
||||||
|
Schema::UserRole::Student
|
||||||
|
end
|
||||||
else
|
else
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -130,6 +132,8 @@ module Backend
|
||||||
def student!(external_check = true) : Bool
|
def student!(external_check = true) : Bool
|
||||||
role! external_check, Schema::UserRole::Student
|
role! external_check, Schema::UserRole::Student
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Custom error handler
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,25 +19,13 @@ module Backend
|
||||||
module Schema
|
module Schema
|
||||||
# Schema helper macros
|
# Schema helper macros
|
||||||
module Helpers
|
module Helpers
|
||||||
# Defines field property and GraphQL specific getter
|
|
||||||
macro field(type)
|
|
||||||
property {{ type.var }} {% if type.value %} = {{ type.value }}{% end %}
|
|
||||||
|
|
||||||
@[GraphQL::Field]
|
|
||||||
def {{ type.var }} : {{ type.type }}
|
|
||||||
@{{ type.var }}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defines DB model field helper functions
|
# Defines DB model field helper functions
|
||||||
macro db_object(type)
|
macro db_object(type)
|
||||||
private property model
|
|
||||||
|
|
||||||
def initialize(@model : {{ type }})
|
def initialize(@model : {{ type }})
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.from_id(id : Int32) : self
|
def self.from_id(id : Int32) : self
|
||||||
new({{ type }}.find!(id))
|
new({{ type }}.query.find! { var(:id) == id })
|
||||||
end
|
end
|
||||||
|
|
||||||
{% space_name = type.names.last.underscore.gsub(/_/, " ").capitalize %}
|
{% space_name = type.names.last.underscore.gsub(/_/, " ").capitalize %}
|
||||||
|
@ -45,7 +33,7 @@ module Backend
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# {{ space_name }}'s ID
|
# {{ space_name }}'s ID
|
||||||
def id : Int32
|
def id : Int32
|
||||||
@model.id.not_nil!.to_i
|
@model.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,7 +26,7 @@ module Backend
|
||||||
def login(username : String, password : String) : LoginPayload
|
def login(username : String, password : String) : LoginPayload
|
||||||
raise "Auth failed" if username.empty? || password.empty?
|
raise "Auth failed" if username.empty? || password.empty?
|
||||||
|
|
||||||
user = Db::User.find_by(username: username)
|
user = Db::User.query.find { var(:username) == username }
|
||||||
raise "Auth failed" unless user && Ldap.authenticate?(Ldap::Constructor.uid(username), password)
|
raise "Auth failed" unless user && Ldap.authenticate?(Ldap::Constructor.uid(username), password)
|
||||||
|
|
||||||
LoginPayload.new(
|
LoginPayload.new(
|
||||||
|
@ -48,7 +48,7 @@ module Backend
|
||||||
rescue LDAP::Client::AuthError
|
rescue LDAP::Client::AuthError
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
user = Db::User.create!(username: input.username, role: input.role.to_s, admin: input.admin)
|
user = Db::User.create!(username: input.username, role: input.role.to_db, skif: input.skif, admin: input.admin)
|
||||||
Worker::Jobs::CacheLdapUserJob.new(user.id.not_nil!.to_i).enqueue
|
Worker::Jobs::CacheLdapUserJob.new(user.id.not_nil!.to_i).enqueue
|
||||||
|
|
||||||
User.new(user)
|
User.new(user)
|
||||||
|
@ -60,7 +60,7 @@ module Backend
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
user = Db::User.find!(id)
|
user = Db::User.find!(id)
|
||||||
user.destroy!
|
user.delete
|
||||||
|
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
@ -90,7 +90,7 @@ module Backend
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
teacher = Db::Teacher.find!(id)
|
teacher = Db::Teacher.find!(id)
|
||||||
teacher.destroy!
|
teacher.delete
|
||||||
|
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
@ -101,7 +101,7 @@ module Backend
|
||||||
context.teacher! external_check: false
|
context.teacher! external_check: false
|
||||||
|
|
||||||
Teacher.new(
|
Teacher.new(
|
||||||
Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students, skif: input.skif)
|
Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ module Backend
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
user = Db::User.find!(input.user_id)
|
user = Db::User.find!(input.user_id)
|
||||||
raise "User not a student" unless user.role.student?
|
raise "User not a student" unless user.role.to_api.student?
|
||||||
|
|
||||||
student = Db::Student.create!(user_id: user.id)
|
student = Db::Student.create!(user_id: user.id)
|
||||||
Student.new(student)
|
Student.new(student)
|
||||||
|
@ -123,18 +123,18 @@ module Backend
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
student = Db::Student.find!(id)
|
student = Db::Student.find!(id)
|
||||||
student.destroy!
|
student.delete
|
||||||
|
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Self register as student
|
# Self register as student
|
||||||
def register_student(context : Context, input : StudentInput) : Student
|
def register_student(context : Context) : Student
|
||||||
context.student! external_check: false
|
context.student! external_check: false
|
||||||
|
|
||||||
Student.new(
|
Student.new(
|
||||||
Db::Student.create!(user_id: context.user.not_nil!.id, skif: input.skif)
|
Db::Student.create!(user_id: context.user.not_nil!.id)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -144,18 +144,16 @@ module Backend
|
||||||
context.student!
|
context.student!
|
||||||
|
|
||||||
raise "Not enough teachers" if input.teacher_ids.size < Backend.config.minimum_teacher_selection_count
|
raise "Not enough teachers" if input.teacher_ids.size < Backend.config.minimum_teacher_selection_count
|
||||||
teacher_role_count = Db::User.where(role: Db::UserRole::Teacher.to_s).count.run.as(Int64)
|
teacher_role_count = Db::User.query.where(role: Db::UserRole::Teacher).count
|
||||||
raise "Teachers not registered" if teacher_role_count != Db::Teacher.count ||
|
raise "Teachers not registered" if teacher_role_count != Db::Teacher.query.count || teacher_role_count.zero?
|
||||||
teacher_role_count.zero?
|
|
||||||
|
|
||||||
skif = context.external.as(Db::Student).skif
|
|
||||||
input.teacher_ids.each do |id|
|
input.teacher_ids.each do |id|
|
||||||
teacher = Db::Teacher.find(id)
|
teacher = Db::Teacher.find(id)
|
||||||
|
|
||||||
if teacher.nil?
|
if teacher.nil?
|
||||||
raise "Teachers not found"
|
raise "Teachers not found"
|
||||||
elsif teacher.skif != skif
|
elsif teacher.user.skif != context.user.not_nil!.skif
|
||||||
if teacher.skif
|
if teacher.user.skif
|
||||||
raise "Teacher is SKIF, student is not"
|
raise "Teacher is SKIF, student is not"
|
||||||
else
|
else
|
||||||
raise "Teacher is not SKIF, student is"
|
raise "Teacher is not SKIF, student is"
|
||||||
|
@ -165,7 +163,7 @@ module Backend
|
||||||
|
|
||||||
student = context.external.not_nil!.as(Db::Student)
|
student = context.external.not_nil!.as(Db::Student)
|
||||||
vote = Db::Vote.create!(student_id: student.id)
|
vote = Db::Vote.create!(student_id: student.id)
|
||||||
Db::TeacherVote.import(input.teacher_ids.map_with_index { |id, i| Db::TeacherVote.new(vote_id: vote.id, teacher_id: id.to_i64, priority: i) })
|
Db::TeacherVote.import(input.teacher_ids.map_with_index { |id, i| Db::TeacherVote.new({vote_id: vote.id, teacher_id: id, priority: i}) })
|
||||||
|
|
||||||
Vote.new(vote)
|
Vote.new(vote)
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,7 +52,7 @@ module Backend
|
||||||
def users(context : Context) : Array(User)
|
def users(context : Context) : Array(User)
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
Db::User.all.map { |user| User.new(user) }
|
Db::User.query.map { |user| User.new(user) }
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -60,7 +60,7 @@ module Backend
|
||||||
def admins(context : Context) : Array(User)
|
def admins(context : Context) : Array(User)
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
Db::User.where(admin: true).map { |user| User.new(user) }
|
Db::User.query.where(admin: true).map { |user| User.new(user) }
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -72,7 +72,7 @@ module Backend
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# All teachers
|
# All teachers
|
||||||
def teachers : Array(Teacher)
|
def teachers : Array(Teacher)
|
||||||
Db::Teacher.all.map { |teacher| Teacher.new(teacher) }
|
Db::Teacher.query.map { |teacher| Teacher.new(teacher) }
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -88,7 +88,7 @@ module Backend
|
||||||
def students(context : Context) : Array(Student)
|
def students(context : Context) : Array(Student)
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
Db::Student.all.map { |student| Student.new(student) }
|
Db::Student.query.map { |student| Student.new(student) }
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -104,7 +104,7 @@ module Backend
|
||||||
def votes(context : Context) : Array(Vote)
|
def votes(context : Context) : Array(Vote)
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
Db::Vote.all.map { |vote| Vote.new(vote) }
|
Db::Vote.query.map { |vote| Vote.new(vote) }
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -120,7 +120,7 @@ module Backend
|
||||||
def teacher_votes(context : Context) : Array(TeacherVote)
|
def teacher_votes(context : Context) : Array(TeacherVote)
|
||||||
context.admin!
|
context.admin!
|
||||||
|
|
||||||
Db::TeacherVote.all.map { |vote| TeacherVote.new(vote) }
|
Db::TeacherVote.query.map { |vote| TeacherVote.new(vote) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,12 +30,6 @@ module Backend
|
||||||
User.new(@model.user)
|
User.new(@model.user)
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
|
||||||
# Student at SKIF
|
|
||||||
def skif : Bool
|
|
||||||
@model.skif
|
|
||||||
end
|
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Student's vote
|
# Student's vote
|
||||||
def vote : Vote?
|
def vote : Vote?
|
||||||
|
@ -43,26 +37,22 @@ module Backend
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::InputObject]
|
# @[GraphQL::InputObject]
|
||||||
# Student base input
|
# # Student base input
|
||||||
class StudentInput < GraphQL::BaseInputObject
|
# class StudentInput < GraphQL::BaseInputObject
|
||||||
# Student at SKIF
|
# @[GraphQL::Field]
|
||||||
getter skif
|
# def initialize
|
||||||
|
# end
|
||||||
@[GraphQL::Field]
|
# end
|
||||||
def initialize(@skif : Bool)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@[GraphQL::InputObject]
|
@[GraphQL::InputObject]
|
||||||
# Student creation input
|
# Student creation input
|
||||||
class StudentCreateInput < StudentInput
|
class StudentCreateInput < GraphQL::BaseInputObject
|
||||||
# Student's user ID
|
# Student's user ID
|
||||||
getter user_id
|
getter user_id
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
def initialize(@user_id : Int32, skif : Bool)
|
def initialize(@user_id : Int32)
|
||||||
super(skif)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,12 +35,6 @@ module Backend
|
||||||
def max_students : Int32
|
def max_students : Int32
|
||||||
@model.max_students
|
@model.max_students
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
|
||||||
# Teacher is at SKIF
|
|
||||||
def skif : Bool
|
|
||||||
@model.skif
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::InputObject]
|
@[GraphQL::InputObject]
|
||||||
|
@ -49,11 +43,8 @@ module Backend
|
||||||
# Teacher's max students
|
# Teacher's max students
|
||||||
getter max_students
|
getter max_students
|
||||||
|
|
||||||
# Teacher at SKIF
|
|
||||||
getter skif
|
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
def initialize(@max_students : Int32, @skif : Bool)
|
def initialize(@max_students : Int32)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -64,8 +55,8 @@ module Backend
|
||||||
getter user_id
|
getter user_id
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
def initialize(@user_id : Int32, max_students : Int32, skif : Bool)
|
def initialize(@user_id : Int32, max_students : Int32)
|
||||||
super(max_students, skif)
|
super(max_students)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,12 +25,12 @@ module Backend
|
||||||
|
|
||||||
# DB representation of the enum
|
# DB representation of the enum
|
||||||
def to_db : Db::UserRole
|
def to_db : Db::UserRole
|
||||||
Db::UserRole.parse(self.to_s)
|
Db::UserRole.from_string(self.to_s.underscore)
|
||||||
end
|
end
|
||||||
|
|
||||||
# GraphQL representation of the DB enum
|
# GraphQL representation of the DB enum
|
||||||
def self.from_db(role : Db::UserRole) : self
|
def self.from_db(role : Db::UserRole) : self
|
||||||
Db::UserRole.to_api
|
role.to_api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,29 +42,31 @@ module Backend
|
||||||
db_object Db::User
|
db_object Db::User
|
||||||
|
|
||||||
# LDAP user data
|
# LDAP user data
|
||||||
getter ldap : Ldap::User?
|
def ldap : Ldap::User
|
||||||
|
unless raw_cache = Redis::CLIENT.get("ldap:user:#{id}")
|
||||||
|
Worker::Jobs::CacheLdapUserJob.new(id).perform
|
||||||
|
raw_cache = Redis::CLIENT.get("ldap:user:#{id}").not_nil!
|
||||||
|
end
|
||||||
|
|
||||||
# Refreshes LDAP user data
|
(@ldap ||= Ldap::User.from_json(raw_cache.not_nil!)).not_nil!
|
||||||
def refresh_ldap : Ldap::User
|
|
||||||
(@ldap ||= Ldap::User.from_json(Redis::CLIENT.get("ldap:user:#{id}").as(String))).not_nil!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's first name
|
# User's first name
|
||||||
def first_name : String
|
def first_name : String
|
||||||
refresh_ldap.first_name
|
ldap.first_name
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's last name
|
# User's last name
|
||||||
def last_name : String
|
def last_name : String
|
||||||
refresh_ldap.last_name
|
ldap.last_name
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's full name
|
# User's full name
|
||||||
def name : String
|
def name : String
|
||||||
refresh_ldap.name
|
ldap.name
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -76,7 +78,7 @@ module Backend
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's email
|
# User's email
|
||||||
def email : String
|
def email : String
|
||||||
refresh_ldap.email
|
ldap.email
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -91,10 +93,16 @@ module Backend
|
||||||
@model.role.to_api
|
@model.role.to_api
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
# User is at SKIF
|
||||||
|
def skif : Bool
|
||||||
|
@model.skif
|
||||||
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's external ID
|
# User's external ID
|
||||||
def external_id : Int32?
|
def external_id : Int32?
|
||||||
case @model.role
|
case @model.role.to_api
|
||||||
when .teacher?
|
when .teacher?
|
||||||
@model.teacher
|
@model.teacher
|
||||||
when .student?
|
when .student?
|
||||||
|
@ -127,12 +135,14 @@ module Backend
|
||||||
class UserCreateInput < GraphQL::BaseInputObject
|
class UserCreateInput < GraphQL::BaseInputObject
|
||||||
getter username
|
getter username
|
||||||
getter role
|
getter role
|
||||||
|
getter skif
|
||||||
getter admin
|
getter admin
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
def initialize(
|
def initialize(
|
||||||
@username : String,
|
@username : String,
|
||||||
@role : UserRole,
|
@role : UserRole,
|
||||||
|
@skif : Bool,
|
||||||
@admin : Bool = false
|
@admin : Bool = false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -132,6 +132,9 @@ module Backend
|
||||||
|
|
||||||
# Database URL
|
# Database URL
|
||||||
getter url : String
|
getter url : String
|
||||||
|
|
||||||
|
# Allow old database migrations to be used
|
||||||
|
getter allow_old_schema : Bool
|
||||||
end
|
end
|
||||||
|
|
||||||
# Configuration for `REDIS`
|
# Configuration for `REDIS`
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
require "granite"
|
require "clear"
|
||||||
require "granite/adapter/pg"
|
require "log"
|
||||||
|
require "db"
|
||||||
|
require "retriable"
|
||||||
|
|
||||||
require "./db/*"
|
require "./db/*"
|
||||||
|
|
||||||
|
@ -24,17 +26,23 @@ module Backend
|
||||||
module Db
|
module Db
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
Granite::Connections << Granite::Adapter::Pg.new(name: "pg", url: Backend.config.db.url)
|
# Migration UIDs
|
||||||
|
MIGRATIONS = {{ run("./macros/migrations.cr", "#{__DIR__}/db/migrations/*.cr").stringify.split("\n") }}
|
||||||
|
|
||||||
# Checks if database schema is up to date
|
def init(severity = {% if flag?(:development) %} ::Log::Severity::Debug {% else %} ::Log::Severity::Info {% end %}) : Nil
|
||||||
def schema_up_to_date_compare : Int32?
|
::Log.builder.bind "clear.*", severity, ::Log::IOBackend.new
|
||||||
migrations = Dir["db/migrations/*.sql"].map { |f| Path[f].basename }.sort!
|
Retriable.retry(on: DB::ConnectionRefused, backoff: false) do
|
||||||
return unless latest_migration = migrations.try(&.last.match(/\d+/).try(&.to_a.first.not_nil!.to_u64))
|
Clear::SQL.init(Backend.config.db.url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
begin
|
def schema_up_to_date? : Bool
|
||||||
MicrateDbVersion.order(tstamp: :desc).limit(1).assembler.select.run.first.version_id <=> latest_migration
|
last_migration = ClearMetadata.query.last!.value
|
||||||
rescue PQ::PQError
|
|
||||||
nil
|
if last_migration == "-1"
|
||||||
|
false
|
||||||
|
else
|
||||||
|
last_migration == MIGRATIONS.last
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
11
docker/backend/src/backend/db/clear_metadata.cr
Normal file
11
docker/backend/src/backend/db/clear_metadata.cr
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Backend
|
||||||
|
module Db
|
||||||
|
class ClearMetadata
|
||||||
|
include Clear::Model
|
||||||
|
self.table = :__clear_metadatas
|
||||||
|
|
||||||
|
column metatype : String, primary: true
|
||||||
|
column value : String, primary: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,26 +1,11 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
# Micrate DB migrator model / configuration
|
class MicrateDbVersion
|
||||||
class MicrateDbVersion < Granite::Base
|
include Clear::Model
|
||||||
table micrate_db_version
|
self.table = :micrate_db_version
|
||||||
|
|
||||||
|
primary_key type: :serial
|
||||||
|
|
||||||
column id : Int32, primary: true
|
|
||||||
column version_id : Int64
|
column version_id : Int64
|
||||||
column is_applied : Bool
|
column is_applied : Bool
|
||||||
column tstamp : Time?
|
column tstamp : Time?
|
||||||
|
|
9
docker/backend/src/backend/db/migrations.cr
Normal file
9
docker/backend/src/backend/db/migrations.cr
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
require "./migrations/*"
|
||||||
|
|
||||||
|
module Backend
|
||||||
|
module Db
|
||||||
|
# DB SQL migrations
|
||||||
|
module Migration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
44
docker/backend/src/backend/db/migrations/1_create_users.cr
Normal file
44
docker/backend/src/backend/db/migrations/1_create_users.cr
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
module Backend
|
||||||
|
module Db
|
||||||
|
module Migrations
|
||||||
|
class CreateUsers1
|
||||||
|
include Clear::Migration
|
||||||
|
|
||||||
|
def change(dir) : Nil
|
||||||
|
create_enum :user_role, %w(teacher student)
|
||||||
|
|
||||||
|
create_table :users, id: false do |t| # We create the table users
|
||||||
|
t.column :id, :serial, primary: true, null: false
|
||||||
|
t.column :username, :string, unique: true, index: true, null: false
|
||||||
|
t.column :role, :user_role, null: false
|
||||||
|
t.column :skif, :bool, null: false
|
||||||
|
t.column :admin, :bool, default: false, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :teachers, id: false do |t|
|
||||||
|
t.column :id, :serial, primary: true, null: false
|
||||||
|
t.references to: "users", on_delete: :cascade, null: false
|
||||||
|
t.column :max_students, :int, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :students, id: false do |t|
|
||||||
|
t.column :id, :serial, primary: true, null: false
|
||||||
|
t.references to: "users", on_delete: :cascade, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :votes, id: false do |t|
|
||||||
|
t.column :id, :serial, primary: true, null: false
|
||||||
|
t.references to: "students", on_delete: :cascade, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :teacher_votes, id: false do |t|
|
||||||
|
t.column :id, :serial, primary: true, null: false
|
||||||
|
t.references to: "votes", on_delete: :cascade, null: false
|
||||||
|
t.references to: "teachers", on_delete: :cascade, null: false
|
||||||
|
t.column :priority, :int, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,33 +1,14 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
# Student model
|
class Student
|
||||||
class Student < Granite::Base
|
include Clear::Model
|
||||||
table students
|
self.table = :students
|
||||||
|
|
||||||
belongs_to :user
|
primary_key type: :serial
|
||||||
has_one :vote
|
|
||||||
|
|
||||||
# Student's ID
|
belongs_to user : User
|
||||||
column id : Int64, primary: true
|
|
||||||
|
|
||||||
# Student is at SKIF
|
has_one vote : Vote?, foreign_key: :student_id
|
||||||
column skif : Bool
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,36 +1,16 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
# Teacher model
|
class Teacher
|
||||||
class Teacher < Granite::Base
|
include Clear::Model
|
||||||
table teachers
|
self.table = :teachers
|
||||||
|
|
||||||
belongs_to :user
|
primary_key type: :serial
|
||||||
has_many teacher_votes : TeacherVote
|
|
||||||
|
|
||||||
# Teacher's ID
|
belongs_to user : User
|
||||||
column id : Int64, primary: true
|
|
||||||
|
|
||||||
# Teacher's max students count
|
|
||||||
column max_students : Int32
|
column max_students : Int32
|
||||||
|
|
||||||
# Teacher is at SKIF
|
has_many teacher_votes : TeacherVote
|
||||||
column skif : Bool
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,49 +1,15 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
# Teacher vote model
|
class TeacherVote
|
||||||
class TeacherVote < Granite::Base
|
include Clear::Model
|
||||||
table teacher_votes
|
self.table = :teacher_votes
|
||||||
|
|
||||||
belongs_to :vote
|
primary_key type: :serial
|
||||||
belongs_to :teacher
|
|
||||||
|
|
||||||
# Teacher votes's ID
|
belongs_to vote : Vote
|
||||||
column id : Int64, primary: true
|
belongs_to teacher : Teacher
|
||||||
|
|
||||||
# Teacher vote's priority
|
|
||||||
column priority : Int32
|
column priority : Int32
|
||||||
|
|
||||||
validate :teacher, "must be vote unique" do |teacher_vote|
|
|
||||||
self.where(vote_id: teacher_vote.vote.id, teacher_id: teacher_vote.teacher.not_nil!.id).count.run.as(Int64).zero?
|
|
||||||
end
|
|
||||||
|
|
||||||
validate :priority, "must be positive" do |teacher_vote|
|
|
||||||
teacher_vote.priority >= 0
|
|
||||||
end
|
|
||||||
|
|
||||||
validate :priority, "must be less than the number of teachers" do |teacher_vote|
|
|
||||||
teacher_vote.priority < Teacher.count
|
|
||||||
end
|
|
||||||
|
|
||||||
validate :priority, "must be vote unique" do |teacher_vote|
|
|
||||||
self.where(vote_id: teacher_vote.vote.id, priority: teacher_vote.priority).count.run.as(Int64).zero?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,26 +1,8 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
# Possible roles a user can have
|
Clear.enum UserRole, :teacher, :student
|
||||||
enum UserRole
|
|
||||||
Teacher
|
|
||||||
Student
|
|
||||||
|
|
||||||
|
struct UserRole
|
||||||
# API representation of the enum
|
# API representation of the enum
|
||||||
def to_api : Api::Schema::UserRole
|
def to_api : Api::Schema::UserRole
|
||||||
Api::Schema::UserRole.parse(self.to_s)
|
Api::Schema::UserRole.parse(self.to_s)
|
||||||
|
@ -32,24 +14,19 @@ module Backend
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# User model
|
class User
|
||||||
class User < Granite::Base
|
include Clear::Model
|
||||||
table users
|
self.table = :users
|
||||||
|
|
||||||
has_one :teacher
|
primary_key type: :serial
|
||||||
has_one :student
|
|
||||||
|
|
||||||
# User's ID
|
|
||||||
column id : Int64, primary: true
|
|
||||||
|
|
||||||
# User's LDAP username
|
|
||||||
column username : String
|
column username : String
|
||||||
|
column role : UserRole
|
||||||
# User's role
|
|
||||||
column role : UserRole, converter: Granite::Converters::Enum(Backend::Db::UserRole, String)
|
|
||||||
|
|
||||||
# User is admin
|
|
||||||
column admin : Bool = false
|
column admin : Bool = false
|
||||||
|
column skif : Bool
|
||||||
|
|
||||||
|
has_one student : Student?, foreign_key: :user_id
|
||||||
|
has_one teacher : Teacher?, foreign_key: :user_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,28 +1,14 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
class Vote < Granite::Base
|
class Vote
|
||||||
table votes
|
include Clear::Model
|
||||||
|
self.table = :votes
|
||||||
|
|
||||||
belongs_to :student
|
primary_key type: :serial
|
||||||
has_many teacher_votes : TeacherVote
|
|
||||||
|
|
||||||
column id : Int64, primary: true
|
belongs_to student : Student
|
||||||
|
|
||||||
|
has_many teacher_votes : TeacherVote, foreign_key: :vote_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -77,7 +77,7 @@ module Backend
|
||||||
|
|
||||||
# Creates user data from DB entry index
|
# Creates user data from DB entry index
|
||||||
def self.from_id(id : Int32) : self
|
def self.from_id(id : Int32) : self
|
||||||
from_db(Db::User.find!(id))
|
from_username(Db::User.query.first(id).select(:id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
1
docker/backend/src/backend/macros/migrations.cr
Normal file
1
docker/backend/src/backend/macros/migrations.cr
Normal file
|
@ -0,0 +1 @@
|
||||||
|
print Dir[ARGV[0]].map { |f| File.basename(f).split("_", 2).first }.uniq!.sort!.join("\n")
|
|
@ -17,6 +17,7 @@
|
||||||
require "service"
|
require "service"
|
||||||
require "log"
|
require "log"
|
||||||
require "retriable"
|
require "retriable"
|
||||||
|
require "clear"
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
# Backend runner
|
# Backend runner
|
||||||
|
@ -44,18 +45,22 @@ module Backend
|
||||||
|
|
||||||
Log.info { "Checking if DB schema is up to date..." }
|
Log.info { "Checking if DB schema is up to date..." }
|
||||||
Retriable.retry(backoff: false, base_interval: 10.seconds, multiplier: 1.0) do
|
Retriable.retry(backoff: false, base_interval: 10.seconds, multiplier: 1.0) do
|
||||||
case Retriable.retry(on: DB::ConnectionRefused, backoff: false) do
|
ex : Clear::SQL::Error? = nil
|
||||||
Db.schema_up_to_date_compare
|
if begin
|
||||||
end
|
Db.schema_up_to_date?
|
||||||
when nil
|
rescue exc : Clear::SQL::Error
|
||||||
Log.fatal { "No database schema is applied. Please run `bash scripts/micrate.sh up` urgently!" }
|
ex = exc
|
||||||
raise Exception.new
|
|
||||||
when -1
|
false
|
||||||
Log.warn { "Database schema is not up to date. Please run `bash scripts/micrate.sh up`." }
|
end && ex.nil?
|
||||||
when 0
|
|
||||||
Log.info { "Database schema is up to date." }
|
Log.info { "Database schema is up to date." }
|
||||||
else
|
else
|
||||||
Log.warn { "Database schema is maybe up to date but not consistent. Please run `bash scripts/micrate.sh up` to be safe." }
|
Log.warn { "Database schema is not up to date. Please run `bash scripts/clear.sh migrate`." }
|
||||||
|
if ex
|
||||||
|
raise ex
|
||||||
|
else
|
||||||
|
raise Exception.new("Database schema is not up to date.") unless Backend.config.db.allow_old_schema
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -39,14 +39,29 @@ module Backend
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GraphQL query request data
|
||||||
|
struct GraphQLQueryData
|
||||||
|
include JSON::Serializable
|
||||||
|
|
||||||
|
# Raw query
|
||||||
|
getter query : String
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
getter variables = {} of String => JSON::Any
|
||||||
|
|
||||||
|
# Operation name
|
||||||
|
getter operation_name : String?
|
||||||
|
end
|
||||||
|
|
||||||
@[ARTA::Post("")]
|
@[ARTA::Post("")]
|
||||||
@[ATHA::QueryParam("development")]
|
@[ATHA::QueryParam("development")]
|
||||||
@[ATHA::ParamConverter("query", converter: ATH::RequestBodyConverter)]
|
def endpoint(request : ATH::Request, development : Bool = false) : ATH::Response
|
||||||
def endpoint(query : Backend::Web::GraphQLQueryData, request : ATH::Request, development : Bool = false) : ATH::Response
|
|
||||||
{% if flag?(:development) %}
|
{% if flag?(:development) %}
|
||||||
Log.notice { "Development request icoming" } if development
|
Log.notice { "Development request icoming" } if development
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
|
query = GraphQLQueryData.from_json(request.body.not_nil!)
|
||||||
|
|
||||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => "application/json"}) do |io|
|
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => "application/json"}) do |io|
|
||||||
Api::Schema::SCHEMA.execute(
|
Api::Schema::SCHEMA.execute(
|
||||||
io,
|
io,
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
|
||||||
module Web
|
|
||||||
# GraphQL query request data
|
|
||||||
struct GraphQLQueryData
|
|
||||||
include AVD::Validatable
|
|
||||||
include ASR::Serializable
|
|
||||||
|
|
||||||
@[Assert::NotBlank]
|
|
||||||
getter query : String
|
|
||||||
|
|
||||||
getter variables : Hash(String, JSON::Any)?
|
|
||||||
|
|
||||||
getter operation_name : String?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -24,11 +24,11 @@ module Backend
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def perform : Nil
|
def perform : Nil
|
||||||
key = "ldap:user:#{id}"
|
key = "ldap:user:#{id}"
|
||||||
user = Db::User.find(id)
|
user = Db::User.find!(id)
|
||||||
if user
|
if user
|
||||||
log "Caching user ##{id}..."
|
log "Caching user ##{id}..."
|
||||||
ldap_user = Ldap::User.from_username(user.username)
|
ldap_user = Ldap::User.from_username(user.username)
|
||||||
Redis::CLIENT.set(key, ldap_user.to_json)
|
Redis::CLIENT.set(key, ldap_user.to_json, ex: (Time.utc + Backend.config.ldap.cache_refresh_interval.minutes).to_unix)
|
||||||
else
|
else
|
||||||
log "User ##{id} not found. Deleting cache..."
|
log "User ##{id} not found. Deleting cache..."
|
||||||
Redis::CLIENT.del(key)
|
Redis::CLIENT.del(key)
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
module Backend
|
|
||||||
module Worker
|
|
||||||
module Jobs
|
|
||||||
# Peridically caches user data in redis cache
|
|
||||||
class CacheLdapUsersJob < Mosquito::PeriodicJob
|
|
||||||
run_every Backend.config.ldap.cache_refresh_interval.minutes
|
|
||||||
|
|
||||||
# :ditto:
|
|
||||||
def perform : Nil
|
|
||||||
Redis::CLIENT.keys("ldap:user:*")
|
|
||||||
.map(&.as(String).split(":")[2].to_i)
|
|
||||||
.concat(Db::User.all.map(&.id.not_nil!.to_i))
|
|
||||||
.uniq!
|
|
||||||
.each do |id|
|
|
||||||
spawn do
|
|
||||||
CacheLdapUserJob.new(id).enqueue
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Fiber.yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -21,14 +21,14 @@ module Backend
|
||||||
class SendTeachersRegistrationEmailJob < Mosquito::QueuedJob
|
class SendTeachersRegistrationEmailJob < Mosquito::QueuedJob
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def perform : Nil
|
def perform : Nil
|
||||||
users = Db::User.where(role: Db::UserRole::Teacher.to_s, teacher_id: nil)
|
users = Db::User.query.where { (x.role == Db::UserRole::Teacher) & (x.teacher_id == nil) }
|
||||||
count = users.count.run.as(Int64).to_i
|
count = users.count.to_i
|
||||||
|
|
||||||
channel = Channel(Nil).new(count)
|
channel = Channel(Nil).new(count)
|
||||||
|
|
||||||
users.each do |user|
|
users.each do |user|
|
||||||
spawn do
|
spawn do
|
||||||
fail unless user.role.teacher?
|
fail unless user.role.to_api.teacher?
|
||||||
|
|
||||||
ldap_user = Ldap::User.from_username(user.username)
|
ldap_user = Ldap::User.from_username(user.username)
|
||||||
log "Sending teacher registration email to #{ldap_user.email} ##{user.id}"
|
log "Sending teacher registration email to #{ldap_user.email} ##{user.id}"
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
require "commander"
|
require "commander"
|
||||||
require "../backend"
|
require "../backend"
|
||||||
|
|
||||||
|
Backend::Db.init
|
||||||
|
|
||||||
cli = Commander::Command.new do |cmd|
|
cli = Commander::Command.new do |cmd|
|
||||||
cmd.use = "backend"
|
cmd.use = "backend"
|
||||||
cmd.short = "Mentorenwahl backend CLI"
|
cmd.short = "Mentorenwahl backend CLI"
|
||||||
|
@ -70,6 +72,13 @@ cli = Commander::Command.new do |cmd|
|
||||||
c.short = "Seeds the database with required data"
|
c.short = "Seeds the database with required data"
|
||||||
c.long = c.short
|
c.long = c.short
|
||||||
|
|
||||||
|
c.flags.add do |f|
|
||||||
|
f.name = "skif"
|
||||||
|
f.long = "--skif"
|
||||||
|
f.default = false
|
||||||
|
f.description = "User at SKIF"
|
||||||
|
end
|
||||||
|
|
||||||
c.flags.add do |f|
|
c.flags.add do |f|
|
||||||
f.name = "admin"
|
f.name = "admin"
|
||||||
f.long = "--admin"
|
f.long = "--admin"
|
||||||
|
@ -77,22 +86,26 @@ cli = Commander::Command.new do |cmd|
|
||||||
f.description = "Register as admin"
|
f.description = "Register as admin"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
c.flags.add do |f|
|
||||||
|
f.name = "yes"
|
||||||
|
f.short = "-y"
|
||||||
|
f.long = "--yes"
|
||||||
|
f.default = false
|
||||||
|
f.description = "Answer yes to all questions"
|
||||||
|
end
|
||||||
|
|
||||||
c.run do |opts, args|
|
c.run do |opts, args|
|
||||||
username = args[0]
|
username = args[0]
|
||||||
role = Backend::Db::UserRole.parse(args[1].downcase)
|
role = Backend::Db::UserRole.from_string(args[1].underscore)
|
||||||
print "Register '#{username}' as '#{role}'#{opts.bool["admin"] ? " with admin privileges" : nil}? [y/N] "
|
unless opts.bool["yes"]
|
||||||
abort unless (gets(chomp: true) || "").strip.downcase == "y"
|
print "Register '#{username}' as '#{role.to_api}'#{opts.bool["admin"] ? " with admin privileges" : nil}? [y/N] "
|
||||||
|
abort unless gets(chomp: true).not_nil!.strip.downcase == "y"
|
||||||
|
end
|
||||||
|
|
||||||
user = Backend::Db::User.create!(username: username, role: role.to_s, admin: opts.bool["admin"])
|
user = Backend::Db::User.create!(username: username, role: role.to_s, skif: opts.bool["skif"], admin: opts.bool["admin"])
|
||||||
Backend::Worker::Jobs::CacheLdapUserJob.new(user.id.not_nil!.to_i).enqueue
|
Backend::Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
||||||
|
|
||||||
puts "Done!"
|
puts "Done!"
|
||||||
|
|
||||||
puts "---"
|
|
||||||
puts "User: #{user.id}"
|
|
||||||
puts "Role: #{user.role}"
|
|
||||||
puts "Admin: #{user.admin}"
|
|
||||||
puts "---"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
7
docker/backend/src/cli/clear.cr
Normal file
7
docker/backend/src/cli/clear.cr
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
require "clear"
|
||||||
|
require "../backend"
|
||||||
|
require "log"
|
||||||
|
|
||||||
|
Backend::Db.init(Log::Severity::Debug)
|
||||||
|
|
||||||
|
Clear::CLI.run(false)
|
|
@ -1,21 +0,0 @@
|
||||||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
|
||||||
# Copyright (C) 2022 Dominic Grimm
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
require "micrate"
|
|
||||||
require "pg"
|
|
||||||
|
|
||||||
Micrate::DB.connection_url = ENV["BACKEND_DB_URL"]?
|
|
||||||
Micrate::Cli.run
|
|
|
@ -11,3 +11,4 @@ Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
.gitignore
|
.gitignore
|
||||||
README.md
|
README.md
|
||||||
|
.nvmrc
|
||||||
|
|
1
docker/frontend/.nvmrc
Normal file
1
docker/frontend/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
16.14.2
|
|
@ -16,4 +16,4 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
docker-compose exec backend ./bin/backend "$@"
|
docker-compose exec backend backend "$@"
|
||||||
|
|
|
@ -16,4 +16,4 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
docker-compose exec backend ./bin/micrate "$@"
|
docker-compose exec backend clear "$@"
|
Loading…
Reference in a new issue