get_your_rep 0.1.9 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +55 -0
- data/.rubocop.yml +40 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/Rakefile +12 -0
- data/bin/console +16 -0
- data/bin/setup +6 -0
- data/config.reek +18 -0
- data/exe/reps +8 -0
- data/get_your_rep.gemspec +32 -0
- data/lib/get_your_rep.rb +18 -19
- data/lib/get_your_rep/cli.rb +113 -0
- data/lib/get_your_rep/delegation.rb +47 -65
- data/lib/get_your_rep/errors.rb +79 -0
- data/lib/get_your_rep/get_your_rep_module.rb +76 -0
- data/lib/get_your_rep/google.rb +50 -172
- data/lib/get_your_rep/office_location.rb +42 -0
- data/lib/get_your_rep/open_states.rb +43 -101
- data/lib/get_your_rep/patriotic.rb +82 -0
- data/lib/get_your_rep/representative.rb +87 -253
- data/lib/get_your_rep/responses/base.rb +34 -0
- data/lib/get_your_rep/responses/google_office.rb +35 -0
- data/lib/get_your_rep/responses/google_rep.rb +75 -0
- data/lib/get_your_rep/responses/open_states_office.rb +40 -0
- data/lib/get_your_rep/responses/open_states_rep.rb +50 -0
- data/lib/get_your_rep/version.rb +4 -0
- metadata +48 -15
- data/lib/core_extensions/array/get_your_rep_mutations.rb +0 -23
- data/lib/core_extensions/hash/get_your_rep_mutations.rb +0 -24
@@ -1,87 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GetYourRep
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
self.replace(self + value)
|
11
|
-
else
|
12
|
-
super
|
13
|
-
end
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
# Overloads the + operator to return a new Delegation instead of an Array.
|
18
|
-
def +(value)
|
19
|
-
super.to_del
|
20
|
-
end
|
21
|
-
|
22
|
-
# Overloads #reverse to return a new Delegation instead of an Array.
|
23
|
-
def reverse
|
24
|
-
self.replace(super)
|
25
|
-
end
|
26
|
-
|
27
|
-
# Collects the first names of every rep in the Delegation.
|
28
|
-
def first_names
|
29
|
-
self.map { |rep| rep.first_name }
|
30
|
-
end
|
31
|
-
|
32
|
-
# Collects the last names of every rep in the Delegation.
|
33
|
-
def last_names
|
34
|
-
self.map { |rep| rep.last_name }
|
35
|
-
end
|
36
|
-
|
37
|
-
# Maps all rep business cards.
|
38
|
-
def business_cards
|
39
|
-
self.map { |rep| rep.business_card }
|
3
|
+
# The Delegation class holds an array of Representative objects. It can enumerate on its reps.
|
4
|
+
class Delegation
|
5
|
+
include Enumerable
|
6
|
+
include GetYourRep::Errors
|
7
|
+
|
8
|
+
# Set the reps attribute, which will hold an array of Representative objects, to an empty array by default.
|
9
|
+
def initialize
|
10
|
+
@reps = []
|
40
11
|
end
|
41
12
|
|
42
|
-
#
|
43
|
-
def
|
44
|
-
|
13
|
+
# Defines the enumerator methods on the :reps attribute.
|
14
|
+
def each(&block)
|
15
|
+
@reps.each(&block)
|
45
16
|
end
|
46
17
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
18
|
+
# Merge reps with another Delegation. This will add the reps of other to the reps of self,
|
19
|
+
# and leave other unchanged. Only accepts a Delegation as an argument.
|
20
|
+
def merge(other)
|
21
|
+
if other.is_a?(self.class)
|
22
|
+
other.reps.each { |rep| add_rep(rep) }
|
23
|
+
self
|
24
|
+
else
|
25
|
+
not_a_del_error
|
26
|
+
end
|
50
27
|
end
|
51
28
|
|
52
|
-
#
|
53
|
-
def
|
54
|
-
|
29
|
+
# Returns a frozen duplicate of the reps array.
|
30
|
+
def reps
|
31
|
+
@reps.dup.freeze
|
55
32
|
end
|
56
33
|
|
57
|
-
#
|
58
|
-
def
|
59
|
-
|
34
|
+
# Empties the reps array.
|
35
|
+
def clear_reps
|
36
|
+
@reps.clear
|
60
37
|
end
|
61
38
|
|
62
|
-
#
|
63
|
-
def
|
64
|
-
|
39
|
+
# Add a Representative to the reps array. Only accepts a Representative as an argument.
|
40
|
+
def add_rep(other)
|
41
|
+
if other.is_a?(Representative)
|
42
|
+
@reps << other
|
43
|
+
other.delegation = self unless other.delegation == self
|
44
|
+
else
|
45
|
+
not_a_rep_error
|
46
|
+
end
|
65
47
|
end
|
66
48
|
|
67
|
-
#
|
68
|
-
def
|
69
|
-
|
49
|
+
# Collects the OfficeLocations of all associated reps.
|
50
|
+
def office_locations
|
51
|
+
map(&:office_locations).flatten
|
70
52
|
end
|
71
53
|
|
72
|
-
#
|
73
|
-
def
|
74
|
-
|
54
|
+
# Collects the full names of all associated reps.
|
55
|
+
def names
|
56
|
+
map(&:name)
|
75
57
|
end
|
76
58
|
|
77
|
-
#
|
78
|
-
def
|
79
|
-
|
59
|
+
# Collects the first names of all associated reps.
|
60
|
+
def first_names
|
61
|
+
map(&:first_name)
|
80
62
|
end
|
81
63
|
|
82
|
-
#
|
83
|
-
def
|
84
|
-
|
64
|
+
# Collects the last names of all associated reps.
|
65
|
+
def last_names
|
66
|
+
map(&:last_name)
|
85
67
|
end
|
86
68
|
end
|
87
69
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GetYourRep
|
3
|
+
# Custom Error handling classes and methods.
|
4
|
+
module Errors
|
5
|
+
# Errors for invalid or inadequate addresses.
|
6
|
+
class AddressError < ArgumentError
|
7
|
+
# Raise when the address is not a string or an integer.
|
8
|
+
# Supports integers for zip codes.
|
9
|
+
def invalid_address_type
|
10
|
+
'Entry must be of types String or Integer'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Raise when a rep cannot be found with the given parameters.
|
14
|
+
def reps_not_found
|
15
|
+
"Error message received. Some reps were not found. Confirm your address and check your parameters. \
|
16
|
+
Try a full address if you used a zip."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Errors raised when type-specific operations are performed invalidly.
|
21
|
+
# These operations will generally be association-building operations.
|
22
|
+
class GetYourRepTypeError < TypeError
|
23
|
+
def not_a_rep # :nodoc:
|
24
|
+
'Object must be a Representative.'
|
25
|
+
end
|
26
|
+
|
27
|
+
def not_an_office # :nodoc:
|
28
|
+
'Object must be an OfficeLocation.'
|
29
|
+
end
|
30
|
+
|
31
|
+
def not_a_delegation # :nodoc:
|
32
|
+
'Object must be a Delegation.'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Error raised when CLI command is invalid.
|
37
|
+
class CommandNotFoundError < NoMethodError
|
38
|
+
def command_not_found # :nodoc:
|
39
|
+
"Command not found. Use 'help' command for a list of possible commands."
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def not_an_office_error # :nodoc:
|
44
|
+
raise GetYourRepTypeError
|
45
|
+
rescue GetYourRepTypeError => error
|
46
|
+
puts error.not_an_office
|
47
|
+
end
|
48
|
+
|
49
|
+
def not_a_del_error # :nodoc:
|
50
|
+
raise GetYourRepTypeError
|
51
|
+
rescue GetYourRepTypeError => error
|
52
|
+
puts error.not_a_delegation
|
53
|
+
end
|
54
|
+
|
55
|
+
def not_a_rep_error # :nodoc:
|
56
|
+
raise GetYourRepTypeError
|
57
|
+
rescue GetYourRepTypeError => error
|
58
|
+
puts error.not_a_rep
|
59
|
+
end
|
60
|
+
|
61
|
+
def address_type_error # :nodoc:
|
62
|
+
raise AddressError
|
63
|
+
rescue AddressError => error
|
64
|
+
puts error.invalid_address_type.bold.red
|
65
|
+
end
|
66
|
+
|
67
|
+
def command_not_found_error # :nodoc:
|
68
|
+
raise CommandNotFoundError
|
69
|
+
rescue CommandNotFoundError => error
|
70
|
+
puts error.command_not_found.bold.red
|
71
|
+
end
|
72
|
+
|
73
|
+
def reps_not_found_error # :nodoc:
|
74
|
+
raise AddressError
|
75
|
+
rescue AddressError => error
|
76
|
+
puts error.reps_not_found.bold.red
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# GetYourRep module functions.
|
3
|
+
module GetYourRep
|
4
|
+
module_function
|
5
|
+
|
6
|
+
include GetYourRep::Errors
|
7
|
+
|
8
|
+
# Get all reps using Google for national legislators,
|
9
|
+
# and OpenStates for state reps.
|
10
|
+
def all_reps(address)
|
11
|
+
@delegation = Google.all_reps(address, congress_only: true)
|
12
|
+
open_states = OpenStates.all_reps(address)
|
13
|
+
@delegation.merge(open_states)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Check if an address value is a zip code or a full address.
|
17
|
+
def address_is_a_zip?(address)
|
18
|
+
address = convert_int_address_to_string(address)
|
19
|
+
if address.is_a?(String)
|
20
|
+
address =~ /^\d{5}(-\d{4})?$/ ? true : false
|
21
|
+
else
|
22
|
+
address_type_error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convert a zip code to a street address using Geocoder to convert zip to coordinates
|
27
|
+
# and coordinates to an address.
|
28
|
+
def convert_zip_to_address(address)
|
29
|
+
@coordinates = Geocoder.coordinates(address)
|
30
|
+
address = Geocoder.address(@coordinates)
|
31
|
+
trim_street_number(address)
|
32
|
+
end
|
33
|
+
|
34
|
+
# If a street number is a range reduce it to one number.
|
35
|
+
def trim_street_number(address)
|
36
|
+
address = address.split(' ')
|
37
|
+
address[0] = address[0].split('-').shift
|
38
|
+
address.join(' ')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Parses a String address and prepares for an HTTP request. Accepts Strings and Integers as args.
|
42
|
+
def parse_address(address)
|
43
|
+
address = convert_int_address_to_string(address)
|
44
|
+
if address.is_a?(String)
|
45
|
+
address.tr(',', '').split.join('%20')
|
46
|
+
else
|
47
|
+
address_type_error
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Geocode an address and returns the coordinates.
|
52
|
+
def get_coordinates(address)
|
53
|
+
address = convert_int_address_to_string(address)
|
54
|
+
if address.is_a?(String)
|
55
|
+
Geocoder.coordinates(address)
|
56
|
+
else
|
57
|
+
address_type_error
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Converts an integer to a string in case a zip was input as an int.
|
62
|
+
def convert_int_address_to_string(address)
|
63
|
+
if address.is_a?(Integer)
|
64
|
+
format('0%o', address)
|
65
|
+
else
|
66
|
+
address
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Creates an empty delegation if no reps were found.
|
71
|
+
def handle_reps_not_found_error
|
72
|
+
reps_not_found_error
|
73
|
+
puts response
|
74
|
+
Delegation.new
|
75
|
+
end
|
76
|
+
end
|
data/lib/get_your_rep/google.rb
CHANGED
@@ -1,194 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module GetYourRep
|
2
|
-
|
3
3
|
# Retrieve your elected representatives from the Google Civic Information API, parse it from
|
4
4
|
# JSON, and assemble it into a usable Ruby Hash-like object called a Representative.
|
5
5
|
# Representatives are then wrapped in an Array-like object called a Delegation.
|
6
6
|
#
|
7
7
|
# You must configure your own Google API key as an environment variable using the constant name
|
8
8
|
# 'GOOGLE_API_KEY' in order for this gem to work.
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
@role = 'representative'
|
32
|
-
state_rep = get_rep
|
33
|
-
|
34
|
-
@role = 'senator'
|
35
|
-
state_sen = get_rep
|
36
|
-
|
37
|
-
#Check the Open States API if Google can't find state reps.
|
38
|
-
if state_rep.empty? && state_sen.empty?
|
39
|
-
@reps << GetYourRep::OpenStates.now(@coordinates)
|
40
|
-
else
|
41
|
-
@reps << state_rep
|
42
|
-
@reps << state_sen
|
9
|
+
module Google
|
10
|
+
class << self
|
11
|
+
include GetYourRep
|
12
|
+
|
13
|
+
# Holds the parsed address for the HTTP request.
|
14
|
+
attr_accessor :address
|
15
|
+
# Holds the Delegation object which will be returned by the all_reps class method.
|
16
|
+
attr_accessor :delegation
|
17
|
+
# Holds the raw JSON data response from the API.
|
18
|
+
attr_accessor :response
|
19
|
+
|
20
|
+
# The Google API key is used to access data from the Google Civic Information API. You must
|
21
|
+
# obtain your own and configure it as an environment variable on your system.
|
22
|
+
API_KEY = ENV['GOOGLE_API_KEY']
|
23
|
+
|
24
|
+
# Makes the call to the Google API and delivers the response after parsing.
|
25
|
+
# Returns a GetYourRep::Delegation object, holding a collection of GetYourRep::Representatives
|
26
|
+
def all_reps(address, congress_only: nil)
|
27
|
+
init(address)
|
28
|
+
ask_google_api(congress_only: congress_only)
|
29
|
+
deliver_response
|
43
30
|
end
|
44
31
|
|
45
|
-
|
46
|
-
|
47
|
-
def self.top_level_reps(address)
|
48
|
-
@reps = now(address)
|
49
|
-
|
50
|
-
@role = 'senator'
|
51
|
-
@reps << get_rep
|
52
|
-
|
53
|
-
@level = 'state'
|
54
|
-
@role = 'executive'
|
55
|
-
@reps << get_rep
|
56
|
-
|
57
|
-
@role = 'vice executive'
|
58
|
-
@reps << get_rep
|
59
|
-
end
|
60
|
-
|
61
|
-
# Parses a String address and prepares for an HTTP request. Accepts Strings and Integers as args.
|
62
|
-
def self.parse_address(address)
|
63
|
-
address = '0%o' % address if address.is_a?(Integer)
|
64
|
-
raise "Entry must be of types String or Integer" if !address.is_a?(String)
|
65
|
-
address = address.tr(',', '').split.join('%20')
|
66
|
-
end
|
67
|
-
|
68
|
-
# Unparses a parsed address
|
69
|
-
# def self.unparsed_address(address)
|
70
|
-
# address.split('%20').join(' ')
|
71
|
-
# end
|
72
|
-
|
73
|
-
# Check if an address value is a zip code or a full address.
|
74
|
-
def self.address_is_a_zip?(address)
|
75
|
-
address = '0%o' % address if address.is_a?(Integer)
|
76
|
-
raise "Entry must be of types String or Integer" if !address.is_a?(String)
|
77
|
-
!(address =~ /^\d{5}(-\d{4})?$/).nil?
|
78
|
-
end
|
79
|
-
|
80
|
-
# Convert a zip code to a street address using Geocoder to convert zip to coordinates
|
81
|
-
# and coordinates to an address.
|
82
|
-
def self.convert_zip_to_address(address)
|
83
|
-
@coordinates = Geocoder.coordinates(address)
|
84
|
-
address = Geocoder.address(@coordinates)
|
85
|
-
trim_street_number(address)
|
86
|
-
end
|
87
|
-
|
88
|
-
# If a street number is a range reduce it to one number.
|
89
|
-
def self.trim_street_number(address)
|
90
|
-
address = address.split(' ')
|
91
|
-
address[0] = address[0].split('-').shift
|
92
|
-
address.join(' ')
|
93
|
-
end
|
94
|
-
|
95
|
-
# Sets parameters for and executes Google API request.
|
96
|
-
def self.get_rep
|
32
|
+
private
|
97
33
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
when 'state'
|
102
|
-
'administrativeArea1'
|
103
|
-
else
|
104
|
-
@level
|
105
|
-
end
|
106
|
-
|
107
|
-
role = case @role
|
108
|
-
when 'representative'
|
109
|
-
'legislatorLowerBody'
|
110
|
-
when 'senator'
|
111
|
-
'legislatorUpperBody'
|
112
|
-
when 'executive'
|
113
|
-
'headOfGovernment'
|
114
|
-
when 'vice executive'
|
115
|
-
'deputyHeadOfGovernment'
|
116
|
-
else
|
117
|
-
@role
|
118
|
-
end
|
119
|
-
|
120
|
-
url = "https://www.googleapis.com/civicinfo/v2/representatives?address=#{@address}%20&includeOffices=true&levels=#{level}&roles=#{role}&fields=offices%2Cofficials&key=#{API_KEY}"
|
121
|
-
@response = HTTParty.get(url).parsed_response
|
122
|
-
deliver_response
|
123
|
-
end
|
124
|
-
|
125
|
-
def self.deliver_response
|
126
|
-
if @response.empty?
|
127
|
-
puts "'Google can't find a rep with that address. Checking the Open States API."
|
128
|
-
return GetYourRep::Delegation.new
|
129
|
-
elsif @response['error']
|
130
|
-
puts 'Error message received. Confirm and re-enter your address and check your parameters.'
|
131
|
-
puts @response
|
132
|
-
return GetYourRep::Delegation.new
|
34
|
+
def init(address)
|
35
|
+
address = convert_zip_to_address(address) if address_is_a_zip?(address)
|
36
|
+
self.address = parse_address(address)
|
133
37
|
end
|
134
|
-
parse_rep
|
135
|
-
@delegation
|
136
|
-
end
|
137
38
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
:office, @response['offices'].first['name'],
|
149
|
-
:party, official['party'],
|
150
|
-
:phone, official['phones'] || [],
|
151
|
-
:office_locations, offices(official),
|
152
|
-
:email, official['emails'] || [],
|
153
|
-
:url, (official['urls'].first if official['urls']),
|
154
|
-
:photo, official['photoUrl'],
|
155
|
-
]
|
156
|
-
parse_channels(official) if official['channels']
|
39
|
+
def ask_google_api(congress_only: nil)
|
40
|
+
if congress_only
|
41
|
+
url = "https://www.googleapis.com/civicinfo/v2/representatives?address=#{address}%20&includeOffices=true&\
|
42
|
+
levels=country&roles=legislatorLowerBody&roles=legislatorUpperBody&fields=offices%2Cofficials&key=#{API_KEY}"
|
43
|
+
else
|
44
|
+
url = "https://www.googleapis.com/civicinfo/v2/representatives?address=#{address}%20&includeOffices=true&\
|
45
|
+
levels=country&levels=administrativeArea1&roles=legislatorLowerBody&roles=legislatorUpperBody&roles=headOfGovernment\
|
46
|
+
&roles=deputyHeadOfGovernment&fields=offices%2Cofficials&key=#{API_KEY}"
|
47
|
+
end
|
48
|
+
self.response = HTTParty.get(url).parsed_response
|
157
49
|
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def self.offices(official)
|
161
|
-
offices = []
|
162
50
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
else
|
170
|
-
office_hash[:type] = 'district'
|
171
|
-
end
|
172
|
-
|
173
|
-
office_hash[:line_1] = office['line1']
|
174
|
-
office_hash[:line_2] = office['line_2']
|
175
|
-
office_hash[:line_3] = "#{office['city'].capitalize}, #{office['state']} #{office['zip']}"
|
176
|
-
offices << office_hash
|
51
|
+
def deliver_response
|
52
|
+
if response.empty? || response['error']
|
53
|
+
handle_reps_not_found_error
|
54
|
+
else
|
55
|
+
parse_reps
|
56
|
+
delegation
|
177
57
|
end
|
178
58
|
end
|
179
59
|
|
180
|
-
|
181
|
-
|
60
|
+
def parse_reps
|
61
|
+
self.delegation = Delegation.new
|
182
62
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
63
|
+
response['officials'].each_with_index do |official, index|
|
64
|
+
external_rep = GoogleRep.new(official)
|
65
|
+
rep_hash = external_rep.build_hash(response['offices'], index)
|
66
|
+
new_rep = Representative.new(rep_hash)
|
67
|
+
delegation.add_rep(new_rep)
|
68
|
+
end
|
187
69
|
end
|
188
|
-
@delegation.last[:twitter] = nil unless @delegation.last[:twitter]
|
189
|
-
@delegation.last[:facebook] = nil unless @delegation.last[:facebook]
|
190
|
-
@delegation.last[:youtube] = nil unless @delegation.last[:youtube]
|
191
|
-
@delegation.last[:googleplus] = nil unless @delegation.last[:googleplus]
|
192
70
|
end
|
193
71
|
end
|
194
72
|
end
|