This is an automated email from the ASF dual-hosted git repository. gstein pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/steve.git
commit 2d68602614db5d0153bb8d4e101a46e3afaedfcd Author: Greg Stein <[email protected]> AuthorDate: Thu Mar 5 01:26:30 2026 -0600 Improve tallying options and storage. * --output json now includes all issues into one dict, and a list of all those who voted (not on what or how; just "seen") * --issue_id now allows tallying a single issue for testing/speed * improve some logging; switch some stuff from print() * Election.tally_issue() returns the list of voters seen, so support the above JSON output feature --- v3/server/bin/tally.py | 61 ++++++++++++++++++++++++++++++++++++++++---------- v3/steve/election.py | 14 ++++++++++-- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/v3/server/bin/tally.py b/v3/server/bin/tally.py index 7884d15..6331797 100755 --- a/v3/server/bin/tally.py +++ b/v3/server/bin/tally.py @@ -27,11 +27,17 @@ import sys import steve.election import steve.persondb +from easydict import EasyDict as edict + _LOGGER = logging.getLogger(__name__) THIS_DIR = pathlib.Path(__file__).resolve().parent DEFAULT_DB_FNAME = THIS_DIR.parent / 'steve.db' +# Self-annotate the format of a JSON output file. +RESULTS_VERSION = 1 +# Future: document format changes across the versions. + def list_elections(db_fname, spy_on_open): """ @@ -86,31 +92,48 @@ def select_election(elections): print("Please enter a number or 'q'.") -def tally_election(election, output_format): +def tally_election(election, issue_id, output_format): """ Tally all issues in the given election and output results. """ + issues = election.list_issues() if not issues: - print('No issues to tally in this election.') + _LOGGER.error('No issues to tally in this election.') return + # Does the user want to just tally a single issue? (faster) + if issue_id: + issues = [issue for issue in issues if issue.iid == issue_id] + if not issues: + _LOGGER.error(f'Issue {issue_id} was not found.') + return + _LOGGER.info(f'Tallying one issue: {issue_id}') + + if len(issues) > 1: + _LOGGER.info(f'Talling {len(issues)} issues ...') + + all_voters = set() results = {} for issue in issues: try: - tally_result = election.tally_issue(issue.iid) + tally_result, issue_voters = election.tally_issue(issue.iid) results[issue.iid] = { 'title': issue.title, 'vtype': issue.vtype, 'human_result': tally_result[0], 'supporting_data': tally_result[1], } + all_voters.update(issue_voters) except Exception as e: print(f'Error tallying issue {issue.iid}: {e}') raise # Fail hard if output_format == 'json': - print(json.dumps(results, indent=2)) + print(json.dumps(edict(version=RESULTS_VERSION, + results=results, + voters=sorted(all_voters), + ), indent=2)) else: # text for iid, data in results.items(): print(f'Issue {iid}: {data["title"]} ({data["vtype"]})') @@ -119,19 +142,25 @@ def tally_election(election, output_format): print('-' * 40) -def main(spy_on_open, election_id, db_fname, output_format): +def main(spy_on_open, election_id, issue_id, db_fname, output_format): """ Main function to run the tally script. """ - if election_id: - election = steve.election.Election(db_fname, election_id) - else: + if issue_id: + db = steve.election.Election.open_database(db_fname) + issue = db.q_get_issue.first_row(issue_id) + if not issue: + raise steve.election.IssueNotFound(issue_id) + election_id = issue.eid + db.conn.close() + elif not election_id: elections = list_elections(db_fname, spy_on_open) election_id = select_election(elections) if not election_id: print('No election selected. Exiting.') return - election = steve.election.Election(db_fname, election_id) + + election = steve.election.Election(db_fname, election_id) # Check for tampering pdb = steve.persondb.PersonDB.open(db_fname) @@ -140,7 +169,7 @@ def main(spy_on_open, election_id, db_fname, output_format): sys.exit(1) # Proceed with tally - tally_election(election, output_format) + tally_election(election, issue_id, output_format) if __name__ == '__main__': @@ -157,7 +186,11 @@ if __name__ == '__main__': ) parser.add_argument( '--election-id', - help='Specify election ID to tally directly (skips interactive selection).', + help='Specify Election ID to tally directly (skips interactive selection).', + ) + parser.add_argument( + '--issue-id', + help='Specify an Issue ID to tally directly (skips interactive selection).', ) parser.add_argument( '--db-path', default=str(DEFAULT_DB_FNAME), help='Path to the database file.' @@ -170,4 +203,8 @@ if __name__ == '__main__': ) args = parser.parse_args() - main(args.spy_on_open_elections, args.election_id, args.db_path, args.output) + if args.election_id and args.issue_id: + _LOGGER.error('ISSUE_ID implies an ELECTION_ID; do not set both.') + sys.exit(1) + + main(args.spy_on_open_elections, args.election_id, args.issue_id, args.db_path, args.output) diff --git a/v3/steve/election.py b/v3/steve/election.py index f99bc3b..89a4b40 100644 --- a/v3/steve/election.py +++ b/v3/steve/election.py @@ -314,9 +314,15 @@ class Election: # The Election should be closed. md = self._all_metadata(self.S_CLOSED) + ### TBD: we need a param to "spy" on Open elections + #md = self._all_metadata() + # Need the issue TYPE issue = self.q_get_issue.first_row(iid) + # Accumulate PID values for each person who voted on IID. + voters = set() + # Accumulate all MOST-RECENT votes for Issue IID. votes = [] @@ -340,6 +346,10 @@ class Election: if row is None: continue + # There is a vote by this PID. Record the voter. + voters.add(mayvote.pid) + + # Get the original votestring using the token/salt. votestring = crypto.decrypt_votestring( vote_token, mayvote.salt, @@ -352,9 +362,9 @@ class Election: # superfluous. But it certainly should not hurt. crypto.shuffle(votes) # in-place - # Perform the tally, and return the results. + # Perform the tally, and return the results and voters. m = vtypes.vtype_module(issue.type) - return m.tally(votes, self.json2kv(issue.kv)) + return m.tally(votes, self.json2kv(issue.kv)), voters def has_voted_upon(self, pid): "Return {ISSUE-ID: BOOL} stating what has been voted upon."
