get_your_rep 0.1.9 → 1.0.0
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.
- 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
|