Skip to content

Repository files navigation

comsocwebapp

A Flask boilerplate for building web applications that solve computational social choice problems: fair allocation, voting and participatory budgeting.

You bring the problem; the package brings the admin dashboard, the participant ballot GUI, invitations and authentication, dummy-user simulation, rule execution with a full audit log, and CSV export.

  • Flask-shaped. create_app() is an ordinary application factory; the three blueprints are ordinary blueprints.
  • Multilingual. English and Hebrew (right-to-left) ship with the package; a new language is one file (see translations.md).
  • Solver-agnostic. adapters/ turns the SQL rows into plain dicts and lists, and bridges them into fairpyx, abcvoting and pabutools.

Prerequisites

  • Python 3.10 or newer (python --version).
  • No database server needed: the default backend is the standard library's sqlite3. PostgreSQL, MySQL and MariaDB are a config key away.
  • The solver libraries are optional — the package ships three built-in rules (approval_scoring, borda, greedy_budget) that depend on nothing.

Installation

git clone https://github.com/ariel-research/comsocwebapp.git
cd comsocwebapp

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

pip install -e .                 # core: Flask only
pip install -e ".[oauth]"        # plus Authlib (for external login providers)
pip install -e ".[postgresql]"   # plus psycopg (or [mysql] for PyMySQL)
pip install -e ".[all]"          # plus fairpyx, abcvoting, pabutools, drivers
pip install -e ".[dev]"          # plus pytest and stress tests

Individual extras are allocation (fairpyx), voting (abcvoting), budgeting (pabutools), oauth, postgresql and mysql.

A script tailored to your own application

Those commands install the package. An application built on it needs a different set, and which set depends entirely on how it is configured: its database driver, the solver libraries whose rules it registers, a database, an account that owns it. Rather than write that down in a README of your own and let it drift, ask the application:

flask --app myapp install-script --output install.sh
flask --app myapp install-script --shell powershell -o install.ps1

The result is a numbered script covering the whole sequence — packages, database, user, tables, first administrator, and a closing db-info check — with only the steps this configuration actually calls for:

Configured as What the script contains
SQLite pip install comsocwebapp, mkdir for the directory, init-db, chmod 600. No CREATE DATABASE, no user, no driver.
PostgreSQL comsocwebapp[postgresql], then CREATE ROLE and CREATE DATABASE … OWNER guarded by WHERE NOT EXISTS, run through psql as a superuser.
MySQL / MariaDB comsocwebapp[mysql], then CREATE DATABASE … utf8mb4 and a GRANT scoped to that one database.

Beyond the engine, two more things come from the configuration: the solver libraries, taken from every register_*_rule call the application makes, and oauth, added when a provider's client id and secret are both set.

It writes a script; it never runs one, and never connects. That is not caution, it is the only arrangement that works:

  • The application's own account cannot create the application's own account. The database step needs a superuser, and at many institutions that is a different person — so it has to be something you can read, review and send on, with the platform-specific ways of reaching psql or mysql spelled out in its comments.
  • The driver is not installed yet. Anything that had to connect in order to work out what to do would fail on precisely the machine an installer is for.

No password is written into the file. The steps that need one read $DATABASE_PASSWORD from the environment as they run, and escape it for the SQL literal it lands in. Keep it in .env, and source that before running the script.

Two caveats worth knowing:

  • The script is a snapshot of the configuration at the moment it was generated. Its header records that configuration and the command that produced it, so regenerate rather than edit when the configuration changes.
  • A database or user name that is not a plain identifier — letters, digits and underscores — is refused rather than quoted, because the name is interpolated into CREATE DATABASE. Rename it, or create the database by hand — database-engines.md has the statements.

In Python, the same text is install.build_script(app):

from comsocwebapp import create_app, install

app = create_app({"DATABASE_URL": "postgresql://comsoc_app:…@localhost/comsoc"})
print(install.build_script(app, "bash", app_ref="myapp"))

Getting started in three commands

export FLASK_APP=comsocwebapp    # Windows PowerShell: $env:FLASK_APP="comsocwebapp"

flask init-db                    # 1. create the tables
flask create-admin               # 2. create the first admin (prompts for email + password)
flask run --debug                # 3. serve on http://127.0.0.1:5000/

Then open http://127.0.0.1:5000/admin/ and log in with the account you just created.

flask init-db drops and recreates every table — it is the "start a fresh event" command, not a migration.

Without shell variables:

flask --app comsocwebapp init-db
flask --app comsocwebapp create-admin
flask --app comsocwebapp run --debug

The database file lands in instance/comsocwebapp.sqlite. Override the location and the session key with environment variables:

