implement cgi interface

This commit is contained in:
Thomas (Tom) C. Gorordo 2026-05-22 01:07:50 -07:00
parent 174df7e572
commit c57068b34c
Signed by: tgorordo
GPG key ID: 0CBED22BB0D94490
6 changed files with 235 additions and 6 deletions

View file

@ -3,3 +3,14 @@
The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner
in the set they are guaranteed the standard Condorcet i.e. Majority winner (they beat all others pairwise).
## TODO
- add cli options & flexibility
- relevant column selection flags
- eligible voter filter flags (row selection)
- output control flags (whether to show input ballots or the voter ids on them, whether to pretty print output, etc.)
- `cgi` [interface on pages.uoregon.edu](https://service.uoregon.edu/TDClient/2030/Portal/KB/Article/43069/Advanced-use-of-pages-uoregon-edu).
- instructions/writeup and README
- polish/finalize packaging and usage (might be some refactoring of where the cli goes)

View file

@ -1,5 +1,5 @@
list:
just --list
just --list --unsorted
run spreadsheet:
uv run smithy {{spreadsheet}}
@ -10,17 +10,17 @@ marimo:
example:
uv run python src/main.py test/test_ballot.csv
format:
uv run ruff format src test
check:
uv run pyright src
test:
uv run pytest -vvv --tb=short --log-cli-level=INFO
format:
uv run ruff format src test
compile:
uv run pyinstaller --clean -F src/main.py --name smithy
uv run pyinstaller --clean -F src/cmd.py --name smithy
clean:
uv run pyclean src test

View file

@ -2,7 +2,7 @@
a = Analysis(
['src/main.py'],
['src/cmd.py'],
pathex=[],
binaries=[],
datas=[],

117
src/cgi/smithy.cgi Normal file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env -S uv run python
import cgi, cgitb
import sys, os
import html
import polars as pl
sys.path.insert(0,
os.path.abspath(
os.path.join(
os.path.dirname(os.path.abspath(__file__)), "../smithy/src"
)
)
)
from smithy import smith_set
cgitb.enable()
form = cgi.FieldStorage()
if 'spreadsheet' in form:
fileitem = form['spreadsheet']
if fileitem.filename:
filename = os.path.basename(fileitem.filename)
filepath = os.path.join("/tmp", filename)
with open(filepath, 'wb') as f:
f.write(fileitem.file.read())
print(f"<h2>Upload Successful!</h2>")
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="../smithy.html">Go Back</a></p>
"""
# 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
]
)
smiths = smith_set(df) # Solve!
message = """
<h1>The Smith set winners are:</h1>
{smiths}
<p><a href="../smithy.html">Go Back</a></p>
"""
except Exception as e:
message = """
<h1>Error<h1>
<p>Internal Error Encountered: {e}
<p><a href="../smithy.html">Go Back</a></p>
"""
else:
message = """
<h1>Error</h1>
<p>Filename not found/valid.</p>
<p><a href="../smithy.html">Go Back</a></p>
"""
else:
message = """
<h1>Error</h1>
<p>No file field found in the form.</p>
<p><a href="../smithy.html">Go Back</a></p>
"""
response = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Smithy - RCV Ballot Counter </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>
{message}
</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/smithy">smithy</a></p>
</footer>
"""
print(response)

101
src/cgi/smithy.html Normal file
View file

@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Smithy - RCV Ballot Counter </title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
}
input[type="file"] {
margin-bottom: 15px;
}
input[type="submit"] {
padding: 8px 16px;
}
</style>
</head>
<body>
<h1>Smithy: RCV Ballot Counter - CGI Application Upload</h1>
<div class="form-container">
<form action="/files/forms/smithy.cgi" method="POST" enctype="multipart/form-data">
<label for="spreadsheet">Upload a ballot-box spreadsheet (.xlsx, .xls, .csv):</label>
<input type="file" id="spreadsheet" name="spreadsheet" accept=".xlsx,.xls,.csv" required>
<br>
<input type="submit" value="Upload and Solve!">
</form>
</div>
<div class="explanation-container">
<h2>About</h2>
<p>This is an upload form for the <a href="https://github.com/tgorordo/smithy">smithy</a> RCV Ballot counter for identifying the Smith set (majority winners) in small elections,
mainly to help with UO Physics Grad student elections of various flavors. 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/smithy/"> inspected here</a>
and you may also inspect the source of this page to verify that <a href="https://pages.uoregon.edu/tgorordo/files/forms/smithy.cgi">this script<a> is called to invoke it.
</p>
<p> The <a href="https://en.wikipedia.org/wiki/Smith_set">Smith set</a> is the minimal set of candidates which can beat all others pairwise (by majority ranking)
- if there is a single winner in the set they are guaranteed the standard <a href=https://en.wikipedia.org/wiki/Condorcet_winner>Condorcet i.e. Majority winner</a>
(they beat all others pairwise).</p>
<h3>Input: Expected Spreadsheet Format</h3>
<p>A ballot-box spreadsheet should be organized by columns as candidates and rows as numerical rankings of the candidates -
i.e. each row is the ranking provided by the ballot of a voter,
e.g.:</p>
<p>
<table>
<tr>
<th>Alice</th>
<th>Bob</th>
<th>Charlie</th>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>3</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>1</td>
<td>2</td>
</tr>
</table>
(which has, as its Smith set a tie between "Alice" and "Bob")</p>
<h3>Output: Smith Set Format</h3>
<p>The form will return a list of the Smith-set winners sorted lexicographically (they are all equally good majority winners).
It might be helpful for clarity to provide the input sheet's columns already in lexicographic order, but this is not required. </p>
</div>
</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/smithy">smithy</a></p>
</footer>
</html>