From c57068b34cad035d57ccc6c600d6e6abd19e507f Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" Date: Fri, 22 May 2026 01:07:50 -0700 Subject: [PATCH] implement cgi interface --- README.md | 11 ++++ justfile | 10 ++-- smithy.spec | 2 +- src/cgi/smithy.cgi | 117 ++++++++++++++++++++++++++++++++++++++++ src/cgi/smithy.html | 101 ++++++++++++++++++++++++++++++++++ src/{main.py => cmd.py} | 0 6 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 src/cgi/smithy.cgi create mode 100644 src/cgi/smithy.html rename src/{main.py => cmd.py} (100%) diff --git a/README.md b/README.md index 434dbc2..bd67a90 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/justfile b/justfile index 4138fd9..9a3fe4a 100644 --- a/justfile +++ b/justfile @@ -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 diff --git a/smithy.spec b/smithy.spec index 95e0479..c7ab993 100644 --- a/smithy.spec +++ b/smithy.spec @@ -2,7 +2,7 @@ a = Analysis( - ['src/main.py'], + ['src/cmd.py'], pathex=[], binaries=[], datas=[], diff --git a/src/cgi/smithy.cgi b/src/cgi/smithy.cgi new file mode 100644 index 0000000..031e53e --- /dev/null +++ b/src/cgi/smithy.cgi @@ -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"

Upload Successful!

") + + 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

+ """ + + # 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 = """ +

The Smith set winners are:

+ {smiths} +

Go Back

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

Error

+

Internal Error Encountered: {e} +

Go Back

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

Error

+

Filename not found/valid.

+

Go Back

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

Error

+

No file field found in the form.

+

Go Back

+ """ + +response = f""" + + + + + + Smithy - RCV Ballot Counter + + + + {message} + + +""" + +print(response) diff --git a/src/cgi/smithy.html b/src/cgi/smithy.html new file mode 100644 index 0000000..9d09156 --- /dev/null +++ b/src/cgi/smithy.html @@ -0,0 +1,101 @@ + + + + + + Smithy - RCV Ballot Counter + + + +

Smithy: RCV Ballot Counter - CGI Application Upload

+ +
+
+ + +
+ +
+
+ +
+ +

About

+

This is an upload form for the smithy 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 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. +

+ +

The Smith set 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 Condorcet i.e. Majority winner +(they beat all others pairwise).

+ +

Input: Expected Spreadsheet Format

+

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.:

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
AliceBobCharlie
123
213
132
312
+(which has, as its Smith set a tie between "Alice" and "Bob")

+ +

Output: Smith Set Format

+

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.

+
+ + + diff --git a/src/main.py b/src/cmd.py similarity index 100% rename from src/main.py rename to src/cmd.py