This commit is contained in:
Dominic Grimm 2023-03-05 13:52:30 +01:00
parent 76ab5cbc87
commit e7b370b40f
No known key found for this signature in database
GPG Key ID: 6F294212DEAAC530
11 changed files with 269 additions and 63 deletions

View File

@ -14,9 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# 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=

View File

@ -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

View File

@ -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

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="de-DE">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Zuordnungen</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet" />
<style>
html,
body {
font-family: "JetBrains Mono", monospace;
}
.column {
width: 25%;
}
.border {
border: 1px solid;
}
.padded {
padding: 1%;
}
.left,
.right {
float: left;
}
hr {
border-top: 1px solid;
}
code {
font-family: "JetBrains Mono", monospace;
background: #f4f4f4;
word-wrap: break-word;
box-decoration-break: clone;
padding: 0.1rem 0.3rem 0.2rem;
border-radius: 0.2rem;
}
ul {
margin: 0;
padding: 0;
}
ul.dashed {
list-style-type: none;
}
ul.dashed > li {
text-indent: -5px;
}
ul.dashed > li:before {
content: "-";
text-indent: -5px;
}
hr {
border: none;
height: 1px;
background: black;
}
.link {
font-style: italic;
font-size: small;
font-weight: lighter;
}
</style>
</head>
<body>
<div>
<h1>Mentorenwahl: Zuordnungen</h1>
<table width="100%" cellspacing="0" border="0" class="border-table">
<colgroup>
<col width="25%" />
<col width="75%" />
</colgroup>
<tr>
<th class="border">Zeitpunkt</th>
<%- time_unix = @time.to_unix -%>
<td class="border padded"><%= @time %> (<%= time_unix.negative? ? "-" : "+" %><%= time_unix %>)</td>
</tr>
<tr>
<th class="border">Anzahl Lehrer</th>
<td class="border padded"><%= @assignments.size %></td>
</tr>
</table>
</div>
<div>
<h2>Zuordnungen</h2>
<table width="100%" cellspacing="0" border="0">
<colgroup>
<col width="25%" />
<col width="75%" />
</colgroup>
<tr>
<th class="border">Lehrer</th>
<th class="border">Schüler</th>
</tr>
<%- @assignments.each do |a| -%>
<tr>
<td class="border padded"><%= a.teacher.last_name %>, <%= a.teacher.first_name %></td>
<td class="border padded">
<ul class="dashed">
<%- a.students.each do |s| -%>
<li><%= s[:user].last_name %>, <%= s[:user].first_name %> (<%= s[:class] %>)</li>
<%- end -%>
</ul>
</td>
</tr>
<%- end -%>
</table>
</div>
</body>
</html>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="de">
<html lang="de-DE">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />

View File

@ -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}

View File

@ -1,21 +1,22 @@
<!DOCTYPE html>
<html lang="de">
<html lang="de-DE">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script
src="https://kit.fontawesome.com/7efbac0aa5.js"
crossorigin="anonymous"
></script>
<script src="https://kit.fontawesome.com/7efbac0aa5.js" crossorigin="anonymous"></script>
<link data-trunk rel="css" href="assets/css/bulma.css" />
<link data-trunk rel="css" href="assets/css/styles.css" />
</head>
<link data-trunk rel="css" href="assets/css/bulma.css" />
<link data-trunk rel="css" href="assets/css/styles.css" />
</head>
<body>
<noscript>
<strong>Diese Website funktioniert leider nicht ohne JavaScript.</strong>
</noscript>
</body>
</html>
<body>
<noscript>
<strong>Diese Website funktioniert leider nicht ohne JavaScript.</strong>
</noscript>
</body>
</html>