Skip to content

Commit

Permalink
implementation of assignment algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
JohanGrims committed Sep 17, 2024
1 parent 5b26971 commit af2655a
Show file tree
Hide file tree
Showing 20 changed files with 1,285 additions and 622 deletions.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

<title>WaldorfWahlen</title>
</head>
<body class="mdui-theme-dark">
<body class="mdui-theme-auto">
<div id="root"></div>
<style>
.firebase-emulator-warning {
Expand Down
398 changes: 398 additions & 0 deletions python/assign.ipynb

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions python/assign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import firebase_admin
import pulp
from firebase_admin import auth, credentials
from flask import Flask, jsonify, request, send_file
from flask_cors import CORS

app = Flask(__name__)
CORS(app)


cred = credentials.Certificate("waldorfwahlen-service-account.json")

# Initialize the Firebase app with the credentials from the service account JSON file
firebase_admin.initialize_app(cred)


# example data
# {"token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjAyMTAwNzE2ZmRkOTA0ZTViNGQ0OTExNmZmNWRiZGZjOTg5OTk0MDEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vd2FsZG9yZndhaGxlbiIsImF1ZCI6IndhbGRvcmZ3YWhsZW4iLCJhdXRoX3RpbWUiOjE3MjY0MDQ5MDgsInVzZXJfaWQiOiI4VHJxbVpmbU1mWFNjS0xTbW1SelA3MzJiWG0yIiwic3ViIjoiOFRycW1aZm1NZlhTY0tMU21tUnpQNzMyYlhtMiIsImlhdCI6MTcyNjU4Nzk4MywiZXhwIjoxNzI2NTkxNTgzLCJlbWFpbCI6ImpvaGFuLmdyaW1zZWhsQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJqb2hhbi5ncmltc2VobEBnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9fQ.GIZwxuDpvPuH_W8xrzvYa0jxKNJlGllbN0VajkNA4Tqb-tFZJJrVUuQa_bGe9XAw7ZocB2lBKyuatCi93i9fCYdM-NtLE8FSootEF-pt4v8qyvwf1Lz3jP3bjZrrcfLtOi0bL2FvsvqVJFP1PHXh43Eth7SrYOR5z7dbUCMLsqO_VepTYDUeLndmbjRoxLO3beCUUiJN_8jhogxxQ-NWfygUFOVSog8x81KUCTwb21iB5svXXUFJ0zQKwy5bpyfitVW-kgVNnC7kz9cuOPIgSgw6PCiEDN9tZ_QzcrB_KVEshp8ZRASKI4EwDgiEIdC3QQJR1ATUlz1WJirNATVuVA","uid":"8TrqmZfmMfXScKLSmmRzP732bXm2","projects":{"1":{"title":"Project 1","max":3},"2":{"title":"Project 2","max":4},"3":{"title":"Project 3","max":2}},"preferences":{"1":{"name":"Johan","selected":[1,2,3]},"2":{"name":"Sara","selected":[1,2,3]},"3":{"name":"Karl","selected":[1,2,3]},"4":{"name":"Anna","selected":[1,2,3]},"5":{"name":"Eva","selected":[1,2,3]}}}
@app.route("/assign", methods=["POST"])
def assign():
try:
data = request.get_json()
token = data.get("token")
uid = data.get("uid")

print(token)

# Verify the token and UID
decoded_token = auth.verify_id_token(token)

print(decoded_token)
if decoded_token["uid"] == uid:
# read more data from the request

# prefences = {studentId (string): {grade: string, selected: [idProject1, idProject2, ...], name: string},}
preferences = data.get("preferences")
print(
preferences
) # => {'1': {'name': 'Johan', 'selected': [1, 2, 3]}, '2': {'name': 'Sara', 'selected': [1, 2, 3]}, '3': {'name': 'Karl', 'selected': [1, 2, 3]}, '4': {'name': 'Anna', 'selected': [1, 2, 3]}, '5': {'name': 'Eva', 'selected': [1, 2, 3]}}

# projects = {projectId: {title: string, max: string}}
projects = data.get("projects")
print(
projects
) # => {'1': {'title': 'Project 1', 'max': 3}, '2': {'title': 'Project 2', 'max': 4}, '3': {'title': 'Project 3', 'max': 2}}

# transform the ids of the students into consistent integers
student_ids = {}
for i, student_id in enumerate(preferences.keys()):
student_ids[student_id] = i

print(student_ids) # => {'1': 0, '2': 1, '3': 2, '4': 3, '5': 4}

# transform the ids of the projects into consistent integers
project_ids = {}
for i, project_id in enumerate(projects.keys()):
project_ids[project_id] = i

print(project_ids) # => {'1': 0, '2': 1, '3': 2}

# transform the preferences into a format that the solver can understand
student_preferences = []
for student_id, student in preferences.items():
student_preferences.append(
[project_ids[str(project_id)] for project_id in student["selected"]]
)

print(student_preferences)

# transform the projects into a format that the solver can understand

project_max = [int(project["max"]) for project in projects.values()]

print(project_max)

# create variables for the number of participants and courses
num_participants = len(student_preferences)
num_courses = len(project_max)

print(num_participants, num_courses)

# create scores
scores = {"first": 1, "second": 2, "third": 4}

# create the LP problem
problem = pulp.LpProblem("CourseAssignment", pulp.LpMinimize)

# Decision variables: x_ij is 1 if participant i is assigned to course j, otherwise 0
x = pulp.LpVariable.dicts(
"x",
((i, j) for i in range(num_participants) for j in range(num_courses)),
cat="Binary",
)

# Overbooking variables: o_j is 0 or more if course j is overbooked
o = pulp.LpVariable.dicts(
"o", (j for j in range(num_courses)), lowBound=0, cat="Integer"
)

# Objective function: Minimize preference scores and overbooking penalties
problem += pulp.lpSum(
scores["first"] * x[i, student_preferences[i][0]]
+ scores["second"] * x[i, student_preferences[i][1]]
+ scores["third"] * x[i, student_preferences[i][2]]
for i in range(num_participants)
) + pulp.lpSum(10 * o[j] for j in range(num_courses))

# Constraint: Each participant is assigned to exactly one course
for i in range(num_participants):
problem += pulp.lpSum(x[i, j] for j in range(num_courses)) == 1

# Constraint: Maximum number of participants per course (with overbooking)
for j in range(num_courses):
problem += (
pulp.lpSum(x[i, j] for i in range(num_participants))
<= project_max[j] + o[j]
)

# Preference assignment: Participants can only be assigned to their 1st, 2nd, or 3rd choice
for i in range(num_participants):
for j in range(num_courses):
if j not in student_preferences[i]:
problem += x[i, j] == 0

# Solve the problem
problem.solve()

# Extract the solution
solution = {}
for i in range(num_participants):
for j in range(num_courses):
if x[i, j].varValue == 1:
solution[list(student_ids.keys())[i]] = list(projects.keys())[j]

return jsonify(solution)

else:
return jsonify({"error": "not authorized"}), 401

except Exception as e:
return jsonify({"error": str(e)}), 500


if __name__ == "__main__":
app.run(host="ec2-13-48-226-69.eu-north-1.compute.amazonaws.com", port=5175)
6 changes: 3 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function App() {
{activeVotes.map((vote) => (
<mdui-list-item
key={vote.id}
href={`/v/${vote.id}`}
href={`/${vote.id}`}
rounded
end-icon="arrow_forward"
>
Expand All @@ -47,7 +47,7 @@ function App() {
{scheduledVotes.map((e) => (
<mdui-list-item
rounded
href={`/v/${e.id}`}
href={`/${e.id}`}
end-icon="arrow_forward"
>
{e.title}
Expand All @@ -74,7 +74,7 @@ function App() {
{expiredVotes.map((e) => (
<mdui-list-item
rounded
href={`/r/${e.id}`}
href={`/${e.id}`}
end-icon="arrow_forward"
>
{e.title}
Expand Down
49 changes: 18 additions & 31 deletions src/Error.jsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,29 @@
import { useRouteError } from "react-router-dom";
import "./error.css";

export default function ErrorPage() {
let error = useRouteError();
document.title = "Oops! " + (error?.status || "Unknown Error");

console.error(error);
return (
<div className="error-container">
<div className="error-content">
<div className="error-message">
<h2>
Fehler{" "}
<span style={{ fontFamily: "monospace", fontSize: "70px" }}>
{error.status}
</span>
</h2>
<b>{error.statusText || "Unbekannter Fehler"}</b>
<p>
Leider ist ein unerwarteter Fehler aufgetreten.{" "}
{error.status >= 400 && error.status < 500
? "Es scheint, dass der Fehler auf Ihrer Seite liegt."
: "Wir arbeiten daran, das Problem zu beheben."}
</p>
<div className="error-button" onClick={() => window.history.back()}>
Zurück
</div>
</div>
<div className="error-details">
<p>
Wenn Sie möchten, können Sie helfen, den Fehler zu beheben, indem
Sie die folgenden Informationen per Mail weiterleiten:
</p>
<div className="error-stacktrace">
{JSON.stringify(error, null, 2)}
</div>
</div>
<mdui-dialog open headline={`Fehler ${error?.status || "400"}`}>
<div className="mdui-prose">
<p>{error?.statusText || "Unknown Error Message"}</p>
<p>
{error.status >= 400 && error.status < 500
? "Es scheint, dass der Fehler auf Ihrer Seite liegt."
: "Wir arbeiten daran, das Problem zu beheben."}
</p>
</div>
</div>
<p />
<div className="button-container">
<mdui-button onClick={() => window.history.back()} variant="text">
Zurück
</mdui-button>
<mdui-button onClick={() => window.location.reload()}>
Neu laden
</mdui-button>
</div>
</mdui-dialog>
);
}
39 changes: 39 additions & 0 deletions src/Gateway.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { doc, getDoc } from "firebase/firestore/lite";
import React from "react";
import { useLoaderData, useNavigate } from "react-router-dom";
import { db } from "./firebase";

export default function Gateway() {
const { voteData } = useLoaderData();

const navigate = useNavigate();

React.useEffect(() => {
if (!voteData.active || Date.now() > voteData.endTime.seconds * 1000) {
navigate(`/r/${voteData.id}`);
return;
}
if (Date.now() < voteData.startTime.seconds * 1000) {
navigate(`/s/${voteData.id}`);
return;
}
if (localStorage.getItem(voteData.id)) {
navigate(`/x/${voteData.id}`);
}
navigate(`/v/${voteData.id}`);
}, []);
return null;
}

export async function loader({ params }) {
const vote = await getDoc(doc(db, `/votes/${params.id}`));
if (!vote.exists()) {
new Response("Not Found", {
status: 404,
statusText: "Wahl nicht gefunden",
});
}
const voteData = { id: vote.id, ...vote.data() };

return { voteData };
}
Loading

0 comments on commit af2655a

Please sign in to comment.