diff --git a/src/cli.py b/src/carouselcmd.py similarity index 100% rename from src/cli.py rename to src/carouselcmd.py diff --git a/src/gui.py b/src/carouselgui.py similarity index 100% rename from src/gui.py rename to src/carouselgui.py diff --git a/src/cgi/carousel.cgi b/src/cgi/carousel.cgi new file mode 100755 index 0000000..c17615e --- /dev/null +++ b/src/cgi/carousel.cgi @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +if [[ "$QUERY_STRING" == "source" ]]; then + echo "Content-Type: text/plain" + echo + sed 's/&/\&/g; s//\>/g' "$0" + exit 0 +fi + +exec "$SCRIPT_DIR/../../.venv/bin/python" "$SCRIPT_DIR/script.py" diff --git a/src/cgi/carousel.py b/src/cgi/carousel.py deleted file mode 100644 index c1aa166..0000000 --- a/src/cgi/carousel.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env -S uv run python - -import os -from dotenv import load_dotenv - -load_dotenv() -logdir = os.getenv("CAROUSEL_LOGDIR") - -import cgi, cgitb - -cgitb.enable(display=0, logdir=logdir) - -form = cgi.FieldStorage() - -from ..carousel import * - -print("Content-Type: text/html") -print() - -print("Carousel Matches!") -print("

Carousel Stable Matches Found:

") - -print("TODO - INPUTH ECHO NOT IMPLEMENTED") -print("TODO - OUTPUT NOT IMPLEMENTED") diff --git a/src/cgi/default.env b/src/cgi/default.env deleted file mode 100644 index de9a32a..0000000 --- a/src/cgi/default.env +++ /dev/null @@ -1 +0,0 @@ -CAROUSEL_LOGDIR="$HOME/logs/carousel/" diff --git a/src/cgi/form.html b/src/cgi/form.html index 96774d3..9b65334 100644 --- a/src/cgi/form.html +++ b/src/cgi/form.html @@ -1,663 +1,75 @@ -/* Disclaimer: This file was generated using an LLM */ - Dynamic Data Entry Form + Carousel - Stable Matcher - (Back to ~/tgorordo) -
-

Carousel Stable Matcher

- -
-
Table Input
-
File Upload
-
- -
-
- -
-
-

Applicant Preferences

-
- - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - -
-
-
- - -
-
-

Reviewer Preferences

-
-
- - - - -
-
- -
-
-
- -
- - - - - - - - - - - - - +

Carousel: Stable Matcher - CGI Application Upload

- - - - - - - -
-
-
-
- -
-
-
- - -
-
-
-
- -
- -
-
-
-
-
+
+ + + +
+ + +
-
-
- Input/Output Format Documentation -

- The matcher assumes a specific tabular scheme for the input data - to read more about how this works see the - README. +

- You can enter preference information directly in the tables above, or upload an Excel spreadsheet with - two sheets of preferences, or a CSV containing a ranking matrix. -

More on each format scheme: +

About

+

This is an upload form for the carousel stable matcher for , +mainly to help with UO Physics TA assignments. +This form uses the UO pages.uoregon.edu CGI Capability, +so the implementation of smithy being invoked can be inspected here +and you may also inspect the source of this page to verify that this script is called to invoke it - though +you have to trust it to quine itself faithfully. +

+

A stable matching (of the "college admissions" problem) is one in which there is no pair of, say, TA and course which would prefer +each other over their respective assignment given by the matching. This form runs a version of the +Gale-Shapley (1962) deferred-acceptance algorithm, which finds the stable matching +that is optimal for the TA preferrences. Other stable solutions can be found by using the underlying +python application manually. +

- -

Tabular Input

- You can enter tables of preferences directly in the first tab of the form above. - - TODO - -

Example:

- Suppose we want to figure out a Grad TA-to-Class assignment. Graduate students will be our "Applicants" - - each one will be assigned a class to TA -, and classes will be our "Reviewers" - taking a quota of needed TAs. - - TODO - -

File Upload

- - For more complicated input, a file upload might be preferable to table form submission - (e.g. if there's a validation issue you won't have to manually re-enter the table, just tweak and re-upload the file). - - Two file formats are supported, Excel and CSV: - -

Excel

- - TODO - -

CSV

- - TODO - -
-
- -
- - +

Input: Expected Spreadsheet Format

+

A matching-preference spreadsheet should be organized as a list of TA assignment preferences +and a list of course preferences/constraints, i.e. with the following structure: +TODO

+ +

Output: Matching Format

+

The form will return a matching of TAs to course assignments of the form: TODO

+ + diff --git a/src/cgi/script.py b/src/cgi/script.py new file mode 100644 index 0000000..2e5b16b --- /dev/null +++ b/src/cgi/script.py @@ -0,0 +1,145 @@ +import traceback +import sys, os +import html + +import re + +import polars as pl + +sys.path.insert( + 0, + os.path.abspath( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "../carousel/src") + ), +) +from carousel import match_from_prefs + +print("Content-Type: text/html\n") +message = "" + +content_type = os.environ.get("CONTENT_TYPE", "") +content_length = int(os.environ.get("CONTENT_LENGTH", 0) or 0) +boundary = content_type.split("boundary=")[-1].encode() + +body = sys.stdin.buffer.read(content_length) +parts = body.split(b"--" + boundary) + +spreadsheet = None + +for part in parts: + if b"Content-Disposition" in part and b'name="spreadsheet"' in part: + header, _, data = part.partition(b"\r\n\r\n") + + filename_match = re.search(rb'filename="([^"]+)"', header) + if filename_match: + filename = filename_match.group(1).decode() + filedata = data.rstrip(b"\r\n--") + + spreadsheet = (filename, filedata) + break + + +if spreadsheet is not None: + filename, filedata = spreadsheet + + if filename and filedata: + filepath = os.path.join("/tmp", filename) + + with open(filepath, "wb") as f: + f.write(filedata) + + try: + if filename.endswith(".csv"): + df = pl.read_csv(filepath) + elif filename.endswith((".xlsx", ".xls")): + df = pl.read_excel(filepath) + else: + message = """ +

Error

+

File extension is not valid. Use CSV (.csv) or Excel (.xlsx, .xls).

+

Go Back

+ """ + + # TODO: unpack df appropriately + if df is not None: + # Normalize + df = df.with_columns( + [ + pl.col(c) + .cast(pl.Utf8) + .str.strip_chars() + .cast(pl.Int64, strict=False) + .fill_null(0) + for c in df.columns + ] + ) + + match = match_from_prefs(df) # Solve! + + message = f""" +

The TA-optimal (Gale-Shapley) match is:

+ {match} +

Go Back

+ """ + else: + message = """ +

Error

+

DataFrame was empty.

+

Go Back

+ """ + + except Exception as e: + message = f""" +

Error

+

Internal Error Encountered: {e} +

Go Back

+ """ + traceback.print_exc() + + else: + message = """ +

Error

+

Filename or File Data not found/valid in form submission.

+

Go Back

+ """ + +else: + message = """ +

Error

+

No file field found in the form.

+

Go Back

+ """ + +print(""" + + + + + + Carousel - Stable Matcher + + + +""") +print(message) +print(""" + + + +""")