diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a842ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +secrets.env +venv diff --git a/create_secrets.sh b/create_secrets.sh new file mode 100755 index 0000000..7b5c3a9 --- /dev/null +++ b/create_secrets.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo "BEARER=???" > secrets.env +echo "API_VOTE=https://voting.2021.egoi.ch/api/votes/{question_id}" >> secrets.env diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a830e70 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-dotenv +requests diff --git a/vote.py b/vote.py new file mode 100755 index 0000000..1058b32 --- /dev/null +++ b/vote.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import requests +import os + +from io import StringIO + +from dotenv import load_dotenv + +def get_votes(ids): + votes = {} + for id in ids: + res = requests.get(os.environ['API_VOTE'].format(question_id=id), headers={'Authorization': 'Bearer ' + os.environ['BEARER']}) + if not res.ok: + print('Could not get votes for id', id) + continue + f = StringIO(res.text) + reader = csv.DictReader(f) + for row in reader: + votes.setdefault(row['Delegation'], []).append(row['Option']) + return list(votes.values()) + +def get_candidates_from_votes(votes): + candidates = set() + for vote in votes: + for option in vote: + candidates.add(option) + return candidates + +def determine_winners(people, votes): + """Single Transferable Vote (https://en.wikipedia.org/wiki/Single_transferable_vote)""" + elected = [] + elected_set = set() + candidates = get_candidates_from_votes(votes) + surplus_votes = {x: 0 for x in candidates} + quota = len(votes) / people + 1 + + print('Votes:', len(votes)) + print('Quota:', quota) + print('Candidates:', len(candidates)) + + def one_round(): + print('-----------') + print('Start Round') + print('-----------') + round_votes = {x: 0 for x in candidates} + for vote in votes: + vote_weight = 1 + for option in vote: + if option in elected_set: + vote_weight *= surplus_votes[option] + if option in candidates and option not in elected_set: + round_votes[option] += vote_weight + break + vote_counts = [] + for candidate in candidates: + if candidate in elected_set: + continue + vote_counts.append((round_votes[candidate], candidate)) + vote_counts.sort() + print('Vote counts:') + for (count, candidate) in reversed(vote_counts): + print(' {}: {}'.format(candidate, count)) + if vote_counts[-1][0] >= quota or len(candidates) == people: + elected.append((vote_counts[-1][1], vote_counts[-1][0])) + elected_set.add(vote_counts[-1][1]) + + surplus = vote_counts[-1][0] - quota + if surplus > 0: + surplus_votes[vote_counts[-1][1]] = surplus / vote_counts[-1][0] + + print('Elect', vote_counts[-1][1]) + elif len(candidates) > people: + candidates.remove(vote_counts[0][1]) + print('Remove', vote_counts[0][1]) + + while len(elected_set) < people: + one_round() + + return elected + +def test_votes(): + return [ + ['O'], + ['O'], + ['O'], + ['O'], + ['P', 'O'], + ['P', 'O'], + ['C', 'S'], + ['C', 'S'], + ['C', 'S'], + ['C', 'S'], + ['C', 'S'], + ['C', 'S'], + ['C', 'S'], + ['C', 'S'], + ['C', 'H'], + ['C', 'H'], + ['C', 'H'], + ['C', 'H'], + ['S'], + ['H'], + ] + +def main(): + load_dotenv('secrets.env') + parser = argparse.ArgumentParser(description='Determine voted persons from list of votes.') + parser.add_argument('--test', action='store_true', help='Use test values for the votes') + parser.add_argument('people', type=int, help='Number of people who need to be voted.') + parser.add_argument('questions', type=int, nargs='+', help='IDs of the questions.') + args = parser.parse_args() + if args.test: + votes = test_votes() + else: + votes = get_votes(args.questions) + winners = determine_winners(args.people, votes) + + print('-------------') + print('Final results') + print('-------------') + for i, (winner, votes) in enumerate(winners): + print('{:2} with {:2} votes: {}'.format(i+1, int(votes), winner)) + +if __name__ == '__main__': + main()