campaign_cash 2.3.2 → 2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.rdoc +12 -1
- data/lib/campaign_cash/base.rb +100 -100
- data/lib/campaign_cash/candidate.rb +103 -103
- data/lib/campaign_cash/committee.rb +102 -99
- data/lib/campaign_cash/contribution.rb +46 -36
- data/lib/campaign_cash/electioneering_communication.rb +4 -4
- data/lib/campaign_cash/filing.rb +59 -60
- data/lib/campaign_cash/filing_summary.rb +8 -8
- data/lib/campaign_cash/form.rb +7 -7
- data/lib/campaign_cash/independent_expenditure.rb +37 -35
- data/lib/campaign_cash/individual_contribution.rb +11 -11
- data/lib/campaign_cash/late_contribution.rb +37 -0
- data/lib/campaign_cash/president.rb +49 -49
- data/lib/campaign_cash/version.rb +1 -1
- data/lib/campaign_cash.rb +1 -1
- data/test/campaign_cash/test_candidate.rb +1 -1
- data/test/campaign_cash/test_committee.rb +2 -2
- data/test/campaign_cash/test_independent_expenditure.rb +1 -1
- data/test/campaign_cash/test_late_contribution.rb +24 -0
- data/test/test_helper.rb +1 -1
- metadata +87 -49
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -8,10 +8,11 @@
|
|
8
8
|
|
9
9
|
== DESCRIPTION:
|
10
10
|
|
11
|
-
Simple ruby wrapper for The New York Times Campaign Finance API[http://developer.nytimes.com/docs/read/campaign_finance_api]. You'll need an API key. Tested under Ruby 1.8.7, 1.9.2 and 1.9.3 and JRuby 1.6.7.
|
11
|
+
Simple ruby wrapper for portions of The New York Times Campaign Finance API[http://developer.nytimes.com/docs/read/campaign_finance_api]. You'll need an API key. Tested under Ruby 1.8.7, 1.9.2 and 1.9.3 and JRuby 1.6.7.
|
12
12
|
|
13
13
|
== News
|
14
14
|
|
15
|
+
* July 19, 2012: Version 2.4 released. Updated for new API responses and to return integers and floats.
|
15
16
|
* April 10, 2012: Version 2.3.2 released. Bugfix for Committee#find.
|
16
17
|
* March 22, 2012: Version 2.3.1 released. Added filing_id to Filing objects and Committee#unamended_filings method.
|
17
18
|
* March 15, 2012: Version 2.3 released. Added committee_type to Filing objects and offset to Candidate methods.
|
@@ -213,6 +214,16 @@ If you're just interested in grabbing the contributions from a certain filing or
|
|
213
214
|
q.date_coverage_to == "2011-12-31".to_date && q.report_title == "YEAR-END"
|
214
215
|
end
|
215
216
|
|
217
|
+
== Late Contributions
|
218
|
+
|
219
|
+
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.
|
220
|
+
|
221
|
+
LateContribution.latest
|
222
|
+
#=> [#<CampaignCash::LateContribution:0x10116c6d0 @contributor_city="Brownsville", @contributor_zip=nil, @fec_committee_id="C00466854", @contributor_fec_id=nil, @fec_candidate_id="H0TN08246", @contributor_employer="Simmco", @transaction_id="20716.C7755", @contributor_street_2=nil, @contributor_prefix=nil, @office_state="TN", @contributor_state="TN", @fec_filing_id=798678, @contributor_street_1="PO Box 545", @contribution_date="2012-07-16", @contributor_last_name="Blurton", @contributor_organization_name=nil, @contributor_middle_name=nil, @contributor_suffix=nil, @contribution_amount="2500.0", @cycle=2012, @contributor_first_name="David", @entity_type="IND", @contributor_occupation="Owner">, ...]
|
223
|
+
|
224
|
+
LateContribution.candidate("H0TN08246")
|
225
|
+
LateContribution.committee("C00466854") # must be a candidate committee
|
226
|
+
|
216
227
|
== Electioneering Communications
|
217
228
|
|
218
229
|
Electioneering Communications are broadcast ads funded by third party groups that mention one or more candidates, but don't specifically support or oppose one. <tt>ElectioneeringCommunication</tt> objects are available newest first, or by committee ID or date (all in groups of 20). Within each object is an array of <tt>electioneering_communication_candidates</tt> mentioned in the ad.
|
data/lib/campaign_cash/base.rb
CHANGED
@@ -6,105 +6,105 @@ require 'ostruct'
|
|
6
6
|
|
7
7
|
module CampaignCash
|
8
8
|
class Base
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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 = 2012
|
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
109
|
end
|
110
110
|
end
|
@@ -1,145 +1,145 @@
|
|
1
1
|
module CampaignCash
|
2
2
|
class Candidate < Base
|
3
|
-
|
3
|
+
|
4
4
|
# Represents a candidate object based on the FEC's candidate and candidate summary files.
|
5
5
|
# A candidate is a person seeking a particular office within a particular two-year election
|
6
6
|
# cycle. Each candidate is assigned a unique ID within a cycle.
|
7
7
|
attr_reader :name, :id, :state, :district, :party, :fec_uri, :committee_id,
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
14
|
def initialize(params={})
|
15
15
|
params.each_pair do |k,v|
|
16
16
|
instance_variable_set("@#{k}", v)
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# Creates a new candidate object from a JSON API response.
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.parse_state(state)
|
61
|
+
state.split('/').last[0..1] if state
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.parse_office(id)
|
65
|
+
return nil unless id
|
66
|
+
if id[0..0] == "H"
|
67
|
+
'house'
|
68
|
+
elsif id[0..0] == 'S'
|
69
|
+
'senate'
|
70
|
+
else
|
71
|
+
'president'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.parse_district(uri)
|
76
|
+
if uri and uri.split('/').last.split('.').first.to_i > 0
|
77
|
+
uri.split('/').last.split('.').first.to_i
|
78
|
+
else
|
79
|
+
0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.categories
|
84
|
+
{
|
85
|
+
"individual_total" => "Contributions from individuals",
|
86
|
+
"contribution_total" => "Total contributions",
|
87
|
+
"candidate_loan" => "Loans from candidate",
|
88
|
+
"receipts_total" => "Total receipts",
|
89
|
+
"refund_total" => "Total refunds",
|
90
|
+
"pac_total" => "Contributions from PACs",
|
91
|
+
"disbursements_total" => "Total disbursements",
|
92
|
+
"end_cash" => "Cash on hand",
|
93
|
+
"debts_owed" => "Debts owed by",
|
94
94
|
}
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
95
|
+
end
|
96
|
+
|
97
|
+
# Retrieve a candidate object via its FEC candidate id within a cycle.
|
98
|
+
# Defaults to the current cycle.
|
99
99
|
def self.find(fecid, cycle=CURRENT_CYCLE)
|
100
|
-
|
101
|
-
|
102
|
-
|
100
|
+
reply = invoke("#{cycle}/candidates/#{fecid}")
|
101
|
+
result = reply['results']
|
102
|
+
self.create(result.first) if result.first
|
103
103
|
end
|
104
|
-
|
104
|
+
|
105
105
|
# Returns leading candidates for given categories from campaign filings within a cycle.
|
106
106
|
# See [the API docs](http://developer.nytimes.com/docs/read/campaign_finance_api#h3-candidate-leaders) for
|
107
107
|
# a list of acceptable categories to pass in. Defaults to the current cycle.
|
108
108
|
def self.leaders(category, cycle=CURRENT_CYCLE)
|
109
|
-
|
110
|
-
|
109
|
+
reply = invoke("#{cycle}/candidates/leaders/#{category}",{})
|
110
|
+
results = reply['results']
|
111
111
|
results.map{|c| self.create(c)}
|
112
112
|
end
|
113
|
-
|
113
|
+
|
114
114
|
# Returns an array of candidates matching a search term within a cycle. Defaults to the
|
115
115
|
# current cycle.
|
116
116
|
def self.search(name, cycle=CURRENT_CYCLE, offset=nil)
|
117
|
-
|
118
|
-
|
117
|
+
reply = invoke("#{cycle}/candidates/search", {:query => name, :offset => offset})
|
118
|
+
results = reply['results']
|
119
119
|
results.map{|c| self.create_from_search_results(c)}
|
120
120
|
end
|
121
|
-
|
121
|
+
|
122
122
|
# Returns an array of newly created FEC candidates within a current cycle. Defaults to the
|
123
123
|
# current cycle.
|
124
124
|
def self.new_candidates(cycle=CURRENT_CYCLE, offset=nil)
|
125
|
-
|
126
|
-
|
125
|
+
reply = invoke("#{cycle}/candidates/new",{:offset => offset})
|
126
|
+
results = reply['results']
|
127
127
|
results.map{|c| self.create(c)}
|
128
128
|
end
|
129
|
-
|
129
|
+
|
130
130
|
# Returns an array of candidates for a given state within a cycle, with optional chamber and
|
131
131
|
# district parameters. For example, House candidates from New York. Defaults to the current cycle.
|
132
132
|
def self.state(state, chamber=nil, district=nil, cycle=CURRENT_CYCLE, offset=nil)
|
133
133
|
path = "#{cycle}/seats/#{state}"
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
134
|
+
if chamber
|
135
|
+
path += "/#{chamber}"
|
136
|
+
path += "/#{district}" if district
|
137
|
+
end
|
138
138
|
reply = invoke(path,{:offset => offset})
|
139
139
|
results = reply['results']
|
140
140
|
results.map{|c| self.create_from_search_results(c)}
|
141
141
|
end
|
142
|
-
|
142
|
+
|
143
143
|
instance_eval { alias :state_chamber :state }
|
144
144
|
end
|
145
145
|
end
|