Update
This commit is contained in:
parent
76ab5cbc87
commit
e7b370b40f
|
@ -14,9 +14,6 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# General
|
|
||||||
URL=
|
|
||||||
|
|
||||||
# Db
|
# Db
|
||||||
POSTGRES_USER="mw"
|
POSTGRES_USER="mw"
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
|
@ -30,7 +27,7 @@ AUTH_UNTIS_PASSWORD=
|
||||||
|
|
||||||
# Backend
|
# Backend
|
||||||
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT=6
|
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT=6
|
||||||
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT=16777216
|
BACKEND_ASSIGNMENT_RUN_TIME=60
|
||||||
BACKEND_URL=URL
|
BACKEND_URL=URL
|
||||||
# Backend - API
|
# Backend - API
|
||||||
BACKEND_API_JWT_SECRET=
|
BACKEND_API_JWT_SECRET=
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -19,10 +19,10 @@
|
||||||
all: prod
|
all: prod
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
BUILDKIT_PROGRESS=plain docker compose build --build-arg BUILD_ENV=development
|
docker compose build --build-arg BUILD_ENV=development
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
BUILDKIT_PROGRESS=plain docker compose build
|
docker compose build
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
cd docs && mdbook build
|
cd docs && mdbook build
|
||||||
|
|
|
@ -250,5 +250,41 @@ module Backend
|
||||||
end
|
end
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,14 +55,11 @@ module Backend
|
||||||
build_env.development?
|
build_env.development?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Base URL of application
|
|
||||||
getter url : String
|
|
||||||
|
|
||||||
# Minimum teacher selection count
|
# Minimum teacher selection count
|
||||||
getter minimum_teacher_selection_count : Int32
|
getter minimum_teacher_selection_count : Int32
|
||||||
|
|
||||||
# Assignment possibility count
|
# Assignment max run time for algorithm
|
||||||
getter assignment_possibility_count : UInt32
|
getter assignment_run_time : UInt32
|
||||||
|
|
||||||
@[EnvConfig::Setting(key: "api")]
|
@[EnvConfig::Setting(key: "api")]
|
||||||
# Configuration for `Api`
|
# Configuration for `Api`
|
||||||
|
|
|
@ -10,6 +10,6 @@ module Backend::Db
|
||||||
column max_students : Int32
|
column max_students : Int32
|
||||||
|
|
||||||
has_many teacher_votes : TeacherVote, foreign_key: :teacher_id
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -28,6 +28,11 @@ module Backend
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def perform : Nil
|
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
|
teacher_count = Db::Teacher.query.count
|
||||||
student_count = Db::Student.query.count
|
student_count = Db::Student.query.count
|
||||||
vote_count = Db::Vote.query.count
|
vote_count = Db::Vote.query.count
|
||||||
|
@ -43,8 +48,10 @@ module Backend
|
||||||
elsif vote_count < student_count
|
elsif vote_count < student_count
|
||||||
log "Not all students voted, skipping assignment"
|
log "Not all students voted, skipping assignment"
|
||||||
fail
|
fail
|
||||||
elsif Db::Assignment.query.count > 0
|
end
|
||||||
log "Assignment has already run, skipping another assignment"
|
|
||||||
|
if Db::Teacher.query.sum(:max_students, Int64) < student_count
|
||||||
|
log "Capacity too low, teachers need to adopt more students, skipping assignment"
|
||||||
fail
|
fail
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -68,7 +75,7 @@ module Backend
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
p! "Got teachers"
|
log "Got teachers"
|
||||||
|
|
||||||
students = Db::Student.query
|
students = Db::Student.query
|
||||||
.with_vote(&.with_teacher_votes(&.order_by(priority: :desc)))
|
.with_vote(&.with_teacher_votes(&.order_by(priority: :desc)))
|
||||||
|
@ -89,13 +96,18 @@ module Backend
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
votes_a = votes.to_a
|
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
|
best : {assignment: Hash(Int32, Assignment), priority_score: Int64, teacher_score: Int64}? = nil
|
||||||
empty_assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size)
|
empty_assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size)
|
||||||
p! "Starting assignment"
|
log "Starting assignment"
|
||||||
i = 0
|
valid_count = 0_u64
|
||||||
while i < Backend.config.assignment_possibility_count
|
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 = {} of Int32 => Assignment
|
||||||
assignment_count = empty_assignment_count.clone
|
assignment_count = empty_assignment_count.clone
|
||||||
votes_a.shuffle(Random::Secure).each do |s, tvs|
|
votes_a.shuffle(Random::Secure).each do |s, tvs|
|
||||||
|
@ -113,37 +125,45 @@ module Backend
|
||||||
end
|
end
|
||||||
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
|
priority_score = 0_i64
|
||||||
assignment.each do |_, a|
|
assignment.each do |_, a|
|
||||||
priority_score += a[:priority] ** 2
|
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
|
end
|
||||||
|
|
||||||
teacher_score = 0_i64
|
break if (Time.utc - start_time) > max_span && valid_count > 0
|
||||||
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
|
end
|
||||||
|
|
||||||
pp! best
|
pp! best
|
||||||
|
|
||||||
p! "Saving best assignment into database"
|
log "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
|
Db::Assignment.query.where { active }.to_update.set(active: false).execute
|
||||||
assignment_id = Db::Assignment.create!({
|
assignment_id = Db::Assignment.create!({
|
||||||
active: true,
|
active: true,
|
||||||
|
|
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de-DE">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
|
|
@ -78,9 +78,8 @@ services:
|
||||||
- redis
|
- redis
|
||||||
- auth
|
- auth
|
||||||
environment:
|
environment:
|
||||||
BACKEND_URL: ${URL}
|
|
||||||
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT: ${BACKEND_MINIMUM_TEACHER_SELECTION_COUNT}
|
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_SECRET: ${BACKEND_API_JWT_SECRET}
|
||||||
BACKEND_API_JWT_EXPIRATION: ${BACKEND_API_JWT_EXPIRATION}
|
BACKEND_API_JWT_EXPIRATION: ${BACKEND_API_JWT_EXPIRATION}
|
||||||
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
<!DOCTYPE html>
|
<!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>
|
<script
|
||||||
<meta charset="UTF-8">
|
src="https://kit.fontawesome.com/7efbac0aa5.js"
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
crossorigin="anonymous"
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
></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" />
|
<body>
|
||||||
<link data-trunk rel="css" href="assets/css/styles.css" />
|
<noscript>
|
||||||
</head>
|
<strong>Diese Website funktioniert leider nicht ohne JavaScript.</strong>
|
||||||
|
</noscript>
|
||||||
<body>
|
</body>
|
||||||
<noscript>
|
</html>
|
||||||
<strong>Diese Website funktioniert leider nicht ohne JavaScript.</strong>
|
|
||||||
</noscript>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
Loading…
Reference in New Issue