This commit is contained in:
parent
48b25adb07
commit
1c72c81b85
|
@ -23,6 +23,7 @@ POSTGRES_PASSWORD=
|
|||
|
||||
# Backend
|
||||
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT=6
|
||||
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT=500
|
||||
BACKEND_URL=URL
|
||||
# Backend - API
|
||||
BACKEND_API_GRAPHQL_PLAYGROUND=false
|
||||
|
|
|
@ -78,6 +78,7 @@ services:
|
|||
environment:
|
||||
BACKEND_URL: ${URL}
|
||||
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT: ${BACKEND_MINIMUM_TEACHER_SELECTION_COUNT}
|
||||
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT: ${BACKEND_ASSIGNMENT_POSSIBILITY_COUNT}
|
||||
BACKEND_API_GRAPHQL_PLAYGROUND: ${BACKEND_API_GRAPHQL_PLAYGROUND}
|
||||
BACKEND_API_JWT_SECRET: ${BACKEND_API_JWT_SECRET}
|
||||
BACKEND_API_JWT_EXPIRATION: ${BACKEND_API_JWT_EXPIRATION}
|
||||
|
|
|
@ -14,12 +14,12 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
FROM crystallang/crystal:1.4-alpine as micrate-deps
|
||||
FROM crystallang/crystal:1.5-alpine as micrate-deps
|
||||
WORKDIR /src
|
||||
COPY ./micrate/shard.yml ./micrate/shard.lock ./
|
||||
RUN shards install --production
|
||||
|
||||
FROM crystallang/crystal:1.4-alpine as micrate-builder
|
||||
FROM crystallang/crystal:1.5-alpine as micrate-builder
|
||||
WORKDIR /src
|
||||
# RUN apk add --no-cache sqlite-static
|
||||
COPY --from=micrate-deps /src/shard.yml /src/shard.lock ./
|
||||
|
@ -30,16 +30,14 @@ RUN shards build --release --static --verbose -s -p -t
|
|||
FROM tdewolff/minify as public
|
||||
WORKDIR /src
|
||||
COPY ./public ./src
|
||||
RUN minify -r -o ./tmp_dist ./src
|
||||
RUN mv ./tmp_dist/src ./dist
|
||||
RUN rm -rf ./tmp_dist
|
||||
RUN minify -r -o ./dist ./src
|
||||
|
||||
FROM crystallang/crystal:1.4-alpine as deps
|
||||
FROM crystallang/crystal:1.5-alpine as deps
|
||||
WORKDIR /src
|
||||
COPY ./shard.yml ./shard.lock ./
|
||||
RUN shards install --production
|
||||
|
||||
FROM crystallang/crystal:1.4-alpine as builder
|
||||
FROM crystallang/crystal:1.5-alpine as builder
|
||||
ARG BUILD_ENV
|
||||
WORKDIR /src/mentorenwahl
|
||||
RUN apk add --no-cache pcre2-dev
|
||||
|
|
|
@ -25,4 +25,4 @@ prod:
|
|||
shards build --static --release --verbose -s -p -t
|
||||
|
||||
docs:
|
||||
crystal docs --project-name "Mentorenwahl Backend" -D granite_docs
|
||||
crystal docs --project-name "Mentorenwahl Backend"
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
CREATE TYPE user_roles AS ENUM ('teacher', 'student');
|
||||
|
||||
CREATE TABLE users(
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
role user_roles NOT NULL,
|
||||
skif BOOLEAN NOT NULL,
|
||||
|
@ -28,43 +28,34 @@ CREATE TABLE users(
|
|||
);
|
||||
|
||||
CREATE TABLE teachers(
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL UNIQUE REFERENCES users(id),
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE REFERENCES users(id),
|
||||
max_students INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE students(
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL UNIQUE REFERENCES users(id)
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE REFERENCES users(id)
|
||||
);
|
||||
|
||||
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)
|
||||
id SERIAL PRIMARY KEY,
|
||||
student_id INT 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),
|
||||
id SERIAL PRIMARY KEY,
|
||||
vote_id INT NOT NULL REFERENCES votes(id),
|
||||
teacher_id INT NOT NULL REFERENCES teachers(id),
|
||||
priority INT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE assignments(
|
||||
id SERIAL PRIMARY KEY,
|
||||
student_id INT NOT NULL REFERENCES students(id),
|
||||
teacher_id INT NOT NULL REFERENCES teachers(id)
|
||||
);
|
||||
|
||||
-- +micrate Down
|
||||
-- SQL section ' Down ' is executed when this migration is rolled back
|
||||
DROP TABLE teacher_votes;
|
||||
|
|
|
@ -8,7 +8,7 @@ targets:
|
|||
micrate:
|
||||
main: src/micrate.cr
|
||||
|
||||
crystal: 1.4.0
|
||||
crystal: 1.5.0
|
||||
|
||||
dependencies:
|
||||
micrate:
|
||||
|
|
|
@ -6,35 +6,39 @@ shards:
|
|||
|
||||
athena:
|
||||
git: https://github.com/athena-framework/framework.git
|
||||
version: 0.16.0
|
||||
version: 0.17.0
|
||||
|
||||
athena-config:
|
||||
git: https://github.com/athena-framework/config.git
|
||||
version: 0.3.0
|
||||
version: 0.3.1
|
||||
|
||||
athena-dependency_injection:
|
||||
git: https://github.com/athena-framework/dependency-injection.git
|
||||
version: 0.3.2
|
||||
version: 0.3.3
|
||||
|
||||
athena-event_dispatcher:
|
||||
git: https://github.com/athena-framework/event-dispatcher.git
|
||||
version: 0.1.3
|
||||
version: 0.1.4
|
||||
|
||||
athena-image_size:
|
||||
git: https://github.com/athena-framework/image-size.git
|
||||
version: 0.1.1
|
||||
|
||||
athena-negotiation:
|
||||
git: https://github.com/athena-framework/negotiation.git
|
||||
version: 0.1.1
|
||||
version: 0.1.2
|
||||
|
||||
athena-routing:
|
||||
git: https://github.com/athena-framework/routing.git
|
||||
version: 0.1.1
|
||||
version: 0.1.2
|
||||
|
||||
athena-serializer:
|
||||
git: https://github.com/athena-framework/serializer.git
|
||||
version: 0.2.10
|
||||
version: 0.3.0
|
||||
|
||||
athena-validator:
|
||||
git: https://github.com/athena-framework/validator.git
|
||||
version: 0.1.7
|
||||
version: 0.2.0
|
||||
|
||||
baked_file_system:
|
||||
git: https://github.com/schovi/baked_file_system.git
|
||||
|
@ -60,6 +64,10 @@ shards:
|
|||
git: https://github.com/crystal-lang/crystal-db.git
|
||||
version: 0.11.0
|
||||
|
||||
docker:
|
||||
git: https://github.com/repomaa/docker.cr.git
|
||||
version: 0.1.2
|
||||
|
||||
email:
|
||||
git: https://github.com/arcage/crystal-email.git
|
||||
version: 0.6.4
|
||||
|
@ -100,17 +108,13 @@ shards:
|
|||
git: https://git.dergrimm.net/dergrimm/ldap_escape.git
|
||||
version: 0.1.0
|
||||
|
||||
micrate:
|
||||
git: https://github.com/amberframework/micrate.git
|
||||
version: 0.3.3
|
||||
|
||||
mosquito:
|
||||
git: https://github.com/mosquito-cr/mosquito.git
|
||||
version: 1.0.0.rc1+git.commit.afd53dd241447b60ece9232b6c71669b192baaa4
|
||||
version: 0.11.2
|
||||
|
||||
openssl_ext:
|
||||
git: https://github.com/spider-gazelle/openssl_ext.git
|
||||
version: 2.1.5
|
||||
version: 2.2.0
|
||||
|
||||
pg:
|
||||
git: https://github.com/will/crystal-pg.git
|
||||
|
|
|
@ -26,7 +26,7 @@ targets:
|
|||
backend:
|
||||
main: src/cli/backend.cr
|
||||
|
||||
crystal: 1.4.0
|
||||
crystal: 1.5.0
|
||||
|
||||
dependencies:
|
||||
clear:
|
||||
|
@ -40,7 +40,6 @@ dependencies:
|
|||
github: mrrooijen/commander
|
||||
mosquito:
|
||||
github: mosquito-cr/mosquito
|
||||
branch: master
|
||||
quartz_mailer:
|
||||
github: amberframework/quartz-mailer
|
||||
kilt:
|
||||
|
@ -67,3 +66,5 @@ dependencies:
|
|||
github: schovi/baked_file_system
|
||||
compiled_license:
|
||||
git: https://git.dergrimm.net/dergrimm/compiled_license.git
|
||||
docker:
|
||||
github: repomaa/docker.cr
|
||||
|
|
|
@ -22,19 +22,28 @@ module Backend
|
|||
# GraphQL request context class
|
||||
class Context < GraphQL::Context
|
||||
# Development mode
|
||||
getter development : Bool
|
||||
getter development
|
||||
|
||||
# Authenticated user
|
||||
getter user : Db::User?
|
||||
getter user
|
||||
|
||||
# User is admin
|
||||
getter admin : Bool?
|
||||
getter admin
|
||||
|
||||
# User's role
|
||||
getter role : Schema::UserRole?
|
||||
getter role
|
||||
|
||||
# User's external object
|
||||
getter external : (Db::Teacher | Db::Student)?
|
||||
getter external
|
||||
|
||||
def initialize(
|
||||
@development : Bool,
|
||||
@user : Db::User?,
|
||||
@admin : Bool?,
|
||||
@role : Schema::UserRole?,
|
||||
@external : (Db::Teacher | Db::Student)?
|
||||
)
|
||||
end
|
||||
|
||||
def initialize(headers : HTTP::Headers, @development : Bool, *rest)
|
||||
super(*rest)
|
||||
|
@ -47,8 +56,8 @@ module Backend
|
|||
return unless @user = Db::User.find(data["user_id"].as_i)
|
||||
|
||||
if @user
|
||||
@admin = @user.not_nil!.admin
|
||||
@role = @user.not_nil!.role.to_api
|
||||
@admin = user.not_nil!.admin
|
||||
@role = user.not_nil!.role.to_api
|
||||
@external =
|
||||
case @role.not_nil!
|
||||
when .teacher?
|
||||
|
@ -92,7 +101,7 @@ module Backend
|
|||
return true if @role == role &&
|
||||
if external_check
|
||||
role ==
|
||||
case @external.not_nil!
|
||||
case @external.not_nil! # TODO: Simplify with Germanium in future but for now with macro iteration over `#resolve#constants`
|
||||
when Db::Teacher
|
||||
Schema::UserRole::Teacher
|
||||
when Db::Student
|
||||
|
|
|
@ -27,7 +27,7 @@ module Backend
|
|||
raise "Auth failed" if username.empty? || password.empty?
|
||||
|
||||
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::DN.uid(username), password)
|
||||
|
||||
LoginPayload.new(
|
||||
user: User.new(user),
|
||||
|
@ -153,6 +153,7 @@ module Backend
|
|||
def create_vote(context : Context, input : VoteCreateInput) : Vote
|
||||
context.student!
|
||||
|
||||
raise "Duplicate teachers" if input.teacher_ids.uniq.size != input.teacher_ids.size
|
||||
raise "Not enough teachers" if input.teacher_ids.size < Backend.config.minimum_teacher_selection_count
|
||||
teacher_role_count = Db::User.query.where(role: Db::UserRole::Teacher).count
|
||||
raise "Teachers not registered" if teacher_role_count != Db::Teacher.query.count || teacher_role_count.zero?
|
||||
|
|
|
@ -55,6 +55,13 @@ module Backend
|
|||
Db::User.query.map { |user| User.new(user) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user_by_username(context : Context, username : String) : User
|
||||
context.admin!
|
||||
|
||||
User.new(Db::User.query.find { var(:username) == username }.not_nil!)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# All admins
|
||||
def admins(context : Context) : Array(User)
|
||||
|
@ -112,7 +119,10 @@ module Backend
|
|||
def all_students_voted(context : Context) : Bool
|
||||
context.admin!
|
||||
|
||||
Db::Vote.query.count >= Db::Student.query.count
|
||||
votes = Db::Vote.query.count
|
||||
students = Db::Student.query.count
|
||||
|
||||
students > 0 && votes >= students
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
|
|
|
@ -35,6 +35,12 @@ module Backend
|
|||
def max_students : Int32
|
||||
@model.max_students
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Students' votes
|
||||
def teacher_votes : Array(TeacherVote)
|
||||
Db::TeacherVote.query.where { teacher_id == @model.id }.map { |tv| TeacherVote.new(tv) }
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
|
|
|
@ -35,6 +35,12 @@ module Backend
|
|||
def priority : Int32
|
||||
@model.priority
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Student's vote
|
||||
def vote : Vote
|
||||
Vote.new(@model.vote)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
|
|
|
@ -41,6 +41,8 @@ module Backend
|
|||
|
||||
db_object Db::User
|
||||
|
||||
@ldap : Ldap::User?
|
||||
|
||||
# LDAP user data
|
||||
def ldap : Ldap::User
|
||||
unless raw_cache = Redis::CLIENT.get("ldap:user:#{id}")
|
||||
|
|
95
docker/backend/src/backend/cli.cr
Normal file
95
docker/backend/src/backend/cli.cr
Normal file
|
@ -0,0 +1,95 @@
|
|||
require "commander"
|
||||
require "compiled_license"
|
||||
|
||||
module Backend
|
||||
CLI = Commander::Command.new do |cmd|
|
||||
cmd.use = "backend"
|
||||
cmd.short = "Mentorenwahl backend CLI"
|
||||
|
||||
cmd.run do
|
||||
puts cmd.help
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "version"
|
||||
c.short = "Prints version"
|
||||
c.long = c.short
|
||||
|
||||
c.run do
|
||||
puts VERSION
|
||||
end
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "authors"
|
||||
c.short = "Prints authors"
|
||||
c.long = c.short
|
||||
|
||||
c.run do
|
||||
puts AUTHORS.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "licenses"
|
||||
c.short = "Prints licenses of projects used by this programs"
|
||||
c.long = c.short
|
||||
|
||||
c.run do
|
||||
puts CompiledLicense::LICENSES
|
||||
end
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "run"
|
||||
c.short = "Run the backend"
|
||||
c.long = c.short
|
||||
|
||||
c.run do
|
||||
Runner.new.run
|
||||
end
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "register <username> <role>"
|
||||
c.short = "Seeds the database with required data"
|
||||
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|
|
||||
f.name = "admin"
|
||||
f.long = "--admin"
|
||||
f.default = false
|
||||
f.description = "Register as admin"
|
||||
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|
|
||||
username = args[0]
|
||||
role = Db::UserRole.from_string(args[1].underscore)
|
||||
unless opts.bool["yes"]
|
||||
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 = Db::User.create!(username: username, role: role.to_s, skif: opts.bool["skif"], admin: opts.bool["admin"])
|
||||
Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
||||
|
||||
puts "Done!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -61,6 +61,9 @@ module Backend
|
|||
# Minimum teacher selection count
|
||||
getter minimum_teacher_selection_count : Int32
|
||||
|
||||
# Assignment possibility count
|
||||
getter assignment_possibility_count : Int32
|
||||
|
||||
@[EnvConfig::Setting(key: "api")]
|
||||
# Configuration for `Api`
|
||||
getter api : ApiConfig
|
||||
|
|
29
docker/backend/src/backend/db/assignment.cr
Normal file
29
docker/backend/src/backend/db/assignment.cr
Normal file
|
@ -0,0 +1,29 @@
|
|||
# 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 Db
|
||||
class Assignment
|
||||
include Clear::Model
|
||||
self.table = :assignments
|
||||
|
||||
primary_key type: :serial
|
||||
|
||||
belongs_to student : Student
|
||||
belongs_to teacher : Teacher
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ module Backend
|
|||
belongs_to user : User
|
||||
|
||||
has_one vote : Vote?, foreign_key: :student_id
|
||||
has_one assignment : Assignment?, foreign_key: :student_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,8 @@ module Backend
|
|||
|
||||
column max_students : Int32
|
||||
|
||||
has_many teacher_votes : TeacherVote
|
||||
has_many teacher_votes : TeacherVote, foreign_key: :teacher_id
|
||||
has_many assignments : Assignment, foreign_key: :teacher_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,9 +26,7 @@ module Backend
|
|||
module Ldap
|
||||
extend self
|
||||
|
||||
CLIENT = ConnectionPool.new do
|
||||
create_client
|
||||
end
|
||||
CLIENT = ConnectionPool.new(capacity: 25) { create_client }
|
||||
|
||||
# Creates a new LDAP connection
|
||||
private def create_client : LDAP::Client
|
||||
|
@ -37,7 +35,7 @@ module Backend
|
|||
|
||||
# Checks if credentials are valid
|
||||
def authenticate?(dn : String, password : String) : Bool
|
||||
!!CLIENT.connection(&.authenticate(dn, password))
|
||||
!!create_client.authenticate(dn, password)
|
||||
rescue LDAP::Client::AuthError
|
||||
false
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
module Backend
|
||||
module Ldap
|
||||
# DN construction utilities
|
||||
module Constructor
|
||||
module DN
|
||||
extend self
|
||||
|
||||
# Constructs a CN DN from a username
|
|
@ -67,7 +67,7 @@ module Backend
|
|||
|
||||
# Creates user data from LDAP username
|
||||
def self.from_username(username : String) : self
|
||||
from_dn(Ldap::Constructor.uid(username))
|
||||
from_dn(Ldap::DN.uid(username))
|
||||
end
|
||||
|
||||
# Creates user data from DB entry
|
||||
|
|
|
@ -36,9 +36,6 @@ module Backend
|
|||
|
||||
# Run the backend
|
||||
def run : self
|
||||
Signal::INT.trap { exit }
|
||||
Signal::TERM.trap { exit }
|
||||
|
||||
{% if flag?(:development) %}
|
||||
Log.warn { "Backend is running in development mode! Do not use this in production!" }
|
||||
{% end %}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
require "http/headers"
|
||||
require "mime"
|
||||
|
||||
module Backend
|
||||
module Web
|
||||
|
@ -25,12 +26,12 @@ module Backend
|
|||
@[ARTA::Get("")]
|
||||
def playground : ATH::Response | ATH::Exceptions::HTTPException
|
||||
{% if flag?(:development) %}
|
||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => "text/html"}) do |io|
|
||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => MIME.from_extension(".html")}) do |io|
|
||||
IO.copy(Public.get("index.html"), io)
|
||||
end
|
||||
{% else %}
|
||||
if Backend.config.api.graphql_playground_fully_enabled?
|
||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => "text/html"}) do |io|
|
||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => MIME.from_extension(".html")}) do |io|
|
||||
IO.copy(Public.get("index.html"), io)
|
||||
end
|
||||
else
|
||||
|
@ -62,7 +63,7 @@ module Backend
|
|||
|
||||
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" => MIME.from_extension(".json")}) do |io|
|
||||
Api::Schema::SCHEMA.execute(
|
||||
io,
|
||||
query.query,
|
||||
|
|
|
@ -24,7 +24,7 @@ module Backend
|
|||
Log = ::Log.for(self)
|
||||
|
||||
# Runs web service
|
||||
def run(_unit : ::Service::Unit) : ::Service::Unit?
|
||||
def run(unit : ::Service::Unit) : ::Service::Unit?
|
||||
Log.info { "Starting web service..." }
|
||||
ATH.run(80)
|
||||
Log.info { "Web service stopped." }
|
||||
|
|
|
@ -14,28 +14,101 @@
|
|||
# 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 "random/secure"
|
||||
|
||||
module Backend
|
||||
module Worker
|
||||
module Jobs
|
||||
# Assigns students to teachers when all students voted
|
||||
class AssignStudentsJob < Mosquito::QueuedJob
|
||||
def rescheduleable?
|
||||
false
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def perform : Nil
|
||||
if Db::User.query.count == 0
|
||||
user_count = Db::User.query.count
|
||||
teacher_count = Db::Teacher.query.count
|
||||
student_count = Db::Student.query.count
|
||||
vote_count = Db::Vote.query.count
|
||||
|
||||
if user_count == 0
|
||||
log "No users found, skipping assignment"
|
||||
fail
|
||||
elsif Db::Teacher.query.count == 0
|
||||
elsif teacher_count == 0
|
||||
log "No teachers found, skipping assignment"
|
||||
fail
|
||||
elsif Db::Student.query.count == 0
|
||||
elsif student_count == 0
|
||||
log "No students found, skipping assignment"
|
||||
fail
|
||||
elsif Db::Student.query.count != Db::User.query.where(role: Db::UserRole::Student).count
|
||||
elsif student_count != Db::User.query.where(role: Db::UserRole::Student).count
|
||||
log "Not all students registered, skipping assignment"
|
||||
fail
|
||||
elsif vote_count < student_count
|
||||
log "Not all students voted, skipping assignment"
|
||||
fail
|
||||
end
|
||||
|
||||
pp! Db::Teacher.query.to_a, Db::Student.query.to_a
|
||||
# possibilities = [] of Possibility
|
||||
# teachers = Db::Teacher.query.to_a.select! { |t| t.assignments.count < t.max_students }
|
||||
# empty_assignment = Hash.zip(teachers.map(&.id), [[] of StudentAssignment] * teachers.size)
|
||||
# random_votes = Db::Student.query.to_a.select!(&.vote).map do |s|
|
||||
# {
|
||||
# student: s.id,
|
||||
# teachers: s.vote.not_nil!.teacher_votes.to_a
|
||||
# .sort_by!(&.priority)
|
||||
# .reverse!
|
||||
# .map(&.teacher.id),
|
||||
# }
|
||||
# end
|
||||
|
||||
# Backend.config.assignment_retry_count.times do
|
||||
# pp! random_votes.shuffle!(Random::Secure)
|
||||
# a = empty_assignment.clone
|
||||
# random_votes.
|
||||
|
||||
# possibilities << {assignment: a, weighting: 0}
|
||||
# end
|
||||
|
||||
# pp! possibilities
|
||||
|
||||
teacher_ids = Db::Teacher.query
|
||||
.select("id")
|
||||
.where { raw("(SELECT COUNT(*) FROM assignments WHERE teacher_id = teachers.id)") < max_students }
|
||||
.map(&.id)
|
||||
students = Db::Student.query
|
||||
.where do
|
||||
raw("NOT EXISTS (SELECT 1 FROM assignments WHERE student_id = students.id)") &
|
||||
raw("EXISTS (SELECT 1 FROM votes WHERE student_id = students.id)")
|
||||
end
|
||||
.with_vote(&.with_teacher_votes(&.order_by(:priority, :asc)))
|
||||
# student_ids = students.map(&.id)
|
||||
|
||||
# votes = Hash.zip(
|
||||
# student_ids,
|
||||
# students.map do |s|
|
||||
# s.vote.not_nil!.teacher_votes.map(&.teacher_id)
|
||||
# end,
|
||||
# )
|
||||
# pp! votes, student_ids
|
||||
|
||||
random_votes = students.map do |s|
|
||||
{
|
||||
student: s.id,
|
||||
teachers: s.vote.not_nil!.teacher_votes.map(&.teacher_id),
|
||||
}
|
||||
end
|
||||
|
||||
assignments = [] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}
|
||||
empty_assignment = Hash.zip(teacher_ids, [[] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}] * teacher_ids.size)
|
||||
Backend.config.assignment_possibility_count.times do
|
||||
random_votes.shuffle!(Random::Secure)
|
||||
|
||||
votes = random_votes.dup
|
||||
a = empty_assignment.clone
|
||||
|
||||
end
|
||||
pp! assignments
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@ module Backend
|
|||
Log = ::Log.for(self)
|
||||
|
||||
# Runs worker service
|
||||
def run(_unit : ::Service::Unit) : ::Service::Unit?
|
||||
def run(unit : ::Service::Unit) : ::Service::Unit?
|
||||
Log.info { "Starting worker service..." }
|
||||
Mosquito::Runner.start
|
||||
Log.info { "Worker service stopped." }
|
||||
|
|
|
@ -15,9 +15,12 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
require "commander"
|
||||
require "../backend"
|
||||
require "compiled_license"
|
||||
require "docker"
|
||||
|
||||
require "../backend"
|
||||
|
||||
Docker.setup
|
||||
Backend::Db.init
|
||||
|
||||
cli = Commander::Command.new do |cmd|
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "clear"
|
||||
require "../backend"
|
||||
require "log"
|
||||
|
||||
Backend::Db.init(Log::Severity::Debug)
|
||||
|
||||
Clear::CLI.run(false)
|
Loading…
Reference in a new issue