# 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 . 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