blue_state_digital 0.6.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +3 -0
  7. data/Gemfile +2 -0
  8. data/Guardfile +5 -0
  9. data/LICENSE +24 -0
  10. data/README.md +123 -0
  11. data/Rakefile +11 -0
  12. data/blue_state_digital.gemspec +37 -0
  13. data/lib/blue_state_digital.rb +32 -0
  14. data/lib/blue_state_digital/address.rb +28 -0
  15. data/lib/blue_state_digital/api_data_model.rb +31 -0
  16. data/lib/blue_state_digital/collection_resource.rb +14 -0
  17. data/lib/blue_state_digital/connection.rb +119 -0
  18. data/lib/blue_state_digital/constituent.rb +178 -0
  19. data/lib/blue_state_digital/constituent_group.rb +151 -0
  20. data/lib/blue_state_digital/contribution.rb +73 -0
  21. data/lib/blue_state_digital/dataset.rb +139 -0
  22. data/lib/blue_state_digital/dataset_map.rb +138 -0
  23. data/lib/blue_state_digital/email.rb +22 -0
  24. data/lib/blue_state_digital/email_unsubscribe.rb +11 -0
  25. data/lib/blue_state_digital/error_middleware.rb +29 -0
  26. data/lib/blue_state_digital/event.rb +46 -0
  27. data/lib/blue_state_digital/event_rsvp.rb +17 -0
  28. data/lib/blue_state_digital/event_type.rb +19 -0
  29. data/lib/blue_state_digital/phone.rb +20 -0
  30. data/lib/blue_state_digital/version.rb +3 -0
  31. data/spec/blue_state_digital/address_spec.rb +25 -0
  32. data/spec/blue_state_digital/api_data_model_spec.rb +13 -0
  33. data/spec/blue_state_digital/connection_spec.rb +153 -0
  34. data/spec/blue_state_digital/constituent_group_spec.rb +269 -0
  35. data/spec/blue_state_digital/constituent_spec.rb +422 -0
  36. data/spec/blue_state_digital/contribution_spec.rb +132 -0
  37. data/spec/blue_state_digital/dataset_map_spec.rb +137 -0
  38. data/spec/blue_state_digital/dataset_spec.rb +177 -0
  39. data/spec/blue_state_digital/email_spec.rb +16 -0
  40. data/spec/blue_state_digital/error_middleware_spec.rb +15 -0
  41. data/spec/blue_state_digital/event_rsvp_spec.rb +17 -0
  42. data/spec/blue_state_digital/event_spec.rb +70 -0
  43. data/spec/blue_state_digital/event_type_spec.rb +51 -0
  44. data/spec/blue_state_digital/phone_spec.rb +16 -0
  45. data/spec/fixtures/multiple_event_types.json +234 -0
  46. data/spec/fixtures/single_event_type.json +117 -0
  47. data/spec/spec_helper.rb +21 -0
  48. data/spec/support/matchers/fields.rb +23 -0
  49. metadata +334 -0
