campaign_cash 2.3.2 → 2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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