From f64a07798899b65595a8a2277b43ebcbba7c9575 Mon Sep 17 00:00:00 2001 From: Serdar Tumgoren Date: Fri, 4 Mar 2016 16:21:50 -0800 Subject: [PATCH 1/2] Remove elex3 and elex4 dirs --- elex3/__init__.py | 0 elex3/lib/__init__.py | 0 elex3/lib/parser.py | 40 -------- elex3/lib/scraper.py | 19 ---- elex3/lib/summary.py | 56 ----------- elex3/scripts/save_summary_to_csv.py | 65 ------------ elex3/tests/__init__.py | 0 elex3/tests/sample_results.csv | 5 - elex3/tests/sample_results_parsed.json | 52 ---------- .../tests/sample_results_parsed_tie_race.json | 52 ---------- elex3/tests/test_parser.py | 18 ---- elex3/tests/test_summary.py | 66 ------------- elex4/__init__.py | 0 elex4/lib/__init__.py | 0 elex4/lib/models.py | 55 ----------- elex4/lib/parser.py | 40 -------- elex4/lib/scraper.py | 19 ---- elex4/lib/summary.py | 40 -------- elex4/scripts/save_summary_to_csv.py | 65 ------------ elex4/tests/__init__.py | 0 elex4/tests/sample_results_parsed.json | 46 --------- .../tests/sample_results_parsed_tie_race.json | 46 --------- elex4/tests/test_models.py | 99 ------------------- elex4/tests/test_parser.py | 16 --- elex4/tests/test_summary.py | 53 ---------- 25 files changed, 852 deletions(-) delete mode 100644 elex3/__init__.py delete mode 100644 elex3/lib/__init__.py delete mode 100644 elex3/lib/parser.py delete mode 100644 elex3/lib/scraper.py delete mode 100644 elex3/lib/summary.py delete mode 100644 elex3/scripts/save_summary_to_csv.py delete mode 100644 elex3/tests/__init__.py delete mode 100644 elex3/tests/sample_results.csv delete mode 100644 elex3/tests/sample_results_parsed.json delete mode 100644 elex3/tests/sample_results_parsed_tie_race.json delete mode 100644 elex3/tests/test_parser.py delete mode 100644 elex3/tests/test_summary.py delete mode 100644 elex4/__init__.py delete mode 100644 elex4/lib/__init__.py delete mode 100644 elex4/lib/models.py delete mode 100644 elex4/lib/parser.py delete mode 100644 elex4/lib/scraper.py delete mode 100644 elex4/lib/summary.py delete mode 100644 elex4/scripts/save_summary_to_csv.py delete mode 100644 elex4/tests/__init__.py delete mode 100644 elex4/tests/sample_results_parsed.json delete mode 100644 elex4/tests/sample_results_parsed_tie_race.json delete mode 100644 elex4/tests/test_models.py delete mode 100644 elex4/tests/test_parser.py delete mode 100644 elex4/tests/test_summary.py diff --git a/elex3/__init__.py b/elex3/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex3/lib/__init__.py b/elex3/lib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex3/lib/parser.py b/elex3/lib/parser.py deleted file mode 100644 index e76124d..0000000 --- a/elex3/lib/parser.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -import csv -from collections import defaultdict - - -def parse_and_clean(path): - """Parse downloaded results file and perform various data clean-ups - - - RETURNS: - - Nested dictionary keyed first by race, then candidate. - Candidate value is an array of dicts containing county level results. - - """ - # Create reader for ingesting CSV as array of dicts - reader = csv.DictReader(open(path, 'rb')) - - # Use defaultdict to automatically create non-existent keys with an empty dictionary as the default value. - # See https://pydocs2cn.readthedocs.org/en/latest/library/collections.html#defaultdict-objects - results = defaultdict(dict) - - # Initial data clean-up - for row in reader: - # Perform some data clean-ups and conversions - row['last_name'], row['first_name'] = [name.strip() for name in row['candidate'].split(',')] - row['votes'] = int(row['votes']) - - # Store county-level results by slugified office and district (if there is one), - # then by candidate party and raw name - race_key = row['office'] - if row['district']: - race_key += "-%s" % row['district'] - # Create unique candidate key from party and name, in case multiple candidates have same - cand_key = "-".join((row['party'], row['candidate'])) - # Below, setdefault initializes empty dict and list for the respective keys if they don't already exist. - race = results[race_key] - race.setdefault(cand_key, []).append(row) - - return results diff --git a/elex3/lib/scraper.py b/elex3/lib/scraper.py deleted file mode 100644 index 452d73d..0000000 --- a/elex3/lib/scraper.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -from urllib import urlretrieve - - -def download_results(path): - """Download CSV of fake Virginia election results from GDocs - - Downloads the file to the root of the repo (/path/to/refactoring101/). - - NOTE: This will only download the file if it doesn't already exist - This approach is simplified for demo purposes. In a real-life application, - you'd likely have a considerable amount of additional code - to appropriately handle HTTP timeouts, 404s, and other real-world scenarios. - For example, you might retry a request several times after a timeout, and then - send an email alert that the site is non-responsive. - - """ - url = "https://docs.google.com/spreadsheet/pub?key=0AhhC0IWaObRqdGFkUW1kUmp2ZlZjUjdTYV9lNFJ5RHc&output=csv" - urlretrieve(url, path) diff --git a/elex3/lib/summary.py b/elex3/lib/summary.py deleted file mode 100644 index 9441425..0000000 --- a/elex3/lib/summary.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -from collections import defaultdict -from operator import itemgetter - - -def summarize(results): - """Tally votes for Races and candidates and assign winners. - - RETURNS: - - Dictionary of results - - """ - summary = defaultdict(dict) - - for race_key, cand_results in results.items(): - all_votes = 0 - cands = [] - for cand_key, results in cand_results.items(): - # Populate a new candidate dict using one set of county results - cand = { - 'first_name': results[0]['first_name'], - 'last_name': results[0]['last_name'], - 'party': results[0]['party'], - 'winner': '', - } - # Calculate candidate total votes - cand_total_votes = sum([result['votes'] for result in results]) - cand['votes'] = cand_total_votes - # Add cand totals to racewide vote count - all_votes += cand_total_votes - # And stash the candidate's data - cands.append(cand) - - # sort cands from highest to lowest vote count - sorted_cands = sorted(cands, key=itemgetter('votes'), reverse=True) - - # Determine winner, if any - first = sorted_cands[0] - second = sorted_cands[1] - - if first['votes'] != second['votes']: - first['winner'] = 'X' - - # Get race metadata from one set of results - result = cand_results.values()[0][0] - summary[race_key] = { - 'all_votes': all_votes, - 'date': result['date'], - 'office': result['office'], - 'district': result['district'], - 'candidates': sorted_cands, - } - - return summary - diff --git a/elex3/scripts/save_summary_to_csv.py b/elex3/scripts/save_summary_to_csv.py deleted file mode 100644 index dc546f4..0000000 --- a/elex3/scripts/save_summary_to_csv.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python -""" -This script leverages re-usable bits of code in the lib/ directory to -generate a summary CSV of election results. - -USAGE: - - python save_summary_results_to_csv.py - - -OUTPUT: - - summary_results.csv containing racewide totals for each race/candidate pair. - - -""" -from os.path import dirname, join -import csv - -from elex3.lib.summary import summarize -from elex3.lib.parser import parse_and_clean -from elex3.lib.scraper import download_results - - -def main(): - fname = 'fake_va_elec_results.csv' - path = join(dirname(dirname(__file__)), fname) - download_results(path) - results = parse_and_clean(path) - summary = summarize(results) - write_csv(summary) - - -def write_csv(summary): - """Generates CSV from summary election results data - - CSV is written to 'summary_results.csv' file in elex3/ directory. - - """ - outfile = join(dirname(dirname(__file__)), 'summary_results.csv') - with open(outfile, 'wb') as fh: - # Limit output to cleanly parsed, standardized values - fieldnames = [ - 'date', - 'office', - 'district', - 'last_name', - 'first_name', - 'party', - 'all_votes', - 'votes', - 'winner', - ] - writer = csv.DictWriter(fh, fieldnames, extrasaction='http://github.com/ignore', quoting=csv.QUOTE_MINIMAL) - writer.writeheader() - for race, results in summary.items(): - cands = results.pop('candidates') - for cand in cands: - results.update(cand) - writer.writerow(results) - - - -if __name__ == '__main__': - main() diff --git a/elex3/tests/__init__.py b/elex3/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex3/tests/sample_results.csv b/elex3/tests/sample_results.csv deleted file mode 100644 index 13cfcd8..0000000 --- a/elex3/tests/sample_results.csv +++ /dev/null @@ -1,5 +0,0 @@ -date,office,district,county,candidate,party,votes -2012-11-06,President,,Some County,"Smith, Joe",GOP,10 -2012-11-06,President,,Some County,"Doe, Jane",DEM,11 -2012-11-06,President,,Another County,"Smith, Joe",GOP,5 -2012-11-06,President,,Another County,"Doe, Jane",DEM,5 diff --git a/elex3/tests/sample_results_parsed.json b/elex3/tests/sample_results_parsed.json deleted file mode 100644 index ca32801..0000000 --- a/elex3/tests/sample_results_parsed.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "President": { - "DEM-Doe, Jane": [ - { - "candidate": "Doe, Jane", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 11 - }, - { - "candidate": "Doe, Jane", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 5 - } - ], - "GOP-Smith, Joe": [ - { - "candidate": "Smith, Joe", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 10 - }, - { - "candidate": "Smith, Joe", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 5 - } - ] - } -} \ No newline at end of file diff --git a/elex3/tests/sample_results_parsed_tie_race.json b/elex3/tests/sample_results_parsed_tie_race.json deleted file mode 100644 index 74a98b8..0000000 --- a/elex3/tests/sample_results_parsed_tie_race.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "President": { - "DEM-Doe, Jane": [ - { - "candidate": "Doe, Jane", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 10 - }, - { - "candidate": "Doe, Jane", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 5 - } - ], - "GOP-Smith, Joe": [ - { - "candidate": "Smith, Joe", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 10 - }, - { - "candidate": "Smith, Joe", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 5 - } - ] - } -} diff --git a/elex3/tests/test_parser.py b/elex3/tests/test_parser.py deleted file mode 100644 index b69d1b6..0000000 --- a/elex3/tests/test_parser.py +++ /dev/null @@ -1,18 +0,0 @@ -from os.path import dirname, join -from unittest import TestCase - -from elex3.lib.parser import parse_and_clean - - -class TestParser(TestCase): - - def test_name_parsing(self): - "Parser should split full candidate name into first and last names" - path = join(dirname(__file__), 'sample_results.csv') - results = parse_and_clean(path) - race_key = 'President' - cand_key = 'GOP-Smith, Joe' - # Get one county result - smith = results[race_key][cand_key][0] - self.assertEqual(smith['first_name'], 'Joe') - self.assertEqual(smith['last_name'], 'Smith') diff --git a/elex3/tests/test_summary.py b/elex3/tests/test_summary.py deleted file mode 100644 index c76b16a..0000000 --- a/elex3/tests/test_summary.py +++ /dev/null @@ -1,66 +0,0 @@ -from os.path import dirname, join -from unittest import TestCase -import json - -from elex3.lib.summary import summarize - - -class TestSummaryResults(TestCase): - - # Read the results of the parse_and_clean function stored in a test fixture - json_file = open(join(dirname(__file__), 'sample_results_parsed.json'), 'rb') - SAMPLE_RESULTS = json.load(json_file) - # Q: Why aren't we just using the parse_and_clean method instead of - # using a snapshot of that function's output? - # A: To achieve better test isolation! - - # Q: Why aren't reading in the JSON in a setUp method? - # A: setUp is called before each test method. This ensures we only - # incur the overhead of reading in the JSON once. In python2.7 or newer, - # you should use the setUpClass method instead of a class attribute. - # http://docs.python.org/2/library/unittest.html#unittest.TestCase.setUpClass - - # We will, however, use the setUp method to call the summarize - # funciton afresh before each of our test methods. - def setUp(self): - results = summarize(self.SAMPLE_RESULTS) - self.race = results['President'] - - def test_racewide_vote_total(self): - "Summary results should be annotated with total votes cast in race" - self.assertEqual(self.race['all_votes'], 31) - - def test_candiate_vote_totals(self): - "Summary candidates should reflect total votes from all counties" - # Loop through candidates and find Smith rather than relying on - # default sorting of candidates, which would make this test brittle - # the implementation changed. - smith = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Smith'][0] - self.assertEqual(smith['votes'], 15) - - def test_winner_has_flag(self): - "Winner flag should be assigned to candidates with most votes" - doe = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Doe'][0] - self.assertEqual(doe['winner'], 'X') - - def test_loser_has_no_winner_flag(self): - "Winner flag should not be assigned to candidate that does not have highest vote total" - smith = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Smith'][0] - self.assertEqual(smith['winner'], '') - - -class TestTieRace(TestCase): - - # Q: Why do we need a new class and fixture for this race? - # A: So that we can change the vote counts so that we have a tie, of course! - # We don't *need* a new test class, but hey, why not? - json_file = open(join(dirname(__file__), 'sample_results_parsed_tie_race.json'), 'rb') - SAMPLE_RESULTS = json.load(json_file) - - def test_tie_race_winner_flags(self): - "Winner flag should not be assigned to any candidate in a tie race" - pass - results = summarize(self.SAMPLE_RESULTS) - race = results['President'] - for cand in race['candidates']: - self.assertEqual(cand['winner'], '') diff --git a/elex4/__init__.py b/elex4/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex4/lib/__init__.py b/elex4/lib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex4/lib/models.py b/elex4/lib/models.py deleted file mode 100644 index 6ca396f..0000000 --- a/elex4/lib/models.py +++ /dev/null @@ -1,55 +0,0 @@ -from operator import attrgetter - -class Race(object): - - def __init__(self, date, office, district): - self.date = date - self.office = office - self.district = district - self.total_votes = 0 - self.candidates = {} - - def add_result(self, result): - self.total_votes += result['votes'] - candidate = self.__get_or_create_candidate(result) - candidate.add_votes(result['county'], result['votes']) - - def assign_winner(self): - # sort cands from highest to lowest vote count - sorted_cands = sorted(self.candidates.values(), key=attrgetter('votes'), reverse=True) - - # Determine winner, if any - first = sorted_cands[0] - second = sorted_cands[1] - - if first.votes != second.votes: - first.winner = 'X' - - - # Private methods - def __get_or_create_candidate(self, result): - key = (result['party'], result['candidate']) - try: - candidate = self.candidates[key] - except KeyError: - candidate = Candidate(result['candidate'], result['party']) - self.candidates[key] = candidate - return candidate - - -class Candidate(object): - - def __init__(self, raw_name, party): - self.last_name, self.first_name = self.__parse_name(raw_name) - self.party = party - self.county_results = {} - self.votes = 0 - self.winner = '' - - def add_votes(self, county, votes): - self.county_results[county] = votes - self.votes += votes - - # Private method - def __parse_name(self, raw_name): - return [name.strip() for name in raw_name.split(",")] diff --git a/elex4/lib/parser.py b/elex4/lib/parser.py deleted file mode 100644 index cec2da7..0000000 --- a/elex4/lib/parser.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -import csv -from collections import defaultdict - -from elex4.lib.models import Race - - -def parse_and_clean(path): - """Parse downloaded results file. - - - RETURNS: - - A dictionary containing race key and Race instances as values. - - """ - # Create reader for ingesting CSV as array of dicts - reader = csv.DictReader(open(path, 'rb')) - - results = {} - - # Initial data clean-up - for row in reader: - # Convert votes to integer - row['votes'] = int(row['votes']) - - # Store races by slugified office and district (if there is one) - race_key = row['office'] - if row['district']: - race_key += "-%s" % row['district'] - - try: - race = results[race_key] - except KeyError: - race = Race(row['date'], row['office'], row['district']) - results[race_key] = race - - race.add_result(row) - - return results diff --git a/elex4/lib/scraper.py b/elex4/lib/scraper.py deleted file mode 100644 index 452d73d..0000000 --- a/elex4/lib/scraper.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -from urllib import urlretrieve - - -def download_results(path): - """Download CSV of fake Virginia election results from GDocs - - Downloads the file to the root of the repo (/path/to/refactoring101/). - - NOTE: This will only download the file if it doesn't already exist - This approach is simplified for demo purposes. In a real-life application, - you'd likely have a considerable amount of additional code - to appropriately handle HTTP timeouts, 404s, and other real-world scenarios. - For example, you might retry a request several times after a timeout, and then - send an email alert that the site is non-responsive. - - """ - url = "https://docs.google.com/spreadsheet/pub?key=0AhhC0IWaObRqdGFkUW1kUmp2ZlZjUjdTYV9lNFJ5RHc&output=csv" - urlretrieve(url, path) diff --git a/elex4/lib/summary.py b/elex4/lib/summary.py deleted file mode 100644 index 2f8d03b..0000000 --- a/elex4/lib/summary.py +++ /dev/null @@ -1,40 +0,0 @@ -from collections import defaultdict -from operator import itemgetter - - -def summarize(results): - """Triggers winner assignments and formats data for output. - - RETURNS: - - Dictionary of results - - """ - summary = {} - - for race_key, race in results.items(): - cands = [] - # Call our new assign_winner method - race.assign_winner() - # Loop through Candidate instances and extract a dictionary - # of target values. Basically, we're throwing away county-level - # results since we don't need those for the summary report - for cand in race.candidates.values(): - # Remove lower-level county results - # This is a dirty little trick to botainfor easily obtaining - # a dictionary of candidate attributes. - info = cand.__dict__.copy() - # Remove county results - info.pop('county_results') - cands.append(info) - - summary[race_key] = { - 'all_votes': race.total_votes, - 'date': race.date, - 'office': race.office, - 'district': race.district, - 'candidates': cands, - } - - return summary - diff --git a/elex4/scripts/save_summary_to_csv.py b/elex4/scripts/save_summary_to_csv.py deleted file mode 100644 index 56d3926..0000000 --- a/elex4/scripts/save_summary_to_csv.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python -""" -This script leverages re-usable bits of code in the lib/ directory to -generate a summary CSV of election results. - -USAGE: - - python save_summary_results_to_csv.py - - -OUTPUT: - - summary_results.csv containing racewide totals for each race/candidate pair. - - -""" -from os.path import dirname, join -import csv - -from elex4.lib.summary import summarize -from elex4.lib.parser import parse_and_clean -from elex4.lib.scraper import download_results - - -def main(): - fname = 'fake_va_elec_results.csv' - path = join(dirname(dirname(__file__)), fname) - download_results(path) - results = parse_and_clean(path) - summary = summarize(results) - write_csv(summary) - - -def write_csv(summary): - """Generates CSV from summary election results data - - CSV is written to 'summary_results.csv' file in elex4/ directory. - - """ - outfile = join(dirname(dirname(__file__)), 'summary_results.csv') - with open(outfile, 'wb') as fh: - # Limit output to cleanly parsed, standardized values - fieldnames = [ - 'date', - 'office', - 'district', - 'last_name', - 'first_name', - 'party', - 'all_votes', - 'votes', - 'winner', - ] - writer = csv.DictWriter(fh, fieldnames, extrasaction='http://github.com/ignore', quoting=csv.QUOTE_MINIMAL) - writer.writeheader() - for race, results in summary.items(): - cands = results.pop('candidates') - for cand in cands: - results.update(cand) - writer.writerow(results) - - - -if __name__ == '__main__': - main() diff --git a/elex4/tests/__init__.py b/elex4/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex4/tests/sample_results_parsed.json b/elex4/tests/sample_results_parsed.json deleted file mode 100644 index dd8cc79..0000000 --- a/elex4/tests/sample_results_parsed.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "candidate": "Doe, Jane", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 11 - }, - { - "candidate": "Doe, Jane", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 5 - }, - { - "candidate": "Smith, Joe", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 10 - }, - { - "candidate": "Smith, Joe", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 5 - } -] diff --git a/elex4/tests/sample_results_parsed_tie_race.json b/elex4/tests/sample_results_parsed_tie_race.json deleted file mode 100644 index 375ea85..0000000 --- a/elex4/tests/sample_results_parsed_tie_race.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "candidate": "Doe, Jane", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 10 - }, - { - "candidate": "Doe, Jane", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 5 - }, - { - "candidate": "Smith, Joe", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 10 - }, - { - "candidate": "Smith, Joe", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 5 - } -] diff --git a/elex4/tests/test_models.py b/elex4/tests/test_models.py deleted file mode 100644 index 99506a8..0000000 --- a/elex4/tests/test_models.py +++ /dev/null @@ -1,99 +0,0 @@ -from unittest import TestCase - -from elex4.lib.models import Candidate, Race - - -class TestCandidate(TestCase): - - def test_candidate_name(self): - "Candidates should have first_name and last_name attributes" - cand = Candidate("Smith, Joe", "GOP") - self.assertEquals(cand.first_name, "Joe") - self.assertEquals(cand.last_name, "Smith") - - -class TestCandidateVotes(TestCase): - - def setUp(self): - self.cand = Candidate("Smith, Joe", "GOP") - - def test_default_zero_votes(self): - "Candidate vote count should default to zero" - self.assertEquals(self.cand.votes, 0) - - def test_vote_count_update(self): - "Candidate.add_votes method should update vote count" - self.cand.add_votes("Some County", 20) - self.assertEquals(self.cand.votes, 20) - - def test_county_results_access(self): - "Candidate.add_votes method should store county results" - self.cand.add_votes("Some County", 20) - expected = { "Some County": 20 } - self.assertEquals(self.cand.county_results, expected) - - -class TestRace(TestCase): - - def setUp(self): - self.smith_result = { - 'date': '2012-11-06', - 'candidate': 'Smith, Joe', - 'party': 'Dem', - 'office': 'President', - 'county': 'Fairfax', - 'votes': 2000, - } - self.doe_result = { - 'date': '2012-11-06', - 'candidate': 'Doe, Jane', - 'party': 'GOP', - 'office': 'President', - 'county': 'Fairfax', - 'votes': 1000, - } - self.race = Race("2012-11-06", "President", "") - - def test_total_votes_default(self): - "Race total votes should default to zero" - self.assertEquals(self.race.total_votes, 0) - - def test_total_votes_update(self): - "Race.add_result should update racewide vote count" - self.race.add_result(self.smith_result) - self.assertEquals(self.race.total_votes, 2000) - - def test_add_result_to_candidate(self): - "Race.add_result should update a unique candidate instance" - # Add a vote twice. If it's the same candidate, vote total should be sum of results - self.race.add_result(self.smith_result) - self.race.add_result(self.smith_result) - cand_key = (self.smith_result['party'], self.smith_result['candidate']) - candidate = self.race.candidates[cand_key] - self.assertEquals(candidate.votes, 4000) - - def test_winner_has_flag(self): - "Winner flag should be assigned to candidates with most votes" - self.race.add_result(self.doe_result) - self.race.add_result(self.smith_result) - self.race.assign_winner() - smith = [cand for cand in self.race.candidates.values() if cand.last_name == 'Smith'][0] - self.assertEqual(smith.winner, 'X') - - def test_loser_has_no_winner_flag(self): - "Winner flag should not be assigned to candidate who does not have highest vote total" - self.race.add_result(self.doe_result) - self.race.add_result(self.smith_result) - self.race.assign_winner() - doe = [cand for cand in self.race.candidates.values() if cand.last_name == 'Doe'][0] - self.assertEqual(doe.winner, '') - - def test_tie_race(self): - "Winner flag should not be assigned to any candidate in a tie race" - # Modify Doe vote count to make it a tie - self.doe_result['votes'] = 2000 - self.race.add_result(self.doe_result) - self.race.add_result(self.smith_result) - self.race.assign_winner() - for cand in self.race.candidates.values(): - self.assertEqual(cand.winner, '') diff --git a/elex4/tests/test_parser.py b/elex4/tests/test_parser.py deleted file mode 100644 index a5ba623..0000000 --- a/elex4/tests/test_parser.py +++ /dev/null @@ -1,16 +0,0 @@ -from os.path import dirname, join -from unittest import TestCase - -from elex4.lib.parser import parse_and_clean - - -class TestParser(TestCase): - - def test_name_parsing(self): - "Parser should split full candidate name into first and last names" - path = join(dirname(__file__), 'sample_results.csv') - results = parse_and_clean(path) - race = results['President'] - smith = [cand for cand in race.candidates.values() if cand.last_name == 'Smith'][0] - self.assertEqual(smith.first_name, 'Joe') - self.assertEqual(smith.last_name, 'Smith') diff --git a/elex4/tests/test_summary.py b/elex4/tests/test_summary.py deleted file mode 100644 index 24587be..0000000 --- a/elex4/tests/test_summary.py +++ /dev/null @@ -1,53 +0,0 @@ -from os.path import dirname, join -from unittest import TestCase -import json - -from elex4.lib.models import Race -from elex4.lib.summary import summarize - - -class TestSummaryBase(TestCase): - - def setUp(self): - # Recall that sample data only has a single Presidential race - race = Race('2012-11-06', 'President', '') - for result in self.SAMPLE_RESULTS: - race.add_result(result) - # summarize function expects a dict, keyed by race - summary = summarize({'President': race}) - self.race = summary['President'] - -class TestSummaryResults(TestSummaryBase): - - json_file = open(join(dirname(__file__), 'sample_results_parsed.json'), 'rb') - SAMPLE_RESULTS = json.load(json_file) - - def test_racewide_vote_total(self): - "Summary results should be annotated with total votes cast in race" - self.assertEqual(self.race['all_votes'], 31) - - def test_candiate_vote_totals(self): - "Summary candidates should reflect total votes from all counties" - smith = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Smith'][0] - self.assertEqual(smith['votes'], 15) - - def test_winner_has_flag(self): - "Winner flag should be assigned to candidates with most votes" - doe = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Doe'][0] - self.assertEqual(doe['winner'], 'X') - - def test_loser_has_no_winner_flag(self): - "Winner flag should not be assigned to candidate that does not have highest vote total" - smith = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Smith'][0] - self.assertEqual(smith['winner'], '') - - -class TestTieRace(TestSummaryBase): - - json_file = open(join(dirname(__file__), 'sample_results_parsed_tie_race.json'), 'rb') - SAMPLE_RESULTS = json.load(json_file) - - def test_tie_race_winner_flags(self): - "Winner flag should not be assigned to any candidate in a tie race" - for cand in self.race['candidates']: - self.assertEqual(cand['winner'], '') From 1c40d45cabcdde01069150ec2b633b2c19e810b1 Mon Sep 17 00:00:00 2001 From: Serdar Tumgoren Date: Fri, 4 Mar 2016 16:22:26 -0800 Subject: [PATCH 2/2] clobber elex2 dir --- elex2/__init__.py | 0 elex2/election_results.py | 164 ------------------ elex2/tests/__init__.py | 0 elex2/tests/sample_results.csv | 5 - elex2/tests/sample_results_parsed.json | 52 ------ .../tests/sample_results_parsed_tie_race.json | 52 ------ elex2/tests/test_parser.py | 19 -- elex2/tests/test_summary.py | 65 ------- 8 files changed, 357 deletions(-) delete mode 100644 elex2/__init__.py delete mode 100644 elex2/election_results.py delete mode 100644 elex2/tests/__init__.py delete mode 100644 elex2/tests/sample_results.csv delete mode 100644 elex2/tests/sample_results_parsed.json delete mode 100644 elex2/tests/sample_results_parsed_tie_race.json delete mode 100644 elex2/tests/test_parser.py delete mode 100644 elex2/tests/test_summary.py diff --git a/elex2/__init__.py b/elex2/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex2/election_results.py b/elex2/election_results.py deleted file mode 100644 index 1d4bb91..0000000 --- a/elex2/election_results.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -In this second pass at the election_results.py script, we chop up the code into -functions and add a few tests. - -USAGE: - - python election_results.py - -OUTPUT: - - summary_results.csv - -""" -import csv -import urllib -from operator import itemgetter -from collections import defaultdict -from os.path import dirname, join - - -def main(): - # Download CSV of fake Virginia election results to root of project - path = join(dirname(dirname(__file__)), 'fake_va_elec_results.csv') - download_results(path) - # Process data - results = parse_and_clean(path) - summary = summarize(results) - write_csv(summary) - - -#### PRIMARY FUNCS #### -### These funcs perform the major steps of our application ### - -def download_results(path): - """Download CSV of fake Virginia election results from GDocs""" - url = "https://docs.google.com/spreadsheet/pub?key=0AhhC0IWaObRqdGFkUW1kUmp2ZlZjUjdTYV9lNFJ5RHc&output=csv" - urllib.urlretrieve(url, path) - -def parse_and_clean(path): - """Parse downloaded results file and perform various data clean-ups - - - RETURNS: - - Nested dictionary keyed by race, then candidate. - Candidate value is an array of dicts containing county level results. - - """ - # Create reader for ingesting CSV as array of dicts - reader = csv.DictReader(open(path, 'rb')) - - # Use defaultdict to automatically create non-existent keys with an empty dictionary as the default value. - # See https://pydocs2cn.readthedocs.org/en/latest/library/collections.html#defaultdict-objects - results = defaultdict(dict) - - # Initial data clean-up - for row in reader: - # Parse name into first and last - row['last_name'], row['first_name'] = [name.strip() for name in row['candidate'].split(',')] - # Convert total votes to an integer - row['votes'] = int(row['votes']) - - # Store county-level results by slugified office and district (if there is one), - # then by candidate party and raw name - race_key = row['office'] - if row['district']: - race_key += "-%s" % row['district'] - # Create unique candidate key from party and name, in case multiple candidates have same - cand_key = "-".join((row['party'], row['candidate'])) - # Below, setdefault initializes empty dict and list for the respective keys if they don't already exist. - race = results[race_key] - race.setdefault(cand_key, []).append(row) - - return results - - -def summarize(results): - """Tally votes for Races and candidates and assign winner flag. - - RETURNS: - - Dictionary of results - - """ - summary = defaultdict(dict) - - for race_key, cand_results in results.items(): - all_votes = 0 - cands = [] - for cand_key, results in cand_results.items(): - # Populate a new candidate dict using one set of county results - cand = { - 'first_name': results[0]['first_name'], - 'last_name': results[0]['last_name'], - 'party': results[0]['party'], - 'winner': '', - } - # Calculate candidate total votes - cand_total_votes = sum([result['votes'] for result in results]) - cand['votes'] = cand_total_votes - # Add cand totals to racewide vote count - all_votes += cand_total_votes - # And stash the candidate's data - cands.append(cand) - - # sort cands from highest to lowest vote count - sorted_cands = sorted(cands, key=itemgetter('votes'), reverse=True) - - # Determine winner, if any - first = sorted_cands[0] - second = sorted_cands[1] - - if first['votes'] != second['votes']: - first['winner'] = 'X' - - # Get race metadata from one set of results - result = cand_results.values()[0][0] - # Add results to output - summary[race_key] = { - 'all_votes': all_votes, - 'date': result['date'], - 'office': result['office'], - 'district': result['district'], - 'candidates': sorted_cands, - } - - return summary - - -def write_csv(summary): - """Generates CSV from summary election results data - - CSV is written to 'summary_results.csv' file, inside same directory - as this module. - - """ - outfile = join(dirname((__file__)), 'summary_results.csv') - with open(outfile, 'wb') as fh: - # Limit output to cleanly parsed, standardized values - fieldnames = [ - 'date', - 'office', - 'district', - 'last_name', - 'first_name', - 'party', - 'all_votes', - 'votes', - 'winner', - ] - writer = csv.DictWriter(fh, fieldnames, extrasaction='http://github.com/ignore', quoting=csv.QUOTE_MINIMAL) - writer.writeheader() - for race, results in summary.items(): - cands = results.pop('candidates') - for cand in cands: - results.update(cand) - writer.writerow(results) - - -# Q: What on earth is this __name__ == __main__ thing? -# A: Syntax that let's you execute a module as a script. -# http://docs.python.org/2/tutorial/modules.html#executing-modules-as-scripts -if __name__ == '__main__': - main() diff --git a/elex2/tests/__init__.py b/elex2/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/elex2/tests/sample_results.csv b/elex2/tests/sample_results.csv deleted file mode 100644 index 13cfcd8..0000000 --- a/elex2/tests/sample_results.csv +++ /dev/null @@ -1,5 +0,0 @@ -date,office,district,county,candidate,party,votes -2012-11-06,President,,Some County,"Smith, Joe",GOP,10 -2012-11-06,President,,Some County,"Doe, Jane",DEM,11 -2012-11-06,President,,Another County,"Smith, Joe",GOP,5 -2012-11-06,President,,Another County,"Doe, Jane",DEM,5 diff --git a/elex2/tests/sample_results_parsed.json b/elex2/tests/sample_results_parsed.json deleted file mode 100644 index ca32801..0000000 --- a/elex2/tests/sample_results_parsed.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "President": { - "DEM-Doe, Jane": [ - { - "candidate": "Doe, Jane", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 11 - }, - { - "candidate": "Doe, Jane", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 5 - } - ], - "GOP-Smith, Joe": [ - { - "candidate": "Smith, Joe", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 10 - }, - { - "candidate": "Smith, Joe", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 5 - } - ] - } -} \ No newline at end of file diff --git a/elex2/tests/sample_results_parsed_tie_race.json b/elex2/tests/sample_results_parsed_tie_race.json deleted file mode 100644 index 74a98b8..0000000 --- a/elex2/tests/sample_results_parsed_tie_race.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "President": { - "DEM-Doe, Jane": [ - { - "candidate": "Doe, Jane", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 10 - }, - { - "candidate": "Doe, Jane", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Jane", - "last_name": "Doe", - "office": "President", - "party": "DEM", - "votes": 5 - } - ], - "GOP-Smith, Joe": [ - { - "candidate": "Smith, Joe", - "county": "Some County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 10 - }, - { - "candidate": "Smith, Joe", - "county": "Another County", - "date": "2012-11-06", - "district": "", - "first_name": "Joe", - "last_name": "Smith", - "office": "President", - "party": "GOP", - "votes": 5 - } - ] - } -} diff --git a/elex2/tests/test_parser.py b/elex2/tests/test_parser.py deleted file mode 100644 index 78e4dd5..0000000 --- a/elex2/tests/test_parser.py +++ /dev/null @@ -1,19 +0,0 @@ -from os.path import dirname, join -from unittest import TestCase - -from elex2.election_results import parse_and_clean - - - -class TestParser(TestCase): - - def test_name_parsing(self): - "Parser should split full candidate name into first and last names" - path = join(dirname(__file__), 'sample_results.csv') - results = parse_and_clean(path) - race_key = 'President' - cand_key = 'GOP-Smith, Joe' - # Get one county result - smith = results[race_key][cand_key][0] - self.assertEqual(smith['first_name'], 'Joe') - self.assertEqual(smith['last_name'], 'Smith') diff --git a/elex2/tests/test_summary.py b/elex2/tests/test_summary.py deleted file mode 100644 index 27f6009..0000000 --- a/elex2/tests/test_summary.py +++ /dev/null @@ -1,65 +0,0 @@ -from os.path import dirname, join -from unittest import TestCase -import json - -from elex2.election_results import summarize - - -class TestSummaryResults(TestCase): - - # Read the results of the parse_and_clean function stored in a test fixture - json_file = open(join(dirname(__file__), 'sample_results_parsed.json'), 'rb') - SAMPLE_RESULTS = json.load(json_file) - # Q: Why aren't we just using the parse_and_clean method instead of - # using a snapshot of that function's output? - # A: To achieve better test isolation! - - # Q: Why aren't reading in the JSON in a setUp method? - # A: setUp is called before each test method. This ensures we only - # incur the overhead of reading in the JSON once. In python2.7 or newer, - # you should use the setUpClass method instead of a class attribute. - # http://docs.python.org/2/library/unittest.html#unittest.TestCase.setUpClass - - # We will, however, use the setUp method to call the summarize - # funciton afresh before each of our test methods. - def setUp(self): - results = summarize(self.SAMPLE_RESULTS) - self.race = results['President'] - - def test_racewide_vote_total(self): - "Summary results should be annotated with total votes cast in race" - self.assertEqual(self.race['all_votes'], 31) - - def test_candiate_vote_totals(self): - "Summary candidates should reflect total votes from all counties" - # Loop through candidates and find Smith rather than relying on - # default sorting of candidates, which would make this test brittle - # the implementation changed. - smith = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Smith'][0] - self.assertEqual(smith['votes'], 15) - - def test_winner_has_flag(self): - "Winner flag should be assigned to candidates with most votes" - doe = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Doe'][0] - self.assertEqual(doe['winner'], 'X') - - def test_loser_has_no_winner_flag(self): - "Winner flag should be not be assigned to candidate with that does not have highest vote total" - smith = [cand for cand in self.race['candidates'] if cand['last_name'] == 'Smith'][0] - self.assertEqual(smith['winner'], '') - - -class TestTieRace(TestCase): - - # Q: Why do we need a new class and fixture for this race? - # A: So that we can change the vote counts so that we have a tie, of course! - # We don't *need* a new test class, but hey, why not? - json_file = open(join(dirname(__file__), 'sample_results_parsed_tie_race.json'), 'rb') - SAMPLE_RESULTS = json.load(json_file) - - def test_tie_race_winner_flags(self): - "Winner flag should not be assigned to any candidate in a tie race" - results = summarize(self.SAMPLE_RESULTS) - race = results['President'] - for cand in race['candidates']: - self.assertEqual(cand['winner'], '')