@@ -0,0 +1,138 @@
1
+ module BlueStateDigital
2
+ class DatasetMap < ApiDataModel
3
+
4
+ extend ActiveModel::Naming
5
+ include ActiveModel::Validations
6
+
7
+ UPLOAD_ENDPOINT = '/cons/upload_dataset_map'
8
+ DELETE_ENDPOINT = '/cons/delete_dataset_map'
9
+
10
+ FIELDS = [
11
+ :map_id,
12
+ :type
13
+ ]
14
+ attr_accessor *FIELDS
15
+ attr_reader :data,:data_header,:errors
16
+
17
+ validate :data_must_have_header
18
+
19
+ def initialize(args={})
20
+ super
21
+ @data = []
22
+ @data_header = nil
23
+ @errors = ActiveModel::Errors.new(self)
24
+ end
25
+
26
+ def data
27
+ @data
28
+ end
29
+
30
+ def add_data_row(row)
31
+ @data.push row
32
+ end
33
+
34
+ def add_data_header(header_row)
35
+ @data_header = header_row
36
+ end
37
+
38
+ def save
39
+ #errors.add(:data, "cannot be blank") if @data.blank?
40
+ if valid?
41
+ if connection
42
+ response = connection.perform_request_raw(
43
+ UPLOAD_ENDPOINT,
44
+ { api_ver: 2, content_type: "text/csv", accept: 'application/json' },
45
+ "PUT",
46
+ csv_payload
47
+ )
48
+ if(response.status == 202)
49
+ true
50
+ else
51
+ errors.add(:web_service,"#{response.body}")
52
+ false
53
+ end
54
+ else
55
+ errors.add(:connection,"is missing")
56
+ false
57
+ end
58
+ else
59
+ false
60
+ end
61
+ end
62
+
63
+ def delete
64
+ if map_id.nil?
65
+ errors.add(:map_id,"is missing")
66
+ return false
67
+ end
68
+ if connection
69
+ response = connection.perform_request_raw(
70
+ DELETE_ENDPOINT,
71
+ { api_ver: 2 },
72
+ "POST",
73
+ {map_id: map_id}.to_json
74
+ )
75
+ if(response.status==200)
76
+ true
77
+ else
78
+ errors.add(:web_service,"#{response.body}")
79
+ false
80
+ end
81
+ else
82
+ errors.add(:connection,"is missing")
83
+ false
84
+ end
85
+ end
86
+
87
+ def read_attribute_for_validation(attr)
88
+ send(attr)
89
+ end
90
+ def self.human_attribute_name(attr, options = {})
91
+ attr
92
+ end
93
+ def self.lookup_ancestors
94
+ [self]
95
+ end
96
+
97
+ private
98
+
99
+ def csv_payload
100
+ csv_string = CSV.generate do |csv|
101
+ csv << (@data_header||[])
102
+ @data.each do |row|
103
+ csv << row
104
+ end
105
+ end
106
+ end
107
+
108
+ def data_must_have_header
109
+ errors.add(:data_header, "is missing") if !@data.blank? && @data_header.nil?
110
+ end
111
+ end
112
+
113
+ class DatasetMaps < CollectionResource
114
+ FETCH_ENDPOINT = '/cons/list_dataset_maps'
115
+ def get_dataset_maps
116
+ if connection
117
+ response = connection.perform_request FETCH_ENDPOINT, { api_ver: 2 }, "GET"
118
+ # TODO: Should check response's status code
119
+ begin
120
+ parsed_response = JSON.parse(response)
121
+
122
+ data = parsed_response['data']
123
+ if(data)
124
+ data.map do |dataset|
125
+ DatasetMap.new(dataset)
126
+ end
127
+ else
128
+ nil
129
+ end
130
+ rescue StandardError => e
131
+ raise FetchFailureException.new("#{e}")
132
+ end
133
+ else
134
+ raise NoConnectionException.new
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,22 @@
1
+ # <email>gil+punky1@thoughtworks.com</email>
2
+ # <email_type>personal</email_type>
3
+ # <is_subscribed>1</is_subscribed>
4
+ # <is_primary>1</is_primary>
5
+
6
+ module BlueStateDigital
7
+ class Email < ApiDataModel
8
+ FIELDS = [:email, :email_type, :is_subscribed, :is_primary]
9
+
10
+ attr_accessor *FIELDS
11
+
12
+ def to_xml(builder = Builder::XmlMarkup.new)
13
+ builder.email do | email |
14
+ FIELDS.each do | field |
15
+ email.__send__(field, self.send(field)) if self.send(field)
16
+ end
17
+ end
18
+ builder
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module BlueStateDigital
2
+ class EmailUnsubscribe < ApiDataModel
3
+ attr_accessor :email, :reason
4
+
5
+ def unsubscribe!
6
+ result = connection.perform_request '/cons/email_unsubscribe', {email: email, reason: reason}, 'POST'
7
+ result == ''
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,29 @@
1
+ module BlueStateDigital
2
+ class Unauthorized < ::Faraday::Error::ClientError ; end
3
+ class ResourceDoesNotExist < ::Faraday::Error::ClientError ; end
4
+ class EmailNotFound < ::Faraday::Error::ClientError ; end
5
+
6
+ class ErrorMiddleware < ::Faraday::Response::RaiseError
7
+ def on_complete(env)
8
+ case env[:status]
9
+ when 404
10
+ raise Faraday::Error::ResourceNotFound, response_values(env).to_s
11
+ when 403
12
+ raise BlueStateDigital::Unauthorized, response_values(env).to_s
13
+ when 409
14
+ if env.body =~ /does not exist/
15
+ raise BlueStateDigital::ResourceDoesNotExist, response_values(env).to_s
16
+ elsif env.body =~ /Email not found/
17
+ raise BlueStateDigital::EmailNotFound, response_values(env).to_s
18
+ else
19
+ raise Faraday::Error::ClientError, response_values(env).to_s
20
+ end
21
+ when 407
22
+ # mimic the behavior that we get with proxy requests with HTTPS
23
+ raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
24
+ when ClientErrorStatuses
25
+ raise Faraday::Error::ClientError, response_values(env).to_s
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ module BlueStateDigital
2
+ class Event < ApiDataModel
3
+
4
+ class EventSaveValidationException < StandardError
5
+ def initialize(validation_errors)
6
+ error_message = ""
7
+ validation_errors.each { |field, errors| error_message << "Validation errors on field #{field}: #{errors}\n" }
8
+
9
+ super(error_message)
10
+ end
11
+ end
12
+
13
+ FIELDS = [:event_id_obfuscated, :event_type_id, :creator_cons_id, :name, :description, :venue_name, :venue_country, :venue_zip, :venue_city, :venue_state_cd, :start_date, :end_date]
14
+ attr_accessor *FIELDS
15
+
16
+ def save
17
+ if self.event_id_obfuscated.blank?
18
+ response_json_text = connection.perform_request '/event/create_event', {accept: 'application/json', event_api_version: '2', values: self.to_json}, "POST"
19
+ else
20
+ response_json_text = connection.perform_request '/event/update_event', {accept: 'application/json', event_api_version: '2', values: self.to_json}, "POST"
21
+ end
22
+
23
+ response = JSON.parse(response_json_text)
24
+ if response['validation_errors']
25
+ raise EventSaveValidationException, response['validation_errors']
26
+ else
27
+ self.event_id_obfuscated = response['event_id_obfuscated']
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ def to_json
34
+ event_attrs = self.event_id_obfuscated.blank? ? { } : { event_id_obfuscated: self.event_id_obfuscated }
35
+ (FIELDS - [:event_id_obfuscated, :start_date, :end_date]).each do |field|
36
+ event_attrs[field] = self.send(field)
37
+ end
38
+
39
+ duration_in_minutes = ((end_date - start_date) / 60).to_i
40
+ day_attrs = { start_datetime_system: start_date.strftime('%Y-%m-%d %H:%M:%S %z'), duration: duration_in_minutes }
41
+ event_attrs[:days] = [ day_attrs ]
42
+
43
+ event_attrs.to_json
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ module BlueStateDigital
2
+ class EventRSVP < ApiDataModel
3
+ FIELDS = [:event_id_obfuscated, :will_attend, :email, :zip, :country]
4
+ attr_accessor *FIELDS
5
+
6
+ def save
7
+ connection.perform_graph_request '/addrsvp', self.attributes, "POST"
8
+ end
9
+
10
+ def attributes
11
+ FIELDS.inject({}) do |attrs, field|
12
+ attrs[field] = self.send(field)
13
+ attrs
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module BlueStateDigital
2
+ class EventType < ApiDataModel
3
+ FIELDS = [:event_type_id, :name, :description]
4
+ attr_accessor *FIELDS
5
+ end
6
+
7
+ class EventTypes < CollectionResource
8
+ def get_event_types
9
+ from_response(connection.perform_request('/event/get_available_types', {}, 'GET'))
10
+ end
11
+
12
+ private
13
+
14
+ def from_response(response)
15
+ parsed_response = JSON.parse(response)
16
+ parsed_response.collect { |pet| EventType.new(pet) }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # <phone>0408573670</phone>
2
+ # <phone_type>unknown</phone_type>
3
+ # <is_subscribed>1</is_subscribed>
4
+ # <is_primary>1</is_primary>
5
+ module BlueStateDigital
6
+ class Phone < ApiDataModel
7
+ FIELDS = [:phone, :phone_type, :is_primary, :is_subscribed]
8
+ attr_accessor *FIELDS
9
+
10
+ def to_xml(builder = Builder::XmlMarkup.new)
11
+ builder.phone do | phone |
12
+ FIELDS.each do | field |
13
+ phone.__send__(field, self.send(field)) if self.send(field)
14
+ end
15
+ end
16
+ builder
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module BlueStateDigital
2
+ VERSION = "0.6.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe BlueStateDigital::Address do
4
+ subject { BlueStateDigital::Address.new({latitude: "40.1", longitude: "40.2"}) }
5
+ specify { expect(subject.latitude).to eq("40.1") }
6
+ specify { expect(subject.longitude).to eq("40.2") }
7
+
8
+
9
+ it "should return a builder" do
10
+ expect(subject.to_xml).to be_a(Builder)
11
+ end
12
+
13
+ describe :to_hash do
14
+ it "should return a hash of all fields" do
15
+ attr_hash = BlueStateDigital::Address::FIELDS.inject({}) {|h,k| h[k]="#{k.to_s}_value"; h}
16
+ phone = BlueStateDigital::Address.new attr_hash
17
+ expect(phone.to_hash).to eq(attr_hash)
18
+ end
19
+ it "should include nil fields" do
20
+ expected_hash = BlueStateDigital::Address::FIELDS.inject({}) {|h,k| h[k]=nil; h}
21
+ phone = BlueStateDigital::Address.new {}
22
+ expect(phone.to_hash).to eq(expected_hash)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyModel < BlueStateDigital::ApiDataModel
4
+ attr_accessor :attr1, :attr2
5
+ end
6
+
7
+ describe BlueStateDigital::ApiDataModel do
8
+ it "should initialize model with attributes" do
9
+ model = DummyModel.new({ attr1: "1", attr2: "2" })
10
+ expect(model.attr1).to eq("1")
11
+ expect(model.attr2).to eq("2")
12
+ end
13
+ end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+ require 'blue_state_digital/connection'
3
+ require 'timecop'
4
+
5
+ describe BlueStateDigital::Connection do
6
+ let(:api_host) { 'enoch.bluestatedigital.com' }
7
+ let(:api_id) { 'sfrazer' }
8
+ let(:api_secret) { '7405d35963605dc36702c06314df85db7349613f' }
9
+ let(:connection) { BlueStateDigital::Connection.new({host: api_host, api_id: api_id, api_secret: api_secret})}
10
+
11
+ if Faraday::VERSION != "0.8.9"
12
+ describe '#compute_hmac' do
13
+ it "should not escape whitespaces on params" do
14
+ timestamp = Time.parse('2014-01-01 00:00:00 +0000')
15
+ Timecop.freeze(timestamp) do
16
+ api_call = '/somemethod'
17
+ api_ts = timestamp.utc.to_i.to_s
18
+ expect(OpenSSL::HMAC).to receive(:hexdigest) do |digest, key, data|
19
+ expect(digest).to eq('sha1')
20
+ expect(key).to eq(api_secret)
21
+ expect(data).to match(/name=string with multiple whitespaces/)
22
+ end
23
+
24
+ api_mac = connection.compute_hmac("/page/api#{api_call}", api_ts, { api_ver: '2', api_id: api_id, api_ts: api_ts, name: 'string with multiple whitespaces' })
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "#perform_request" do
31
+ context 'POST' do
32
+ it "should perform POST request" do
33
+ timestamp = Time.now
34
+ Timecop.freeze(timestamp) do
35
+ api_call = '/somemethod'
36
+ api_ts = timestamp.utc.to_i.to_s
37
+ api_mac = connection.compute_hmac("/page/api#{api_call}", api_ts, { api_ver: '2', api_id: api_id, api_ts: api_ts })
38
+
39
+ stub_url = "https://#{api_host}/page/api/somemethod?api_id=#{api_id}&api_mac=#{api_mac}&api_ts=#{api_ts}&api_ver=2"
40
+ stub_request(:post, stub_url).with do |request|
41
+ expect(request.body).to eq("a=b")
42
+ expect(request.headers['Accept']).to eq('text/xml')
43
+ expect(request.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
44
+ true
45
+ end.to_return(body: "body")
46
+
47
+ response = connection.perform_request(api_call, params = {}, method = "POST", body = "a=b")
48
+ expect(response).to eq("body")
49
+ end
50
+ end
51
+
52
+ context 'well stubbed' do
53
+ before(:each) do
54
+ faraday_client = double(request: nil, response: nil, adapter: nil, options: {})
55
+ expect(faraday_client).to receive(:post).and_yield(post_request).and_return(post_request)
56
+ allow(Faraday).to receive(:new).and_yield(faraday_client).and_return(faraday_client)
57
+ end
58
+
59
+ let(:post_request) do
60
+ pr = double(headers: headers, body: '', url: nil)
61
+ allow(pr).to receive(:body=)
62
+ options = double()
63
+ allow(options).to receive(:timeout=)
64
+ allow(pr).to receive(:options).and_return(options)
65
+ pr
66
+ end
67
+
68
+ let(:headers) { {} }
69
+
70
+ it "should override Content-Type with param" do
71
+ connection = BlueStateDigital::Connection.new({host: api_host, api_id: api_id, api_secret: api_secret})
72
+
73
+ connection.perform_request '/somemethod', { content_type: 'application/json' }, 'POST'
74
+
75
+ expect(headers.keys).to include('Content-Type')
76
+ expect(headers['Content-Type']).to eq('application/json')
77
+ end
78
+
79
+ it "should override Accept with param" do
80
+ connection = BlueStateDigital::Connection.new({host: api_host, api_id: api_id, api_secret: api_secret})
81
+
82
+ connection.perform_request '/somemethod', { accept: 'application/json' }, 'POST'
83
+
84
+ expect(headers.keys).to include('Accept')
85
+ expect(headers['Accept']).to eq('application/json')
86
+ end
87
+ end
88
+ end
89
+
90
+ it "should perform PUT request" do
91
+ timestamp = Time.now
92
+ Timecop.freeze(timestamp) do
93
+ api_call = '/somemethod'
94
+ api_ts = timestamp.utc.to_i.to_s
95
+ api_mac = connection.compute_hmac("/page/api#{api_call}", api_ts, { api_ver: '2', api_id: api_id, api_ts: api_ts })
96
+
97
+ stub_url = "https://#{api_host}/page/api/somemethod?api_id=#{api_id}&api_mac=#{api_mac}&api_ts=#{api_ts}&api_ver=2"
98
+ stub_request(:put, stub_url).with do |request|
99
+ expect(request.body).to eq("a=b")
100
+ expect(request.headers['Accept']).to eq('text/xml')
101
+ expect(request.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
102
+ true
103
+ end.to_return(body: "body")
104
+
105
+ response = connection.perform_request(api_call, params = {}, method = "PUT", body = "a=b")
106
+ expect(response).to eq("body")
107
+ end
108
+ end
109
+
110
+ it "should perform GET request" do
111
+ timestamp = Time.now
112
+ Timecop.freeze(timestamp) do
113
+ api_call = '/somemethod'
114
+ api_ts = timestamp.utc.to_i.to_s
115
+ api_mac = connection.compute_hmac("/page/api#{api_call}", api_ts, { api_ver: '2', api_id: api_id, api_ts: api_ts })
116
+
117
+ stub_url = "https://#{api_host}/page/api/somemethod?api_id=#{api_id}&api_mac=#{api_mac}&api_ts=#{api_ts}&api_ver=2"
118
+ stub_request(:get, stub_url).to_return(body: "body")
119
+
120
+ response = connection.perform_request(api_call, params = {})
121
+ expect(response).to eq("body")
122
+ end
123
+ end
124
+ end
125
+
126
+ describe "perform_graph_request" do
127
+ let(:faraday_client) { double(request: nil, response: nil, adapter: nil) }
128
+
129
+ it "should perform Graph API request" do
130
+ post_request = double
131
+ expect(post_request).to receive(:url).with('/page/graph/rsvp/add', {param1: 'my_param', param2: 'my_other_param'})
132
+ expect(faraday_client).to receive(:post).and_yield(post_request).and_return(post_request)
133
+ allow(Faraday).to receive(:new).and_yield(faraday_client).and_return(faraday_client)
134
+ connection = BlueStateDigital::Connection.new({host: api_host, api_id: api_id, api_secret: api_secret})
135
+
136
+ connection.perform_graph_request('/rsvp/add', {param1: 'my_param', param2: 'my_other_param'}, 'POST')
137
+ end
138
+ end
139
+
140
+ describe "#get_deferred_results" do
141
+ it "should make a request" do
142
+ expect(connection).to receive(:perform_request).and_return("foo")
143
+ expect(connection.get_deferred_results("deferred_id")).to eq("foo")
144
+ end
145
+ end
146
+
147
+ describe "#compute_hmac" do
148
+ it "should compute proper hmac hash" do
149
+ params = { api_id: api_id, api_ts: '1272659462', api_ver: '2' }
150
+ expect(connection.compute_hmac('/page/api/circle/list_circles', '1272659462', params)).to eq('c4a31bdaabef52d609cbb5b01213fb267af4e808')
151
+ end
152
+ end
153
+ end