diff --git a/.example.env b/.example.env index a766173..ad3afe6 100644 --- a/.example.env +++ b/.example.env @@ -14,9 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# General -URL= - # Db POSTGRES_USER="mw" POSTGRES_PASSWORD= @@ -30,7 +27,7 @@ AUTH_UNTIS_PASSWORD= # Backend BACKEND_MINIMUM_TEACHER_SELECTION_COUNT=6 -BACKEND_ASSIGNMENT_POSSIBILITY_COUNT=16777216 +BACKEND_ASSIGNMENT_RUN_TIME=60 BACKEND_URL=URL # Backend - API BACKEND_API_JWT_SECRET= diff --git a/Makefile b/Makefile index 1e9d9b7..e2a7a73 100644 --- a/Makefile +++ b/Makefile @@ -19,10 +19,10 @@ all: prod dev: - BUILDKIT_PROGRESS=plain docker compose build --build-arg BUILD_ENV=development + docker compose build --build-arg BUILD_ENV=development prod: - BUILDKIT_PROGRESS=plain docker compose build + docker compose build docs: cd docs && mdbook build diff --git a/backend/src/backend/cli.cr b/backend/src/backend/cli.cr index 035dcd4..560848e 100644 --- a/backend/src/backend/cli.cr +++ b/backend/src/backend/cli.cr @@ -250,5 +250,41 @@ module Backend end end end + + # ameba:disable Lint/ShadowingOuterLocalVar + cmd.commands.add do |cmd| + cmd.use = "assignments:export" + cmd.short = "Generates report for all assignments" + cmd.long = cmd.short + + cmd.run do + time = Time.local + + a_id = Db::Assignment.query.select(:id).where { active }.first!.id + + html = Templates::Assignments.new( + time, + Db::Teacher.query + .with_student_assignments(&.where { assignment_id == a_id }.with_student(&.with_user.with_class_model)) + .to_a + .select(&.student_assignments.count.positive?) + .map do |t| + Templates::Assignments::Assignment.new( + Templates::Assignments::User.new(t.user.first_name, t.user.last_name), + t.student_assignments + .map do |sa| + { + user: Templates::Assignments::User.new(sa.student.user.first_name, sa.student.user.last_name), + class: sa.student.class_model.name, + } + end + .sort_by! { |sa| {sa[:class], sa[:user].last_name, sa[:user].first_name} } + ) + end + .sort_by! { |a| {a.teacher.last_name, a.teacher.first_name} } + ).to_s + puts "Filepath: #{Auth.generate_pdf(html).filename}" + end + end end end diff --git a/backend/src/backend/config.cr b/backend/src/backend/config.cr index 84ff3f1..7172f0f 100644 --- a/backend/src/backend/config.cr +++ b/backend/src/backend/config.cr @@ -55,14 +55,11 @@ module Backend build_env.development? end - # Base URL of application - getter url : String - # Minimum teacher selection count getter minimum_teacher_selection_count : Int32 - # Assignment possibility count - getter assignment_possibility_count : UInt32 + # Assignment max run time for algorithm + getter assignment_run_time : UInt32 @[EnvConfig::Setting(key: "api")] # Configuration for `Api` diff --git a/backend/src/backend/db/teacher.cr b/backend/src/backend/db/teacher.cr index dd863f3..54ed062 100644 --- a/backend/src/backend/db/teacher.cr +++ b/backend/src/backend/db/teacher.cr @@ -10,6 +10,6 @@ module Backend::Db column max_students : Int32 has_many teacher_votes : TeacherVote, foreign_key: :teacher_id - has_many assignments : Assignment, foreign_key: :teacher_id + has_many student_assignments : StudentAssignment, foreign_key: :teacher_id end end diff --git a/backend/src/backend/templates/assignments.cr b/backend/src/backend/templates/assignments.cr new file mode 100644 index 0000000..3a80147 --- /dev/null +++ b/backend/src/backend/templates/assignments.cr @@ -0,0 +1,27 @@ +class Backend::Templates::Assignments + struct User + property first_name + property last_name + + def initialize(@first_name : String, @last_name : String) + end + end + + alias Student = {user: User, class: String} + + struct Assignment + property teacher + property students + + def initialize(@teacher : User, @students : Array(Student)) + end + end + + def initialize( + @time : Time, + @assignments : Array(Assignment) + ) + end + + ECR.def_to_s "templates/html/assignments.min.html.ecr" +end diff --git a/backend/src/backend/worker/jobs/assignment_job.cr b/backend/src/backend/worker/jobs/assignment_job.cr index c3f100e..5a626b1 100644 --- a/backend/src/backend/worker/jobs/assignment_job.cr +++ b/backend/src/backend/worker/jobs/assignment_job.cr @@ -28,6 +28,11 @@ module Backend # :ditto: def perform : Nil + if Db::Config.query.where { active }.first!.can_vote + log "Voting still allowed, skipping assignment" + fail + end + teacher_count = Db::Teacher.query.count student_count = Db::Student.query.count vote_count = Db::Vote.query.count @@ -43,8 +48,10 @@ module Backend 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" + end + + if Db::Teacher.query.sum(:max_students, Int64) < student_count + log "Capacity too low, teachers need to adopt more students, skipping assignment" fail end @@ -68,7 +75,7 @@ module Backend end end ) - p! "Got teachers" + log "Got teachers" students = Db::Student.query .with_vote(&.with_teacher_votes(&.order_by(priority: :desc))) @@ -89,13 +96,18 @@ module Backend end ) votes_a = votes.to_a - p! "Got students' votes" + log "Got students' votes" 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" - i = 0 - while i < Backend.config.assignment_possibility_count + log "Starting assignment" + valid_count = 0_u64 + invalid_count = 0_u64 + + max_span = Time::Span.new(minutes: Backend.config.assignment_run_time) + start_time = Time.utc + + loop do assignment = {} of Int32 => Assignment assignment_count = empty_assignment_count.clone votes_a.shuffle(Random::Secure).each do |s, tvs| @@ -113,37 +125,45 @@ module Backend end end - next if assignment.size != student_count + if assignment.size != student_count + invalid_count += 1 + log "Invalid combination, next (#{assignment.size} != #{student_count}, valid: #{valid_count} invalid: #{invalid_count})" + else + log "Valid combination (valid: #{valid_count} invalid: #{invalid_count})!" - priority_score = 0_i64 - assignment.each do |_, a| - priority_score += a[:priority] ** 2 + 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 + + valid_count += 1 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 + break if (Time.utc - start_time) > max_span && valid_count > 0 end pp! best - 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]}) }) + log "Saving best assignment into database" Db::Assignment.query.where { active }.to_update.set(active: false).execute assignment_id = Db::Assignment.create!({ active: true, diff --git a/backend/templates/html/assignments.html.ecr b/backend/templates/html/assignments.html.ecr new file mode 100644 index 0000000..0b46b87 --- /dev/null +++ b/backend/templates/html/assignments.html.ecr @@ -0,0 +1,129 @@ + + + + + + + Zuordnungen + + + + + + + +
+

