From 8bfd9a4e2acbf9ffecda23e17da1dc8b8b63413e Mon Sep 17 00:00:00 2001 From: Dominic Grimm Date: Sun, 5 Feb 2023 17:25:03 +0100 Subject: [PATCH] Add temporary stat meassuring --- backend/Dockerfile | 12 +- .../20220414171336_create_users.sql | 8 + backend/src/backend/api/schema/mutation.cr | 2 +- backend/src/backend/api/schema/query.cr | 2 +- backend/src/backend/cli.cr | 229 ++++++++++++++++++ backend/src/backend/db/assignment.cr | 9 +- backend/src/backend/db/student_assignment.cr | 12 + .../src/backend/worker/jobs/assignment_job.cr | 27 ++- 8 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 backend/src/backend/db/student_assignment.cr diff --git a/backend/Dockerfile b/backend/Dockerfile index 3399b3a..908c6fe 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -69,18 +69,18 @@ RUN if [ "${BUILD_ENV}" = "development" ]; then \ xargs -I % sh -c 'mkdir -p $(dirname deps%); cp % deps%;'; \ fi -FROM alpine as config -WORKDIR /usr/src/config -RUN mkdir ./tmp -RUN chmod -R 1777 ./tmp +# FROM alpine as config +# WORKDIR /usr/src/config +# RUN mkdir ./tmp +# RUN chmod -R 1777 ./tmp -FROM scratch as runner +FROM busybox as runner LABEL maintainer="Dominic Grimm " \ org.opencontainers.image.description="Backend of Mentorenwahl" \ org.opencontainers.image.licenses="GPL-3.0" \ org.opencontainers.image.source="https://git.dergrimm.net/mentorenwahl/mentorenwahl" \ org.opencontainers.image.url="https://git.dergrimm.net/mentorenwahl/mentorenwahl" -COPY --from=config /usr/src/config/tmp /tmp +# COPY --from=config /usr/src/config/tmp /tmp WORKDIR /usr/src/mentorenwahl COPY --from=micrate-builder /usr/src/micrate/bin/micrate ./bin/micrate COPY --from=builder /usr/src/mentorenwahl/db ./db diff --git a/backend/db/migrations/20220414171336_create_users.sql b/backend/db/migrations/20220414171336_create_users.sql index 3346b9b..a26f10b 100644 --- a/backend/db/migrations/20220414171336_create_users.sql +++ b/backend/db/migrations/20220414171336_create_users.sql @@ -68,6 +68,14 @@ CREATE TABLE teacher_votes( CREATE TABLE assignments( id serial PRIMARY KEY, + active boolean NOT NULL, + priority_score bigint NOT NULL, + teacher_score bigint NOT NULL +); + +CREATE TABLE student_assignments( + id serial PRIMARY KEY, + assignment_id int NULL NULL REFERENCES assignments(id), student_id int NOT NULL REFERENCES students(id) UNIQUE, teacher_id int NOT NULL REFERENCES teachers(id) ); diff --git a/backend/src/backend/api/schema/mutation.cr b/backend/src/backend/api/schema/mutation.cr index c262017..ba9d7ac 100644 --- a/backend/src/backend/api/schema/mutation.cr +++ b/backend/src/backend/api/schema/mutation.cr @@ -222,7 +222,7 @@ module Backend def set_voting(context : Context, state : Bool) : Bool? context.admin! - config = Db::Config.query.where { active == true }.first! + config = Db::Config.query.where { active }.first! config.can_vote = state config.save! diff --git a/backend/src/backend/api/schema/query.cr b/backend/src/backend/api/schema/query.cr index bf5d0e3..f574b5b 100644 --- a/backend/src/backend/api/schema/query.cr +++ b/backend/src/backend/api/schema/query.cr @@ -146,7 +146,7 @@ module Backend # teacher_role_count = Db::User.query.where(role: Db::UserRole::Teacher).count # teacher_role_count > 0 && teacher_role_count == Db::Teacher.query.count - Db::Config.query.select(:can_vote).where { active == true }.first!.can_vote + Db::Config.query.select(:can_vote).where { active }.first!.can_vote end @[GraphQL::Field] diff --git a/backend/src/backend/cli.cr b/backend/src/backend/cli.cr index 5ed2d02..c7dbbc0 100644 --- a/backend/src/backend/cli.cr +++ b/backend/src/backend/cli.cr @@ -1,8 +1,124 @@ require "commander" require "tallboy" require "wannabe_bool" +require "csv" module Backend + alias TeacherVote = {student: Int32, priority: Int32} + alias Assignment = {teacher: Int32, priority: Int32} + + def do_assignment(roll_count : UInt32) : Time::Span + 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 + teacher_ids = teachers.map(&.id) + teacher_max_students = Hash.zip(teacher_ids, teachers.map(&.max_students)) + teacher_votes : Hash(Int32, Array(TeacherVote)) = Hash.zip( + teacher_ids, + teachers.map do |t| + t.teacher_votes.map do |tv| + { + student: tv.vote.student.id, + priority: tv.priority, + } + end + end + ) + + students = Db::Student.query + .with_vote(&.with_teacher_votes(&.order_by(priority: :desc))) + .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, + priority: tv.priority, + } + end + end + ) + votes_a = votes.to_a + + t1 = Time.utc + best : {assignment: Hash(Int32, Assignment), priority_score: Int64, teacher_score: Int64}? = nil + empty_assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size) + roll_count.times.each do |i| + p! i + assignment = {} of Int32 => Assignment + assignment_count = empty_assignment_count.clone + votes_a.shuffle(Random::Secure).each do |s, tvs| + tvs.each do |tv| + if assignment_count[tv[:teacher]] < teacher_max_students[tv[:teacher]] + if assignment[s]?.nil? + assignment_count[tv[:teacher]] += 1 + assignment[s] = {teacher: tv[:teacher], priority: tv[:priority]} + else + assignment_count[assignment[s][:teacher]] -= 1 + assignment_count[tv[:teacher]] += 1 + assignment[s] = {teacher: tv[:teacher], priority: tv[:priority]} + end + end + end + end + + priority_score = 0_i64 + assignment.each do |_, a| + priority_score += a[:priority] ** 2 + end + + teacher_score = 0_i64 + assignment_count.each do |t, c| + teacher_score += (teacher_max_students[t] ** c) * teacher_max_students[t] + end + + if best.nil? + best = { + assignment: assignment, + priority_score: priority_score, + teacher_score: teacher_score, + } + elsif priority_score < best.not_nil![:priority_score] && teacher_score < best.not_nil![:teacher_score] + best = { + assignment: assignment, + priority_score: priority_score, + teacher_score: teacher_score, + } + end + end + t2 = Time.utc + + pp! best + + # Db::Assignment.import(best.not_nil![:assignment].map { |s, a| Db::Assignment.new({student_id: s, teacher_id: a[:teacher]}) }) + Db::Assignment.query.where { active }.to_update.set(active: false).execute + assignment_id = Db::Assignment.create!({ + active: true, + priority_score: best.not_nil![:priority_score], + teacher_score: best.not_nil![:teacher_score], + }).id + Db::StudentAssignment.import( + best.not_nil![:assignment].map do |s, a| + Db::StudentAssignment.new({ + assignment_id: assignment_id, + student_id: s, + teacher_id: a[:teacher], + }) + end + ) + + t2 - t1 + end + CLI = Commander::Command.new do |cmd| cmd.use = "backend" cmd.short = "Mentorenwahl backend CLI" @@ -249,5 +365,118 @@ module Backend end end end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "stats" + cmd.short = "Runs tests and outputs statistics for analysis" + cmd.long = cmd.short + + cmd.run do + result = CSV.build do |csv| + csv.row "roll_count", "students", "time", "priority_score", "teacher_score" + + (2..100).each do |i| + p! i + p! student_count = 0...i * 10 + p! teacher_count = 0...i * 5 + + Db::User.import( + student_count.map do |x| + Db::User.new({ + username: "student#{x}", + password_hash: "$2a$12$eEkFG9OAfaAgwPSHTIVfMedH9VIijpRIz1jddkxuJnbe5zwfVIQ6y", + initial_password: "12345", + password_changed: false, + first_name: "Student#{x}", + last_name: "Student#{x}", + role: Db::UserRole::Student, + admin: false, + }) + end.concat( + teacher_count.map do |x| + Db::User.new({ + username: "teacher#{x}", + password_hash: "$2a$12$eEkFG9OAfaAgwPSHTIVfMedH9VIijpRIz1jddkxuJnbe5zwfVIQ6y", + initial_password: "12345", + password_changed: false, + first_name: "Teacher#{x}", + last_name: "Teacher#{x}", + role: Db::UserRole::Teacher, + admin: false, + }) + end + ) + ) + + class_id = Db::Class.create!({name: "Default class"}).id + student_user_ids = Db::User.query.select(:id).where { role == Db::UserRole::Student }.map(&.id) + Db::Student.import( + student_user_ids.map do |id| + Db::Student.new({ + user_id: id, + class_id: class_id, + }) + end + ) + student_ids = Db::Student.query.select(:id).map(&.id) + + teacher_user_ids = Db::User.query.select(:id).where { role == Db::UserRole::Teacher }.map(&.id) + Db::Teacher.import( + teacher_user_ids.map do |id| + Db::Teacher.new({ + user_id: id, + max_students: id % 5, + }) + end + ) + teacher_ids = Db::Teacher.query.select(:id).map(&.id) + + Db::Vote.import( + student_ids.map do |id| + Db::Vote.new({student_id: id}) + end + ) + vote_ids = Db::Vote.query.select(:id).map(&.id) + Db::TeacherVote.import( + vote_ids.flat_map do |id| + size = 6 + id % 4 + tvs = [] of Int32 + while tvs.size < size + t_id = teacher_ids.sample + tvs << t_id unless t_id.in?(tvs) + end + + tvs.map_with_index do |t, j| + Db::TeacherVote.new({ + vote_id: id, + teacher_id: t, + priority: j, + }) + end + end + ) + + dt = do_assignment(Backend.config.assignment_possibility_count) + a = Db::Assignment.query.where { active }.first! + csv.row Backend.config.assignment_possibility_count, student_ids.size, dt.seconds, a.priority_score, a.teacher_score + + puts csv + + Db::StudentAssignment.query.to_delete.execute + Db::Assignment.query.to_delete.execute + Db::TeacherVote.query.to_delete.execute + Db::Vote.query.to_delete.execute + Db::Teacher.query.to_delete.execute + Db::Student.query.to_delete.execute + Db::Class.query.to_delete.execute + Db::User.query.to_delete.execute + end + end + + puts result + File.write("stats.csv", result.to_s) + end + end end end diff --git a/backend/src/backend/db/assignment.cr b/backend/src/backend/db/assignment.cr index f4e1706..c5fa50c 100644 --- a/backend/src/backend/db/assignment.cr +++ b/backend/src/backend/db/assignment.cr @@ -19,9 +19,12 @@ module Backend::Db include Clear::Model self.table = :assignments - primary_key type: :serial + primary_key type: serial - belongs_to student : Student - belongs_to teacher : Teacher + column active : Bool + column priority_score : Int64 + column teacher_score : Int64 + + has_many student_assignments : StudentAssignment, foreign_key: :assignment_id end end diff --git a/backend/src/backend/db/student_assignment.cr b/backend/src/backend/db/student_assignment.cr new file mode 100644 index 0000000..af2d246 --- /dev/null +++ b/backend/src/backend/db/student_assignment.cr @@ -0,0 +1,12 @@ +module Backend::Db + class StudentAssignment + include Clear::Model + self.table = :student_assignments + + primary_key type: :serial + + belongs_to assignment : Assignment + belongs_to student : Student + belongs_to teacher : Teacher + end +end diff --git a/backend/src/backend/worker/jobs/assignment_job.cr b/backend/src/backend/worker/jobs/assignment_job.cr index bbd517b..f47117f 100644 --- a/backend/src/backend/worker/jobs/assignment_job.cr +++ b/backend/src/backend/worker/jobs/assignment_job.cr @@ -68,6 +68,7 @@ module Backend end end ) + p! "Got teachers" students = Db::Student.query .with_vote(&.with_teacher_votes(&.order_by(priority: :desc))) @@ -88,9 +89,11 @@ module Backend end ) votes_a = votes.to_a + p! "Got students' votes" - best : {assignment: Hash(Int32, Assignment), priority_score: UInt32, teacher_score: UInt32}? = nil + best : {assignment: Hash(Int32, Assignment), priority_score: Int64, teacher_score: Int64}? = nil empty_assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size) + p! "Starting assignment" Backend.config.assignment_possibility_count.times.each do assignment = {} of Int32 => Assignment assignment_count = empty_assignment_count.clone @@ -109,12 +112,12 @@ module Backend end end - priority_score = 0_u32 + priority_score = 0_i64 assignment.each do |_, a| priority_score += a[:priority] ** 2 end - teacher_score = 0_u32 + teacher_score = 0_i64 assignment_count.each do |t, c| teacher_score += (teacher_max_students[t] ** c) * teacher_max_students[t] end @@ -136,7 +139,23 @@ module Backend pp! best - Db::Assignment.import(best.not_nil![:assignment].map { |s, a| Db::Assignment.new({student_id: s, teacher_id: a[:teacher]}) }) + p! "Saving best assignment into database" + # Db::Assignment.import(best.not_nil![:assignment].map { |s, a| Db::Assignment.new({student_id: s, teacher_id: a[:teacher]}) }) + Db::Assignment.query.where { active }.to_update.set(active: false).execute + assignment_id = Db::Assignment.create!({ + active: true, + priority_score: best.not_nil![:priority_score], + teacher_score: best.not_nil![:teacher_score], + }) + Db::StudentAssignment.import( + best.not_nil![:assignment].map do |s, a| + Db::StudentAssignment.new({ + assignment_id: assignment_id, + student_id: s, + teacher_id: a[:teacher], + }) + end + ) end end end