export DATABASE_FILE=/var/lib/comsoc/event.sqlite
export COMSOCWEBAPP_SECRET_KEY="$(python -c 'import secrets;print(secrets.token_hex(32))')"
export COMSOCWEBAPP_LANGUAGE=he                  # GUI language; default "en"
export DATABASE_URL=postgresql://user:pw@host/comsoc   # another engine entirely

Every setting is spelled in the environment exactly as it is in app.configDATABASE_FILE, DATABASE_URL, OAUTH_GITHUB_CLIENT_ID and the rest. The two exceptions above take a COMSOCWEBAPP_ prefix because their bare names are not ours to claim: SECRET_KEY is defined by half of PyPI, and LANGUAGE is a POSIX locale variable that is already set on most Linux machines.

Setting COMSOCWEBAPP_SECRET_KEY is required in production — the default "dev" key exists only so flask run works out of the box.

Runnable examples

Each example seeds its own database and starts a server, so you can see a complete application in one command:

python examples/participatory_budgeting.py   # city budget, approval ballots
python examples/committee_voting.py          # board election, approval ballots, in Hebrew
python examples/fair_allocation.py           # estate division, point ballots

They all print an admin login (admin@example.com / admin) and an invitation link you can open in a private window to see the participant side.

Local configuration (secrets stay off GitHub)

Each example splits its configuration in two, which is the split worth copying into an application of your own:

  • Not secret — the port, the database path, the language, the template folder — is the CONFIG dict at the top of the file.
  • Secret — the session key, the external-login client secrets, the database password — is an examples/.env file, never the code.

Each example calls load_dotenv() on start-up (via python-dotenv, a core dependency), so the file is picked up automatically and create_app reads those values from the environment. The .env file is git-ignored, so your secrets never get committed; a tracked template, examples/example.env, shows what it may contain, with every line commented out.

Copy the template and fill in what you need:

cp examples/example.env examples/.env         # Windows: copy examples\example.env examples\.env

Then edit examples/.env — for instance, to switch on GitHub sign-in, uncomment and fill:

OAUTH_GITHUB_CLIENT_ID=Iv1.xxxxxxxx
OAUTH_GITHUB_CLIENT_SECRET=yyyyyyyy

A commented line (or a missing .env altogether) simply changes nothing, so the examples run out of the box with no config at all. The keys read are COMSOCWEBAPP_SECRET_KEY, the OAUTH_<GOOGLE|GITHUB|ORCID>_CLIENT_ID / _CLIENT_SECRET pairs — see external-login.md for how to obtain them and register the redirect URI — and DATABASE_PASSWORD or DATABASE_URL (see database-engines.md). Only the session key is prefixed, for the reason given above.

Left unset, COMSOCWEBAPP_SECRET_KEY falls back to the development key "dev", which is fine for a demo on your own machine and must not be used anywhere reachable: anyone who knows it can forge a session, including an admin's.

The admin workflow

  1. New poll — give it a title, a problem type (committee, budgeting or allocation), a preference format (approval, ranking, points, budget) and the limit that goes with the type: the committee size, the money to spend, or the points a ballot distributes. The type is what tells a committee election from a participatory budget — the same approval ballots serve both — so it decides which rules are offered and whether project costs are collected.
  2. Add options one by one, or bulk-upload a CSV with the columns name,description,cost.
  3. Generate invitationspersonal links die once redeemed; generic links can be mailed to a list, and the unique index on users.email is what stops anyone voting twice.
  4. Generate dummy users to test a rule before real people arrive; choose the distribution (uniform, normal, exponential), the bounds and a seed for reproducibility. Delete them all with one button.
  5. Set the status to open so participants can vote (closed locks it).
  6. Run a rule over real users only, dummy users only, or both. The outcome and the step-by-step log are stored in execution_logs and shown to admins and participants alike.
  7. Export anonymised preferences and execution logs as CSV.

Using the package as a library

from comsocwebapp import create_app, db, dummy, adapters, rules

app = create_app()

with app.app_context():
    db.init_db()
    poll_id = db.insert_returning_id(
        "INSERT INTO polls (title, pref_format, status, budget_limit)"
        " VALUES (?, 'approval', 'open', 1000)",
        ("My election",))
    db.execute("INSERT INTO options (poll_id, name, description, cost)"
               " VALUES (?, ?, ?, ?)", (poll_id, "Park", "A new park", 400))

    dummy.generate_dummy_users(poll_id, 50, seed=1)

    result = rules.run_rule("approval_scoring", poll_id,
                            adapters.SCOPE_DUMMY, committee_size=2)
    rules.record_execution(poll_id, "approval_scoring", result)
    print(result.outcome, result.log_lines, sep="\n")

