mentorenwahl/backend/src/backend/worker/jobs/assignment_job.cr

171 lines
6.0 KiB
Crystal

# 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
# Assigns students to teachers when all students voted
class AssignStudentsJob < Mosquito::QueuedJob
# run_every 1.minute
def rescheduleable? : Bool
false
end
alias TeacherVote = {student: Int32, priority: Int32}
alias Assignment = {teacher: Int32, priority: Int32}
# :ditto:
def perform : Nil
teacher_count = Db::Teacher.query.count
student_count = Db::Student.query.count
vote_count = Db::Vote.query.count
if teacher_count == 0
log "No teachers found, skipping assignment"
fail
elsif student_count == 0
log "No students found, skipping assignment"
fail
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
elsif Db::Assignment.query.count > 0
log "Assignment has already run, skipping another assignment"
fail
end
teachers = Db::Teacher.query
.where do
raw("EXISTS (SELECT 1 FROM teacher_votes WHERE teacher_id = teachers.id)") &
(max_students > 0)
end
.with_teacher_votes
.to_a
vote_index = Hash.zip(teachers.map(&.id), [0] * teachers.size)
teacher_votes : Hash(Int32, Array(TeacherVote)) = Hash.zip(
teachers.map(&.id),
teachers.map do |t|
t.teacher_votes.map do |tv|
vote_index[t.id] += 1
{
student: tv.vote.student.id,
priority: tv.priority,
}
end
end
)
teachers.sort_by! { |t| vote_index[t.id] }
students = Db::Student.query
.with_vote(&.with_teacher_votes(&.order_by(priority: :asc)))
.to_a
student_ids = students.map(&.id)
votes = Hash.zip(
student_ids,
students.map do |s|
s.vote.not_nil!.teacher_votes
.to_a
.select { |tv| teacher_votes.has_key?(tv.teacher.id) }
.map do |tv|
{
teacher: tv.teacher.id,
teacher_max_students: tv.teacher.max_students,
}
end
end
)
best_assignment = {
assignment: {} of Int32 => Assignment,
score: Float32::INFINITY,
}
Backend.config.assignment_possibility_count.times.each do
assignment = {} of Int32 => Assignment
assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size)
# teachers.each do |t|
# queue = Deque.new(teacher_votes[t.id].shuffle)
# count = 1
# while count < t.max_students
# break unless x = queue.shift?
# tv = x.not_nil!
# if assignment[tv[:student]]?.nil? || assignment[tv[:student]][:priority] <= tv[:priority]
# assignment[tv[:student]] = {teacher: t.id, priority: tv[:priority]}
# count += 1
# end
# end
# end
votes.to_a.shuffle.each do |s, tvs|
tvs.each_with_index do |tv, i|
if assignment[s]?.nil?
assignment_count[tv[:teacher]] += 1
assignment[s] = {teacher: tv[:teacher], priority: i}
elsif assignment_count[tv[:teacher]] < tv[:teacher_max_students]
assignment_count[assignment[s][:teacher]] -= 1
assignment_count[tv[:teacher]] += 1
assignment[s] = {teacher: tv[:teacher], priority: i}
end
end
end
pp! assignment, assignment_count
score = 0_f32
# positivity = 0
# assignment.each do |s, a|
# ratio = (vote_sizes[s] - a[:priority]) / vote_sizes[s]
# score += 2 ** ratio
# positivity += ratio > 0.5 ? 1 : -1
# end
assignment.each do |s, a|
size = votes[s].size
p! a[:priority], (votes[s].size - a[:priority]) / size
# score += 1 if ((votes[s].size - a[:priority]) / size) >= 0.5
score += a[:priority]
end
# full_score = score ** positivity
if score < best_assignment[:score]
best_assignment = {
assignment: assignment,
score: score,
}
end
end
pp! best_assignment
str = String.build do |str|
str << "===========================\n"
best_assignment[:assignment].each do |s, a|
str << "#{Db::Student.query.find!(s).user.username} : #{Db::Teacher.query.find!(a[:teacher]).user.username} (#{a[:priority]} / #{votes[s].size})\n"
end
str << "===========================\n"
end
print str
end
end
end
end
end