blue_state_digital 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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