Adding your own rule

from comsocwebapp import adapters, rules

@rules.register_rule("my_rule", formats=("approval",),
                     poll_types=("committee",))
def my_rule(poll_id, scope=adapters.SCOPE_ALL, committee_size=None, **params):
    # This run's size if the admin typed one, else the size the poll
    # declares -- never a constant in your own file.
    size = rules.committee_size_for(poll_id, committee_size)
    matrix = adapters.preference_matrix(poll_id, scope)   # {user: {option: value}}
    winners = ...
    return rules.RuleResult(outcome=winners,
                            log_lines=["why these winners were chosen"])

The rule appears in the admin's dropdown as soon as the module is imported. Two optional filters narrow where it is offered, and they answer different questions: formats=("approval",) — what ballots it reads — and poll_types=("committee",) — what problem it solves, matched against the poll's own poll_type column. Omit either to mean "any". See rules.register_rule.

Bridging to the solver libraries

instance          = adapters.to_fairpyx_instance(poll_id)      # fairpyx
profile, ids      = adapters.to_abcvoting_profile(poll_id)     # abcvoting
instance, profile = adapters.to_pabutools_instance(poll_id)    # pabutools

Each imports its library lazily, so an install without that extra still runs.

Adding rules from a solver library

The package registers no library rules of its own — installing abcvoting adds nothing to the admin's dropdown by itself. The three libraries offer dozens of rules between them, and which ones your voters should be offered is your application's decision, not the package's. Asking for one is one line:

from comsocwebapp import adapters

for rule_id in ("seqphragmen", "cc", "monroe"):     # approval-based committees
    adapters.register_abcvoting_rule(rule_id)

adapters.register_pabutools_rule("greedy_utilitarian_welfare",   # budgeting
                                 sat_class="Cost_Sat")
adapters.register_fairpyx_rule("iterated_maximum_matching")      # allocation

Each rule then appears in the admin's dropdown, named <library>_<rule>, on any poll whose preference format fits it. Put these calls at module level in your application file, next to your own @rules.register_rule functions: the registry is filled at import time, before the app serves anything.

Call Takes Choose from
adapters.register_abcvoting_rule(rule_id) an abcvoting rule id abcvoting.abcrules.MAIN_RULE_IDS — ~27: pav, seqphragmen, cc, monroe, equal-shares, minimaxav, …
adapters.register_pabutools_rule(name) a function name, plus an optional sat_class pabutools.rulesmethod_of_equal_shares, sequential_phragmen, greedy_utilitarian_welfare, maximin_support, …
adapters.register_fairpyx_rule(algorithm) an algorithm name fairpyx.algorithmsround_robin, iterated_maximum_matching, almost_egalitarian_allocation, …

All three also take name= to override the generated rule name and headline= (abcvoting takes its headline from the library) to set the first line of the run log. Nothing that belongs to a run is fixed at registration — the committee size and the scope are chosen by the admin each time the rule is executed, so one registration serves every committee size. They handle the parts that are easy to get wrong:

  • The library is imported when the rule runs, never at registration, so your file still loads where the library is missing.
  • A missing library registers nothing and returns None, so a rule that could not run is never offered to an admin — no available() check needed in your code.
  • Each call binds its own rule id, so a for loop is safe. Writing the decorator inline in a loop instead is the classic bug: every rule would end up computing the last id.
  • The library's own account of the run goes into the log — see below.

All four runnable examples do this, and between them they are the reference: committee_voting.py and standalone_faculty_hiring/app.py register abcvoting rules, participatory_budgeting.py three pabutools rules, fair_allocation.py four fairpyx algorithms. Copy the block from whichever one matches your problem.

If a rule needs shaping the helper cannot express, drop to @rules.register_rule and use the bridge directly — that is what the helpers themselves do. To add a whole library the package does not know about yet, see comsocwebapp/adapters/README.md.

What the execution log shows

Every run writes an execution log that both the admin and the participants can read. It opens with the app's own summary — rule, electorate size, winners by name — and then, for a library-backed rule, continues with the library's own account of how it got there. For sequential Phragmén that is one block per seat: the candidate added, the maximum load it produced, and any tie that had to be broken.

None of the three libraries produces that by default, and no two produce it the same way, so the adapters ask each in its own dialect: abcvoting is put into DETAILS verbosity and its printing captured, pabutools rules are called with verbose=True where they accept it, fairpyx narrates through the standard logging module. Coverage follows what each library offers — abcvoting's sequential rules are the most talkative, pabutools.sequential_phragmen takes no tracing argument at all and contributes nothing beyond the summary.

