| # |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| """ |
| Candidate or Party Voting Plugin |
| Currrently supports an arbitrary number of candidates from up to 26 parties |
| """ |
| import re |
| |
| from lib import constants |
| |
| def validateCOP(vote, issue): |
| "Tries to validate a vote, returns why if not valid, None otherwise" |
| parties = {} |
| if not 'candidates' in issue: |
| return "Invalid issue data detected" |
| for c in issue['candidates']: |
| if not 'pletter' in c: |
| return "Invalid issue data detected" |
| parties[c['pletter']] = True |
| letters = [chr(i) for i in range(ord('a'), ord('a') + len(parties))] |
| ivote = -1 |
| try: |
| ivote = int(vote) |
| except: |
| pass # This is a fast way to determine vote type, passing here is FINE! |
| if not vote in letters and (ivote < 0 or ivote > len(issue['candidates'])): |
| return "Invalid characters in vote. Accepted are: %s" % ", ".join(letters,range(1,len(issue['candidates'])+1)) |
| return None |
| |
| def parseCandidatesCOP(data): |
| data = data if data else "" |
| candidates = [] |
| pletter = '' |
| cletter = '' |
| pname = '' |
| s = 0 |
| for line in data.split("\n"): |
| line = line.strip() |
| if len(line) > 0: |
| arr = line.split(":", 1) |
| letter = arr[0] |
| letter = letter.lower() |
| |
| # Party delimiter? |
| if letter in [chr(i) for i in range(ord('a'), ord('a') + 26)] and len(arr) > 1 and len(arr[1]) > 0: |
| pname = arr[1] |
| pletter = letter |
| else: |
| candidates.append({ |
| 'name': line, |
| 'letter': str(s), |
| 'pletter': pletter, |
| 'pname': pname |
| }) |
| s += 1 |
| return candidates |
| |
| |
| def tallyCOP(votes, issue): |
| m = re.match(r"cop(\d+)", issue['type']) |
| if not m: |
| raise Exception("Not a COP vote!") |
| |
| numseats = int(m.group(1)) |
| parties = {} |
| |
| for c in issue['candidates']: |
| if not c['pletter'] in parties: |
| parties[c['pletter']] = { |
| 'name': c['pname'], |
| 'letter': c['pletter'], |
| 'surplus': 0, |
| 'candidates': [] |
| } |
| parties[c['pletter']]['candidates'].append({ |
| 'letter': c['letter'], |
| 'name': c['name'], |
| 'votes': 0, |
| 'elected': False |
| }) |
| |
| |
| debug = [] |
| winners = [] |
| |
| |
| # Tally up all scores and surplus |
| for key in votes: |
| vote = votes[key] |
| |
| for party in parties: |
| if parties[party]['letter'] == vote: |
| parties[party]['surplus'] += 1 |
| else: |
| for candidate in parties[party]['candidates']: |
| if candidate['letter'] == vote: |
| candidate['votes'] += 1 |
| |
| |
| numvotes = len(votes) |
| |
| if numseats < len(issue['candidates']): |
| |
| # Start by assigning all surplus (party votes) to the first listed candidate |
| iterations = 0 |
| |
| while numseats > len(winners) and iterations < 9999: # Catch forever-looping counts (in case of bug) |
| quota = (numvotes / numseats * 1.0) # Make it a float to prevent from rounding down for now |
| for party in parties: |
| surplus = 0 |
| movedOn = False |
| for candidate in parties[party]['candidates']: |
| # If a candidate has not yet been elected, and has >= votes than the required quota, elect her/him |
| if not candidate['elected'] and numseats > len(winners): |
| if candidate['votes'] >= quota: |
| candidate['elected'] = True |
| winners.append("%s (%s) %u" % ( candidate['name'], parties[party]['name'], candidate['votes'])) |
| |
| # Did X receive more votes than needed? if so, add back to the party surplus |
| surplus += candidate['votes'] - quota |
| |
| # If surplus of votes, add it to the next candidate in the same party |
| if surplus > 0: |
| for candidate in parties[party]['candidates']: |
| if not candidate['elected']: |
| candidate['votes'] += surplus |
| movedOn = True |
| break |
| |
| # If surplus but no candidates left, decrease the number of votes required by the surplus |
| if not movedOn: |
| numvotes -= surplus |
| |
| # Everyone's a winner!! |
| else: |
| for party in parties: |
| for candidate in parties[party]['candidates']: |
| winners.append("%s (%s) %u" % ( candidate['name'], parties[party]['name'], candidate['votes'])) |
| |
| |
| |
| # Return the data |
| return { |
| 'votes': len(votes), |
| 'winners': winners, |
| 'winnernames': winners, |
| 'debug': debug |
| }, """ |
| Winners: |
| - %s |
| """ % "\n - ".join(winners) |
| |
| |
| constants.appendVote ( |
| { |
| 'key': "cop1", |
| 'description': "Candidate or Party Vote with 1 seat", |
| 'category': 'cop', |
| 'validate_func': validateCOP, |
| 'vote_func': None, |
| 'parsers': { |
| 'candidates': parseCandidatesCOP |
| }, |
| 'tally_func': tallyCOP |
| }, |
| ) |
| |
| # Add ad nauseam |
| for i in range(2,constants.MAX_NUM+1): |
| constants.appendVote ( |
| { |
| 'key': "cop%02u" % i, |
| 'description': "Candidate or Party Vote with %u seats" % i, |
| 'category': 'cop', |
| 'validate_func': validateCOP, |
| 'vote_func': None, |
| 'parsers': { |
| 'candidates': parseCandidatesCOP |
| }, |
| 'tally_func': tallyCOP |
| }, |
| ) |