Skip to content

Elections and recounts

Bjarni Rúnar Einarsson edited this page Sep 29, 2017 · 18 revisions

(This page is current as of September 2017)

The Default Election Process

  1. Users can announce their candidacy up to a certain date/time
  2. At another date/time, voting begins and users submit sorted lists of candidates
  3. At the final date/time, voting ends
  4. Someone (an admin or a cron job) invokes manage.py processelections:
    1. election.models.Elections.process is invoked
    2. An instance of election.utils.BallotCounter counts the votes
    3. The same election.utils.BallotCounter writes a copy of raw (but anonymized) ballots to a JSON file in the filesystem, the name/path of which are determined by settings.BALLOT_SAVEFILE_FORMAT.
    4. The original ballot data will be deleted from the database
    5. A set of election.models.ElectionResultRow objects will be created, representing the results

Manual counting

If the cron jobs are disabled, an admin will need to manually trigger the initial count. This is done like so:

$ manage.py processelections
...

If not all elections are to be processed, giving the election ID as the first argument will limit the count to only that election and no others:

$ manage.py processelections 123
...

Hint: The election ID is clearly visible in the URL of the election itself on the web. Easy peasy!

Temporarily hiding election results

Currently the wasa2il code-base does not directly support hiding election results. However, this can be easily "hacked in" by editing wasa2il/templates/election/election_view.html and adding requirements to the various if statements that control what is visible and what is not.

An ugly git diff example limiting the output to only members of staff:

diff --git a/wasa2il/templates/election/election_view.html b/wasa2il/templates/election/election_view.html
index a14e966..f3adf12 100644
--- a/wasa2il/templates/election/election_view.html
+++ b/wasa2il/templates/election/election_view.html
@@ -52,7 +52,7 @@
 {% if election.is_closed %}
     <div class="alert alert-danger">
         {% trans "This election is closed." %}
-        {% if user_result and election.results_are_ordered %}
+        {% if user.is_staff and user_result and election.results_are_ordered %}
             <b>{% blocktrans %}You ended up in {{user_result}}. place.{% endblocktrans %}</b>
         {% endif %}
     </div>
@@ -193,7 +193,7 @@
             {% if not election.results_are_ordered %}
                 <div class="alert alert-info">{% trans 'The voting system used in this election generates a non-ordered list. All winners are in the same place.' %}</div>
             {% endif %}
-            {% if ordered_candidates %}
+            {% if user.is_staff and ordered_candidates %}
             <div id="election_candidates_winners" style="margin-top: 1em;">
                 <ol class="candidates" id="candidates_winners" {% if not election.results_are_ordered %}style="list-style: none; padding-left: 0px;"{% endif %}>
                 {% for candidate in ordered_candidates %}
@@ -236,7 +236,7 @@
     </div>
 </div>

-{% if statistics and election.stats_publish_files and election.is_closed %}
+{% if user.is_staff and statistics and election.stats_publish_files and election.is_closed %}
 <div class="row">
     <p class="stats_downloads" style="text-align: right;">
         {% trans "Download detailed statistics as:" %}

Manual recounts

The file election/utils.py can be invoked as a stand-alone tool. It can be used to recount ballots from a JSON file using different algorithms, and/or omitting certain candidates.

An example of the common case, recounting after one or more candidates have withdrawn from the election:

$ python election/utils.py \
    -e quitter1 -e quitter2 \
    count schulze /path/to/ballot/data.json
...

Getting real names

The list of usernames can be converted into a more human-readable list of real names, by using the manage.py lookup_usernames command:

$ manage.py lookup_usernames user1, user2, user3
...

(Commas will be ignored, to facilitate copy-pasting directly from the election/utils.py output into the lookup tool)

Avoiding randomness in recounts

Note that if two users are tied, the libraries used by election/utils.py will perform a virtual coin toss to order those candidates. As a result, recounts may randomly change the order of candidates for no other reason than pure chance. It may be worth recounting multiple times to detect when this happens, and manually preserve the order chosen by the first election in such cases.

Supported counting methods

At the time of writing, election/utils.py supports the following voting systems: schulze, stcom, condorcet, stv1, ..., stv5, stv10

Most of these are standard counting methods that you can read about on Wikipedia or elsewhere. The exception is the stcom method: it implements the Icelandic pirate party steering committee election system; currently a combination of schulze and plain condorcet (to verify whether the chairperson is unambiguously elected).

More election/utils.py examples

The built-in help is likely to be more current than this document:

$ python election/utils.py --help
usage: utils.py [-h] [-e EXCLUDE] [--keep-gaps]
                operation system filenames [filenames ...]

positional arguments:
  operation             Operation to perform (count)
  system                Counting system to use (schulze, stv5, ...)
  filenames             Ballot files to read

optional arguments:
  -h, --help            show this help message and exit
  -e EXCLUDE, --exclude EXCLUDE
                        Candidate(s) to exclude when counting
  --keep-gaps           Preserve gaps if ballots are not sequential

We can use the included test-data to verify that Schulze will arbitrarily break Condorcet ties/cycles; if you run this a few times you'll get different results each time.

$ python election/utils.py count schulze test_data/condorcet_cycle.json 
Voting system:
        Schulze, Ordered list (schulze)

Loaded 3 ballots from:
        test_data/condorcet_cycle.json

Schulze old and new match, hooray.
Results:
        Bjarni, Smari, Bjorn

Counting using the condorcet method should however correctly return no results:

$ python election/utils.py count condorcet test_data/condorcet_cycle.json 
Voting system:
        Condorcet (condorcet)

Loaded 3 ballots from:
        test_data/condorcet_cycle.json

Results:

To break the cycle, we can try omitting one candidate:

$ python election/utils.py -e Bjorn count condorcet test_data/condorcet_cycle.json 
Voting system:
        Condorcet (condorcet)

Loaded 3 ballots from:
        test_data/condorcet_cycle.json

Results:
        Bjarni

Hooray!

Miscellania

  • If you're not sure what cron-jobs exist, try this: crontab -l

  • Since during elections, the wasa2il database contains non-anonymized ballots, we recommend using a backup system which prevents admins from easily accessing old data. This prevents after-the-fact attacks on voter privacy. This is what the Icelandic pirates use.

  • The schulze method should tolerate candidates withdrawing from an election right in the middle. It's unclear whether the same can be said about other counting methods supported by wasa2il, further research is needed before implementing a user-friendly "instant withdraw" feature.