A few algorithms explain themselves to each voter separately instead: fairpyx.iterated_maximum_matching tells an agent, in the second person, what it valued and what it therefore received. That text quotes the reader's own ballot, so it is stored per user rather than in the shared log — a participant sees only their own under What this means for you, and the admin, who can already read every ballot, sees them all. See comsocwebapp/adapters/README.md for how a rule of your own supplies either kind.

Database

Seven tables — users, polls, options, invitations, preferences, execution_logs, execution_log_voters — defined in comsocwebapp/schema.sql exactly as specified in database.md. Only VARCHAR, INTEGER, TIMESTAMP and TEXT are used — TEXT for the two columns holding a solver library's own narrative, which has no sensible length bound. Booleans are INTEGER 0/1; there are no JSON columns, arrays or ENUMs.

SQLite, PostgreSQL, MySQL or MariaDB — a matter of configuration, not of code:

pip install "comsocwebapp[postgresql]"       # or [mysql]
export DATABASE_URL="postgresql://user:pw@localhost:5432/comsoc"
flask --app comsocwebapp init-db

The default is SQLite, which needs no server and no driver. flask db-info shows what you are connected to, and flask db-copy --to URL moves an existing event to another engine — keeping every id, so invitation links still work afterwards. Only one construct in the whole project is dialect-dependent (the auto-increment column in schema.sql, rewritten when it runs); the runtime queries avoid RETURNING, ON CONFLICT, MERGE and everything else specific to an engine, and where an UPSERT would normally be used db.upsert_preference() does a portable UPDATE-then-INSERT.

database-engines.md has the configuration keys, the per-engine setup, the migration procedure and a troubleshooting table.

Security

  • Passwords are stored as Werkzeug PBKDF2 hashes; login errors never reveal whether an email is registered.
  • Every SQL parameter is bound, never interpolated.
  • Session cookies are signed, HttpOnly and SameSite=Lax.
  • A dependency-free CSRF token guards every POST (disable with CSRF_ENABLED = False if your app already uses Flask-WTF).
  • Personal invitation tokens are 32 URL-safe characters from secrets, and are consumed on first use.

Multilingual GUI

The whole GUI is translated, and an application picks its language with one config key:

app = create_app({"LANGUAGE": "he"})       # Hebrew, right-to-left

"en" (the default) and "he" ship with the package. "he" is right-to-left: the page comes out as <html dir="rtl"> and the stylesheet — written with logical properties throughout — mirrors itself, with no separate rtl stylesheet. python examples/committee_voting.py is the working demonstration.

Adding a language is one file: copy comsocwebapp/languages/en.py to fr.py, set NAME and DIRECTION, and translate the right-hand side of LABELS. Files in that folder are discovered automatically; an application that cannot write into the installed package calls i18n.register_language() instead. translations.md is the full guide, including what is deliberately not translated (your poll titles, option names and rule names) and how pytest tests/test_v5.py checks a new translation for missing keys and mangled placeholders.

External login (Google, GitHub, ORCID)

Participants can register and log in with an existing Google, GitHub or ORCID account. The feature is optional and off by default: install the oauth extra and set each provider's OAUTH_<PROVIDER>_CLIENT_ID / _CLIENT_SECRET, and the buttons appear on their own. See external-login.md for the full setup, the identity model, and how to add another provider.

Running the tests

pip install -e ".[dev]"
pytest

Project layout

comsocwebapp/
├── __init__.py       application factory, `create-admin` CLI command
├── db.py             connections, parameterised queries, portable UPSERT
├── dialects.py       SQLite / PostgreSQL / MySQL: the few places they differ
├── install.py        `install-script`: the install this configuration needs
├── schema.sql        the seven tables
├── poll.py        wrapper to create a poll and number its options
├── auth.py           invitation tokens, registration, login, guards
├── oauth.py          optional Google / GitHub / ORCID sign-in
├── admin.py          admin blueprint  (/admin/...)
├── participant.py    participant blueprint (/, /vote/..., /results/...)
├── adapters/         SQL rows -> dicts -> fairpyx / abcvoting / pabutools
│   ├── generic.py    library-independent shapes
│   └── <library>.py  one file per solver library (+ README.md)
├── dummy.py          bulk dummy users with randomised preferences
├── rules.py          rule registry, built-in rules, execution logging
├── security.py       CSRF protection
├── i18n.py           GUI language registry, lookup and fallbacks
├── languages/        one file per language (en.py, he.py)
├── templates/        Jinja templates for both GUIs
└── static/style.css  responsive, dependency-free, direction-agnostic styling
examples/             runnable reference applications (+ a standalone one)
tests/                pytest suite

License

See LICENSE.

About

A Flask-like package for creating web-applications for computational social choice

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages