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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4e8917551b5e90a45a3c448cf6f58804e9175bb3
4
+ data.tar.gz: b22e282e7cd262c56f50f51fe21cf64dd25cc51e
5
+ SHA512:
6
+ metadata.gz: ae5ab46979afce07ca7d8af7b8ce5d28e99765c403055f901502948deb9109ac9dbb2a2f4c68e6e38c4e3fcd6f7c8905f49d14ecac4d29f4e5eb308e7bf93edb
7
+ data.tar.gz: 8667e65c8301f175e99ecf8ec7152849c8bb612e1c9ccd26773665b015bc7834e8cea566849b75dad4aa1c1d19d330dee761334d671bdc53c1cef93a63de2db3
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .idea
2
+ *.swp
3
+ Gemfile.lock
4
+ test.rb
5
+ *.tmproj
6
+ tmp/
7
+ pkg/
8
+ example.rb
9
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ blue_state_digital
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.3
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.2.3"
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'rspec', :version => 2, :cli => "--color" do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2012, ControlShift, Ltd.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of the ControlShift, Ltd. nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL CONTROLSHIFT LTD. BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Blue State Digital Gem
2
+
3
+ [![Build Status](https://travis-ci.org/controlshift/blue_state_digital.svg?branch=master)](https://travis-ci.org/controlshift/blue_state_digital)
4
+
5
+ ## Usage
6
+
7
+ ```ruby
8
+ gem blue_state_digital
9
+ ```
10
+
11
+ Configuration:
12
+
13
+ ```ruby
14
+ connection = BlueStateDigital::Connection.new(host:'foo.com' api_id: 'bar', api_secret: 'magic_secret')
15
+ cons = BlueStateDigital::Constituent.new({firstname: 'George', lastname: 'Washington', emails: [{ email: 'george@washington.com'}]}.merge({connection: connection}))
16
+ cons.save
17
+ cons.Id # created constituent ID
18
+ ```
19
+
20
+ Use the event machine adapter:
21
+
22
+ ```ruby
23
+ connection = BlueStateDigital::Connection.new(host:'foo.com' api_id: 'bar', api_secret: 'magic_secret', adapter: :em_synchrony)
24
+ ```
25
+
26
+ ### Unsubscribes
27
+
28
+ ```ruby
29
+ connection = BlueStateDigital::Connection.new(host:'foo.com' api_id: 'bar', api_secret: 'magic_secret')
30
+ unsub = BlueStateDigital::EmailUnsubscribe.new({email: 'george@washington.com', reason: 'tea in the harbor'}.merge({connection: connection}))
31
+ unsub.unsubscribe! # raises on error, returns true on success.
32
+
33
+
34
+ ### Dataset integration
35
+
36
+ [BSD API for uploading Dataset](https://cshift.cp.bsd.net/page/api/doc#---------------------upload_dataset-----------------)
37
+ This creates a new personalization dataset from a slug, map_type, and CSV data, or if the supplied slug already exists, replaces the existing dataset. The CSV data can be attached by ```add_data_header``` and ```add_data_row``` methods. ```save``` method will upload the dataset and return true if successful.
38
+ ```ruby
39
+ dataset=BlueStateDigital::Dataset.new({slug: "downballot_dataset",map_type:"downballot"}.merge(connection: connection))
40
+ dataset.add_data_header(%w(index house house_link senate senate_link))
41
+ dataset.add_data_row(%w(NJ01 Elect\ Camille\ Andrew http://camilleandrew.com Elect\ Bob\ Smith http://bobsmith.com))
42
+ dataset.add_data_row(%w(NJ02 Elect\ David\ Kurkowski http://davidforcongress.com Elect\ Joe\ Jim http://joejim.com))
43
+ dataset.save
44
+ ```
45
+ If there is an error while saving, it will be available via ```dataset.errors```
46
+ ```ruby
47
+ dataset=BlueStateDigital::Dataset.new({map_type:"downballot"}.merge(connection: connection))
48
+ dataset.save
49
+ => false
50
+ dataset.errors.full_messages
51
+ => ["slug can't be blank"]
52
+ ```
53
+
54
+ [BSD API for listing Datasets](https://cshift.cp.bsd.net/page/api/doc#---------------------list_datasets-----------------)
55
+ This returns a list of all personalization datasets.
56
+ ```ruby
57
+ connection.datasets.get_datasets
58
+ => [
59
+ {
60
+ "dataset_id":42,
61
+ "slug":"my_dataset",
62
+ "rows":100,
63
+ "map_type":"state"
64
+ },
65
+ {
66
+ "dataset_id":43,
67
+ "slug":"downballot_dataset",
68
+ "rows":50,
69
+ "map_type":"downballot"
70
+ }
71
+ ]
72
+ ```
73
+
74
+ [BSD API for deleting a Dataset](https://cshift.cp.bsd.net/page/api/doc#---------------------delete_dataset-----------------)
75
+ This deletes a personalization dataset.
76
+ ```ruby
77
+ dataset=BlueStateDigital::Dataset.new({dataset_id: 420}.merge(connection: connection))
78
+ dataset.delete
79
+ ```
80
+ If there is any error ```delete``` will return ``false``` and the ```errors``` can be inspected for reasons.
81
+
82
+ ### Dataset Map integration
83
+
84
+ [BSD API for uploading Dataset Map](https://cshift.cp.bsd.net/page/api/doc#---------------------upload_dataset_map-----------------)
85
+ This takes a CSV of dataset map data and creates or updates all of the mappings found in the CSV. The CSV data can be attached by ```add_data_header``` and ```add_data_row``` methods. ```save``` method will upload the dataset and return true if successful.
86
+ ```ruby
87
+ dataset_map=BlueStateDigital::DatasetMap.new({}.merge(connection: connection))
88
+ dataset_map.add_data_header(%w(cons_id downballot houseparty))
89
+ dataset_map.add_data_row(%w(123456 NJ01 HP01))
90
+ dataset_map.add_data_row(%w(123457,NJ02,HP01))
91
+ dataset_map.save
92
+ ```
93
+ If there is an error while saving, it will be available via ```dataset_map.errors```
94
+
95
+ [BSD API for listing Dataset Maps](https://cshift.cp.bsd.net/page/api/doc#---------------------list_dataset_maps-----------------)
96
+ This returns a list of all personalization dataset maps.
97
+ ```ruby
98
+ connection.dataset_maps.get_dataset_maps
99
+ => [
100
+ {
101
+ "map_id":1,
102
+ "type":"state"
103
+ },
104
+ {
105
+ "map_id":2,
106
+ "type":"downballot"
107
+ },
108
+ {
109
+ "map_id":3,
110
+ "type":"houseparty"
111
+ }
112
+ ]
113
+ ```
114
+
115
+ [BSD API for deleting a Dataset Map](https://cshift.cp.bsd.net/page/api/doc#---------------------delete_dataset-----------------)
116
+ This deletes a personalization dataset map.
117
+ ```ruby
118
+ dataset_map=BlueStateDigital::DatasetMap.new({map_id: 111}.merge(connection: connection))
119
+ dataset.delete
120
+
121
+ ## CI
122
+ [![Build Status](https://secure.travis-ci.org/controlshift/blue_state_digital.png)](http://travis-ci.org/controlshift/blue_state_digital)
123
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc 'Default: run specs.'
5
+ task :default => :spec
6
+
7
+ desc "Run specs"
8
+ RSpec::Core::RakeTask.new do |t|
9
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
10
+ t.rspec_opts = '--color'
11
+ end
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "blue_state_digital/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "blue_state_digital"
7
+ s.version = BlueStateDigital::VERSION
8
+ s.authors = ["Nathan Woodhull", "Sean Ho"]
9
+ s.email = ["woodhull@gmail.com", "seanho@thoughtworks.com"]
10
+ s.homepage = "https://github.com/controlshift/blue_state_digital"
11
+ s.summary = "Simple wrapper for Blue State Digital."
12
+ s.description = %q{Simple wrapper for Blue State Digital.}
13
+ s.rubyforge_project = s.name
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ # specify any dependencies here
21
+ s.add_development_dependency "rspec"
22
+ s.add_development_dependency "webmock", "1.9.0"
23
+ s.add_development_dependency "timecop"
24
+ s.add_development_dependency "guard"
25
+ s.add_development_dependency "guard-rspec"
26
+ s.add_development_dependency "rake"
27
+ s.add_development_dependency "pry"
28
+ s.add_development_dependency "pry-byebug"
29
+ s.add_development_dependency "rb-fsevent"
30
+ s.add_dependency "activesupport", ">= 3"
31
+ s.add_dependency "activemodel", ">= 3"
32
+ s.add_dependency "faraday", '>= 0.8.9'
33
+ s.add_dependency "builder"
34
+ s.add_dependency "nokogiri"
35
+ s.add_dependency "crack"
36
+ s.add_dependency "hashie"
37
+ end
@@ -0,0 +1,32 @@
1
+ require 'openssl'
2
+ require 'builder'
3
+ require 'nokogiri'
4
+ require 'active_support/all'
5
+ require 'active_model'
6
+ require 'csv'
7
+ require 'crack/xml'
8
+ require 'faraday'
9
+ require 'hashie'
10
+
11
+ require "blue_state_digital/version" unless defined?(BlueStateDigital::VERSION)
12
+ require "blue_state_digital/connection"
13
+ require "blue_state_digital/collection_resource"
14
+ require "blue_state_digital/api_data_model"
15
+ require "blue_state_digital/address"
16
+ require "blue_state_digital/email"
17
+ require "blue_state_digital/phone"
18
+ require "blue_state_digital/constituent"
19
+ require "blue_state_digital/constituent_group"
20
+ require "blue_state_digital/event_type"
21
+ require "blue_state_digital/event"
22
+ require "blue_state_digital/event_rsvp"
23
+ require "blue_state_digital/contribution"
24
+ require "blue_state_digital/dataset"
25
+ require "blue_state_digital/dataset_map"
26
+ require "blue_state_digital/error_middleware"
27
+ require "blue_state_digital/email_unsubscribe"
28
+
29
+
30
+ I18n.enforce_available_locales = false
31
+
32
+ Faraday::Response.register_middleware :error_middleware => lambda { BlueStateDigital::ErrorMiddleware }
@@ -0,0 +1,28 @@
1
+ # <addr1>123 Fake St.</addr1>
2
+ # <addr2></addr2>
3
+ # <city>Anytown</city>
4
+ # <state_cd>CA</state_cd>
5
+ # <zip>92345</zip>
6
+ # <zip_4>8311</zip_4>
7
+ # <country>US</country>
8
+ # <is_primary>1</is_primary>
9
+ # <latitude>42.000</latitude>
10
+ # <longitude>71.000</longitude>
11
+
12
+
13
+ module BlueStateDigital
14
+ class Address < ApiDataModel
15
+ FIELDS = [:addr1, :addr2, :city, :state_cd, :zip, :zip_4, :country, :is_primary, :latitude, :longitude]
16
+ attr_accessor *FIELDS
17
+
18
+ def to_xml(builder = Builder::XmlMarkup.new)
19
+ builder.addr do | addr |
20
+ FIELDS.each do | field |
21
+ addr.__send__(field, self.send(field)) if self.send(field)
22
+ end
23
+ end
24
+ builder
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ module BlueStateDigital
2
+ class ApiDataModel
3
+
4
+ class NoConnectionException < StandardError
5
+ def initialize
6
+ super("No connection available")
7
+ end
8
+ end
9
+ class FetchFailureException < StandardError
10
+ def initialize(msg)
11
+ super
12
+ end
13
+ end
14
+
15
+ FIELD = nil
16
+
17
+ attr_accessor :connection
18
+ def initialize(attrs = {})
19
+ attrs.each do |key, value|
20
+ if self.respond_to?("#{key}=")
21
+ self.send("#{key}=", value)
22
+ end
23
+ end
24
+ end
25
+
26
+ def to_hash
27
+ self.class::FIELDS.inject({}) {|h, key| h[key] = self.send(key); h}
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ module BlueStateDigital
2
+ class CollectionResource
3
+ class FetchFailureException < StandardError
4
+ def initialize(msg)
5
+ super
6
+ end
7
+ end
8
+ attr_accessor :connection
9
+
10
+ def initialize(connection)
11
+ self.connection = connection
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,119 @@
1
+ module BlueStateDigital
2
+ class Connection
3
+ API_VERSION = 2
4
+ API_BASE = '/page/api'
5
+ GRAPH_API_BASE = '/page/graph'
6
+
7
+ attr_reader :constituents, :constituent_groups, :datasets, :dataset_maps
8
+
9
+ def initialize(params = {})
10
+ @api_id = params[:api_id]
11
+ @api_secret = params[:api_secret]
12
+ @client = Faraday.new(:url => "https://#{params[:host]}/") do |faraday|
13
+ faraday.request :url_encoded # form-encode POST params
14
+ if defined?(Rails) && Rails.env.development?
15
+ faraday.response :logger # log requests to STDOUT
16
+ end
17
+ faraday.response :error_middleware
18
+ faraday.adapter(params[:adapter] || Faraday.default_adapter) # make requests with Net::HTTP by default
19
+ end
20
+ set_up_resources
21
+ end
22
+
23
+ def perform_request(call, params = {}, method = "GET", body = nil)
24
+ perform_request_raw(call,params,method,body).body
25
+ end
26
+
27
+ def perform_request_raw(call, params = {}, method = "GET", body = nil)
28
+ path = API_BASE + call
29
+ if method == "POST" || method == "PUT"
30
+ @client.send(method.downcase.to_sym) do |req|
31
+ content_type = params.delete(:content_type) || 'application/x-www-form-urlencoded'
32
+ accept = params.delete(:accept) || 'text/xml'
33
+ req.url(path, extended_params(path, params))
34
+ req.body = body
35
+ req.options.timeout = 120
36
+ req.headers['Content-Type'] = content_type
37
+ req.headers['Accept'] = accept
38
+ end
39
+ else
40
+ @client.get(path, extended_params(path, params))
41
+ end
42
+ end
43
+
44
+ def perform_graph_request(call, params, method = 'POST')
45
+ path = GRAPH_API_BASE + call
46
+
47
+ if method == "POST"
48
+ @client.post do |req|
49
+ req.url(path, params)
50
+ end
51
+ end
52
+ end
53
+
54
+ def compute_hmac(path, api_ts, params)
55
+ # Support Faraday 0.9.0 forward
56
+ # Faraday now normalizes request parameters via sorting by default but also allows
57
+ # the params encoder to be configured by client. It includes Faraday::NestedParamsEncoder
58
+ # and Faraday::FlatParamsEncoder, but a 3rd party one can be provided.
59
+ #
60
+ # When computing the hmac, we need to normalize/sort the exact same way.
61
+
62
+ if Faraday::VERSION == "0.8.9"
63
+ # do it the old way
64
+ canon_params= params.map { |k, v| "#{k.to_s}=#{v.to_s}" }.join('&')
65
+
66
+ else # 0.9.0+ do it the new way
67
+
68
+ # Find out which one is in use or select default
69
+ params_encoder = @client.options[:params_encoder] || Faraday::Utils.default_params_encoder
70
+
71
+ # Call that params_encoder when creating signing string. Note we must unescape for BSD
72
+ canon_params = URI.decode_www_form_component(params_encoder.encode(params))
73
+
74
+ end
75
+ signing_string = [@api_id, api_ts, path, canon_params].join("\n")
76
+
77
+ OpenSSL::HMAC.hexdigest('sha1', @api_secret, signing_string)
78
+
79
+ end
80
+
81
+ def extended_params(path, params)
82
+ api_ts = Time.now.utc.to_i.to_s
83
+ extended_params = { api_ver: API_VERSION, api_id: @api_id, api_ts: api_ts }.merge(params)
84
+ extended_params[:api_mac] = compute_hmac(path, api_ts, extended_params)
85
+ extended_params
86
+ end
87
+
88
+ def set_up_resources # :doc:
89
+ @constituents = BlueStateDigital::Constituents.new(self)
90
+ @constituent_groups = BlueStateDigital::ConstituentGroups.new(self)
91
+ @datasets = BlueStateDigital::Datasets.new(self)
92
+ @dataset_maps = BlueStateDigital::DatasetMaps.new(self)
93
+ end
94
+
95
+ def get_deferred_results(deferred_id)
96
+ perform_request '/get_deferred_results', {deferred_id: deferred_id}, "GET"
97
+ end
98
+
99
+ def retrieve_results(deferred_id)
100
+ begin
101
+ return get_deferred_results(deferred_id)
102
+ rescue Faraday::Error::ClientError => e
103
+ if e.response[:status] == 503
104
+ return nil
105
+ end
106
+ end
107
+ end
108
+
109
+ def wait_for_deferred_result(deferred_id)
110
+ result = nil
111
+ while result.nil? || (result.respond_to?(:length) && result.length == 0)
112
+ result = retrieve_results(deferred_id)
113
+ sleep(2) if result.nil? || (result.respond_to?(:length) && result.length == 0)
114
+ end
115
+ result
116
+ end
117
+
118
+ end
119
+ end