OpenSTV-1.6.1/ 0001777 0001754 0001001 00000000000 11407513225 011406 5 ustar jeff None OpenSTV-1.6.1/CHANGELOG.txt 0000777 0001754 0001001 00000022520 11400774167 013450 0 ustar jeff None $Id: CHANGELOG.txt 722 2010-03-08 14:14:34Z jeff.oneill $ OpenSTV 1.6 - Provide YAML report for integration with DemoChoice - Added support for equal rankings and skipped rankings on ballots - Added support for ballot IDs - Restructuring of data structures for ballots - Reports are now done as plugins - Improvements to report generation including writing to file instead of memory - Counting methods are much faster with a large number of candidates - Check folder Plugins for external plugins - Reorganized package structure so that all imports use absolute paths - OpenSTV uses threads for counting and loading ballots so user doesn't see a frozen screen - Allow manual tie-breaking in OpenSTV OpenSTV 1.5 - Improved format of election results - No limit on the number of candidates (previously was limited to 255 candidates) - Election methods are now done as plugins so users can add their own methods (although only for advanced users at this point). - Cambridge STV can save the winner's ballots in files for determining replacements after a vacancy - Added statutes of some rules to the documentation - Added draft of New Zealand Meek (work in progress) - Added new method called QPQ - Comments now allowed in ballot files - Changes under the hood that make OpenSTV easier to maintain. OpenSTV 1.4 - Fixed bug that broke Election Options dialog on mac - Fixed bug when navigating from empty ballot - Fixed bug in remembering working directory on mac OpenSTV 1.3 - Rewrote OpenSTV user interface: (1) User can only run an election from a saved ballot file (previously, you could use a ballot file in memory). (2) User can edit/create ballots in a separate Ballot File Editor window and save them to a file. - Can save election results in HTML. - More explanatory output for Condorcet elections. - Added subclasses for different ballot types to make it easier to support new types. - Changes under the hood that make OpenSTV easier to maintain. OpenSTV 1.2 - Changed output to be more descriptive and easier to read. - Changed implementation of SuppVote to use all rankings. - Changed implementation of IRV to stop sooner (won't change outcome). - Small change in reporting of the threshold for ERS97 (won't change outcome). - Print substages for ERS97. - Now possible to have unlimited precision (but max is set to 20 for gui). - Meek/Warren implemented in fixed point. OpenSTV 1.1 - Added Scottish STV rules. - Added GPCA2000 rules. - Added approval voting. - Bug fixes in N. Ireland STV. - Simplified IRV. Now always eliminates candidates one by one and stops when candidate has a majority (instead of stopping when 2 candidates remain). - Show help files within OpenSTV instead of external browser because the external browser was not working with some platforms. - BCSTV, FTSTV, and Borda now implemented in fixed point instead of floating point. Coombs, Meek, and Warren still in floating point. - Major code rewrite in STV.py. OpenSTV 1.0 - Changed name to OpenSTV! - Test code now in a separate file and added more tests. - Although the tree-packed ballots greatly increased speed, they also greatly increased memory consumption. The tree-data structure is now created dynamically as necessary rather than built completely. This retains (and sometimes improves) the speed, and memory consumption is negligable. Condorcet no longer uses trees. - Ballot data now based on bytes instead of ints which uses 1/4th less memory. The number of candidates is now limited to 255. - Modifications to allow the use of psyco to speed up the code. Psyco on average speeds up the code by a factor of 2-4 times. - Now properly raises and catches warnings and exceptions. - Fixed bug in Condorcet completion methods. - Changed code structure so that each method is a class rather than a function. This allows for cleaner code and better code reuse. - Fixed bugs with withdrawn candidates. Withdrawn candidates are now excised from a copy of the ballots before the start of the election. Since it is done with a copy, it can later be undone. - "raw" ballot format renamed as "text" format because raw also means unpacked ballots. pSTV 0.9 Implemented the supplemental vote and N. Ireland STV. Started Dail STV. Can shuffle the ballots (interesting for Cambridge STV and random transfer STV). Simplified the rounding of floats to make the code cleaner. Implemented tree-packed ballots. Used tree-packed ballots with Meek, Warren, and Condorcet, greatly speeding up the implementations of these methods. Faster implementation of Bucklin and Coombs. For the curious, here are the timing statistics, in seconds, as they have changed over time on a test of data from eight real elections: SNTV: 0.6 IRV: 7.1 -> 1.2 BCSTV: 4.6 Coombs: 68.6 -> 40.4 -> 24.8 Cambridge: 3.7 -> 2.0 ERS97: 6.4 -> 5.5 Meek: 48.3 -> 38.7 -> 5.9 Warren: 38.0 -> 30.7 -> 3.0 Bucklin: 9.4 -> 3.6 Borda: 2.3 Condorcet: 106.0 -> 13.3 -> 7.7 pSTV 0.8 Added tabbed windows for comparing multiple election results. The font size can be changed for each tab separately. The display precision is now chosen in the Election menu and can only be changed by rerunning the election. The ERS97 implementation is finally in fixed point and the desired precision can be specified (for future implementation of ERS97-like rules such as Malta STV). Can now eliminate candidates before running an election. Previously, when loading a BLT file, eliminated candidates were removed from the ballots. Now, the eliminated candidates are noted but they remain on the ballots. This works better with allowing the user to selectively eliminate candidates, and the user can override the eliminated candidates specified in the BLT file. Added more comments and did some random code cleanups. pSTV 0.7 Renamed Simple STV as British Columbia STV. Fixed several UI bugs for Mac. Made comparisons of floating point numbers safer. Added timing to tests. Made IRV much faster. IRV eliminates all losers simultaneously. Fixed bug in Bucklin. Fixed bug in Coombs. Changed internal ballot format: got rid of Blist and append ballots immediately. Revamped ballot entry to make it easier to use. Show brief description of method after it is selected. Added symmetric completion of ballots option for Borda. pSTV 0.6 Condorcet completion methods include Borda, IRV, and SSD. Changed Methods menu to have an option to show only STV methods. Added Bucklin. pSTV 0.5.2 Added Condorcet SSD. pSTV 0.5.1 The primary reason for this release is to change DAT files to BLT files. I've also added Borda count and Coombs methods. pSTV 0.5 Now uses innosetup to create a windows installer. Also has options to create desktop/quicklaunch icons and associate dat files with pSTV (with an icon). Reverted back to previous ERS CSV format (Brian abandoned V6). sql output conforms to spec. DAT files now loaded faster than before. When reading DAT files will handle multiple rankings of a candidate. Should now work properly even with very few candidates or voters. Will return with fewer winners than asked for if not enough candidates receive more than 0 votes. pSTV *should* never crash. pSTV 0.4.2 Can now append DAT files. Now continues gracefully when loading a ballot file with a bad format. Raw ballot files can now accept a weight. Added an edit menu with a copy command. Updated ERSCSV files to V6 and can now use this format for any method (previously it could only be used for ERS97 rules). Changed -m ers to produce a table to be used with SQL and to work with methods other than ERS97 STV. These last two changes do not conform precisely to specs and will need to be tweaked. pSTV 0.4.1 Added substage tiebreaking for ERS97 rules. pSTV 0.4 Previous implementation of Meek STV was actually Warren STV. Now Meek is correctly implemented and Warren has been added. Added an icon. Clarified file formats in saving and loading ballots. Changed how output is handled (though invisible to user): STV.py now prints to stdout and pSTV.py captures this stream. Can now save output in ERS style result sheet. pSTV 0.3 Formal release with Cambridge rules. Fixed little bugs in reading dat files. pSTV 0.2.5 Removed spurious tie break messages when choosing winners for ERS97. pSTV 0.2.4 Added exclusion of candidates in reading dat files. pSTV 0.2.3 Fixed little bugs in reading dat files. pSTV 0.2.2 Implemented Cambridge, MA rules. Results are identical to CC2003, SC2003, CC2001, SC2001, and CC1999 elections. Fixed nasty floating point bug that caused an error with ERS97 tests performed by Brian Wichmann. Comparison (>=) wasn't accurate because of difference at around 1e-14. pSTV 0.2.1 Several bug fixes for bugs found by Brian Wichmann when verifying ERS97 implementation. pSTV 0.2 Many, many changes from the previous version. Highlights: fixed bug in tiebreaking, ERS97 fully implemented and tested, dynamic and fractional thresholds for RT and FT STV, all menu items now work, thorough testing, added Simple STV rules. pSTV 0.1 Initial Release. Methods implemented are SNTV, IRV, Random Transfer STV, Fractional Transfer STV, and Meek STV. Allows for different thresholds and delayed transfer of surplus. Ballots can be loaded from a file or entered via the program. The interface needs work and some features, such as printing, are not implemented. Probably many, many bugs that need to be found and fixed. OpenSTV-1.6.1/openstv/ 0001777 0001754 0001001 00000000000 11407513225 013104 5 ustar jeff None OpenSTV-1.6.1/openstv/ballots.py 0000777 0001754 0001001 00000045771 11400774167 015145 0 ustar jeff None """Module for working with ballot data""" ## Copyright (C) 2003-2010 Jeffrey O'Neill ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. __revision__ = "$Id: ballots.py 722 2010-03-08 14:14:34Z jeff.oneill $" import os from openstv.plugins import getLoaderPlugins, getLoaderPluginClass ################################################################## class Ballots(object): """Class for working with ballot data. A Ballots object is concetptually a list of ballots. A ballot is a list of rankings, with an optional ID. A ranking is typically a candidate index number, but can also be -1 (skipped ranking) or a list of candidate index numbers (equal rankings). Only unique ballots are stored, and the list of ballots is a list of pointers to the appropriate ballot. For methods where the outcome can depend on the order of the ballots (e.g., Cambridge STV) the individual ballots are used, but for methods where the outcome is independent of the order, only the unique ballots are used along with a weight (the number of times that ballot appears). A ballots object may only contain valid ballot data. If the ballot data contains an error (e.g., a candidate index number that is out of range), an error should be raised immediately. """ def __init__(self, customBallotIDs=False): self.title = "Title" # An election title. self.date = "" # The date of the election. self.numSeats = 1 # The number of seats to be filled. self.customBallotIDs = customBallotIDs # Whether custom ballot IDs are # used. If fale, the ballot IDs are just 1 to N. self.exceptionQueue = None # Used to erport exceptions back to GUI self.dirtyBallots = None # For clean ballots this is a pointer to the # dirty ballots from which they were created self._names = [] self._n2i = {} # A list of candidate names. The index for a particular name is the # candidate number. The first name is thus assigned to candidate # number 0. _n2i is a shortcut for getting the inedex from the name. self.withdrawn = [] # A list of the candidates who are withdrawn from the election, specified # by the candidate number. # Conceptually each ballot is a list of candidates and each ballot has # a ballot ID used to identify that ballot. Where no ballot IDs are # provided, ballots are assigned IDs in numerical order. # Note that ballotIDs are assigned to individual ballots and not to # weighted ballots. Where a ballot file specifies a weighted ballot, # then a number of individual ballots will be included and each individual # ballot will be given a ballotID. # In any ballot list, many of the ballots will be identical so, instead # of storing each ballot, only unique ballots will be stored. self.uniqueBallots = [] # This is a list of unique ballots. Each item of the list is a ballot, # and each ballot is a list of candidate numbers that represent the # candidates being ranked. For example, if a ballot is [2 4 1], then # candidate number 2 is ranked first, candidate number 4 is ranked second, # and candidate number 1 is ranked third. self.uniqueBallotCount = [] # This is the weight for each unique ballot. self.uniqueBallotIndexToBallotIndices = [] # This list has the same length as self.uniqueBallots. Each item of the # list is a Python set that contains the indices of self.ballotOrder # corresponding to the unique ballot. self.uniqueBallotsLookup = {} # The keys to this dictionary are string representations of unique # ballots and the values are the indices into self.uniqueBallots. This # dictionary indicates whether a given ballot has already been seen, and # if so, where the ballot exists in self.uniqueBallots. self.ballotOrder = [] # The length of this list is the total number of ballots. Each entry is # the index into self.uniqueBallots of the corresponding ballot. self.ballotIDsList = [] # A list of the ballot IDs in the order specified in the ballot file. # If the file does not have ballot IDs, then this list remains empty and # the ballotID is computed from the ballot index (1 .. N). self.loader = None def copy(self, copyBallots=True): # Documentation for copy module says it doesn't work with arrays ballotList = Ballots() ballotList.customBallotIDs = self.customBallotIDs ballotList.title = self.title ballotList.date = self.date ballotList.numSeats = self.numSeats ballotList.names = self.names[:] ballotList.withdrawn = self.withdrawn[:] if copyBallots: for i in xrange(self.numBallots): ballot = self.getBallot(i) ballotID = self.getBallotID(i) if self.customBallotIDs else None ballotList.appendBallot(ballot, ballotID) # Don't want the copy to save to the same file as the original ballotList.loader = None return ballotList @property def numBallots(self): return len(self.ballotOrder) @property def numWeightedBallots(self): return len(self.uniqueBallots) def getNumCandidates(self): return len(self.names) def setNumCandidates(self, numCandidates): assert(len(self.names) == 0) names = [] for i in range(numCandidates): names.append("Candidate No. %d" % (i + 1)) self.names = names numCandidates = property(getNumCandidates, setNumCandidates) def getNames(self): return self._names def setNames(self, names): self._names = list(names) for index, name in enumerate(names): self._n2i[name] = index names = property(getNames, setNames) def checkBallot(self, ballot): # Check to make sure ballot data is valid. At this point only possible # errors should be that the candidate number is too large. nc = self.numCandidates for ranking in ballot: if isinstance(ranking, list): self.checkBallot(ranking) elif ranking > nc - 1: raise RuntimeError, "Ballot has invalid data: %s" % str(ballot) def appendBallot(self, ballot, ballotID=None): "Append a ballot to this Ballots object." # Check to make sure whether ballot IDs are allowed assert((ballotID == None) ^ (self.customBallotIDs)) # XOR # Not sure if we want to do this. May make more sense to have the # ballot loader do the checking. #self.checkBallot(ballot) # String representation of ballot for determining whether it is unique ballotString = str(ballot) # Record the ballot ID if there is one if ballotID is not None: self.ballotIDsList.append(ballotID) ballotIndex = len(self.ballotOrder) # Index of the ballot being added if ballotString in self.uniqueBallotsLookup: # We have seen this ballot before uniqueBallotIndex = self.uniqueBallotsLookup[ballotString] self.uniqueBallotIndexToBallotIndices[uniqueBallotIndex].add(ballotIndex) self.uniqueBallotCount[uniqueBallotIndex] += 1 else: # We have not seen this ballot before self.uniqueBallots.append(ballot) self.uniqueBallotCount.append(1) uniqueBallotIndex = len(self.uniqueBallots) - 1 self.uniqueBallotsLookup[ballotString] = uniqueBallotIndex self.uniqueBallotIndexToBallotIndices.append(set([ballotIndex])) self.ballotOrder.append(uniqueBallotIndex) def appendBallotUsingNames(self, ballot, ballotID=None): "Append a ballot to this Ballots object." ballot2 = [] for name in ballot: ballot2.append(self._n2i[name]) self.appendBallot(ballot2, ballotID) def getWeight(self, i): "Return the weight of the ith weighted ballot." return self.uniqueBallotCount[i] def getWeightedBallot(self, i): "Return the ith weighted ballot." return (self.uniqueBallotCount[i], self.uniqueBallots[i][:]) def getSortedWeightedBallots(self): "This is used to compare two ballot lists for testing purposes." # We should replace this with a diff-like function that returns true # or false to indicate whether two ballots objects are the same. sortedBallots = [(str(self.uniqueBallots[i]), self.uniqueBallotCount[i]) for i in xrange(self.numWeightedBallots)] sortedBallots.sort() return sortedBallots def getBallot(self, i): j = self.ballotOrder[i] return self.uniqueBallots[j][:] def getBallotID(self, i): if self.customBallotIDs: return self.ballotIDsList[i] else: return i + 1 def getBallotAndID(self, i): return (self.getBallot(i), self.getBallotID(i)) def getBallotsAndIDs(self): if self.customBallotIDs: ballotIDs = self.ballotIDsList[:] else: ballotIDs = range(1, self.numBallots + 1) return zip([self.uniqueBallots[i][:] for i in self.ballotOrder], ballotIDs) def setBallot(self, i, ballot): # This is an expensive operation but it is only done when the user # is editing ballots so it does not need to be done that quickly oldBallots = self.getBallotsAndIDs() oldBallots[i] = (ballot[:], oldBallots[i][1]) self.deleteBallots() for ballot, ballotID in oldBallots: if not self.customBallotIDs: ballotID = None self.appendBallot(ballot, ballotID) def deleteBallot(self, i): # This is an expensive operation but it is only done when the user # is editing ballots so it does not need to be done that quickly oldBallots = self.getBallotsAndIDs() oldBallots.pop(i) self.deleteBallots() for ballot, ballotID in oldBallots: if not self.customBallotIDs: ballotID = None self.appendBallot(ballot, ballotID) def deleteBallots(self): self.uniqueBallots = [] self.uniqueBallotIndexToBallotIndices = [] self.uniqueBallotsLookup = {} self.ballotIDsList = [] self.ballotOrder = [] def getTopChoiceFromBallot(self, i, choices): "Return the top choice on a ballot among candidates still in the running." j = self.ballotOrder[i] ballot = self.uniqueBallots[j] for c in ballot: if c in choices: return c return None def getTopChoiceFromWeightedBallot(self, i, choices): "Return the top choice on a ballot among candidates still in the running." ballot = self.uniqueBallots[i] for c in ballot: if c in choices: return c return None def getCleanBallots(self, removeEmpty=True, removeOvervotes=True, removeDupes=True, removeWithdrawn=True): """Ballots can be cleaned in several ways: (1) Removing withdrawn candidates. This is done if withdrawn is not None. (2) Removing empty ballots. This is done if removeEmpty is True. (3) Removing overvotes (more than one candidate is given the same ranking). This is done if removeOvervotes is True. (4) Remove duplicate rankings (same candidate is ranked more than once on a ballot). This is done if removeDupes is True. (5) Remove skipped rankings. A skipped ranking is indicated by a "-1" instead of a candidate number. These are always removed. (6) Does not currently check for duplicate ballot IDs, but we might want to add this later. """ # We want to keep track of the link between dirty and clean ballots cleanBallots = self.copy(False) cleanBallots.withdrawn = [] cleanBallots.customBallotIDs = True cleanBallots.dirtyBallots = self # Set up a translation list for candidate numbers for removing # withdrawn candidates. c2 = c2c[c] translates an original candidate # number "c" to a translated candidate number "c2" taking into account # candidates that have been removed from the ballots. If a candidate is # withdrawn, c2c returns None. c2c = range(self.numCandidates) if removeWithdrawn: n = 0 for i in range(self.numCandidates): if i in self.withdrawn: c2c[i] = None n += 1 else: c2c[i] -= n # Loop over ballots and perform requested cleaning for i in xrange(self.numBallots): ballot, ballotID = self.getBallotAndID(i) seenCandidates = set() cleanBallot = [] # This will be a cleaned version of ballot for item in ballot: # Candidate may have to pass two tests to get in the cleaned ballots. # First, candidate must not be withdrawn. # Second, candidate must not already be on the ballot when removeDupes # is true. if isinstance(item, list): assert(len(item) > 1) if removeOvervotes: continue cleanItem = [] for c in item: if c == -1: continue # Skipped ranking c2 = c2c[c] # Candidate number after removing withdrawn candidates if not ((c in self.withdrawn) or (removeDupes and c2 in seenCandidates)): assert(c2 is not None) cleanItem.append(c2) seenCandidates.add(c2) if len(cleanItem) > 1: cleanBallot.append(cleanItem) elif len(cleanItem) == 1: cleanBallot.append(cleanItem[0]) else: c = item if c == -1: continue # Skipped ranking c2 = c2c[c] # Candidate number after removing withdrawn candidates if not ((c in self.withdrawn) or (removeDupes and c2 in seenCandidates)): assert(c2 is not None) cleanBallot.append(c2) seenCandidates.add(c2) if not removeEmpty or len(cleanBallot) > 0: cleanBallots.appendBallot(cleanBallot, ballotID) # Remove the withdrawn candidates names cleanBallots.names = [self.names[c] for c in range(self.numCandidates) if c not in self.withdrawn] return cleanBallots def appendFile(self, fName): "Append ballot data from a file." ballotList = Ballots() ballotList.loadUnknown(fName) if (ballotList.numSeats != self.numSeats or ballotList.names != self.names or ballotList.withdrawn != self.withdrawn): raise RuntimeError, \ "Can't append ballots. The numbers of seats and candidates, \n"\ "the names of the candidates, and the withdrawn candidates \n"\ "must be identical." for i in xrange(ballotList.numBallots): ballot = ballotList.getBallot(i) ballotID = ballotList.getBallotID(i) if self.customBallotIDs else None self.appendBallot(ballot, ballotID) def save(self): "Save back to the last file I was saved or loaded from" self.loader.save(self) def saveAs(self, fName, packed=False): "Create a new ballot loader and save ballots" extension = os.path.splitext(fName)[1][1:] loaderClass = getLoaderPluginClass(extension) if loaderClass is None: # If we don't know then the default is blt format loaderClass = getLoaderPluginClass("blt") self.loader = loaderClass() self.loader.save(self, fName, packed) def loadKnown(self, fName, extension=None, exclude0 = True): "Load a file based on its file extension." if extension is None: extension = os.path.splitext(fName)[1][1:] loaderClass = getLoaderPluginClass(extension, exclude0) if loaderClass is None: raise RuntimeError, "Do not know how to load files with extension %s." % (extension) self.loader = loaderClass() self.loader.load(self, fName) def loadUnknown(self, fName, exclude0 = True): "Load a file of unknown format." # Try the loader that claims this extension first. If that one doesn't # work then we try the others. Get the loader classes in the right order. extension = os.path.splitext(fName)[1][1:] loaderClasses = getLoaderPlugins("classes", exclude0) bestGuess = getLoaderPluginClass(extension, exclude0) if bestGuess is not None: loaderClasses.remove(bestGuess) loaderClasses.insert(0, bestGuess) errorMsg = "Could not load ballots from file %s." % fName # Try them in order for loaderClass in loaderClasses: try: self.loader = loaderClass() self.loader.load(self, fName) except RuntimeError, (msg,): errorMsg += "\n" + msg.strip() else: break else: # None of the ballot loaders succeeded so raise an exception if self.exceptionQueue is None: raise RuntimeError(errorMsg) else: self.exceptionQueue.put(errorMsg) def getFileName(self): "The name of the last file I was saved or loaded from" if (self.loader is not None): return self.loader.fName else: return None def reorderCandidates(self, order=None): "Reorder candidates in alphabetical order or the order specified." if order == None: # Default is alphabetical order order = range(self.numCandidates) order.sort(key=lambda c: self.names[c]) # Check to make sure that all candidates are included check = order[:] check.sort() if check != range(self.numCandidates): raise RuntimeError, "Must specify all the candidates when reordering." # Set up a translation list. # order gives the desired candidate order, e.g., [4 0 3 1 2] # Thus, we want 4->0, 0->1, 3->2, 1->3, and 2-> 4 # c2c does this translation c2c = [0] * self.numCandidates for i, c in enumerate(order): c2c[c] = i # Translate all the candidate numbers. This must be done in two places: # (1) The ballots in uniqueBallots # (2) The keys in uniqueBallotsLookup # Easier to create a new uniqueBallotsLookup self.uniqueBallotsLookup = {} # Loop over all the weighted ballots for i in xrange(self.numWeightedBallots): for j, c in enumerate(self.uniqueBallots[i]): self.uniqueBallots[i][j] = c2c[c] ballotString = str(list(self.uniqueBallots[i])) self.uniqueBallotsLookup[ballotString] = i # Put the names in the right order oldNames = self.names[:] names = self.names for c in range(self.numCandidates): cc = c2c[c] names[cc] = oldNames[c] self.names = names def joinList(self, itemList, convert="names"): assert(len(itemList) > 0) if convert == "names": tmp = itemList[:] itemList = [self.names[c] for c in tmp] text = " ".join(itemList) sep = "; " if text.find(",") != -1 else ", " if len(itemList) == 1: txt = itemList[0] elif len(itemList) == 2: txt = itemList[0] + " and " + itemList[1] else: txt = sep.join(itemList[:-1]) txt += sep + "and " + itemList[-1] return txt def isalnum(self): """Are all candidate names alphanumeric?""" for name in self.names: if not name.isalnum(): return False return True OpenSTV-1.6.1/openstv/BFE.py 0000777 0001754 0001001 00000060425 11400774167 014072 0 ustar jeff None "Module that provides a GUI for editing ballots." ## OpenSTV Copyright (C) 2003-2010 Jeffrey O'Neill ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. __revision__ = "$Id: BFE.py 719 2010-03-01 03:43:54Z jeff.oneill $" import wx import os import warnings import wx.lib.mixins.listctrl as listmix from openstv.STV import * from openstv.ballots import Ballots from openstv.utils import getHome ################################################################## class BFEFrame(wx.Frame): def __init__(self, parent, mode): wx.Frame.__init__(self, parent, -1, "Ballot File Editor") warnings.showwarning = self.catchWarnings self.MakeMenu() self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) self.logfName = "" fn = os.path.join(getHome(), "Icons", "blt.ico") icon = wx.Icon(fn, wx.BITMAP_TYPE_ICO) self.SetIcon(icon) # Edit a new ballot file if mode == "new": # Create an empty ballots class instance self.b = Ballots() # Get the candidate names from the user dlg = CandidatesDialog(parent, self.b) dlg.Center() if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() self.Destroy() return dlg.Destroy() # Edit an existing ballot file elif mode == "old": dlg = wx.FileDialog(self, "Edit Ballot File", style=wx.OPEN|wx.CHANGE_DIR) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() self.Destroy() return fName = dlg.GetPath() dlg.Destroy() # Open the file try: self.b = Ballots() self.b.loadUnknown(fName) except RuntimeError, msg: wx.MessageBox(str(msg), "Error", wx.OK|wx.ICON_ERROR) self.Destroy() return else: assert(0) # Set the window title to include the filename fn = self.b.getFileName() if fn is not None: title = "%s - Ballot File Editor" % os.path.basename(fn) else: title = "%s - Ballot File Editor" % "New File" self.SetTitle(title) # Create a notebook with an editing page and a log page nb = wx.Notebook(self, -1) self.panel = BallotsPanel(nb, self.b) nb.AddPage(self.panel, "Ballots") self.logN = 1 # counter for display purposes self.log = wx.TextCtrl(nb, -1, style=wx.TE_MULTILINE|wx.TE_READONLY|\ wx.TE_WORDWRAP|wx.FIXED) self.log.SetMaxLength(0) nb.AddPage(self.log, "Log") # Initialize if mode == "new": self.panel.NeedToSaveBallots = True self.Log("Created a new ballot file.") elif mode == "old": self.panel.NeedToSaveBallots = False self.Log("Loaded %d ballots from file %s." %\ (self.b.numBallots, os.path.basename(self.b.getFileName()))) else: assert(0) # Set up the sizer sizer = wx.BoxSizer() sizer.Add(nb, 1, wx.EXPAND, 0) self.SetSizer(sizer) sizer.Fit(self) sizer.SetSizeHints(self) def catchWarnings(self, message, category, filename, lineno): "Catch any warnings and display them in a dialog box." wx.MessageBox(str(message), "Warning", wx.OK|wx.ICON_INFORMATION) def Log(self, txt): # create a prompt for each new line prompt = "%3d: " % self.logN self.log.AppendText(prompt + txt + "\n") self.logN += 1 def MakeMenu(self): fileMenu = wx.Menu() append = fileMenu.Append(-1, "A&ppend ballots from file...") saveBallots = fileMenu.Append(-1, "&Save ballots") saveBallotsAs = fileMenu.Append(-1, "Save ballots &as...") saveLog = fileMenu.Append(-1, "Save &log") saveLogAs = fileMenu.Append(-1, "Save log as...") exitBFE = fileMenu.Append(wx.ID_EXIT, "E&xit") self.Bind(wx.EVT_MENU, self.OnAppendBF, append) self.Bind(wx.EVT_MENU, self.OnSaveBallots, saveBallots) self.Bind(wx.EVT_MENU, self.OnSaveBallotsAs, saveBallotsAs) self.Bind(wx.EVT_MENU, self.OnSaveLog, saveLog) self.Bind(wx.EVT_MENU, self.OnSaveLogAs, saveLogAs) self.Bind(wx.EVT_MENU, self.OnExit, exitBFE) if wx.Platform == "__WXMAC__": wx.App.SetMacExitMenuItemId(wx.ID_EXIT) menuBar = wx.MenuBar() menuBar.Append(fileMenu, "&File") self.SetMenuBar(menuBar) def OnAppendBF(self, event): # Get the filename of the ballots to be appended dlg = wx.FileDialog(self, "Select Ballot File", style=wx.OPEN|wx.CHANGE_DIR) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return fName = dlg.GetPath() dlg.Destroy() # Attempt to append the ballots try: oldNumBallots = self.b.numBallots self.b.appendFile(fName) self.b = self.b.getCleanBallots(removeEmpty=False, removeWithdrawn=False) except RuntimeError, msg: wx.MessageBox(str(msg), "Error", wx.OK|wx.ICON_ERROR) else: self.Log("Appended %d ballots from file %s." %\ (self.b.numBallots - oldNumBallots, os.path.basename(fName))) self.panel.NeedToSaveBallots = True self.panel.NeedToSaveLog = True self.panel.UpdatePanel() def OnSaveBallots(self, event): if self.b.getFileName() is None: self.OnSaveBallotsAs(event) return try: self.b.save() except RuntimeError, msg: wx.MessageBox(str(msg), "Error", wx.OK|wx.ICON_ERROR) return self.panel.NeedToSaveBallots = False def OnSaveBallotsAs(self, event): # Ask the user to choose the filename. dlg = wx.FileDialog(self, "Save Ballot File", style=wx.SAVE|wx.OVERWRITE_PROMPT|wx.CHANGE_DIR) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return fName = dlg.GetPath() dlg.Destroy() # Save try: self.b.saveAs(fName) except RuntimeError, msg: wx.MessageBox(str(msg), "Error", wx.OK|wx.ICON_ERROR) return self.panel.NeedToSaveBallots = False # Set the window title to include the filename title = "%s - Ballot File Editor" % os.path.basename(fName) self.SetTitle(title) self.panel.fNameC.SetLabel(os.path.basename(fName)) def OnSaveLog(self, event): if self.logfName == "": self.OnSaveLogAs(event) return try: self.log.SaveFile(self.logfName) except RuntimeError, msg: wx.MessageBox(str(msg), "Error", wx.OK|wx.ICON_ERROR) return self.panel.NeedToSaveLog = False def OnSaveLogAs(self, event): dlg = wx.FileDialog(self, "Save Log to a File", style=wx.SAVE|wx.OVERWRITE_PROMPT|wx.CHANGE_DIR) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return self.logfName = dlg.GetPath() dlg.Destroy() try: self.log.SaveFile(self.logfName) except RuntimeError, msg: wx.MessageBox(str(msg), "Error", wx.OK|wx.ICON_ERROR) return self.panel.NeedToSaveLog = False def OnExit(self, event): self.Close() def OnCloseWindow(self, event): # Check to see if the current ballot is empty and warn user if self.panel.currentBallot == []: txt = "The current ballot is empty. Ok to close editor?" code = wx.MessageBox(txt, "Warning", wx.OK|wx.CANCEL|wx.ICON_QUESTION) if code == wx.CANCEL: # Don't exit event.Veto() return # Ask user if we should save ballots. if self.panel.NeedToSaveBallots == True: if self.b.getFileName() is None: msg = "Do you want to save the ballots?" else: msg = "Do you want to save the ballots to %s?" % self.b.getFileName() saveBallots = wx.MessageBox(msg, "Warning", wx.YES_NO|wx.CANCEL|wx.ICON_INFORMATION) if saveBallots == wx.CANCEL: event.Veto() # Don't exit return elif saveBallots == wx.YES: self.OnSaveBallots(None) # Save ballots elif saveBallots == wx.NO: # If user is discarding ballot changes then don't ask to save the log self.panel.NeedToSaveLog = False # Ask user if we should also save the log. if self.panel.NeedToSaveLog == True: msg = "Do you want to save the log for ballot file %s?"\ % os.path.basename(self.b.getFileName()) saveLog = wx.MessageBox(msg, "Warning", wx.YES_NO|wx.CANCEL|wx.ICON_INFORMATION) if saveLog == wx.CANCEL: event.Veto() # Don't exit return elif saveLog == wx.YES: self.OnSaveLog(None) # Save log self.Destroy() ################################################################## class BallotCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin): def __init__(self, parent, ID): style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.LC_VRULES wx.ListCtrl.__init__(self, parent, ID, style=style) listmix.ListCtrlAutoWidthMixin.__init__(self) def computeColumnWidth(self): # This is another kluge to overcome a difficulty with ListCtrl. # You can easily set the column width to the largest entry or to # the width of the header, but not the largest of the two. # This hack does that. self.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER) w1 = self.GetColumnWidth(1) self.SetColumnWidth(1, wx.LIST_AUTOSIZE) w2 = self.GetColumnWidth(1) w = max(w1, w2) return w ## def GetMinSize(self): ## # This is adapted from _doResize() in ## # /c/Python25/Lib/site-packages/wx-2.8-msw-ansi/wx/lib/mixins/listctrl.py ## # but I couldn't get it to work. ## w = self.GetColumnWidth(0) + self.GetColumnWidth(1) ## if self.GetItemCount() > self.GetCountPerPage(): ## w = wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X) ## return (w, -1) ################################################################## class BallotsPanel(wx.Panel): def __init__(self, parent, b): wx.Panel.__init__(self, parent, -1) self.NeedToSaveBallots = False self.NeedToSaveLog = False self.b = b self.i = 0 # The number of the ballot being displayed if self.b.numBallots == 0: self.b.appendBallot([]) self.currentBallot = self.b.getBallot(self.i) # Information box informationBox = wx.StaticBox(self, -1, "Information") fNameL = wx.StaticText(self, -1, "Filename:") fn = self.b.getFileName() if fn is None: fn = "New File" else: fn = os.path.basename(fn) self.fNameC = wx.StaticText(self, -1, fn) numBallotsL = wx.StaticText(self, -1, "No. of ballots:") self.numBallotsC = wx.StaticText(self, -1, "%d" % self.b.numBallots) numSeatsL = wx.StaticText(self, -1, "No. of seats:") numSeatsC = wx.StaticText(self, -1, "%d" % self.b.numSeats) numCandidatesL = wx.StaticText(self, -1, "No. of candidates:") numCandidatesC = wx.StaticText(self, -1, "%d" % self.b.numCandidates) titleL = wx.StaticText(self, -1, "Title:") title = "" if vars(self.b).has_key("title"): title = self.b.title titleC = wx.TextCtrl(self, -1, title) self.Bind(wx.EVT_TEXT, self.OnTitle, titleC) # Rankings box rankingsBox = wx.StaticBox(self, -1, "Rankings") txt = """\ Click on a candidate's name to assign that candidate the next available ranking, and double-click on a candidate's name to remove the ranking and reorder the remaining candidates.""" rankingsHelp = wx.StaticText(self, -1, txt) # Navigation box navigationBox = wx.StaticBox(self, -1, "Navigation") first = wx.Button(self, -1, "|<", style=wx.BU_EXACTFIT) prev = wx.Button(self, -1, "<", style=wx.BU_EXACTFIT) next = wx.Button(self, -1, ">", style=wx.BU_EXACTFIT) last = wx.Button(self, -1, ">|", style=wx.BU_EXACTFIT) self.spin = wx.SpinCtrl(self, -1, size=(60, -1)) self.spin.SetRange(1, self.b.numBallots) self.spin.SetValue(1) go = wx.Button(self, -1, "Go", style=wx.BU_EXACTFIT) self.Bind(wx.EVT_BUTTON, self.OnNav, first) self.Bind(wx.EVT_BUTTON, self.OnNav, prev) self.Bind(wx.EVT_BUTTON, self.OnNav, next) self.Bind(wx.EVT_BUTTON, self.OnNav, last) self.Bind(wx.EVT_BUTTON, self.OnNav, go) # Operations box operationsBox = wx.StaticBox(self, -1, "Operations") clear = wx.Button(self, -1, "Clear This Ballot") delete = wx.Button(self, -1, "Delete This Ballot") append = wx.Button(self, -1, "Append New Ballot") exitBFE = wx.Button(self, -1, "Exit") self.Bind(wx.EVT_BUTTON, self.OnClear, clear) self.Bind(wx.EVT_BUTTON, self.OnDelete, delete) self.Bind(wx.EVT_BUTTON, self.OnAppend, append) self.Bind(wx.EVT_BUTTON, self.OnExit, exitBFE) # Ballot box self.ballotBox = wx.StaticBox(self, -1, "Ballot No. %d" % (self.i + 1)) self.ballotC = BallotCtrl(self, -1) self.ballotC.InsertColumn(0, " R ", wx.LIST_FORMAT_RIGHT) self.ballotC.InsertColumn(1, "Candidate") self.ballotBox.SetLabel("Ballot No. %d" % (self.i+1)) for c, name in enumerate(self.b.names): if c in self.currentBallot: r = self.currentBallot.index(c) self.ballotC.InsertStringItem(c, str(r+1)) else: self.ballotC.InsertStringItem(c, "") self.ballotC.SetStringItem(c, 1, name) self.ballotC.SetColumnWidth(0, wx.LIST_AUTOSIZE_USEHEADER) w = self.ballotC.computeColumnWidth() self.ballotC.SetColumnWidth(1, w) self.Bind(wx.EVT_LIST_COL_BEGIN_DRAG, self.OnColBeginDrag, self.ballotC) self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnListClick, self.ballotC) self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnListDClick, self.ballotC) # Sizers sizer = wx.BoxSizer(wx.HORIZONTAL) leftSizer = wx.BoxSizer(wx.VERTICAL) # Information informationSizer = wx.StaticBoxSizer(informationBox, wx.VERTICAL) fgs = wx.FlexGridSizer(5, 2, 5, 5) fgs.Add(fNameL, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(self.fNameC, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(numBallotsL, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(self.numBallotsC, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(numSeatsL, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(numSeatsC, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(numCandidatesL, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(numCandidatesC, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(titleL, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(titleC, 1, wx.EXPAND|wx.ALL) fgs.AddGrowableCol(1) informationSizer.Add(fgs, 0, wx.EXPAND|wx.ALL, 5) leftSizer.Add(informationSizer, 0, wx.EXPAND|wx.ALL, 5) # Rankings rankingsSizer = wx.StaticBoxSizer(rankingsBox, wx.VERTICAL) rankingsSizer.Add(rankingsHelp, 0, wx.ALIGN_CENTER|wx.ALL, 5) leftSizer.Add(rankingsSizer, 0, wx.EXPAND|wx.ALL, 5) # Navigation navigationSizer = wx.StaticBoxSizer(navigationBox, wx.VERTICAL) hSizer = wx.BoxSizer(wx.HORIZONTAL) hSizer.Add(first, 0, wx.ALIGN_CENTER|wx.ALL, 5) hSizer.Add(prev, 0, wx.ALIGN_CENTER|wx.ALL, 5) hSizer.Add(next, 0, wx.ALIGN_CENTER|wx.ALL, 5) hSizer.Add(last, 0, wx.ALIGN_CENTER|wx.ALL, 5) navigationSizer.Add(hSizer, 0, wx.ALIGN_CENTER, 0) hSizer = wx.BoxSizer(wx.HORIZONTAL) hSizer.Add(self.spin, 0, wx.ALIGN_CENTER|wx.ALL, 5) hSizer.Add(go, 0, wx.ALIGN_CENTER|wx.ALL, 5) navigationSizer.Add(hSizer, 0, wx.ALIGN_CENTER, 0) leftSizer.Add(navigationSizer, 0, wx.EXPAND|wx.ALL, 5) # Operations operationsSizer = wx.StaticBoxSizer(operationsBox, wx.VERTICAL) gs = wx.GridSizer(2, 2, 5, 5) gs.Add(clear, 0, wx.EXPAND) gs.Add(delete, 0, wx.EXPAND) gs.Add(append, 0, wx.EXPAND) gs.Add(exitBFE, 0, wx.EXPAND) operationsSizer.Add(gs, 0, wx.ALIGN_CENTER|wx.ALL, 5) leftSizer.Add(operationsSizer, 0, wx.EXPAND|wx.ALL, 5) # Ballot ballotSizer = wx.StaticBoxSizer(self.ballotBox, wx.VERTICAL) ballotSizer.Add(self.ballotC, 1, wx.EXPAND|wx.ALL, 5) # Need this ugly hack since wx.ListCtrl doesn't properly set its size w = self.ballotC.GetColumnWidth(0) + self.ballotC.GetColumnWidth(1)\ + wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X) + 5 ballotSizer.SetItemMinSize(self.ballotC, (w, -1)) sizer.Add(leftSizer, 0, wx.EXPAND, 0) sizer.Add(ballotSizer, 0, wx.EXPAND|wx.ALL, 5) self.SetSizer(sizer) sizer.Fit(self) def OnExit(self, event): # Must be a cleaner way to do this? self.GetGrandParent().Close() def Log(self, txt): self.GetGrandParent().Log(txt) def OnColBeginDrag(self, event): # Don't allow column resizing event.Veto() def emptyBallotOK(self): # Check if user is ok with navigating away from an empty ballot txt = "The current ballot is empty. Ok to navigate to another ballot?" code = wx.MessageBox(txt, "Warning", wx.OK|wx.CANCEL|wx.ICON_QUESTION) return code == wx.OK def storeCurrentBallot(self): # Store a copy of the current ballot to the ballots object self.b.setBallot(self.i, self.currentBallot) self.NeedToSaveBallots = True self.NeedToSaveLog = True def UpdatePanel(self): # Display the ballot number, and no. of ballots self.ballotBox.SetLabel("Ballot No. %d" % (self.i + 1)) self.spin.SetValue(self.i + 1) self.spin.SetRange(1, self.b.numBallots) self.numBallotsC.SetLabel("%d" % self.b.numBallots) # Get rid of unsupported features if -1 in self.currentBallot: warnings.warn("Skipped rankings discarded from ballot %d." % (self.i+1)) for ranking in self.currentBallot: if isinstance(ranking, list): warnings.warn("Duplicate rankings discarded from ballot %d" % (self.i+1)) break self.currentBallot = [r for r in self.currentBallot if r != -1 and not isinstance(r, list)] # Update the list box to show the current ballot for c in xrange(self.b.numCandidates): if self.currentBallot.count(c) > 1: warnings.warn("Candidate %s appears on ballot %d more than once. Later rankings ignored." % (self.b.names[c], self.i+1)) if c in self.currentBallot: r = self.currentBallot.index(c) self.ballotC.SetStringItem(c, 0, str(r+1)) else: self.ballotC.SetStringItem(c, 0, "") def OnClear(self, event): self.currentBallot = [] self.storeCurrentBallot() self.UpdatePanel() self.Log("Cleared the rankings of ballot %d." % (self.i+1)) def OnDelete(self, event): # Can't delete the last ballot if self.b.numBallots == 1: txt = "Can't delete. Must have at least one ballot." wx.MessageBox(txt, "Error", wx.OK|wx.ICON_ERROR) return # Delete the current ballot self.b.deleteBallot(self.i) self.Log("Deleted ballot %d." % (self.i+1)) self.NeedToSaveBallots = True self.NeedToSaveLog = True # Update the control self.i = min(self.i, self.b.numBallots - 1) self.currentBallot = self.b.getBallot(self.i) self.UpdatePanel() def OnAppend(self, event): if len(self.currentBallot) == 0 and not self.emptyBallotOK(): return # Add blank ballot to end self.b.appendBallot([]) self.Log("Appended ballot %d." % self.b.numBallots) self.NeedToSaveBallots = True self.NeedToSaveLog = True # Update the control self.i = self.b.numBallots - 1 self.currentBallot = self.b.getBallot(self.i) self.UpdatePanel() def OnNav(self, event): if len(self.currentBallot) == 0 and not self.emptyBallotOK(): return # Move to another ballot label = event.GetEventObject().GetLabel() if label == "|<": self.i = 0 elif label == "<": self.i -= 1 elif label == ">": self.i += 1 elif label == ">|": self.i = self.b.numBallots - 1 elif label == "Go": self.i = self.spin.GetValue() - 1 else: assert(0) self.i = max(0, self.i) self.i = min(self.b.numBallots - 1, self.i) # Update the control self.currentBallot = self.b.getBallot(self.i) self.UpdatePanel() def OnListClick(self, event): c = event.m_itemIndex name = self.b.names[c] if c not in self.currentBallot: self.currentBallot.append(c) self.storeCurrentBallot() self.UpdatePanel() rank = len(self.currentBallot) self.Log("Added candidate %s to ballot %d with rank %d." % (name, self.i+1, rank)) # Unselect the clicked item. In some situations, the default behavior # is that an item of the list ctrl is selected, which prevents the user # from clicking directly on that item (the user must click elsewhere and # then click on the item). self.ballotC.SetItemState(c, 0, wx.LIST_STATE_SELECTED) def OnListDClick(self, event): c = event.m_itemIndex name = self.b.names[c] if c in self.currentBallot: self.currentBallot.remove(c) self.storeCurrentBallot() self.UpdatePanel() self.Log("Removed candidate %s from ballot %d." % (name, self.i+1)) # See comment in OnListClick self.ballotC.SetItemState(c, 0, wx.LIST_STATE_SELECTED) def OnTitle(self, event): titleC = event.GetEventObject() self.b.title = titleC.GetValue().strip() self.NeedToSaveBallots = True ################################################################## class CandidatesDialog(wx.Dialog): def __init__(self, parent, b): wx.Dialog.__init__(self, parent, -1, "New Ballot File") self.b = b self.names = [] # Explanation txt = wx.StaticText(self, -1, """\ Enter the candidates' names one by one. To remove a candidate whose name has already been entered, double click on the candidate's name below.""") # Candidate entry candidateL = wx.StaticText(self, -1, "Candidate to add:") self.candidateC = wx.TextCtrl(self, -1, "", style=wx.TE_PROCESS_ENTER) self.Bind(wx.EVT_TEXT_ENTER, self.OnEnter, self.candidateC) candidateB = wx.Button(self, -1, "Add") self.Bind(wx.EVT_BUTTON, self.OnAdd, candidateB) # Candidate list listL = wx.StaticText(self, -1, "Candidates:") self.listC = wx.ListBox(self, -1, choices=self.b.names, size=(-1, 100)) self.Bind(wx.EVT_LISTBOX_DCLICK, self.OnListDClick, self.listC) blank = wx.StaticText(self, -1, "") # Buttons ok = wx.Button(self, wx.ID_OK) self.Bind(wx.EVT_BUTTON, self.OnOK, ok) cancel = wx.Button(self, wx.ID_CANCEL) # Sizers sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(txt, 0, wx.ALL, 5) sizer.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 5) fgs = wx.FlexGridSizer(2, 3, 5, 5) fgs.Add(candidateL, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(self.candidateC, 0, wx.EXPAND) fgs.Add(candidateB, 0) fgs.Add(listL, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) fgs.Add(self.listC, 0, wx.EXPAND) fgs.Add(blank, 0) fgs.AddGrowableCol(1) sizer.Add(fgs, 0, wx.EXPAND|wx.ALL, 5) bs = wx.StdDialogButtonSizer() bs.AddButton(ok) bs.AddButton(cancel) bs.Realize() sizer.Add(bs, 0, wx.EXPAND|wx.ALL, 5) self.SetSizer(sizer) sizer.Fit(self) def OnOK(self, event): # Check to see if name is entered and not added name = self.candidateC.GetValue().strip() if name == "": event.Skip() else: wx.MessageBox("Name entered but not added. Please hit the 'Add' " "button or clear the name in the text box to continue.", "Message", wx.OK|wx.ICON_INFORMATION) self.b.names = self.names def OnEnter(self, event): self.OnAdd(event) def OnAdd(self, event): # Get the name in the text box name = self.candidateC.GetValue().strip() # Make sure the name is unique if name in self.names: wx.MessageBox("Can't have two candidates with the same name.", "Error", wx.OK|wx.ICON_ERROR) self.candidateC.SetFocus() return # Make sure the name is not empty if name == "": wx.MessageBox("Candidate name cannot be blank.", "Error", wx.OK|wx.ICON_ERROR) self.candidateC.SetFocus() return self.names.append(name) self.listC.Set(self.names) # Clear the text box to allow user to enter another name self.candidateC.Clear() self.candidateC.SetFocus() def OnListDClick(self, event): # Remove the candidate from the ballots instance and update the control c = self.listC.GetSelection() self.names.pop(c) self.listC.Set(self.names) OpenSTV-1.6.1/openstv/Help.html 0000777 0001754 0001001 00000021643 11400774167 014701 0 ustar jeff None
I. Overview
The single transferable vote (STV) is an election method that is used to obtain proportional representation. STV is used for national elections in Ireland, Northern Ireland, Malta, Australia, Scotland, and New Zealand. In addition, many organizations use STV, including the Church of England and the Green Party of the United States. However, no two of the above use precisely the same definition of STV. With the same set of ballots, these different methods could produce different winners. STV can best be described as a category of election methods and users of STV must specify precisely which version of STV is to be used. The purpose of this software is to help organizations use STV and also to specify precisely which STV rules they will use.
The basic structure of an STV method can be described as follows:
Different STV methods define more precisely how the above is implemented. STV methods must define the winning threshold and also how votes are to be transferred from candidates with a surplus and from candidates who are eliminated. Specific implementations of STV define both of these and may also modify the basic structure.
People have different points of view as to which STV method should be used in a particular situation. For a group that values simplicity, we recommend Scottish STV. It is the easiest method to understand and it provides accurate proportional representation. Another advantage is that OpenSTV's implementation of Scottish STV has been validated against the counting software used in Scotland.
For a group that desires the most accurate proportional representation, we recommend Meek STV. The transfer of votes is more complicated with Meek STV, and thus more difficult to explain to the voters. Meek is, however, generally accepted as the most accurate STV method.
The rest of this document assumes that you already chosen the particular STV method that you would like to use and guides the process of using OpenSTV. OpenSTV provides two basic operations:
II. Running an Election
To run an election, you must already have a complete ballot file. If you do not have a complete ballot file, then you can create or edit a ballot file as described below. To start the process, select "Run Election..." from the File menu.
The first step is to select the file that contains the ballots and the election method to be used. You do not need to specify the format of the ballot file as OpenSTV will determine that automatically.
The next window allows you to specify more information, optionally withdraw candidates, and to set options particular to the election method chosen (if any). This window should be self explanatory.
The election results will appear in a new tab. You then have several options for saving the results. You can copy and paste the text into a different program, or using the File menu, you can save the results in one of three formats. The text format is exactly what you see on the screen. The html format can viewed in a browser. The CSV (comma separated value) output is used by the Electoral Reform Society and can be loaded into any spreadsheet program.
The File menu also allows you to close the tabs containing election results; you can't close the console tab unless all results tabs have been closed. The font size in any tab can be changed to the desired size via the Options menu. If the election results are wider than the window size, then choosing a smaller font size may allow for easier viewing of election results.
III. Editing Ballots
OpenSTV has a built-in ballot file editor. If you would like to create a new ballot file from scratch, select "New Ballot File..." from the File menu. If you would like to edit an existing ballot file, select "Edit Ballot File..." from the File menu.
When you create a new ballot file through OpenSTV, it will always be in the BLT format, and you cannot save the ballot file in any other format. The first step in creating a new ballot file is to enter the names of the candidates, and this process is self explanatory. Once you enter the candidates' names, you are presented with the editing window.
If you are editing an existing file, then you can select ballot files in BLT format or in text format (described below), and OpenSTV will always save the file in the same format (you can't save in a different format). After selecting the ballot file to edit, you are presented with the editing window. You can have multiple ballot editing windows open at a time.
The ballot editing window contains two tabs. The Ballots tab allows you to view and edit ballots, and this process should be self-explanatory. OpenSTV only accepts valid ballots, and an empty ballot is valid. You don't need to rank all of the candidates on each ballot, but you cannot skip rankings and you cannot give two or more candidates the same ranking. The Log tab creates an audit trail of how the ballots have been edited, and you can save the log to a file.
The File menu allows you to save the ballots and the log or to save them under different filenames, but you can't change the file format. The File menu also allows you to append an existing ballot file to the ballot file you are currently editing. To append, the two ballot files must have the same number of candidates, the same number of seats, and identical candidate names.
IV. Ballot File Formats
Two ballot file formats are supported: BLT format and text format.
The BLT format is preferred and is the format used by the Electoral Reform Society. Here is an example:
4 2 # four candidates are competing for two seats -2 # Bob has withdrawn (optional) 1 4 1 3 2 0 # first ballot 1 2 4 1 3 0 1 1 4 2 3 0 # The first number is the ballot weight (>= 1). 1 1 2 4 3 0 # The last 0 is an end of ballot marker. 1 1 4 3 0 # Numbers in between correspond to the candidates 1 3 2 4 1 0 # on the ballot. 1 3 4 1 2 0 1 3 4 1 2 0 # Chuck, Diane, Amy, Bob 1 4 3 2 0 1 2 3 4 1 0 # last ballot 0 # end of ballots marker "Amy" # candidate 1 "Bob" # candidate 2 "Chuck" # candidate 3 "Diane" # candidate 4 "Gardening Club Election" # title
The text format is a simple format that is easily created with any test editor. Here is an example:
Amy Bob Chuck Bob Amy Chuck Chuck Bob
The first ballot lists Amy first, Bob second, and Chuck third. Note that candidate names must be alphanumeric. You can also add a weight to a ballot by prefixing it with a number and a colon:
10: Amy Bob Chuck 5: Bob Amy Chuck 2: Chuck Bob
V. Advanced Use
Download and install the Linux version of OpenSTV to use OpenSTV from the command line or via scripting.
The runelection.py
command runs an election for the
given method and ballot file. Results are printed to stdout.
runElection.py [-d] [-r report] [-t tiebreak] method ballotfile -d: enable debug -r: report format: text*, html, csv -t: tiebreak method: random*, alpha, index *default
The runelection.py
command runs an election for the
given method and ballot file. Results are printed to stdout. The
following methods are available:
Approval
Borda
Bucklin
CambridgeSTV
Condorcet
Coombs
ERS97STV
FTSTV
GPCA2000STV
IRV
MeekQXSTV
MeekSTV
NIrelandSTV
QPQ
RTSTV
SNTV
ScottishSTV
SuppVote
WarrenQXSTV
WarrenSTV
You can also write your own Python scripts to run elections. To do this, you would import OpenSTV's Python modules into your own Python program. Use runElection.py as an example of how to write your own scripts using OpenSTV.
OpenSTV-1.6.1/openstv/Icons/ 0001777 0001754 0001001 00000000000 11407513225 014157 5 ustar jeff None OpenSTV-1.6.1/openstv/Icons/blt.icns 0000777 0001754 0001001 00000113454 11400774167 015637 0 ustar jeff None icns —,h8mk &