Mentorenwahl: Zuordnungen

+ + + + + + + + + <%- time_unix = @time.to_unix -%> + + + + + + + +
Zeitpunkt<%= @time %> (<%= time_unix.negative? ? "-" : "+" %><%= time_unix %>)
Anzahl Lehrer<%= @assignments.size %>
+
+ +
+

Zuordnungen

+ + + + + + + + + + + + <%- @assignments.each do |a| -%> + + + + + <%- end -%> +
LehrerSchüler
<%= a.teacher.last_name %>, <%= a.teacher.first_name %> +
    + <%- a.students.each do |s| -%> +
  • <%= s[:user].last_name %>, <%= s[:user].first_name %> (<%= s[:class] %>)
  • + <%- end -%> +
+
+
+ + diff --git a/backend/templates/html/users.html.ecr b/backend/templates/html/users.html.ecr index 6d22e6b..c1c7ada 100644 --- a/backend/templates/html/users.html.ecr +++ b/backend/templates/html/users.html.ecr @@ -1,5 +1,5 @@ - + diff --git a/docker-compose.yml b/docker-compose.yml index 4ea42c6..04edded 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -78,9 +78,8 @@ services: - redis - auth environment: - BACKEND_URL: ${URL} BACKEND_MINIMUM_TEACHER_SELECTION_COUNT: ${BACKEND_MINIMUM_TEACHER_SELECTION_COUNT} - BACKEND_ASSIGNMENT_POSSIBILITY_COUNT: ${BACKEND_ASSIGNMENT_POSSIBILITY_COUNT} + BACKEND_ASSIGNMENT_RUN_TIME: ${BACKEND_ASSIGNMENT_RUN_TIME} BACKEND_API_JWT_SECRET: ${BACKEND_API_JWT_SECRET} BACKEND_API_JWT_EXPIRATION: ${BACKEND_API_JWT_EXPIRATION} BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER} diff --git a/frontend/index.html b/frontend/index.html index 22f6574..d34a799 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,21 +1,22 @@ - + + + + + - - - - + - + + + - - - - - - - - - \ No newline at end of file + + + +