mirror of
https://github.com/tgorordo/carousel.git
synced 2026-06-05 18:12:14 -07:00
Compare commits
3 commits
a54eb0b2b7
...
338006b428
| Author | SHA1 | Date | |
|---|---|---|---|
| 338006b428 | |||
| b95b2db4a9 | |||
| c4b42d2baa |
12 changed files with 825 additions and 1353 deletions
28
justfile
28
justfile
|
|
@ -1,14 +1,17 @@
|
||||||
list:
|
list:
|
||||||
just --list
|
just --list
|
||||||
|
|
||||||
#run:
|
run *args:
|
||||||
# uv run carousel
|
uv run src/carouselcmd.py {{args}}
|
||||||
|
|
||||||
python *arguments:
|
python *arguments:
|
||||||
uv run python -c {{arguments}}
|
uv run python -c {{arguments}}
|
||||||
|
|
||||||
test:
|
marimo:
|
||||||
uv run pytest -vvv --tb=short --log-cli-level=INFO
|
uv run marimo --edit
|
||||||
|
|
||||||
|
sync *args:
|
||||||
|
uv sync {{args}}
|
||||||
|
|
||||||
format:
|
format:
|
||||||
uv run ruff format src test
|
uv run ruff format src test
|
||||||
|
|
@ -16,22 +19,21 @@ format:
|
||||||
check:
|
check:
|
||||||
uv run pyright src
|
uv run pyright src
|
||||||
|
|
||||||
sync:
|
test:
|
||||||
uv sync --upgrade
|
uv run pytest -vvv --tb=short --log-cli-level=INFO
|
||||||
|
|
||||||
lock:
|
compile:
|
||||||
uv pip compile pyproject.toml -o requirements.txt --group dev
|
uv run --with-requirements src/carouselcmd.py pyinstaller --clean -F src/carouselcmd.py
|
||||||
|
|
||||||
build:
|
|
||||||
uv run pyinstaller src/cli.py
|
|
||||||
uv run pyinstaller src/gui.py
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
uv run pyclean src test
|
uv run pyclean src test
|
||||||
uv run ruff clean
|
uv run ruff clean
|
||||||
rm -rf main.spec cli.spec gui.spec build dist .pytest_cache .hypothesis .benchmarks __marimo__
|
rm -rf carouselcmd.spec carouselgui.spec build dist .pytest_cache .hypothesis .benchmarks __marimo__
|
||||||
|
|
||||||
wipe:
|
wipe:
|
||||||
just clean
|
just clean
|
||||||
rm -rf .venv
|
rm -rf .venv
|
||||||
|
|
||||||
|
lock:
|
||||||
|
uv lock
|
||||||
|
uv pip compile pyproject.toml -o requirements.txt --group dev
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,17 @@ readme = "README.md"
|
||||||
authors = [{ name = "Thomas (Tom) C. Gorordo", email = "tcgorordo@gmail.com" }]
|
authors = [{ name = "Thomas (Tom) C. Gorordo", email = "tcgorordo@gmail.com" }]
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click>=8.1.8",
|
|
||||||
"numpy>=2.2.4",
|
"numpy>=2.2.4",
|
||||||
"polars>=1.26.0",
|
"polars>=1.26.0",
|
||||||
"pyside6>=6.9.0",
|
"rustworkx>=0.17.1",
|
||||||
"rich>=14.0.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
#[project.scripts]
|
||||||
carousel = "carousel:main"
|
#carousel = "carousel:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["uv_build>=0.11.7,<0.12.0"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "uv_build"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|
@ -33,10 +31,7 @@ dev = [
|
||||||
"ruff>=0.11.2",
|
"ruff>=0.11.2",
|
||||||
"ty>=0.0.0a5",
|
"ty>=0.0.0a5",
|
||||||
]
|
]
|
||||||
srv = [
|
|
||||||
"legacy-cgi>=2.6.3",
|
|
||||||
"python-dotenv>=1.1.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
testpaths = "test"
|
testpaths = "test"
|
||||||
|
|
@ -45,6 +40,3 @@ log_cli = true
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
include = ["src"]
|
include = ["src"]
|
||||||
exclude = ["test"]
|
exclude = ["test"]
|
||||||
|
|
||||||
reportMissingImports = "error"
|
|
||||||
reportMissingTypeStubs = false
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ certifi==2025.8.3
|
||||||
# httpx
|
# httpx
|
||||||
click==8.1.8
|
click==8.1.8
|
||||||
# via
|
# via
|
||||||
# carousel (pyproject.toml)
|
|
||||||
# marimo
|
# marimo
|
||||||
# uvicorn
|
# uvicorn
|
||||||
distro==1.9.0
|
distro==1.9.0
|
||||||
|
|
@ -75,12 +74,8 @@ markdown==3.9
|
||||||
# via
|
# via
|
||||||
# marimo
|
# marimo
|
||||||
# pymdown-extensions
|
# pymdown-extensions
|
||||||
markdown-it-py==3.0.0
|
|
||||||
# via rich
|
|
||||||
markupsafe==3.0.2
|
markupsafe==3.0.2
|
||||||
# via jinja2
|
# via jinja2
|
||||||
mdurl==0.1.2
|
|
||||||
# via markdown-it-py
|
|
||||||
narwhals==2.3.0
|
narwhals==2.3.0
|
||||||
# via
|
# via
|
||||||
# altair
|
# altair
|
||||||
|
|
@ -90,7 +85,9 @@ nbformat==5.10.4
|
||||||
nodeenv==1.9.1
|
nodeenv==1.9.1
|
||||||
# via pyright
|
# via pyright
|
||||||
numpy==2.2.4
|
numpy==2.2.4
|
||||||
# via carousel (pyproject.toml)
|
# via
|
||||||
|
# carousel (pyproject.toml)
|
||||||
|
# rustworkx
|
||||||
openai==1.106.1
|
openai==1.106.1
|
||||||
# via marimo
|
# via marimo
|
||||||
packaging==24.2
|
packaging==24.2
|
||||||
|
|
@ -123,9 +120,7 @@ pydantic==2.11.7
|
||||||
pydantic-core==2.33.2
|
pydantic-core==2.33.2
|
||||||
# via pydantic
|
# via pydantic
|
||||||
pygments==2.19.1
|
pygments==2.19.1
|
||||||
# via
|
# via marimo
|
||||||
# marimo
|
|
||||||
# rich
|
|
||||||
pyinstaller==6.13.0
|
pyinstaller==6.13.0
|
||||||
# via carousel (pyproject.toml:dev)
|
# via carousel (pyproject.toml:dev)
|
||||||
pyinstaller-hooks-contrib==2025.3
|
pyinstaller-hooks-contrib==2025.3
|
||||||
|
|
@ -134,14 +129,6 @@ pymdown-extensions==10.16.1
|
||||||
# via marimo
|
# via marimo
|
||||||
pyright==1.1.399
|
pyright==1.1.399
|
||||||
# via carousel (pyproject.toml:dev)
|
# via carousel (pyproject.toml:dev)
|
||||||
pyside6==6.9.0
|
|
||||||
# via carousel (pyproject.toml)
|
|
||||||
pyside6-addons==6.9.0
|
|
||||||
# via pyside6
|
|
||||||
pyside6-essentials==6.9.0
|
|
||||||
# via
|
|
||||||
# pyside6
|
|
||||||
# pyside6-addons
|
|
||||||
pytest==8.3.5
|
pytest==8.3.5
|
||||||
# via
|
# via
|
||||||
# carousel (pyproject.toml:dev)
|
# carousel (pyproject.toml:dev)
|
||||||
|
|
@ -156,8 +143,6 @@ referencing==0.36.2
|
||||||
# via
|
# via
|
||||||
# jsonschema
|
# jsonschema
|
||||||
# jsonschema-specifications
|
# jsonschema-specifications
|
||||||
rich==14.0.0
|
|
||||||
# via carousel (pyproject.toml)
|
|
||||||
rpds-py==0.27.1
|
rpds-py==0.27.1
|
||||||
# via
|
# via
|
||||||
# jsonschema
|
# jsonschema
|
||||||
|
|
@ -166,15 +151,12 @@ ruff==0.11.6
|
||||||
# via
|
# via
|
||||||
# carousel (pyproject.toml:dev)
|
# carousel (pyproject.toml:dev)
|
||||||
# marimo
|
# marimo
|
||||||
|
rustworkx==0.17.1
|
||||||
|
# via carousel (pyproject.toml)
|
||||||
setuptools==78.1.0
|
setuptools==78.1.0
|
||||||
# via
|
# via
|
||||||
# pyinstaller
|
# pyinstaller
|
||||||
# pyinstaller-hooks-contrib
|
# pyinstaller-hooks-contrib
|
||||||
shiboken6==6.9.0
|
|
||||||
# via
|
|
||||||
# pyside6
|
|
||||||
# pyside6-addons
|
|
||||||
# pyside6-essentials
|
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
# via
|
# via
|
||||||
# anyio
|
# anyio
|
||||||
|
|
|
||||||
45
src/carouselcmd.py
Normal file
45
src/carouselcmd.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.13"
|
||||||
|
# dependencies = [
|
||||||
|
# "click>=8.4.1",
|
||||||
|
# "rich>=15.0.0",
|
||||||
|
# "polars>=1.40.1",
|
||||||
|
# "rustworkx>=0.17.1"
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
import sys, io
|
||||||
|
import click
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
import polars as pl
|
||||||
|
from carousel import match_from_prefs
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument("preferences", type=click.Path(exists=True, dir_okay=False))
|
||||||
|
@click.option(
|
||||||
|
"--show-preferences",
|
||||||
|
"-b",
|
||||||
|
is_flag=True,
|
||||||
|
help="Show relevant preferences (after selections).",
|
||||||
|
)
|
||||||
|
@click.option("--pretty", "-p", is_flag=True, help="Pretty-print output.")
|
||||||
|
def cli(preferences: str, show_ballots=False, pretty=False) -> None:
|
||||||
|
"""
|
||||||
|
Compute the Gale-Shapley stable match of some preferences -- .csv or .xls(x).
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
12
src/cgi/carousel.cgi
Executable file
12
src/cgi/carousel.cgi
Executable file
|
|
@ -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; s/>/\>/g' "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$SCRIPT_DIR/../../.venv/bin/python" "$SCRIPT_DIR/script.py"
|
||||||
|
|
@ -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("<title>Carousel Matches!</title>")
|
|
||||||
print("<h1>Carousel Stable Matches Found:</h1>")
|
|
||||||
|
|
||||||
print("TODO - INPUTH ECHO NOT IMPLEMENTED")
|
|
||||||
print("TODO - OUTPUT NOT IMPLEMENTED")
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
CAROUSEL_LOGDIR="$HOME/logs/carousel/"
|
|
||||||
|
|
@ -1,663 +1,75 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
/* Disclaimer: This file was generated using an LLM */
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Dynamic Data Entry Form</title>
|
<title> Carousel - Stable Matcher </title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Georgia, serif;
|
font-family: Arial, sans-serif;
|
||||||
line-height: 1.6;
|
max-width: 600px;
|
||||||
color: #1a1a1a;
|
margin: 40px auto;
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f8f8f8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: #333;
|
font-size: 24px;
|
||||||
text-align: center;
|
margin-bottom: 20px;
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
padding-bottom: 15px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
|
label {
|
||||||
h2 {
|
display: block;
|
||||||
color: #333;
|
margin-bottom: 8px;
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
|
input[type="file"] {
|
||||||
.container {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
input[type="submit"] {
|
||||||
.controls-container {
|
padding: 8px 16px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fill-default-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fill-default-btn {
|
|
||||||
background: #777;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fill-default-btn:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled-table {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: #555;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: background 0.3s;
|
|
||||||
font-family: Georgia, serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.secondary {
|
|
||||||
background: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.secondary:hover {
|
|
||||||
background: #777;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 600px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
th input {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-family: Georgia, serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
td input {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-family: Georgia, serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-upload {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-upload label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-info {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-section {
|
|
||||||
text-align: center;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button {
|
|
||||||
background: #444;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
transition: background 0.3s;
|
|
||||||
font-family: Georgia, serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button:hover {
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 5px;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
margin-top: 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #666;
|
|
||||||
width: 0%;
|
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
background-color: #f1f5f1;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: #f5f1f1;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
padding: 10px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #f0f0f0;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 3px 3px 0 0;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: 1px solid #fff;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border-left-color: #555;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 10px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
(<a href="https://pages.uoregon.edu/tgorordo">Back to ~/tgorordo</a>)
|
<h1>Carousel: Stable Matcher - CGI Application Upload</h1>
|
||||||
<div class="container">
|
|
||||||
<h1>Carousel Stable Matcher</h1>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
<div class="tab active" id="table-tab">Table Input</div>
|
|
||||||
<div class="tab" id="file-tab">File Upload</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="data-form" action="/cgi-bin/carousel.py" method="post" enctype="multipart/form-data">
|
|
||||||
<div class="tab-content active" id="table-content">
|
|
||||||
<!-- First Table Section -->
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="form-header">
|
|
||||||
<h2>Applicant Preferences</h2>
|
|
||||||
<div class="controls">
|
|
||||||
<button type="button" id="add-column-1">Add Column</button>
|
|
||||||
<button type="button" id="add-row-1">Add Row</button>
|
|
||||||
<button type="button" class="secondary" id="remove-column-1">Remove Column</button>
|
|
||||||
<button type="button" class="secondary" id="remove-row-1">Remove Row</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container">
|
|
||||||
<table id="data-table-1">
|
|
||||||
<thead>
|
|
||||||
<tr id="header-row-1">
|
|
||||||
<th><input type="text" placeholder="Applicant 1" name="table1_header_0" required></th>
|
|
||||||
<th><input type="text" placeholder="Applicant 2" name="table1_header_1" required></th>
|
|
||||||
<th><input type="text" placeholder="Applicant 3" name="table1_header_2" required></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><input type="text" placeholder="Preference 1..." name="table1_row_0_col_0"></td>
|
|
||||||
<td><input type="text" placeholder="Preference 1..." name="table1_row_0_col_1"></td>
|
|
||||||
<td><input type="text" placeholder="Preference 1..." name="table1_row_0_col_2"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><input type="text" name="table1_row_1_col_0"></td>
|
|
||||||
<td><input type="text" name="table1_row_1_col_1"></td>
|
|
||||||
<td><input type="text" name="table1_row_1_col_2"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Second Table Section -->
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="form-header">
|
|
||||||
<h2>Reviewer Preferences</h2>
|
|
||||||
<div class="controls-container">
|
|
||||||
<div class="controls">
|
|
||||||
<button type="button" id="add-column-2">Add Column</button>
|
|
||||||
<button type="button" id="add-row-2">Add Row</button>
|
|
||||||
<button type="button" class="secondary" id="remove-column-2">Remove Column</button>
|
|
||||||
<button type="button" class="secondary" id="remove-row-2">Remove Row</button>
|
|
||||||
</div>
|
|
||||||
<div class="fill-default-control">
|
|
||||||
<label for="fill-default">
|
|
||||||
<button type="button" id="fill-default-btn" class="fill-default-btn">Fill Default</button>
|
|
||||||
<input type="checkbox" id="fill-default" name="fill_default"> (Use default uniform preferences.)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container" id="table2-container">
|
|
||||||
<table id="data-table-2">
|
|
||||||
<thead>
|
|
||||||
<tr id="header-row-2">
|
|
||||||
<th><input type="text" placeholder="Reviewer 1" name="table2_header_0" required></th>
|
|
||||||
<th><input type="text" placeholder="Reviewer 2" name="table2_header_1" required></th>
|
|
||||||
<th><input type="text" placeholder="Reviewer 3" name="table2_header_2" required></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><input type="text" placeholder="Preference 1..." name="table2_row_0_col_0"></td>
|
|
||||||
<td><input type="text" placeholder="Preference 1..." name="table2_row_0_col_1"></td>
|
|
||||||
<td><input type="text" placeholder="Preference 1..." name="table2_row_0_col_2"></td>
|
|
||||||
|
|
||||||
</tr>
|
<div class="form-container">
|
||||||
<tr>
|
<form action="./carousel.cgi" method="POST" enctype="multipart/form-data">
|
||||||
<td><input type="text" name="table2_row_1_col_0"></td>
|
<label for="spreadsheet">Upload an assignment preference spreadsheet (.xlsx, .xls, .csv):</label>
|
||||||
<td><input type="text" name="table2_row_1_col_1"></td>
|
<input type="file" id="spreadsheet" name="spreadsheet" accept=".xlsx,.xls,.csv" required>
|
||||||
<td><input type="text" name="table2_row_1_col_2"></td>
|
<br>
|
||||||
</tr>
|
<input type="submit" value="Upload and Match!">
|
||||||
</tbody>
|
</form>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="file-content">
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="file-upload">
|
|
||||||
<label for="file-input">Upload a CSV or Excel file:</label>
|
|
||||||
<input type="file" id="file-input" name="file" accept=".csv,.xlsx,.xls">
|
|
||||||
<div class="file-info" id="file-info"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-section submit-section">
|
|
||||||
<button type="submit" class="submit-button">
|
|
||||||
<span id="spinner" class="spinner"></span>
|
|
||||||
<span id="submit-text">Submit Preferences</span>
|
|
||||||
</button>
|
|
||||||
<div class="progress-bar" id="progress-bar">
|
|
||||||
<div class="progress" id="progress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="message" id="message"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="explanation-container">
|
||||||
<details>
|
|
||||||
<summary><b>Input/Output Format Documentation</b></summary>
|
|
||||||
<p>
|
|
||||||
The matcher assumes a specific tabular scheme for the input data - to read more about how this works see the
|
|
||||||
<a href="https://github.com/tgorordo/carousel">README</a>.
|
|
||||||
|
|
||||||
You can enter preference information directly in the tables above, or upload an Excel spreadsheet with
|
<h2>About</h2>
|
||||||
two sheets of preferences, or a CSV containing a ranking matrix.
|
<p>This is an upload form for the <a href="https://github.com/tgorordo/carousel">carousel</a> stable matcher for ,
|
||||||
</p> More on each format scheme:
|
mainly to help with <a href="https://blogs.uoregon.edu/physicsgsg/">UO Physics TA assignments</a>.
|
||||||
|
This form uses the <a href="https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069">UO pages.uoregon.edu CGI Capability</a>,
|
||||||
|
so the implementation of smithy being invoked can be <a href="https://pages.uoregon.edu/tgorordo/files/carousel/"> inspected here</a>
|
||||||
|
and you may also inspect the source of this page to verify that <a href="https://pages.uoregon.edu/tgorordo/files/carousel/src/cgi/carousel.cgi?source">this script<a> is called to invoke it - though
|
||||||
|
you have to trust it to <a href="https://en.wikipedia.org/wiki/Quine_(computing)">quine</a> itself faithfully.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p> A <a href=https://en.wikipedia.org/wiki/Stable_matching_problem>stable matching (of the "college admissions" problem)</a> 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
|
||||||
|
<a href="https://en.wikipedia.org/wiki/Gale%E2%80%93Shapley_algorithm">Gale-Shapley (1962) deferred-acceptance algorithm</a>, which finds the stable matching
|
||||||
|
that is optimal for the TA preferrences. Other stable solutions can be found by using <a href="https://github.com/tgorordo/carousel">the underlying
|
||||||
|
python application manually</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Input: Expected Spreadsheet Format</h3>
|
||||||
<h3>Tabular Input</h3>
|
<p>A matching-preference spreadsheet should be organized as a list of TA assignment preferences
|
||||||
You can enter tables of preferences directly in the first tab of the form above.
|
and a list of course preferences/constraints, i.e. with the following structure:
|
||||||
|
TODO</p>
|
||||||
TODO
|
|
||||||
|
<h3>Output: Matching Format</h3>
|
||||||
<h4>Example:</h4>
|
<p>The form will return a matching of TAs to course assignments of the form: TODO</p>
|
||||||
Suppose we want to figure out a Grad TA-to-Class assignment. Graduate students will be our "Applicants"
|
</div>
|
||||||
- each one will be assigned a class to TA -, and classes will be our "Reviewers" - taking a quota of needed TAs.
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
<h3>File Upload</h3>
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
<h4>Excel</h4>
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
<h4>CSV</h4>
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Tab switching functionality
|
|
||||||
document.getElementById('table-tab').addEventListener('click', () => {
|
|
||||||
switchTab('table');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('file-tab').addEventListener('click', () => {
|
|
||||||
switchTab('file');
|
|
||||||
});
|
|
||||||
|
|
||||||
function switchTab(tabName) {
|
|
||||||
// Hide all tabs and contents
|
|
||||||
document.querySelectorAll('.tab').forEach(tab => {
|
|
||||||
tab.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.tab-content').forEach(content => {
|
|
||||||
content.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the selected tab and content
|
|
||||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
|
||||||
document.getElementById(`${tabName}-content`).classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table manipulation functions
|
|
||||||
function setupTableControls(tableNumber) {
|
|
||||||
const addColumnBtn = document.getElementById(`add-column-${tableNumber}`);
|
|
||||||
const addRowBtn = document.getElementById(`add-row-${tableNumber}`);
|
|
||||||
const removeColumnBtn = document.getElementById(`remove-column-${tableNumber}`);
|
|
||||||
const removeRowBtn = document.getElementById(`remove-row-${tableNumber}`);
|
|
||||||
const dataTable = document.getElementById(`data-table-${tableNumber}`);
|
|
||||||
const headerRow = document.getElementById(`header-row-${tableNumber}`);
|
|
||||||
|
|
||||||
addColumnBtn.addEventListener('click', () => {
|
|
||||||
const colCount = headerRow.cells.length;
|
|
||||||
|
|
||||||
// Add header cell
|
|
||||||
const newHeader = document.createElement('th');
|
|
||||||
newHeader.innerHTML = `<input type="text" placeholder="Column ${colCount + 1}" name="table${tableNumber}_header_${colCount}" required>`;
|
|
||||||
headerRow.appendChild(newHeader);
|
|
||||||
|
|
||||||
// Add cell to each row
|
|
||||||
const rows = dataTable.querySelectorAll('tbody tr');
|
|
||||||
rows.forEach((row, rowIndex) => {
|
|
||||||
const newCell = document.createElement('td');
|
|
||||||
newCell.innerHTML = `<input type="text" name="table${tableNumber}_row_${rowIndex}_col_${colCount}">`;
|
|
||||||
row.appendChild(newCell);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
addRowBtn.addEventListener('click', () => {
|
|
||||||
const tbody = dataTable.querySelector('tbody');
|
|
||||||
const rows = tbody.querySelectorAll('tr');
|
|
||||||
const rowCount = rows.length;
|
|
||||||
const colCount = headerRow.cells.length;
|
|
||||||
|
|
||||||
// Create new row
|
|
||||||
const newRow = document.createElement('tr');
|
|
||||||
|
|
||||||
// Add cells to the new row
|
|
||||||
for (let i = 0; i < colCount; i++) {
|
|
||||||
const newCell = document.createElement('td');
|
|
||||||
newCell.innerHTML = `<input type="text" name="table${tableNumber}_row_${rowCount}_col_${i}">`;
|
|
||||||
newRow.appendChild(newCell);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody.appendChild(newRow);
|
|
||||||
});
|
|
||||||
|
|
||||||
removeColumnBtn.addEventListener('click', () => {
|
|
||||||
const colCount = headerRow.cells.length;
|
|
||||||
|
|
||||||
if (colCount <= 1) {
|
|
||||||
alert('Table must have at least one column.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the last header cell
|
|
||||||
headerRow.deleteCell(colCount - 1);
|
|
||||||
|
|
||||||
// Remove the last cell from each row
|
|
||||||
const rows = dataTable.querySelectorAll('tbody tr');
|
|
||||||
rows.forEach(row => {
|
|
||||||
row.deleteCell(colCount - 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
removeRowBtn.addEventListener('click', () => {
|
|
||||||
const tbody = dataTable.querySelector('tbody');
|
|
||||||
const rows = tbody.querySelectorAll('tr');
|
|
||||||
|
|
||||||
if (rows.length <= 1) {
|
|
||||||
alert('Table must have at least one row.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the last row
|
|
||||||
tbody.removeChild(rows[rows.length - 1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup controls for both tables
|
|
||||||
setupTableControls(1);
|
|
||||||
setupTableControls(2);
|
|
||||||
|
|
||||||
// Fill Default functionality for Table 2
|
|
||||||
const fillDefaultCheckbox = document.getElementById('fill-default');
|
|
||||||
const fillDefaultBtn = document.getElementById('fill-default-btn');
|
|
||||||
const table2Container = document.getElementById('table2-container');
|
|
||||||
|
|
||||||
fillDefaultBtn.addEventListener('click', () => {
|
|
||||||
fillDefaultCheckbox.checked = !fillDefaultCheckbox.checked;
|
|
||||||
toggleTable2State();
|
|
||||||
});
|
|
||||||
|
|
||||||
fillDefaultCheckbox.addEventListener('change', toggleTable2State);
|
|
||||||
|
|
||||||
function toggleTable2State() {
|
|
||||||
if (fillDefaultCheckbox.checked) {
|
|
||||||
table2Container.classList.add('disabled-table');
|
|
||||||
} else {
|
|
||||||
table2Container.classList.remove('disabled-table');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// File upload handling
|
|
||||||
const fileInput = document.getElementById('file-input');
|
|
||||||
const fileInfo = document.getElementById('file-info');
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
fileInfo.textContent = `Selected: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
|
|
||||||
} else {
|
|
||||||
fileInfo.textContent = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form submission
|
|
||||||
const dataForm = document.getElementById('data-form');
|
|
||||||
const submitButton = document.querySelector('.submit-button');
|
|
||||||
const spinner = document.getElementById('spinner');
|
|
||||||
const submitText = document.getElementById('submit-text');
|
|
||||||
const progressBar = document.getElementById('progress-bar');
|
|
||||||
const progress = document.getElementById('progress');
|
|
||||||
const message = document.getElementById('message');
|
|
||||||
|
|
||||||
dataForm.addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Show spinner and disable button
|
|
||||||
spinner.style.display = 'inline-block';
|
|
||||||
submitButton.disabled = true;
|
|
||||||
submitText.textContent = 'Submitting...';
|
|
||||||
progressBar.style.display = 'block';
|
|
||||||
message.style.display = 'none';
|
|
||||||
message.className = 'message';
|
|
||||||
|
|
||||||
// Collect form data
|
|
||||||
const formData = new FormData(dataForm);
|
|
||||||
|
|
||||||
// Add a flag to indicate which tab is active
|
|
||||||
formData.append('input_type', document.getElementById('table-content').classList.contains('active') ? 'table' : 'file');
|
|
||||||
|
|
||||||
// If table input is active, add table structure info
|
|
||||||
if (document.getElementById('table-content').classList.contains('active')) {
|
|
||||||
// Table 1 info
|
|
||||||
const colCount1 = document.getElementById('header-row-1').cells.length;
|
|
||||||
const rowCount1 = document.getElementById('data-table-1').querySelectorAll('tbody tr').length;
|
|
||||||
formData.append('table1_col_count', colCount1);
|
|
||||||
formData.append('table1_row_count', rowCount1);
|
|
||||||
|
|
||||||
// Table 2 info
|
|
||||||
const colCount2 = document.getElementById('header-row-2').cells.length;
|
|
||||||
const rowCount2 = document.getElementById('data-table-2').querySelectorAll('tbody tr').length;
|
|
||||||
formData.append('table2_col_count', colCount2);
|
|
||||||
formData.append('table2_row_count', rowCount2);
|
|
||||||
formData.append('table2_use_default', document.getElementById('fill-default').checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate upload progress (in a real application, you'd track actual upload progress)
|
|
||||||
let width = 0;
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
if (width >= 90) {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
} else {
|
|
||||||
width += 5;
|
|
||||||
progress.style.width = width + '%';
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// Send form data to server
|
|
||||||
fetch(dataForm.action, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
progress.style.width = '100%';
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Server error: ' + response.status);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
// Show success message
|
|
||||||
message.textContent = data.message || 'Data submitted successfully!';
|
|
||||||
message.classList.add('success');
|
|
||||||
message.style.display = 'block';
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// Show error message
|
|
||||||
console.error('Error:', error);
|
|
||||||
message.textContent = 'An error occurred while submitting the data. Please try again.';
|
|
||||||
message.classList.add('error');
|
|
||||||
message.style.display = 'block';
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// Hide spinner and re-enable button
|
|
||||||
setTimeout(() => {
|
|
||||||
spinner.style.display = 'none';
|
|
||||||
submitButton.disabled = false;
|
|
||||||
submitText.textContent = 'Submit Data';
|
|
||||||
progressBar.style.display = 'none';
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
<footer>
|
||||||
|
<hr>
|
||||||
|
<p>Author: <a href="https://pages.uoregon.edu/tgorordo">Thomas (Tom) C. Gorordo</a>
|
||||||
|
Source: <a href="https://github.com/tgorordo/pages.uoregon.edu">pages.uoregon.edu/tgorordo</a>,
|
||||||
|
<a href="https://github.com/tgorordo/carousel">carousel</a></p>
|
||||||
|
</footer>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
145
src/cgi/script.py
Normal file
145
src/cgi/script.py
Normal file
|
|
@ -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 = """
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>File extension is not valid. Use CSV (.csv) or Excel (.xlsx, .xls).</p>
|
||||||
|
<p><a href="form.html">Go Back</a></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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"""
|
||||||
|
<h1>The TA-optimal (Gale-Shapley) match is:</h1>
|
||||||
|
{match}
|
||||||
|
<p><a href="form.html">Go Back</a></p>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
message = """
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>DataFrame was empty.</p>
|
||||||
|
<p><a href="form.html">Go Back</a></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
message = f"""
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>Internal Error Encountered: {e}
|
||||||
|
<p><a href="form.html">Go Back</a></p>
|
||||||
|
"""
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
else:
|
||||||
|
message = """
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>Filename or File Data not found/valid in form submission.</p>
|
||||||
|
<p><a href="form.html">Go Back</a></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
else:
|
||||||
|
message = """
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>No file field found in the form.</p>
|
||||||
|
<p><a href="form.html">Go Back</a></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title> Carousel - Stable Matcher </title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
""")
|
||||||
|
print(message)
|
||||||
|
print("""
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
<hr>
|
||||||
|
<p>Author: <a href="https://pages.uoregon.edu/tgorordo">Thomas (Tom) C. Gorordo</a>
|
||||||
|
Source: <a href="https://github.com/tgorordo/pages.uoregon.edu">pages.uoregon.edu/tgorordo</a>,
|
||||||
|
<a href="https://github.com/tgorordo/carousel">carousel</a></p>
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
|
""")
|
||||||
11
src/cli.py
11
src/cli.py
|
|
@ -1,11 +0,0 @@
|
||||||
import click
|
|
||||||
import carousel
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
def cli():
|
|
||||||
carousel.main()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
cli()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue