├── .gitignore ├── lib ├── campaign_cash │ ├── version.rb │ ├── form.rb │ ├── individual_contribution.rb │ ├── late_contribution.rb │ ├── electioneering_communication.rb │ ├── contribution.rb │ ├── filing_summary.rb │ ├── filing.rb │ ├── base.rb │ ├── independent_expenditure.rb │ ├── president.rb │ ├── candidate.rb │ └── committee.rb └── campaign_cash.rb ├── Gemfile ├── Rakefile ├── test ├── campaign_cash │ ├── test_form.rb │ ├── test_filing_summary.rb │ ├── test_late_contribution.rb │ ├── test_individual_contribution.rb │ ├── test_filing.rb │ ├── test_electioneering_communication.rb │ ├── test_independent_expenditure.rb │ ├── test_president.rb │ ├── test_candidate.rb │ └── test_committee.rb └── test_helper.rb ├── LICENSE ├── Gemfile.lock ├── campaign_cash.gemspec └── README.rdoc /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | *nyt_api_key.rb 5 | -------------------------------------------------------------------------------- /lib/campaign_cash/version.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | VERSION = "2.9.2" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | # Specify your gem's dependencies in campaign_cash.gemspec 3 | gemspec -------------------------------------------------------------------------------- /lib/campaign_cash.rb: -------------------------------------------------------------------------------- 1 | %w(base candidate committee contribution individual_contribution filing filing_summary form independent_expenditure president electioneering_communication late_contribution).each do |f| 2 | require File.join(File.dirname(__FILE__), '../lib/campaign_cash', f) 3 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | include Rake::DSL 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rake/testtask' 6 | Rake::TestTask.new(:test) do |test| 7 | test.libs << 'lib' << 'test' 8 | test.pattern = 'test/**/test_*.rb' 9 | test.verbose = true 10 | end 11 | 12 | task :default => :test 13 | -------------------------------------------------------------------------------- /test/campaign_cash/test_form.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestForm < Minitest::Test 4 | include CampaignCash 5 | 6 | context "form types" do 7 | setup do 8 | @forms = Form.form_types 9 | end 10 | 11 | should "return a list of objects of the Filing type" do 12 | assert_kind_of(Form, @forms.first) 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /test/campaign_cash/test_filing_summary.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestFilingSummary < Minitest::Test 4 | include CampaignCash 5 | 6 | context "filing summaries" do 7 | setup do 8 | @filing_summary = FilingSummary.by_id(751678) 9 | end 10 | should "get a filing by form id" do 11 | assert_kind_of(FilingSummary, @filing_summary) 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/campaign_cash/form.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class Form < Base 3 | 4 | attr_reader :id, :name 5 | 6 | def initialize(params={}) 7 | params.each_pair do |k,v| 8 | instance_variable_set("@#{k}", v) 9 | end 10 | end 11 | 12 | def self.create(params={}) 13 | self.new id: params['id'], 14 | name: params['name'] 15 | end 16 | 17 | def self.form_types 18 | reply = Base.invoke("#{Base::CURRENT_CYCLE}/filings/types") 19 | results = reply['results'] 20 | @forms = results.map{|c| Form.create(c)} 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 ProPublica Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this library except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'rubygems' 3 | require 'shoulda' 4 | require 'json' 5 | require 'ostruct' 6 | 7 | %w(base candidate committee contribution individual_contribution filing filing_summary form independent_expenditure president electioneering_communication late_contribution).each do |f| 8 | require File.join(File.dirname(__FILE__), '../lib/campaign_cash', f) 9 | end 10 | 11 | # set your NYT Campaign Finance API key as an environment variable to run the tests 12 | API_KEY = ENV['NYT_CAMPFIN_API_KEY'] 13 | CampaignCash::Base.api_key = API_KEY 14 | 15 | module TestCampaignCash 16 | end 17 | -------------------------------------------------------------------------------- /test/campaign_cash/test_late_contribution.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestLateContribution < Minitest::Test 4 | include CampaignCash 5 | 6 | context "get late contributions" do 7 | cmte = LateContribution.committee("C00505255") 8 | cand = LateContribution.candidate("H2NC09092") 9 | 10 | should "return a list of objects of the LateContribution type from a committee or an empty list" do 11 | if cmte.size > 0 12 | assert_kind_of(LateContribution, cmte.first) 13 | else 14 | assert_equal([], cmte) 15 | end 16 | end 17 | 18 | should "return a list of objects of the LateContribution type from a candidate or an empty list for a cmte" do 19 | if cand.size > 0 20 | assert_kind_of(LateContribution, cand.first) 21 | else 22 | assert_equal([], cand) 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | campaign_cash (2.9.2) 5 | json 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activesupport (5.2.0) 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | i18n (>= 0.7, < 2) 13 | minitest (~> 5.1) 14 | tzinfo (~> 1.1) 15 | concurrent-ruby (1.0.5) 16 | i18n (1.0.1) 17 | concurrent-ruby (~> 1.0) 18 | json (2.1.0) 19 | minitest (5.11.3) 20 | rake (12.3.1) 21 | shoulda (3.5.0) 22 | shoulda-context (~> 1.0, >= 1.0.1) 23 | shoulda-matchers (>= 1.4.1, < 3.0) 24 | shoulda-context (1.2.2) 25 | shoulda-matchers (2.8.0) 26 | activesupport (>= 3.0.0) 27 | thread_safe (0.3.6) 28 | tzinfo (1.2.5) 29 | thread_safe (~> 0.1) 30 | 31 | PLATFORMS 32 | ruby 33 | 34 | DEPENDENCIES 35 | bundler (>= 1.0.0) 36 | campaign_cash! 37 | rake (>= 10.0) 38 | shoulda 39 | 40 | BUNDLED WITH 41 | 1.16.0 42 | -------------------------------------------------------------------------------- /test/campaign_cash/test_individual_contribution.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestIndividualContribution < Minitest::Test 4 | include CampaignCash 5 | 6 | context "get contributions" do 7 | objs_collection = [] 8 | objs_collection << IndividualContribution.committee("C00496497") 9 | objs_collection << IndividualContribution.filing("724196") 10 | 11 | should "return a list of objects of the IndividualContribution type from a committee or an empty list" do 12 | if objs_collection.first.size > 0 13 | assert_kind_of(IndividualContribution, objs_collection.first) 14 | else 15 | assert_equal([], objs_collection.first) 16 | end 17 | end 18 | 19 | should "return a list of objects of the IndividualContribution type from a filing or an empty list for a cmte" do 20 | if objs_collection[1].size > 0 21 | assert_kind_of(IndividualContribution, objs_collection[1]) 22 | else 23 | assert_equal([], objs_collection[1]) 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /campaign_cash.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "campaign_cash/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "campaign_cash" 7 | s.version = CampaignCash::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ['Derek Willis'] 10 | s.email = ['derek.willis@propublica.org'] 11 | s.homepage = "http://rubygems.org/gems/campaign_cash" 12 | s.description = "A client for the ProPublica Campaign Finance API" 13 | s.summary = "Following the money." 14 | 15 | s.required_rubygems_version = ">= 1.3.6" 16 | s.rubyforge_project = "campaign_cash" 17 | s.add_runtime_dependency "json" 18 | 19 | s.add_development_dependency "rake", ">= 10.0" 20 | s.add_development_dependency "bundler", ">= 1.0.0" 21 | s.add_development_dependency "shoulda" 22 | 23 | s.files = `git ls-files`.split("\n") 24 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 25 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 26 | s.require_paths = ["lib"] 27 | end 28 | -------------------------------------------------------------------------------- /test/campaign_cash/test_filing.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestFiling < Minitest::Test 4 | include CampaignCash 5 | 6 | context "a day" do 7 | setup do 8 | month, day, year = "11", "27", "2010" 9 | @filings = Filing.date(year, month, day) 10 | end 11 | 12 | should "return a list of objects of the Filing type" do 13 | assert_kind_of(Filing, @filings.first) 14 | end 15 | end 16 | 17 | context "today's filings" do 18 | setup do 19 | @filings = Filing.today 20 | end 21 | 22 | should "return a list of objects of the Filing type or an empty list" do 23 | if @filings.size > 0 24 | assert_kind_of(Filing, @filings.first) 25 | else 26 | assert_equal([], @filings) 27 | end 28 | end 29 | end 30 | 31 | context "recent statements of organization" do 32 | setup do 33 | @filings = Filing.by_type(2012, "F1") 34 | end 35 | 36 | should "return a list of the 20 most recent Filings that are statements of organization" do 37 | assert_equal @filings.size, 20 38 | assert_equal @filings.first.report_title, "STATEMENT OF ORGANIZATION" 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /test/campaign_cash/test_electioneering_communication.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestElectioneeringCommunication < Minitest::Test 4 | include CampaignCash 5 | 6 | context "get electioneering communications" do 7 | latest = ElectioneeringCommunication.latest 8 | committee = ElectioneeringCommunication.committee("C30001655") 9 | date = ElectioneeringCommunication.date("02/06/2012") 10 | 11 | should "return a list of latest objects of the ElectioneeringCommunication type or an empty list" do 12 | if latest.size > 0 13 | assert_kind_of(ElectioneeringCommunication, latest.first) 14 | else 15 | assert_equal([], latest) 16 | end 17 | end 18 | 19 | should "return a list of objects of the ElectioneeringCommunication type or an empty list for a cmte" do 20 | if committee.size > 0 21 | assert_kind_of(ElectioneeringCommunication, committee.first) 22 | else 23 | assert_equal([], committee) 24 | end 25 | end 26 | 27 | should "return a list of latest objects of the ElectioneeringCommunication type or an empty list by date" do 28 | if date.size > 0 29 | assert_kind_of(ElectioneeringCommunication, date.first) 30 | else 31 | assert_equal([], date) 32 | end 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /lib/campaign_cash/individual_contribution.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class IndividualContribution < Base 3 | 4 | attr_reader :zip5, :system_code, :occupation, :conduit_zip, :conduit_address_two, :city, :zip, :increased_limit, :donor_candidate, :conduit_name, :conduit_city, :prefix, :memo_text, :back_ref_tran_id, :aggregate_amount, :filing_id, :donor_office, :back_ref_sched_name, :address_two, :pac_name, :exclude, :conduit_address_one, :amount, :transaction_type, :lng, :flag_orgind, :date, :fec_form_type, :donor_state, :donor_cand_id, :last_name, :full_name, :employer, :donor_district, :conduit_state, :address_one, :tran_id, :suffix, :donor_cmte_id, :display_name, :transaction_description, :prigen, :memo_code, :linenumber, :lat, :amended_cd, :state, :middle_name, :first_name 5 | 6 | def initialize(params={}) 7 | params.each_pair do |k,v| 8 | instance_variable_set("@#{k}", v) 9 | end 10 | end 11 | 12 | def self.create(params={}) 13 | self.new(params) 14 | end 15 | 16 | def self.committee(fecid, offset=nil) 17 | cycle = CURRENT_CYCLE 18 | results = invoke("#{cycle}/contributions/committee/#{fecid}", {offset: offset})['results'] 19 | results.map{|c| IndividualContribution.create(c) } 20 | end 21 | 22 | def self.filing(form_id, offset=nil) 23 | cycle = CURRENT_CYCLE 24 | results = invoke("#{cycle}/contributions/filing/#{form_id}", {offset: offset})['results'] 25 | results.map{|c| IndividualContribution.create(c) } 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/campaign_cash/late_contribution.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class LateContribution < Base 3 | 4 | attr_reader :contribution_date, :contributor_prefix, :contributor_zip, :contributor_suffix, :contributor_state, 5 | :entity_type, :contributor_employer, :contributor_occupation, :fec_committee_id, :transaction_id, :contributor_last_name, 6 | :office_state, :contributor_fec_id, :contribution_amount, :contributor_street_1, :contributor_street_2, :contributor_city, 7 | :contributor_middle_name, :cycle, :fec_filing_id, :fec_candidate_id, :contributor_first_name, :contributor_organization_name 8 | 9 | def initialize(params={}) 10 | params.each_pair do |k,v| 11 | instance_variable_set("@#{k}", v) 12 | end 13 | end 14 | 15 | def self.create(params={}) 16 | self.new(params) 17 | end 18 | 19 | def self.latest(offset = nil) 20 | cycle = CURRENT_CYCLE 21 | results = invoke("#{cycle}/contributions/48hour", {offset: offset})['results'] 22 | results.map {|obj| create(obj)} 23 | end 24 | 25 | def self.candidate(candidate_id, offset = nil) 26 | cycle = CURRENT_CYCLE 27 | results = invoke("#{cycle}/candidates/#{candidate_id}/48hour", {offset: offset})['results'] 28 | results.map {|obj| create(obj)} 29 | end 30 | 31 | def self.committee(committee_id, offset = nil) 32 | cycle = CURRENT_CYCLE 33 | results = invoke("#{cycle}/committees/#{committee_id}/48hour", {offset: offset})['results'] 34 | results.map {|obj| create(obj)} 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/campaign_cash/electioneering_communication.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class ElectioneeringCommunication < Base 3 | attr_reader :payee_zip, :committee_name, :entity_type, :filing_id, :payee_organization, :electioneering_communication_candidates, :amount, :fec_committee_id, :payee_city, :transaction_id, :amended_from, :communication_date, :payee_first_name, :payee_state, :back_reference_tran_id_number, :expenditure_date, :cycle, :payee_address_1, :back_reference_sched_name, :payee_address_2, :payee_last_name, :election_code, :payee_middle_name, :payee_suffix, :purpose, :unique_id, :filed_date 4 | 5 | def initialize(params={}) 6 | params.each_pair do |k,v| 7 | instance_variable_set("@#{k}", v) 8 | end 9 | end 10 | 11 | def self.create(params={}) 12 | self.new(params) 13 | end 14 | 15 | def self.latest(offset = nil) 16 | cycle = CURRENT_CYCLE 17 | results = invoke("#{cycle}/electioneering_communications", {offset: offset})['results'] 18 | results.map {|obj| ElectioneeringCommunication.create(obj)} 19 | end 20 | 21 | def self.committee(committee_id, offset = nil) 22 | cycle = CURRENT_CYCLE 23 | results = invoke("#{cycle}/committees/#{committee_id}/electioneering_communications", {offset: offset})['results'] 24 | results.map {|obj| ElectioneeringCommunication.create(obj)} 25 | end 26 | 27 | def self.date(date, offset = nil) 28 | cycle = CURRENT_CYCLE 29 | d = Date.strptime(date, '%m/%d/%Y') 30 | results = invoke("#{cycle}/electioneering_communications/#{d.year}/#{d.month}/#{d.day}", {offset: offset})['results'] 31 | results.map {|obj| ElectioneeringCommunication.create(obj)} 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/campaign_cash/test_independent_expenditure.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestIndependentExpenditure < Minitest::Test 4 | include CampaignCash 5 | 6 | context "latest independent expenditures" do 7 | setup do 8 | @independent_expenditures = IndependentExpenditure.latest 9 | end 10 | 11 | should "return a list of objects of the IndependentExpenditure type" do 12 | assert_kind_of(IndependentExpenditure, @independent_expenditures.first) 13 | end 14 | end 15 | 16 | context "independent expenditures by date" do 17 | setup do 18 | @independent_expenditures = IndependentExpenditure.date("12/13/2011") 19 | end 20 | 21 | should "return at least one independent expenditure against Mitt Romney" do 22 | assert_equal("P80003353", @independent_expenditures.select{|ie| ie.candidate == 'P80003353'}.first.candidate) 23 | end 24 | end 25 | 26 | context "a committee's independent expenditures in a cycle" do 27 | setup do 28 | @independent_expenditures = IndependentExpenditure.committee("C00490045", 2012) 29 | end 30 | 31 | should "return an array of IEs in the presidential race" do 32 | assert_equal("President", @independent_expenditures.first.office) 33 | end 34 | end 35 | 36 | context "independent expenditures about a given candidate" do 37 | setup do 38 | @independent_expenditures = IndependentExpenditure.candidate("P60003654", 2012) 39 | @candidate = Candidate.find('P60003654', 2012) 40 | end 41 | 42 | should "return an array of IEs about presidential candidate Newt Gingrich" do 43 | assert_equal("President", @independent_expenditures.first.office) 44 | assert_equal(@candidate.name, "GINGRICH, NEWT") 45 | end 46 | end 47 | 48 | context "independent expenditures in the presidential campaign" do 49 | setup do 50 | @independent_expenditures = IndependentExpenditure.president 51 | end 52 | 53 | should "return an array of IEs about the presidential campaign" do 54 | assert_equal(['President'], @independent_expenditures.map{|i| i.office }.uniq) 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /test/campaign_cash/test_president.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestPresident < Minitest::Test 4 | include CampaignCash 5 | 6 | context "President.summary" do 7 | setup do 8 | reply = Base.invoke('2012/president/totals', {}) 9 | @results = reply['results'] 10 | @summaries = @results.map{|r| President.create_summary(r)} 11 | @summary = @summaries.first 12 | end 13 | 14 | should "return an array of objects of the President type" do 15 | assert_kind_of(President, @summary) 16 | end 17 | 18 | %w(name total_contributions total_receipts).each do |attr| 19 | should "assign the value of the @#{attr} attribute from the '#{attr}' key in the hash" do 20 | assert_equal(@results.first[attr], @summary.send(attr)) 21 | end 22 | end 23 | 24 | should "assign the office to 'president'" do 25 | assert_equal('president', @summary.office) 26 | end 27 | 28 | should "assign the party to 'REP' if the candidate is republican or 'DEM' if democrat" do 29 | party = @results.first['party'] 30 | party = "REP" if party == "R" 31 | party = "DEM" if party == "D" 32 | assert_equal(party, @summary.party) 33 | end 34 | 35 | end 36 | 37 | context "President.detail" do 38 | setup do 39 | reply = Base.invoke('2012/president/candidates/C00496034', {}) 40 | @results = reply['results'] 41 | @detail = President.create_detail(@results.first) 42 | end 43 | 44 | should "return an array of objects of the President type" do 45 | assert_kind_of(President, @detail) 46 | end 47 | 48 | %w(net_primary_contributions total_refunds).each do |attr| 49 | should "assign the value of the @#{attr} attribute from the '#{attr}' key in the hash" do 50 | assert_equal(@results.first[attr], @detail.send(attr)) 51 | end 52 | end 53 | 54 | should "assign the office to 'president'" do 55 | assert_equal('president', @detail.office) 56 | end 57 | 58 | should "assign the party to 'REP' if the candidate is republican or 'DEM' if democrat" do 59 | party = @results.first['party'] 60 | party = "REP" if party == "R" 61 | party = "DEM" if party == "D" 62 | assert_equal(party, @detail.party) 63 | end 64 | end 65 | end -------------------------------------------------------------------------------- /lib/campaign_cash/contribution.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class Contribution < Base 3 | 4 | attr_reader :date, :candidate_uri, :primary_general, :amount, :state, :name, 5 | :image_uri, :party, :district, :committee_uri, :results, :total_results, :total_amount, 6 | :cycle, :record_number, :transaction_id, :transaction_type 7 | 8 | def initialize(params={}) 9 | params.each_pair do |k,v| 10 | instance_variable_set("@#{k}", v) 11 | end 12 | end 13 | 14 | def self.to_candidate(params={}) 15 | self.new committee: parse_committee(params['committee']), 16 | cycle: params['cycle'].to_i, 17 | total_amount: params['total_amount'].to_f, 18 | results: params['results'].map{|c| OpenStruct.new({ 19 | candidate: parse_candidate(params['candidate_uri']), 20 | date: date_parser(c['date']), 21 | primary_general: c['primary_general'], 22 | amount: c['amount'].to_f, 23 | state: c['state'], 24 | name: c['name'], 25 | image_uri: c['image_uri'], 26 | party: c['party'], 27 | district: c['district'], 28 | record_number: c['record_number'], 29 | transaction_type: c['transaction_type'], 30 | transaction_id: c['transaction_id'] 31 | })} 32 | 33 | end 34 | 35 | def self.all_candidates(params={}) 36 | self.new committee: parse_committee(params['committee']), 37 | cycle: params['cycle'].to_i, 38 | total_amount: params['total_amount'].to_f, 39 | total_results: params['total_results'].to_i, 40 | results: params['results'].map{|c| OpenStruct.new({ 41 | date: date_parser(c['date']), 42 | candidate: parse_candidate(c['candidate_uri']), 43 | primary_general: c['primary_general'], 44 | amount: c['amount'].to_f, 45 | state: c['state'], 46 | name: c['name'], 47 | image_uri: c['image_uri'], 48 | party: c['party'], 49 | district: c['district'], 50 | record_number: c['record_number'], 51 | transaction_type: c['transaction_type'], 52 | transaction_id: c['transaction_id'] 53 | })} 54 | 55 | end 56 | 57 | def self.find(fecid, cycle=CURRENT_CYCLE, candidate=nil) 58 | if candidate 59 | reply = invoke("#{cycle}/committees/#{fecid}/contributions/candidates/#{candidate}") 60 | to_candidate(reply) 61 | else 62 | reply = invoke("#{cycle}/committees/#{fecid}/contributions") 63 | all_candidates(reply) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/campaign_cash/filing_summary.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class FilingSummary < Base 3 | 4 | attr_reader :pac_contributions_period, :contributions_2500, :party_contributions_cycle, :fundraising_offsets_period, :primary_general, :pac_refunds_cycle, :party_refunds_cycle, :fundraising_expenses_period, :net_party_contributions, :num_contributions_200_499, :individual_refunds_cycle, :candidate_contributions_period, :legal_offsets_cycle, :num_contributions_500_1499, :net_operating_expenses, :total_debts_owed, :refunds_200_499, :refunds_1500_2499, :transfers_out_period, :net_primary, :liquidate_period, :contributions_200_499, :operating_offsets_cycle, :num_contributions_1500_2499, :candidate_loan_repayments_period, :committee_uri, :federal_funds_cycle, :net_candidate_contributions, :candidate_loans_period, :filing_id, :net_pac_contributions, :num_refunds_200_499, :candidate_loan_repayments_cycle, :total_offsets_cycle, :party_contributions_period, :net_individual_contributions, :num_refunds_1500_2499, :net_general, :total_receipts_cycle, :federal_funds_period, :transfers_out_cycle, :cycle, :other_loan_repayments_period, :contributions_500_1499, :legal_offsets_period, :date_coverage_from, :contributions_less_than_200, :fec_form_type, :candidate_contributions_cycle, :cash_on_hand_beginning, :individual_refunds_period, :fundraising_expenses_cycle, :num_refunds_500_1499, :cash_on_hand_close, :num_contributions_less_than_200, :total_loans_cycle, :candidate_uri, :contributions_1500_2499, :refunds_2500, :flag_most_current_report, :total_loan_repayments_period, :other_loans_period, :operating_expenditures_cycle, :other_disbursements_period, :total_loans_period, :transfers_in_period, :transfers_in_cycle, :report, :legal_expenses_period, :candidate_loans_cycle, :individual_contributions_period, :other_disbursements_cycle, :total_disbursements_period, :other_loans_cycle, :net_transfers_in, :net_disbursements, :operating_expenditures_period, :fundraising_offsets_cycle, :date_coverage_to, :total_loan_repayments_cycle, :total_disbursements_cycle, :party_refunds_period, :individual_contributions_cycle, :net_total_contributions, :other_loan_repayments_cycle, :total_refunds_cycle, :total_receipts_period, :total_offsets_period, :total_contributions_period, :total_contributions_cycle, :refunds_500_1499, :pac_refunds_period, :flag_valid_report, :legal_expenses_cycle, :num_refunds_less_than_200, :refunds_less_than_200, :pac_contributions_cycle, :net_legal_expenses, :net_fundraising_expenses, :num_contributions_2500, :total_refunds_period, :operating_offsets_period, :num_refunds_2500 5 | 6 | def initialize(params={}) 7 | params.each_pair do |k,v| 8 | instance_variable_set("@#{k}", v) 9 | end 10 | end 11 | 12 | def self.create(params={}) 13 | self.new(params) 14 | end 15 | 16 | def self.by_id(id) 17 | cycle = CURRENT_CYCLE 18 | result = FilingSummary.create(Base.invoke("#{cycle}/filings/#{id}")["results"]) 19 | end 20 | end 21 | end 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/campaign_cash/filing.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class Filing < Base 3 | 4 | attr_reader :committee_name, :date_coverage_from, :amended_uri, :fec_uri, :date_coverage_to, :committee, :report_title, :amended, :date_filed, 5 | :cycle, :form_type, :original_filing, :original_uri, :paper, :committee_type, :filing_id, :receipts_total, :disbursements_total, :cash_on_hand, 6 | :is_amendment 7 | 8 | def initialize(params={}) 9 | params.each_pair do |k,v| 10 | instance_variable_set("@#{k}", v) 11 | end 12 | end 13 | 14 | def self.create(params={}, name=nil) 15 | self.new date_coverage_from: date_parser(params['date_coverage_from']), 16 | date_coverage_to: date_parser(params['date_coverage_to']), 17 | date_filed: date_parser(params['date_filed']), 18 | committee: parse_committee(params['committee']), 19 | report_title: params['report_title'], 20 | fec_uri: params['fec_uri'], 21 | amended: params['amended'], 22 | amended_uri: params['amended_uri'], 23 | original_filing: params['original_filing'], 24 | original_uri: params['original_uri'], 25 | paper: params['paper'], 26 | form_type: params['form_type'], 27 | filing_id: params['filing_id'], 28 | committee_type: Committee.get_committee_type(params['committee_type']), 29 | committee_name: params['committee_name'], 30 | receipts_total: params['receipts_total'], 31 | contributions_total: params['contributions_total'], 32 | disbursements_total: params['disbursements_total'], 33 | cash_on_hand: params['cash_on_hand'], 34 | is_amendment: params['is_amendment'] 35 | end 36 | 37 | def self.today(offset=nil) 38 | cycle=CURRENT_CYCLE 39 | reply = Base.invoke("#{cycle}/filings", {offset: offset}) 40 | results = reply['results'] 41 | results.map{|c| Filing.create(c)} 42 | end 43 | 44 | def self.date(year, month, day, offset=nil) 45 | cycle = cycle_from_date(Date.strptime("#{month}/#{day}/#{year}", '%m/%d/%Y')) 46 | reply = Base.invoke("#{cycle}/filings/#{year}/#{month}/#{day}", {offset: offset}) 47 | results = reply['results'] 48 | results.map{|c| Filing.create(c)} 49 | end 50 | 51 | def self.search(query, cycle, offset=nil) 52 | reply = Base.invoke("#{cycle}/filings/search", {query: query, offset: offset}) 53 | results = reply['results'] 54 | results.map{|c| Filing.create(c)} 55 | end 56 | 57 | def self.form_types 58 | cycle=CURRENT_CYCLE 59 | reply = Base.invoke("#{cycle}/filings/types",{}) 60 | results = reply['results'] 61 | results.map{|ft| OpenStruct.new({id: ft['id'], name: ft['name'].strip})} 62 | end 63 | 64 | def self.amendments(offset=nil) 65 | cycle=CURRENT_CYCLE 66 | reply = Base.invoke("#{cycle}/filings/amendments", {offset: offset}) 67 | results = reply['results'] 68 | results.map{|c| Filing.create(c)} 69 | end 70 | 71 | def self.by_type(cycle, form_type) 72 | reply = Base.invoke("#{cycle}/filings/types/#{form_type}") 73 | results = reply['results'] 74 | results.map{|c| Filing.create(c)} 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/campaign_cash/base.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'open-uri' 3 | require 'json' 4 | require 'date' 5 | require 'ostruct' 6 | 7 | module CampaignCash 8 | class Base 9 | API_SERVER = 'api.nytimes.com' 10 | API_VERSION = 'v3' 11 | API_NAME = 'elections/us' 12 | API_BASE = "/svc/#{API_NAME}/#{API_VERSION}/finances" 13 | CURRENT_CYCLE = 2016 14 | 15 | @@api_key = nil 16 | @@copyright = nil 17 | 18 | class << self 19 | 20 | ## 21 | # The copyright footer to be placed at the bottom of any data from the New York Times. Note this is only set after an API call. 22 | def copyright 23 | @@copyright 24 | end 25 | 26 | def cycle 27 | @@cycle 28 | end 29 | 30 | def base_uri 31 | @@base_uri 32 | end 33 | 34 | ## 35 | # Set the API key used for operations. This needs to be called before any requests against the API. To obtain an API key, go to http://developer.nytimes.com/ 36 | def api_key=(key) 37 | @@api_key = key 38 | end 39 | 40 | def api_key 41 | @@api_key 42 | end 43 | 44 | def date_parser(date) 45 | date ? Date.strptime(date, '%Y-%m-%d') : nil 46 | end 47 | 48 | def parse_candidate(candidate) 49 | return nil if candidate.nil? 50 | candidate.split('/').last.split('.').first 51 | end 52 | 53 | def parse_committee(committee) 54 | return nil if committee.nil? 55 | committee.split('/').last.split('.').first 56 | end 57 | 58 | # Returns the election cycle (even-numbered) from a date. 59 | def cycle_from_date(date=Date.today) 60 | date.year.even? ? date.year : date.year+1 61 | end 62 | 63 | def check_offset(offset) 64 | raise "Offset must be a multiple of 20" if offset % 20 != 0 65 | end 66 | 67 | ## 68 | # Builds a request URI to call the API server 69 | def build_request_url(path, params) 70 | URI::HTTP.build host: API_SERVER, 71 | path: "#{API_BASE}/#{path}.json", 72 | query: params.map {|k,v| "#{k}=#{v}"}.join('&') 73 | end 74 | 75 | def invoke(path, params={}) 76 | begin 77 | if @@api_key.nil? 78 | raise "You must initialize the API key before you run any API queries" 79 | end 80 | 81 | full_params = params.merge({"api-key" => @@api_key}) 82 | full_params.delete_if {|k,v| v.nil?} 83 | 84 | check_offset(params[:offset]) if params[:offset] 85 | 86 | uri = build_request_url(path, full_params) 87 | 88 | reply = uri.read 89 | parsed_reply = JSON.parse reply 90 | 91 | if parsed_reply.nil? 92 | raise "Empty reply returned from API" 93 | end 94 | 95 | @@copyright = parsed_reply['copyright'] 96 | @@cycle = parsed_reply['cycle'] 97 | @@base_uri = parsed_reply['base_uri'] 98 | 99 | parsed_reply 100 | rescue OpenURI::HTTPError => e 101 | if e.message =~ /^404/ 102 | return nil 103 | end 104 | 105 | raise "Error connecting to URL #{uri} #{e}" 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/campaign_cash/test_candidate.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestCandidate < Minitest::Test 4 | include CampaignCash 5 | 6 | context "Candidate.create" do 7 | setup do 8 | reply = Base.invoke('2010/candidates/H4NY07011', {}) 9 | @result = reply['results'].first 10 | @candidate = Candidate.create(@result) 11 | end 12 | 13 | should "return an object of the Candidate type" do 14 | assert_kind_of(Candidate, @candidate) 15 | end 16 | 17 | %w(name id party fec_uri).each do |attr| 18 | should "assign the value of the @#{attr} attribute from the '#{attr}' key in the hash" do 19 | assert_equal(@result[attr], @candidate.send(attr)) 20 | end 21 | end 22 | 23 | should "assign the committee_id to a stripped version of the attribute from the 'committee' key in the hash" do 24 | assert_equal(Base.parse_committee(@result['committee']), @candidate.committee_id) 25 | end 26 | 27 | end 28 | 29 | context "Candidate search" do 30 | setup do 31 | reply = Base.invoke('2014/candidates/search', {:query => "Udall"}) 32 | @results = reply['results'] 33 | @candidates = @results.map{|c| Candidate.create_from_search_results(c)} 34 | end 35 | 36 | should "return two candidate objects" do 37 | assert_equal @candidates.size, 2 38 | assert_kind_of(Candidate, @candidates.first) 39 | assert_kind_of(Candidate, @candidates.last) 40 | end 41 | 42 | should "assign the committee_id to a stripped version of the attribute from the 'committee' key in the hash" do 43 | assert_equal(Base.parse_committee(@results.first['committee']), @candidates.first.committee_id) 44 | end 45 | 46 | end 47 | 48 | context "New Candidates" do 49 | setup do 50 | reply = Base.invoke('2012/candidates/new', {}) 51 | results = reply['results'] 52 | @candidates = results.map{|c| Candidate.create(c)} 53 | end 54 | 55 | should "return 20 new candidates" do 56 | assert_equal @candidates.size, 20 57 | assert_kind_of(Candidate, @candidates.first) 58 | assert_kind_of(Candidate, @candidates.last) 59 | end 60 | end 61 | 62 | context "candidate leaders" do 63 | setup do 64 | reply = Base.invoke('2012/candidates/leaders/end-cash', {}) 65 | results = reply['results'] 66 | @candidates = results.map{|c| Candidate.create(c)} 67 | end 68 | 69 | should "return 20 candidates each with a greater end_cash value than the next" do 70 | assert (@candidates[0].end_cash >= @candidates[1].end_cash) 71 | assert (@candidates[1].end_cash >= @candidates[2].end_cash) 72 | assert (@candidates[2].end_cash >= @candidates[3].end_cash) 73 | end 74 | end 75 | 76 | context "state candidates" do 77 | should "return 32 total candidates from Rhode Island" do 78 | assert_equal Candidate.state('RI', nil, nil, 2010).size, 24 79 | end 80 | 81 | should "return 29 House candidates from Rhode Island" do 82 | assert_equal Candidate.state('RI', "house", nil, 2010).size, 24 83 | end 84 | 85 | should "return least 3 Senate candidates from Rhode Island" do 86 | assert_equal Candidate.state('RI', "senate", nil, 2012).size, 3 87 | end 88 | 89 | should "return 17 House candidates from District 1 of Rhode Island" do 90 | assert_equal Candidate.state('RI', "house", 1, 2010).size, 14 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /lib/campaign_cash/independent_expenditure.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class IndependentExpenditure < Base 3 | 4 | attr_reader :committee, :district, :state, :committee_name, :purpose, :candidate, :candidate_name, 5 | :support_or_oppose, :date, :amount, :office, :amendment, :date_received, :payee, :fec_uri, 6 | :transaction_id, :unique_id, :filing_id, :amended_from 7 | 8 | def initialize(params={}) 9 | params.each_pair do |k,v| 10 | instance_variable_set("@#{k}", v) 11 | end 12 | end 13 | 14 | def self.create(params={}) 15 | self.new committee: parse_committee(params['fec_committee']), 16 | committee_name: params['fec_committee_name'], 17 | candidate: parse_candidate(params['fec_candidate']), 18 | office: params['office'], 19 | state: params['state'] ? params['state'].strip : nil, 20 | district: params['district'], 21 | date: date_parser(params['date']), 22 | support_or_oppose: params['support_or_oppose'], 23 | payee: params['payee'], 24 | purpose: params['purpose'], 25 | amount: params['amount'].to_f, 26 | fec_uri: params['fec_uri'], 27 | date_received: date_parser(params['date_received']), 28 | amendment: params['amendment'], 29 | transaction_id: params['transaction_id'], 30 | candidate_name: params['candidate_name'], 31 | filing_id: params['filing_id'].to_i, 32 | amended_from: params['amended_from'], # <= original filing ID will be nil if amendment is false 33 | # unique_id is a SHA1 of filing_id and transaction_id 34 | # If the expenditure is amended, the unique_id will be amended_from + transaction_id 35 | # so it can be used as an overrideable unique key 36 | unique_id: params['unique_id'] 37 | end 38 | 39 | def self.latest(offset=nil) 40 | reply = Base.invoke("#{Base::CURRENT_CYCLE}/independent_expenditures",{offset: offset}) 41 | results = reply['results'] 42 | results.map{|c| IndependentExpenditure.create(c)} 43 | end 44 | 45 | def self.date(date,offset=nil) 46 | d = Date.strptime(date, '%m/%d/%Y') 47 | cycle = cycle_from_date(d) 48 | reply = Base.invoke("#{cycle}/independent_expenditures/#{d.year}/#{d.month}/#{d.day}", {offset: offset}) 49 | results = reply['results'] 50 | results.map{|c| IndependentExpenditure.create(c)} 51 | end 52 | 53 | def self.committee(id, cycle, offset=nil) 54 | independent_expenditures = [] 55 | reply = Base.invoke("#{cycle}/committees/#{id}/independent_expenditures",{offset: offset}) 56 | results = reply['results'] 57 | comm = reply['fec_committee'] 58 | results.each do |result| 59 | result['fec_committee'] = comm 60 | independent_expenditures << IndependentExpenditure.create(result) 61 | end 62 | independent_expenditures 63 | end 64 | 65 | def self.candidate(id, cycle, offset=nil) 66 | independent_expenditures = [] 67 | reply = Base.invoke("#{cycle}/candidates/#{id}/independent_expenditures",{offset: offset}) 68 | results = reply['results'] 69 | cand = reply['fec_candidate'] 70 | results.each do |result| 71 | result['fec_candidate'] = cand 72 | independent_expenditures << IndependentExpenditure.create(result) 73 | end 74 | independent_expenditures 75 | end 76 | 77 | def self.president(cycle=CURRENT_CYCLE,offset=nil) 78 | reply = Base.invoke("#{cycle}/president/independent_expenditures",{offset: offset}) 79 | results = reply['results'] 80 | results.map{|c| IndependentExpenditure.create(c)} 81 | end 82 | 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/campaign_cash/test_committee.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TestCampaignCash::TestCommittee < Minitest::Test 4 | include CampaignCash 5 | 6 | context "Committee.create_from_api" do 7 | setup do 8 | reply = Base.invoke('2010/committees/C00312223', {}) 9 | @result = reply['results'].first 10 | @committee = Committee.create(@result) 11 | end 12 | 13 | should "return an object of the Committee type" do 14 | assert_kind_of(Committee, @committee) 15 | end 16 | 17 | %w(name id state party fec_uri candidate).each do |attr| 18 | should "assign the value of the @#{attr} attribute from the '#{attr}' key in the hash" do 19 | assert_equal(@result[attr], @committee.send(attr)) 20 | end 21 | end 22 | end 23 | 24 | context "Committee search" do 25 | setup do 26 | @committees = Committee.search("Obama for America", 2012) 27 | end 28 | 29 | should "return two committee objects" do 30 | assert_equal @committees.size, 1 31 | assert_kind_of(Committee, @committees.first) 32 | assert_kind_of(Committee, @committees.last) 33 | end 34 | end 35 | 36 | context "New Committees" do 37 | setup do 38 | reply = Base.invoke('2010/committees/new', {}) 39 | results = reply['results'] 40 | @committees = results.map{|c| Committee.create_from_search_results(c)} 41 | end 42 | 43 | should "return 20 new committees" do 44 | assert_equal @committees.size, 20 45 | assert_kind_of(Committee, @committees.first) 46 | assert_kind_of(Committee, @committees.last) 47 | end 48 | end 49 | 50 | context "committee filings" do 51 | setup do 52 | reply = Base.invoke('2010/committees/C00312223/filings', {}) 53 | results = reply['results'] 54 | @filings = results.map{|f| Filing.create(f)} 55 | end 56 | 57 | should "return 11 filings" do 58 | assert_equal @filings.size, 11 59 | end 60 | end 61 | 62 | context "committee unamended filings" do 63 | setup do 64 | reply = Base.invoke('2012/committees/C00431171/filings', {}) 65 | results = reply['results'].select{|f| f['amended'] == false} 66 | @filings = results.map{|f| Filing.create(f)} 67 | end 68 | 69 | should "return filings that are not amended" do 70 | assert_equal @filings.select{|f| f.amended == true}.size, 0 71 | end 72 | end 73 | 74 | context "committee detail" do 75 | setup do 76 | @committee = Committee.find('C00084475', 2012) 77 | end 78 | 79 | should "return 18 other cycles" do 80 | assert_equal @committee.other_cycles.size, 18 81 | end 82 | end 83 | 84 | context "committee contributions" do 85 | setup do 86 | @contribution = Contribution.find('C00458588', 2010) 87 | end 88 | 89 | should "return 141 total results" do 90 | assert_equal @contribution.total_results, 141 91 | end 92 | 93 | should "return a $5,000 contribution to Renee Ellmers" do 94 | assert_equal @contribution.results.detect{|c| c.candidate == "H0NC02059"}.amount, 5000 95 | end 96 | 97 | end 98 | 99 | context "committee contributions to a candidate" do 100 | setup do 101 | reply = Base.invoke('2010/committees/C00458588/contributions/candidates/H0NC02059', {}) 102 | @contribution = Contribution.to_candidate(reply) 103 | end 104 | 105 | should "return 2 results totaling $10,000" do 106 | assert_equal @contribution.results.size, 2 107 | assert_equal @contribution.total_amount, 10000 108 | end 109 | end 110 | 111 | context "superpacs" do 112 | setup do 113 | reply = Base.invoke('2012/committees/superpacs') 114 | @committees = reply['results'].map{|c| Committee.create_from_search_results(c) } 115 | end 116 | 117 | should "return an array of super pacs" do 118 | assert_equal([true], @committees.map{|c| c.super_pac }.uniq) 119 | end 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /lib/campaign_cash/president.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class President < Base 3 | 4 | attr_reader :committee_id, :name, :id, :party, :office, :date_coverage_from, :date_coverage_to, :total_receipts, :total_disbursements, 5 | :end_cash, :total_refunds, :total_contributions, :net_individual_contributions, :net_pac_contributions, :transfers_in, 6 | :net_party_contributions, :net_candidate_contributions, :net_primary_contributions, :net_general_contributions, 7 | :federal_funds, :contributions_less_than_200, :contributions_200_499, :contributions_500_1499, :contributions_1500_2499, 8 | :contributions_max 9 | 10 | def initialize(params={}) 11 | params.each_pair do |k,v| 12 | instance_variable_set("@#{k}", v) 13 | end 14 | end 15 | 16 | # Creates a new president summary object from a JSON API presidential response. 17 | def self.create_summary(params={}) 18 | self.new name: params['name'], 19 | id: params['candidate_id'], 20 | party: get_party(params['party']), 21 | office: 'president', 22 | committee_id: params['committee_id'], 23 | total_receipts: params['total_receipts'], 24 | total_disbursements: params['total_disbursements'], 25 | end_cash: params['cash_on_hand'], 26 | date_coverage_from: params['date_coverage_from'], 27 | date_coverage_to: params['date_coverage_to'] 28 | end 29 | 30 | # Creates a detailed president object 31 | def self.create_detail(params={}) 32 | self.new name: params['candidate_name'], 33 | id: params['candidate_id'], 34 | party: get_party(params['party']), 35 | office: 'president', 36 | committee_id: params['committee_id'], 37 | total_receipts: params['total_receipts'], 38 | total_contributions: params['total_contributions'], 39 | total_disbursements: params['total_disbursements'], 40 | end_cash: params['cash_on_hand'], 41 | date_coverage_from: params['date_coverage_from'], 42 | date_coverage_to: params['date_coverage_to'], 43 | total_refunds: params['total_refunds'], 44 | net_individual_contributions: params['net_individual_contributions'], 45 | net_pac_contributions: params['net_pac_contributions'], 46 | net_party_contributions: params['net_party_contributions'], 47 | net_candidate_contributions: params['net_candidate_contributions'], 48 | net_primary_contributions: params['net_primary_contributions'], 49 | net_general_contributions: params['net_general_contributions'], 50 | federal_funds: params['federal_funds'], 51 | transfers_in: params['transfers_in'], 52 | contributions_less_than_200: params['total_contributions_less_than_200'], 53 | contributions_200_499: params['contributions_200_499'], 54 | contributions_500_1499: params['contributions_500_1499'], 55 | contributions_1500_2499: params['contributions_1500_2499'], 56 | contributions_max: params['total_contributions_max'] 57 | end 58 | 59 | # Returns an array of presidential candidates for a given cycle, defaults to the current cycle. 60 | # Only returns candidates that The New York Times is tracking for financial activity. 61 | def self.summary(cycle=CURRENT_CYCLE) 62 | reply = invoke("#{cycle}/president/totals", {}) 63 | results = reply['results'] 64 | results.map{|c| self.create_summary(c)} 65 | end 66 | 67 | # Returns a President object for a given presidential candidate in a given cycle, defaults to the current cycle. 68 | # Only returns candidates tracked by The New York Times. 69 | def self.detail(id, cycle=CURRENT_CYCLE) 70 | reply = invoke("#{cycle}/president/candidates/#{id}", {}) 71 | results = reply['results'].first 72 | create_detail(results) 73 | end 74 | 75 | private 76 | 77 | def self.get_party party_identifier 78 | return "DEM" if party_identifier == "D" 79 | return "REP" if party_identifier == "R" 80 | party_identifier 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/campaign_cash/candidate.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class Candidate < Base 3 | 4 | # Represents a candidate object based on the FEC's candidate and candidate summary files. 5 | # A candidate is a person seeking a particular office within a particular two-year election 6 | # cycle. Each candidate is assigned a unique ID within a cycle. 7 | attr_reader :name, :id, :state, :district, :party, :fec_uri, :committee_id, 8 | :mailing_city, :mailing_address, :mailing_state, :mailing_zip, 9 | :total_receipts, :total_contributions, :total_from_individuals, 10 | :total_from_pacs, :candidate_loans, :total_disbursements, 11 | :total_refunds, :debts_owed, :begin_cash, :end_cash, :status, 12 | :date_coverage_to, :date_coverage_from, :relative_uri, :office 13 | 14 | def initialize(params={}) 15 | params.each_pair do |k,v| 16 | instance_variable_set("@#{k}", v) 17 | end 18 | end 19 | 20 | # Creates a new candidate object from a JSON API response. 21 | def self.create(params={}) 22 | self.new name: params['name'], 23 | id: params['id'], 24 | state: parse_state(params['state']), 25 | office: parse_office(params['id']), 26 | district: parse_district(params['district']), 27 | party: params['party'], 28 | fec_uri: params['fec_uri'], 29 | committee_id: parse_committee(params['committee']), 30 | mailing_city: params['mailing_city'], 31 | mailing_address: params['mailing_address'], 32 | mailing_state: params['mailing_state'], 33 | mailing_zip: params['mailing_zip'], 34 | total_receipts: params['total_receipts'].to_f, 35 | total_contributions: params['total_contributions'].to_f, 36 | total_from_individuals: params['total_from_individuals'].to_f, 37 | total_from_pacs: params['total_from_pacs'].to_f, 38 | candidate_loans: params['candidate_loans'].to_f, 39 | total_disbursements: params['total_disbursements'].to_f, 40 | total_refunds: params['total_refunds'].to_f, 41 | debts_owed: params['debts_owed'].to_f, 42 | begin_cash: params['begin_cash'].to_f, 43 | end_cash: params['end_cash'].to_f, 44 | status: params['status'], 45 | date_coverage_from: params['date_coverage_from'], 46 | date_coverage_to: params['date_coverage_to'] 47 | end 48 | 49 | def self.create_from_search_results(params={}) 50 | self.new name: params['candidate']['name'], 51 | id: params['candidate']['id'], 52 | state: params['candidate']['id'][2..3], 53 | office: parse_office(params['candidate']['id'][0..0]), 54 | district: parse_district(params['district']), 55 | party: params['candidate']['party'], 56 | committee_id: parse_committee(params['committee']) 57 | end 58 | 59 | def self.parse_state(state) 60 | state.split('/').last[0..1] if state 61 | end 62 | 63 | def self.parse_office(id) 64 | return nil unless id 65 | if id[0..0] == "H" 66 | 'house' 67 | elsif id[0..0] == 'S' 68 | 'senate' 69 | else 70 | 'president' 71 | end 72 | end 73 | 74 | def self.parse_district(uri) 75 | if uri and uri.split('/').last.split('.').first.to_i > 0 76 | uri.split('/').last.split('.').first.to_i 77 | else 78 | 0 79 | end 80 | end 81 | 82 | def self.categories 83 | { 84 | individual_total: "Contributions from individuals", 85 | contribution_total: "Total contributions", 86 | candidate_loan: "Loans from candidate", 87 | receipts_total: "Total receipts", 88 | refund_total: "Total refunds", 89 | pac_total: "Contributions from PACs", 90 | disbursements_total: "Total disbursements", 91 | end_cash: "Cash on hand", 92 | debts_owed: "Debts owed by", 93 | } 94 | end 95 | 96 | # Retrieve a candidate object via its FEC candidate id within a cycle. 97 | # Defaults to the current cycle. 98 | def self.find(fecid, cycle=CURRENT_CYCLE) 99 | reply = invoke("#{cycle}/candidates/#{fecid}") 100 | result = reply['results'] 101 | self.create(result.first) if result.first 102 | end 103 | 104 | # Returns leading candidates for given categories from campaign filings within a cycle. 105 | # See [the API docs](http://developer.nytimes.com/docs/read/campaign_finance_api#h3-candidate-leaders) for 106 | # a list of acceptable categories to pass in. Defaults to the current cycle. 107 | def self.leaders(category, cycle=CURRENT_CYCLE) 108 | reply = invoke("#{cycle}/candidates/leaders/#{category}",{}) 109 | results = reply['results'] 110 | results.map{|c| self.create(c)} 111 | end 112 | 113 | # Returns an array of candidates matching a search term within a cycle. Defaults to the 114 | # current cycle. 115 | def self.search(name, cycle=CURRENT_CYCLE, offset=nil) 116 | reply = invoke("#{cycle}/candidates/search", {query: name, offset: offset}) 117 | results = reply['results'] 118 | results.map{|c| self.create_from_search_results(c)} 119 | end 120 | 121 | # Returns an array of newly created FEC candidates within a current cycle. Defaults to the 122 | # current cycle. 123 | def self.new_candidates(cycle=CURRENT_CYCLE, offset=nil) 124 | reply = invoke("#{cycle}/candidates/new",{offset: offset}) 125 | results = reply['results'] 126 | results.map{|c| self.create(c)} 127 | end 128 | 129 | # Returns an array of candidates for a given state within a cycle, with optional chamber and 130 | # district parameters. For example, House candidates from New York. Defaults to the current cycle. 131 | def self.state(state, chamber=nil, district=nil, cycle=CURRENT_CYCLE, offset=nil) 132 | path = "#{cycle}/seats/#{state}" 133 | if chamber 134 | path += "/#{chamber}" 135 | path += "/#{district}" if district 136 | end 137 | reply = invoke(path, {offset: offset}) 138 | results = reply['results'] 139 | results.map{|c| self.create_from_search_results(c)} 140 | end 141 | 142 | instance_eval { alias :state_chamber :state } 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /lib/campaign_cash/committee.rb: -------------------------------------------------------------------------------- 1 | module CampaignCash 2 | class Committee < Base 3 | 4 | FILING_FREQUENCY = { 5 | A: 'Administratively Terminated', 6 | D: 'Debt', 7 | M: 'Monthly', 8 | Q: 'Quarterly', 9 | T: 'Terminated', 10 | W: 'Waived' 11 | } 12 | 13 | INTEREST_GROUP = { 14 | C: 'Corporation', 15 | L: 'Labor Union', 16 | M: 'Membership', 17 | T: 'Trade Association', 18 | V: 'Cooperative', 19 | W: 'Corporation without Capital Stock' 20 | } 21 | 22 | COMMITTEE_TYPE = { 23 | C: 'Communication Cost', 24 | D: 'Delegate', 25 | E: 'Electioneering Communications', 26 | H: 'House Candidate', 27 | I: 'Independent Expenditure (Person or Group)', 28 | N: 'Non-Party, Non-Qualified', 29 | P: 'Presidential Candidate', 30 | Q: 'Qualified Non-Party', 31 | S: 'Senate Candidate', 32 | X: 'Non-Qualified Party', 33 | Y: 'Qualified Party', 34 | Z: 'National Party Non-Federal' 35 | } 36 | 37 | COMMITTEE_DESIGNATION = { 38 | A: 'Authorized by a Candidate', 39 | J: 'Joint Fundraiser', 40 | P: 'Principal Campaign Committee', 41 | U: 'Unauthorized', 42 | B: 'Lobbyist/Registrant PAC', 43 | D: 'Leadership PAC' 44 | } 45 | 46 | attr_reader :name, :id, :state, :district, :party, :fec_uri, :candidate, 47 | :city, :address, :state, :zip, :relative_uri, :sponsor_name, 48 | :total_receipts, :total_contributions, :total_from_individuals, 49 | :total_from_pacs, :candidate_loans, :total_disbursements, 50 | :total_refunds, :debts_owed, :begin_cash, :end_cash, 51 | :date_coverage_to, :date_coverage_from, :other_cycles, :super_pac, :filings, 52 | :total_candidate_contributions, :total_independent_expenditures 53 | 54 | def initialize(params={}) 55 | params.each_pair do |k,v| 56 | instance_variable_set("@#{k}", v) 57 | end 58 | end 59 | 60 | def self.create(params={}) 61 | self.new name: params['name'], 62 | id: params['id'], 63 | state: params['state'], 64 | party: params['party'], 65 | fec_uri: params['fec_uri'], 66 | city: params['city'], 67 | address: params['address'], 68 | zip: params['zip'], 69 | sponsor_name: params['sponsor_name'], 70 | leadership: params['leadership'], 71 | super_pac: params['super_pac'], 72 | total_receipts: params['total_receipts'].to_f, 73 | total_contributions: params['total_contributions'].to_f, 74 | total_from_individuals: params['total_from_individuals'].to_f, 75 | total_from_pacs: params['total_from_pacs'].to_f, 76 | candidate_loans: params['candidate_loans'].to_f, 77 | total_disbursements: params['total_disbursements'].to_f, 78 | total_candidate_contributions: params['total_candidate_contributions'].to_f, 79 | total_independent_expenditures: params['total_independent_expenditures'].to_f, 80 | total_refunds: params['total_refunds'].to_f, 81 | debts_owed: params['debts_owed'].to_f, 82 | begin_cash: params['begin_cash'].to_f, 83 | end_cash: params['end_cash'].to_f, 84 | date_coverage_from: date_parser(params['date_coverage_from']), 85 | date_coverage_to: date_parser(params['date_coverage_to']), 86 | candidate_id: parse_candidate(params['candidate']), 87 | filing_frequency: get_frequency(params['filing_frequency']), 88 | interest_group: get_interest_group(params['interest_group']), 89 | committee_type: get_committee_type(params['committee_type']), 90 | designation: get_designation(params['designation']), 91 | other_cycles: params['other_cycles'].map{|cycle| cycle['cycle']['cycle']} 92 | end 93 | 94 | def self.create_from_search_results(params={}) 95 | self.new name: params['name'], 96 | id: params['id'], 97 | city: params['city'], 98 | state: params['state'], 99 | zip: params['zip'], 100 | district: params['district'], 101 | party: params['party'], 102 | candidate_id: parse_candidate(params['candidate']), 103 | treasurer: params['treasurer'], 104 | fec_uri: params['fec_uri'], 105 | leadership: params['leadership'], 106 | super_pac: params['super_pac'] 107 | end 108 | 109 | def self.get_frequency(frequency) 110 | if frequency 111 | FILING_FREQUENCY[frequency.strip] unless frequency.empty? 112 | end 113 | end 114 | 115 | def self.get_interest_group(interest_group) 116 | if interest_group 117 | INTEREST_GROUP[interest_group.strip] unless interest_group.empty? 118 | end 119 | end 120 | 121 | def self.get_committee_type(committee_type) 122 | if committee_type 123 | COMMITTEE_TYPE[committee_type.strip] unless committee_type.empty? 124 | end 125 | end 126 | 127 | def self.get_designation(designation) 128 | if designation 129 | COMMITTEE_DESIGNATION[designation.strip] unless designation.empty? 130 | end 131 | end 132 | 133 | def self.find(fecid, cycle=CURRENT_CYCLE) 134 | reply = invoke("#{cycle}/committees/#{fecid}") 135 | result = reply['results'] 136 | create(result.first) if result.first 137 | end 138 | 139 | def self.search(name, cycle=CURRENT_CYCLE, offset=nil) 140 | name = name.gsub(/ /,"+") 141 | reply = invoke("#{cycle}/committees/search", {query: name, offset: offset}) 142 | results = reply['results'] 143 | results.map{|c| create_from_search_results(c)} 144 | end 145 | 146 | def self.latest(cycle=CURRENT_CYCLE) 147 | reply = invoke("#{cycle}/committees/new",{}) 148 | results = reply['results'] 149 | results.map{|c| create_from_search_results(c)} 150 | end 151 | 152 | def self.superpacs(cycle=CURRENT_CYCLE, offset=nil) 153 | reply = invoke("#{cycle}/committees/superpacs",{offset: offset}) 154 | results = reply['results'] 155 | results.map{|c| create_from_search_results(c)} 156 | end 157 | 158 | def filings(cycle=CURRENT_CYCLE, offset=nil) 159 | reply = Base.invoke("#{cycle}/committees/#{id}/filings",{offset: offset}) 160 | results = reply['results'] 161 | results.map{|r| r["filing_id"] = r["id"]} 162 | results.map{|c| Filing.create(c)} 163 | end 164 | 165 | def unamended_filings(cycle=CURRENT_CYCLE, offset=nil) 166 | reply = Base.invoke("#{cycle}/committees/#{id}/filings",{offset: offset}) 167 | results = reply['results'].select{|f| f['amended'] == false} 168 | results.map{|c| Filing.create(c, name=self.name)} 169 | end 170 | 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | 2 | ____ _ ____ _ 3 | / ___|__ _ _ __ ___ _ __ __ _(_) __ _ _ __ / ___|__ _ ___| |__ 4 | | | / _` | '_ ` _ \| '_ \ / _` | |/ _` | '_ \ | | / _` / __| '_ \ 5 | | |__| (_| | | | | | | |_) | (_| | | (_| | | | | | |__| (_| \__ \ | | | 6 | \____\__,_|_| |_| |_| .__/ \__,_|_|\__, |_| |_| \____\__,_|___/_| |_| 7 | |_| |___/ 8 | 9 | == DESCRIPTION: 10 | 11 | Simple ruby wrapper for portions of the ProPublica Campaign Finance API[https://propublica.github.io/campaign-finance-api-docs/]. You'll need an API key. Tested under Ruby 1.9.3, 2.0.0 and 2.1.0. As of Feb. 12, 2016, 12 | this gem does not work with the Campaign Finance API, but it will soon. 13 | 14 | == News 15 | 16 | * Feb. 12, 2016: Ownership of this gem transferred to ProPublica. 17 | * Sept. 14, 2015: Version 2.9.2 released. Bug fix for Filing#today. 18 | * July 28, 2015: Version 2.9.1 released. Patch for Committee#filings 19 | * July 24, 2015: Version 2.9 released. Made Filing#search cycle-specific. 20 | * June 15, 2015: Version 2.8.1 released. Bumped cycle to 2016 and fixed tests. 21 | * Dec. 16, 2014: Version 2.8 released. Added Filing#search method to support name or id searches. 22 | * Dec. 15, 2014: Version 2.7.2 released. Added `date_filed` to Filing objects. 23 | * May 23, 2014: Version 2.7.1 released. Fixed Committee#search (thanks @alshaw!). 24 | * March 3, 2014: Version 2.7 released. Updated for Ruby 2.x support, dropped support for 1.8.7. 25 | * July 22, 2013: Version 2.6.2 released. Changed current cycle to 2014. 26 | * April 10, 2013: Version 2.6.1 released. Fixed bad attribute name for Filing#filing_id. 27 | * April 9, 2013: Version 2.6 released. Added missing attribute accessors to Filing objects. 28 | * April 9, 2013: Version 2.5.1 released. Fixed a bug that showed committee_name in Filing objects as nil. 29 | * Nov. 13, 2012: Version 2.5 released. Fixes a bug in Filing object attribute assignment. 30 | * July 19, 2012: Version 2.4 released. Updated for new API responses and to return integers and floats. 31 | * April 10, 2012: Version 2.3.2 released. Bugfix for Committee#find. 32 | * March 22, 2012: Version 2.3.1 released. Added filing_id to Filing objects and Committee#unamended_filings method. 33 | * March 15, 2012: Version 2.3 released. Added committee_type to Filing objects and offset to Candidate methods. 34 | * March 9, 2012: Version 2.2.1 released. Added transfers_in attribute to President#detail. 35 | * Feb. 8, 2012: Version 2.1.0 released. Added better support for independent expenditures and FilingSummary class for presidential candidate filings. 36 | * Jan. 28, 2012: Version 2.0.8 released. Bugfix for Candidate#search. Thanks to @benjaminjackson for patch. 37 | * Jan. 24, 2012: Version 2.0.7 released. Added candidate_name attribute accessor to IndependentExpenditure objects. 38 | * Jan. 19, 2012: Version 2.0.6 released. Added transaction_id param to IndependentExpenditure objects. 39 | * Jan. 19, 2012: Version 2.0.5 released. Bugfix to add amendment param to IndependentExpenditure objects. 40 | 41 | == INSTALL: 42 | 43 | Install Campaign Cash as a gem: 44 | 45 | gem install campaign_cash 46 | 47 | For use in a Rails 3 application, put the following in your Gemfile: 48 | 49 | gem 'campaign_cash' 50 | 51 | then issue the 'bundle install' command. Campaign Cash has been tested under Ruby 1.8.7 and 1.9.2. 52 | 53 | == GETTING STARTED: 54 | 55 | require 'rubygems' 56 | require 'campaign_cash' 57 | 58 | You'll want to set your API key as an environment variable in order to run the tests. Otherwise, you'll need to set it like so: 59 | 60 | include CampaignCash 61 | Base.api_key = YOUR_API_KEY 62 | 63 | In a Rails app, you can put those lines in a file in config/initializers/. 64 | 65 | Currently there are methods to support retrieving candidates, presidential candidates, committees, electronic filings, committee contributions and independent expenditures. Almost every method requires at least one parameter, the election cycle. 66 | 67 | == Candidates 68 | 69 | Candidate objects can be created in several ways. To locate a candidate by last name, supply the name and cycle: 70 | 71 | Candidate.search("Bass", 2012) 72 | #=> [#, #] 73 | 74 | Search returns an array of basic information about candidates, including the unique Federal Election Commission-provided identifier, which begins with an "H" for House candidates, "S" for Senate candidates and "P" for presidential candidates. The id, along with the election cycle, can be used to retrieve more information about a candidate: 75 | 76 | Candidate.find("H0NH02017", 2012) 77 | #=> # 78 | 79 | Candidate.find returns an object containing more detailed information for a candidate, including the latest financial summary from the F.E.C. and the candidate's committee ID, which can be used to make other method calls. 80 | 81 | Several other canned queries are available. To list the top 20 candidates in terms of one of the financial categories (available by calling Candidate.categories or listed at http://developer.nytimes.com/docs/read/campaign_finance_api#h3-candidate-leaders), supply the category and cycle: 82 | 83 | Candidate.leaders('end-cash', 2012) 84 | #=> [#, # ...] 85 | 86 | Please note that not all candidates file on the same schedule, so some of the "date_coverage_to" will be different for these responses. Senate candidates, in particular, often file twice a year during the odd-numbered years prior to a general election. 87 | 88 | To retrieve an array of the 20 newest candidates (the F.E.C.'s data is updated every Monday), use the new_candidates class method: 89 | 90 | Candidate.new_candidates 91 | #=> [#, #, 92 | 93 | To retrieve an array of candidates for a particular seat, supply a state abbreviation, chamber ('house' or 'senate'), district (an integer or nil) and cycle. For example, the following asks for candidates for Maryland's 6th Congressional District in 2012: 94 | 95 | Candidate.state_chamber('MD', 'house', 6, 2012) 96 | #=> [#, #, #,...] 97 | 98 | == President 99 | 100 | Presidential candidate objects are like regular Candidates, except that they have additional financial attributes that are calculated by The New York Times from electronic filings. There are two President methods. To retrieve a listing of all presidential candidates being tracked by The Times: 101 | 102 | President.summary 103 | #=> [#, #,...] 104 | 105 | To retrieve detailed information for a single presidential candidate, pass the appropriate committee_id and cycle: 106 | 107 | President.detail('C00431171', 2012) 108 | #=> [#] 109 | 110 | Among the calculated attributes are totals for five ranges of contribution size (the maximum individual contribution for the 2012 cycle is $2,500 per person, per election, while in 2008 it was $2,300). 111 | 112 | == Committees 113 | 114 | Committee objects have information about F.E.C.-registered committees that may or may not be affiliated to a candidate. If a committee is associated with a candidate, it will have values for the district, party and candidate_id attributes. Party committees should also have the party attribute set. If it is defined as a leadership committee, a committee's leadership attribute will be set to True; a similar attribute, super_pac, is set to True if the committee is registered with the F.E.C. as a "Super PAC" able to raise unlimited amounts for independent expenditures. 115 | 116 | Searching for a committee currently executes a "begins with" query, so to find all committees in the 2012 cycle that start with the word 'Growth': 117 | 118 | Committee.search("Growth", 2012) 119 | #=> [#, #] 120 | 121 | As with Candidate.search, the results of Committee.search return a subset of all Committee attributes. The full Committee object includes the most recent financial summary data available, plus the other election cycles that the committee is registered in dating back to 1980. Some committees have a sponsor, which could be a corporation, association or candidate. Also included are several F.E.C.-defined categories, including filing frequency, committee designation (who controls it, essentially), committee type (House, Senate, etc.) and interest group category (corporation, labor union, etc.). These last four attributes are not universally present in the data, which is why a Committee object has an independent leadership attribute even though the designation should indicate if it is a leadership committee. More details these codes is available from the F.E.C.: ftp://ftp.fec.gov/FEC/cm_dictionary.txt. 122 | 123 | To fetch the details for a single committee in a cycle: 124 | 125 | Committee.find("C00326389", 2012) 126 | #=> # 127 | 128 | Individual Committee objects can retrieve an array of filings they have made during a given cycle: 129 | 130 | committee = Committee.find("C00326389", 2012) 131 | committee.filings 132 | #=> [#, @original_uri=nil, @report_title="YEAR-END", @date_coverage_to=#, @form_type="F3", @committee=nil, @original_filing=nil, @fec_uri="http://query.nictusa.com/cgi-bin/dcdev/forms/C00326389/756540/">,...] 133 | 134 | The filings method returns an array of all filings, including originals and amendments. If you want to retrieve the list of unamended filings for a committee within a cycle -- in other words, the latest version of reports -- use the unamended_filings method: 135 | 136 | committee = Committee.find("C00326389", 2012) 137 | committee.unamended_filings 138 | 139 | For more information on Filing objects, see Filings below. 140 | 141 | To retrieve a list of the 20 most recently added committees (note that no cycle parameter is needed; this method always uses the most recent cycle): 142 | 143 | Committee.latest 144 | #=> [#, #, ...] 145 | 146 | "Super PACs" are independent-expenditure only committees formed in the wake of the 2010 Citizens United decision by the U.S. Supreme Court. They can raise money in unlimited donations from individuals, corporations, labor unions and other sources, but must register their intent to do so with the F.E.C. To retrieve the 20 most recent "Super PAC" registrations: 147 | 148 | Committee.superpacs 149 | #=> [#, #,...] 150 | 151 | Committees make contributions to candidates, among other activities. Given a committee, cycle and recipient candidate, to see any contributions from the committee to the candidate: 152 | 153 | Contribution.find('C00388793',2012,'H0NY19139') 154 | #=> #, primary_general="General", state="NY", amount=5000.0, image_uri="http://images.nictusa.com/cgi-bin/fecimg/?11932087309", name="HAYWORTH, NAN", district="19", candidate="H0NY19139">, #, primary_general="Primary", state="NY", amount=5000.0, image_uri="http://images.nictusa.com/cgi-bin/fecimg/?11932087308", name="HAYWORTH, NAN", district="19", candidate="H0NY19139">], @cycle=2012, @total_amount=10000.0, @committee="C00388793"> 155 | 156 | Removing the candidate's id from the previous method will yield an array of all contributions to candidates made by the committee during that cycle: 157 | 158 | Contribution.find('C00388793',2012) 159 | #=> #, primary_general="Primary", state="IL", amount=2000.0, image_uri="http://images.nictusa.com/cgi-bin/fecimg/?11932087311", name="KINZINGER, ADAM", district="11", candidate=nil>, #, primary_general="Primary", state="MI", amount=2500.0, image_uri="http://images.nictusa.com/cgi-bin/fecimg/?11932087316", name="WALBERG, TIMOTHY L. HON.", district="07", candidate=nil>,...] 160 | 161 | Both of the contribution responses have a results method that contains one or more contributions, assembled using Ruby's OpenStruct library. The image_uri attribute is a link to the page of the filing on fec.gov that contains that transaction. 162 | 163 | == Filings 164 | 165 | Committees file reports with the F.E.C. detailing their fundraising and expenditures, as well as administrative matters such as a change of address or in committee status. Campaign Cash provides access to both electronic filings, the method used by most committees, and images of paper filings, which are submitted by Senate candidates and two Senate party committees. To retrieve the most recent 20 filings from today: 166 | 167 | Filing.today 168 | #=> [#, @original_filing=741687, @report_title="AUG MONTHLY", @date_coverage_to=#, @form_type="F3", @committee="C00016899", @paper=nil, @fec_uri="http://query.nictusa.com/cgi-bin/dcdev/forms/C00016899/756832/">, #, @original_filing=nil, @report_title="YEAR-END", @date_coverage_to=#, @form_type="F3", @committee="C00332411", @paper=nil, @fec_uri="http://query.nictusa.com/cgi-bin/dcdev/forms/C00332411/756831/">,...] 169 | 170 | Each filing object contains several attributes that help place it into context. If the filing is amended by another filing, amended will be True and an amended_uri, the link to the updated filing, will appear. If a filing was submitted on paper, paper will be True. To retrieve filings from a specific date: 171 | 172 | Filing.date(2012,1,9) # Jan. 9, 2012 173 | #=> [#, @original_filing=nil, @report_title="YEAR-END", @date_coverage_to=#, @form_type="F3", @committee="C00435966", @paper=nil, @fec_uri="http://query.nictusa.com/cgi-bin/dcdev/forms/C00435966/756828/">, #, @original_filing=nil, @report_title="YEAR-END", @date_coverage_to=#, @form_type="F3", @committee="C00415943", @paper=nil, @fec_uri="http://query.nictusa.com/cgi-bin/dcdev/forms/C00415943/756827/">,...] 174 | 175 | To retrieve a list of Filing form types, which explain a little more about the form_type attributes on Filing objects: 176 | 177 | Filing.form_types 178 | #=> [#, #, #, #,...] 179 | 180 | The form type IDs can be used to retrieve the most recent filings of that type: 181 | 182 | Filing.by_type(2012,'F1') # Latest Statements of Organization 183 | #=> [#, #,...] 184 | 185 | All Filing methods, with the exception of Filing.form_types, support the optional offset parameter, using multiples of 20. 186 | 187 | == Independent Expenditures 188 | 189 | Committees can spend money to elect or defeat candidates independently of their campaigns; such activities are called independent expenditures and are {specifically reported to the F.E.C.}[http://www.fec.gov/finance/disclosure/ie_reports.shtml]. To retrieve the 20 most recent IE transactions: 190 | 191 | IndependentExpenditure.latest 192 | #=> [#, @amount=200.0, @purpose="Online Advertising - 01/08/2012", @committee="C00503789", @district=0, @date=#, @candidate="P20002721", @support_or_oppose="O", @committee_name="DGA ACTION", @office="President", @payee="Facebook", @state="", @fec_uri="http://images.nictusa.com/cgi-bin/fecimg/?12950041238">,...] 193 | 194 | The support_or_oppose attribute contains either an "S" (for "Support") or "O" (for "Oppose"), indicating whether the expenditure is in favor or against the candidate identified. IEs should contain both the candidate mentioned in the expenditure and the office (state and district) that person is running for, but in practice some filings either omit this information or contain an incorrect candidate ID (usually a previous one for the correct candidate). The API does not guarantee that such mistakes will be corrected when you retrieve records. To retrieve recent IEs about a specific candidate, use that candidate's ID: 195 | 196 | IndependentExpenditure.candidate("P60003654", 2012) # find independent expenditures about Newt Gingrich. 197 | #=> [#, @amount=89961.8, @purpose="Direct Mail", @committee="C00490045", @district=0, @committee_name="RESTORE OUR FUTURE, INC.", @date=#, @candidate="P60003654", @support_or_oppose="O", @office="President", @payee="Arena Communications", @state="FL", @fec_uri="http://images.nictusa.com/cgi-bin/fecimg/?12950036002">, #, @amount=108982.0, @purpose="National Email Communication and Production", @committee="C00507525", @district=0, @committee_name="WINNING OUR FUTURE", @date=#, @candidate="P60003654", @support_or_oppose="S", @office="President", @payee="Marketel Media, Inc.", @state="", @fec_uri="http://images.nictusa.com/cgi-bin/fecimg/?12970039675">,...] 198 | 199 | To retrieve IEs by date, use a string date in MM/DD/YYYY format: 200 | 201 | IndependentExpenditure.date('12/28/2011') 202 | #=> #, @amount=19210.0, @purpose="TV Ad", @committee="C00508317", @district=0, @date=#, @candidate="P20002721", @support_or_oppose="S", @committee_name="Leaders for Families Super PAC, Inc.", @office="President", @payee="NCC Media", @state="", @fec_uri="http://images.nictusa.com/cgi-bin/fecimg/?11953345841">, #, @amount=10450.0, @purpose="TV Ad", @committee="C00508317", @district=0, @date=#, @candidate="P20002721", @support_or_oppose="S", @committee_name="Leaders for Families Super PAC, Inc.", @office="President", @payee="WHO", @state="", @fec_uri="http://images.nictusa.com/cgi-bin/fecimg/?11953345842">,...] 203 | 204 | Another way to fetch IEs is by the committee that paid for them, using the committee ID: 205 | 206 | IndependentExpenditure.committee('C00508317', 2012) 207 | #=> [#, @amount=4235.0, @purpose="Radio Ad Buy", @committee="C00508317", @district=0, @date=#, @candidate="P20002721", @support_or_oppose="S", @committee_name="Leaders for Families Super PAC, Inc.", @office="President", @payee="WGIR-FM & WQSO-FM", @state="NH", @fec_uri="http://images.nictusa.com/cgi-bin/fecimg/?12950035907">,...] 208 | 209 | There's also the option to retrieve the 20 most recent IEs for the presidential race: 210 | 211 | IndependentExpenditure.president 212 | #=> [#, @amount=350.0, @purpose="Font Purchase", @committee="C00508002", @district=0, @date=#, @candidate="", @support_or_oppose="S", @committee_name="ENDORSE LIBERTY INC", @office="President", @payee="Commercial Type", @state="", @fec_uri="http://images.nictusa.com/cgi-bin/fecimg/?12970010100">,...] 213 | 214 | The IE responses also accept an optional offset argument, using multiples of 20. 215 | 216 | == Individual Contributions 217 | 218 | IndividualContribution objects are line items from committees' receipt filings. You can get them in three ways, by an FEC committee ID, a filing ID or an FEC candidate ID in groups of 20. These will be returned from newest to oldest. Note: in IndividualContribution responses, pac_name refers to any institutional donation, not just donations from other PACs. 219 | 220 | IndividualContribution.committee("C00490045") # Restore Our Future 221 | #=> [#, ...] 222 | 223 | IndividualContribution.candidate("P80003338") # Barack Obama 224 | IndividualContribution.filing(762683) # Obama for America's 2011 year-end filing 225 | 226 | If you're just interested in grabbing the contributions from a certain filing or date-range, but don't know that filing ID, get it via Committee#filings. Here's how to find the 2011 year-end filing ID for Restore Our Future, which you can then pass to IndividualContributions. 227 | 228 | Committee.find("C00490045", 2012).filings.select do |q| 229 | q.date_coverage_to == "2011-12-31".to_date && q.report_title == "YEAR-END" 230 | end 231 | 232 | == Late Contributions 233 | 234 | During the final 20 days before a primary or general election, candidate committees that receive contributions of at least $1,000 must report them in filings to the F.E.C. within 48 hours of receipt. These contributions may be from individuals or other committees. 235 | 236 | LateContribution.latest 237 | #=> [#, ...] 238 | 239 | LateContribution.candidate("H0TN08246") 240 | LateContribution.committee("C00466854") # must be a candidate committee 241 | 242 | == Electioneering Communications 243 | 244 | Electioneering Communications are broadcast ads funded by third party groups that mention one or more candidates, but don't specifically support or oppose one. ElectioneeringCommunication objects are available newest first, or by committee ID or date (all in groups of 20). Within each object is an array of electioneering_communication_candidates mentioned in the ad. 245 | 246 | ElectioneeringCommunication.latest 247 | #=> [#"HI", "electioneering_communication_id"=>847, "filing_id"=>764901, "transaction_id"=>"C.", "amended_from"=>nil, "back_reference_tran_id_number"=>"E.1", "candidate_name"=>"OBAMA, BARACK", "fec_candidate_id"=>"P80003338", "candidate_district"=>nil}], @back_reference_tran_id_number=nil, @unique_id="1116aadc5d5b7c793f0d0adfebb503709da1029f", @payee_middle_name=nil, @amended_from=nil, @back_reference_sched_name=nil, @payee_organization="Upgrade Films", @payee_state="DC", @transaction_id="E.1", @election_code="P2012", @payee_city="Washington", @payee_address_1="3299 K Street NW, Ste 200", @amount=65, @entity_type="ORG", @payee_first_name=nil, @filing_id=764901, @filed_date="2012-02-07", @payee_last_name=nil, @fec_committee_id="C30001655", @expenditure_date="2012-01-30", @payee_zip="20007">, ...] 248 | 249 | ElectioneeringCommunication.committee("C30001655") # Crossroads GPS 250 | ElectioneeringCommunication.date("02/06/2012") # EC's from February 6, 2012 251 | 252 | 253 | == Note on Patches/Pull Requests 254 | 255 | * Fork the project. 256 | * Make your feature addition or bug fix. 257 | * Add tests for it. This is important so I don't break it in a 258 | future version unintentionally. 259 | * Commit, do not mess with rakefile, version, or history. 260 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 261 | * Send me a pull request. Bonus points for topic branches. 262 | 263 | == Authors 264 | 265 | * Derek Willis, derek.willis@propublica.org 266 | * Al Shaw, almshaw@gmail.com 267 | * Jason Holt, jjh@offensivepolitics.net 268 | * Benjamin Jackson, http://twitter.com/benjaminjackson 269 | * Daniel Leavitt, https://github.com/dleavitt 270 | 271 | == Copyright 272 | 273 | Copyright (c) 2016 ProPublica Inc. See LICENSE for details. 274 | --------------------------------------------------------------------------------