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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 The New York Times Company
1
+ Copyright (c) 2012 The New York Times Company
2
2
 
3
3
  Licensed under the Apache License, Version 2.0 (the "License");
4
4
  you may not use this library except in compliance with the License.
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.
@@ -6,105 +6,105 @@ require 'ostruct'
6
6
 
7
7
  module CampaignCash
8
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 = 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
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
- :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
-
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
- 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'],
35
- :total_contributions => params['total_contributions'],
36
- :total_from_individuals => params['total_from_individuals'],
37
- :total_from_pacs => params['total_from_pacs'],
38
- :candidate_loans => params['candidate_loans'],
39
- :total_disbursements => params['total_disbursements'],
40
- :total_refunds => params['total_refunds'],
41
- :debts_owed => params['debts_owed'],
42
- :begin_cash => params['begin_cash'],
43
- :end_cash => params['end_cash'],
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",
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
- end
96
-
97
- # Retrieve a candidate object via its FEC candidate id within a cycle.
98
- # Defaults to the current cycle.
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
- reply = invoke("#{cycle}/candidates/#{fecid}")
101
- result = reply['results']
102
- self.create(result.first) if result.first
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
- reply = invoke("#{cycle}/candidates/leaders/#{category}",{})
110
- results = reply['results']
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
- reply = invoke("#{cycle}/candidates/search", {:query => name, :offset => offset})
118
- results = reply['results']
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
- reply = invoke("#{cycle}/candidates/new",{:offset => offset})
126
- results = reply['results']
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
- if chamber
135
- path += "/#{chamber}"
136
- path += "/#{district}" if district
137
- end
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