survey-gizmo-ruby 4.1.0 → 5.0.2

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.
@@ -1,3 +1,5 @@
1
+ require 'survey_gizmo/api/option'
2
+
1
3
  module SurveyGizmo; module API
2
4
  # @see SurveyGizmo::Resource::ClassMethods
3
5
  class Question
@@ -10,6 +12,7 @@ module SurveyGizmo; module API
10
12
  attribute :shortname, String
11
13
  attribute :properties, Hash
12
14
  attribute :after, Integer
15
+ attribute :options, Array[Option]
13
16
  attribute :survey_id, Integer
14
17
  attribute :page_id, Integer, default: 1
15
18
  attribute :sub_question_skus, Array
@@ -17,30 +20,39 @@ module SurveyGizmo; module API
17
20
 
18
21
  alias_attribute :_subtype, :type
19
22
 
20
- route '/survey/:survey_id/surveyquestion/:id', via: :get
21
- route '/survey/:survey_id/surveypage/:page_id/surveyquestion', via: :create
22
- route '/survey/:survey_id/surveypage/:page_id/surveyquestion/:id', via: [:update, :delete]
23
+ @route = {
24
+ get: '/survey/:survey_id/surveyquestion/:id',
25
+ create: '/survey/:survey_id/surveypage/:page_id/surveyquestion',
26
+ update: '/survey/:survey_id/surveypage/:page_id/surveyquestion/:id'
27
+ }
28
+ @route[:delete] = @route[:update]
23
29
 
24
30
  def survey
25
31
  @survey ||= Survey.first(id: survey_id)
26
32
  end
27
33
 
28
34
  def options
29
- @options ||= Option.all(survey_id: survey_id, page_id: page_id, question_id: id, all_pages: true)
35
+ return parent_question.options.dup.each { |o| o.question_id = id } if parent_question
36
+
37
+ @options ||= Option.all(children_params.merge(all_pages: true)).to_a
38
+ @options.each { |o| o.attributes = children_params }
30
39
  end
31
40
 
32
41
  def parent_question
33
- @parent_question ||= parent_question_id ? Question.first(survey_id: survey_id, id: parent_question_id) : nil
42
+ return nil unless parent_question_id
43
+ @parent_question ||= Question.first(survey_id: survey_id, id: parent_question_id)
34
44
  end
35
45
 
36
46
  def sub_questions
37
- @sub_questions ||= sub_question_skus.map { |subquestion_id| Question.first(survey_id: survey_id, id: subquestion_id) }
38
- .each { |subquestion| subquestion.parent_question_id = id }
39
- end
40
-
41
- # @see SurveyGizmo::Resource#to_param_options
42
- def to_param_options
43
- { id: self.id, survey_id: self.survey_id, page_id: self.page_id }
47
+ @sub_questions ||= sub_question_skus.map do |sku|
48
+ # As of 2015-12-23, the sub_question_skus attribute can either contain an array of integers if no shortname (alias)
49
+ # was set for any question, or an array of [String, Integer] with the String corresponding to the subquestion
50
+ # shortname and the integer corresponding to the subquestion id if at least one shortname was set.
51
+ sku = sku[1] if sku.is_a?(Array)
52
+ subquestion = Question.first(survey_id: survey_id, id: sku)
53
+ subquestion.parent_question_id = id
54
+ subquestion
55
+ end
44
56
  end
45
57
  end
46
58
  end; end
@@ -20,7 +20,6 @@ module SurveyGizmo; module API
20
20
  attribute :contact_id, Integer
21
21
  attribute :data, String
22
22
  attribute :status, String
23
- attribute :datesubmitted, DateTime
24
23
  attribute :is_test_data, Boolean
25
24
  attribute :sResponseComment, String
26
25
  attribute :variable, Hash # READ-ONLY
@@ -28,16 +27,26 @@ module SurveyGizmo; module API
28
27
  attribute :shown, Hash # READ-ONLY
29
28
  attribute :url, Hash # READ-ONLY
30
29
  attribute :answers, Hash # READ-ONLY
30
+ attribute :datesubmitted, DateTime
31
+ alias_attribute :submitted_at, :datesubmitted
31
32
 
32
- route '/survey/:survey_id/surveyresponse', via: :create
33
- route '/survey/:survey_id/surveyresponse/:id', via: [:get, :update, :delete]
33
+ @route = '/survey/:survey_id/surveyresponse'
34
34
 
35
35
  def survey
36
36
  @survey ||= Survey.first(id: survey_id)
37
37
  end
38
38
 
39
- def to_param_options
40
- { id: self.id, survey_id: self.survey_id }
39
+ def parsed_answers
40
+ answers.select do |k,v|
41
+ next false unless v.is_a?(FalseClass) || v.present?
42
+
43
+ # Strip out "Other" answers that don't actually have the "other" text.
44
+ if k =~ /\[question\((\d+)\),\s*option\((\d+)\)\]/
45
+ !answers.keys.any? { |key| key =~ /\[question\((#{$1})\),\s*option\("(#{$2})-other"\)\]/ }
46
+ else
47
+ true
48
+ end
49
+ end.map { |k,v| Answer.new(children_params.merge(key: k, value: v, answer_text: v, submitted_at: submitted_at)) }
41
50
  end
42
51
  end
43
52
  end; end
@@ -1,3 +1,5 @@
1
+ require 'survey_gizmo/api/page'
2
+
1
3
  module SurveyGizmo; module API
2
4
  # @see SurveyGizmo::Resource::ClassMethods
3
5
  class Survey
@@ -20,23 +22,33 @@ module SurveyGizmo; module API
20
22
  attribute :created_on, DateTime
21
23
  attribute :modified_on, DateTime
22
24
  attribute :copy, Boolean
25
+ # Unfortunately if pages is an attribute, then save and update requests will collide with the differing response
26
+ # types to Survey.all and Survey.first and cause an incorrect reload
27
+ # attribute :pages, Array[Page]
23
28
 
24
- route '/survey/:id', via: [:get, :update, :delete]
25
- route '/survey', via: :create
26
-
27
- def to_param_options
28
- { id: id }
29
- end
29
+ @route = '/survey'
30
30
 
31
31
  def pages
32
- @pages ||= Page.all(survey_id: id, all_pages: true)
32
+ # SurveyGizmo sends down the page info to .first requests but NOT to .all requests, so we must load pages manually
33
+ # We should be able to just .reload this Survey BUT we can't make :pages a Virtus attribute without requiring a
34
+ # call to this method during Survey.save
35
+ @pages ||= Page.all(children_params.merge(all_pages: true)).to_a
36
+ @pages.each { |p| p.attributes = children_params }
33
37
  end
34
38
 
35
- # Sub question handling is in resource.rb. It should probably be here instead but if it gets moved here
36
- # and people try to request all the questions for a specific page directly from a ::API::Question request,
37
- # sub questions will not be included! So I left it there for least astonishment.
39
+ # Sub question handling is in resource.rb and page.rb. It should probably be here instead but if it gets moved
40
+ # here and people try to request all the questions for a specific page directly from a ::API::Question request or
41
+ # from Page.questions, sub questions will not be included! So I left it there for least astonishment.
38
42
  def questions
39
- @questions ||= pages.map { |p| Question.all(survey_id: id, page_id: p.id, all_pages: true) }.flatten
43
+ @questions ||= pages.flat_map { |p| p.questions }
44
+ end
45
+
46
+ def actual_questions
47
+ questions.reject { |q| q.type =~ /^(instructions|urlredirect|logic|media|script|javascript)$/ }
48
+ end
49
+
50
+ def responses(conditions = {})
51
+ Response.all(conditions.merge(children_params).merge(all_pages: !conditions[:page]))
40
52
  end
41
53
 
42
54
  # Statistics array of arrays looks like:
@@ -50,13 +62,13 @@ module SurveyGizmo; module API
50
62
  end
51
63
 
52
64
  def server_has_new_results_since?(time)
53
- Response.all(survey_id: id, filters: [Response.submitted_since_filter(time)]).size > 0
65
+ Response.all(children_params.merge(page: 1, resultsperpage: 1, filters: Response.submitted_since_filter(time))).to_a.size > 0
54
66
  end
55
67
 
56
68
  # As of 2015-12-18, when you request data on multiple surveys from /survey, the team variable comes
57
69
  # back as "0". If you request one survey at a time from /survey/{id}, it is populated correctly.
58
70
  def teams
59
- @individual_survey ||= Survey.first(id: id)
71
+ @individual_survey ||= self.reload
60
72
  @individual_survey.team
61
73
  end
62
74
 
@@ -67,5 +79,9 @@ module SurveyGizmo; module API
67
79
  def belongs_to?(team)
68
80
  team_names.any? { |t| t == team }
69
81
  end
82
+
83
+ def campaigns
84
+ @campaigns ||= Campaign.all(children_params.merge(all_pages: true)).to_a
85
+ end
70
86
  end
71
87
  end; end
@@ -1,30 +1,79 @@
1
1
  module SurveyGizmo
2
2
  class << self
3
- attr_accessor :configuration
4
- end
3
+ attr_writer :configuration
5
4
 
6
- def self.configure
7
- self.configuration ||= Configuration.new
8
- yield(configuration) if block_given?
9
- SurveyGizmo.setup
10
- end
5
+ def configuration
6
+ fail 'Not configured!' unless @configuration
7
+ @configuration
8
+ end
9
+
10
+ def configure
11
+ @configuration ||= Configuration.new
12
+ yield(configuration) if block_given?
13
+ configure_pester
14
+ end
15
+
16
+ def reset!
17
+ self.configuration = Configuration.new
18
+ Pester.configure { |c| c.environments[:survey_gizmo_ruby] = nil }
19
+ configure_pester
20
+ Connection.reset!
21
+ end
22
+
23
+ private
24
+
25
+ def configure_pester
26
+ default_config = {
27
+ on_retry: Pester::Behaviors::Sleep::Constant,
28
+ logger: configuration.logger,
29
+ max_attempts: 2,
30
+ delay_interval: 60,
31
+ retry_error_classes: retryables
32
+ }
33
+
34
+ Pester.configure do |c|
35
+ if c.environments[:survey_gizmo_ruby].nil?
36
+ c.environments[:survey_gizmo_ruby] = default_config
37
+ else
38
+ default_config.each { |k,v| c.environments[:survey_gizmo_ruby][k] ||= v unless k == :retry_error_classes }
11
39
 
12
- def self.reset!
13
- self.configuration = Configuration.new
40
+ # Don't set :retry_error_classes to something when user has configured nothing
41
+ if c.environments[:survey_gizmo_ruby][:retry_error_classes].nil? && !c.environments[:survey_gizmo_ruby].has_key?(:retry_error_classes)
42
+ c.environments[:survey_gizmo_ruby][:retry_error_classes] = retryables
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def retryables
49
+ [
50
+ Net::ReadTimeout,
51
+ Faraday::Error::TimeoutError,
52
+ SurveyGizmo::RateLimitExceededError
53
+ ]
54
+ end
14
55
  end
15
56
 
16
57
  class Configuration
17
- DEFAULT_RESULTS_PER_PAGE = 50
58
+ DEFAULT_REST_API_URL = 'https://restapi.surveygizmo.com'
18
59
  DEFAULT_API_VERSION = 'v4'
60
+ DEFAULT_RESULTS_PER_PAGE = 50
19
61
 
20
- attr_accessor :api_version
21
62
  attr_accessor :user
22
63
  attr_accessor :password
64
+
65
+ attr_accessor :api_debug
66
+ attr_accessor :api_url
67
+ attr_accessor :api_version
68
+ attr_accessor :logger
23
69
  attr_accessor :results_per_page
24
70
 
25
71
  def initialize
26
- @results_per_page = DEFAULT_RESULTS_PER_PAGE
72
+ @api_url = DEFAULT_REST_API_URL
27
73
  @api_version = DEFAULT_API_VERSION
74
+ @results_per_page = DEFAULT_RESULTS_PER_PAGE
75
+ @logger = ::Logger.new(STDOUT)
76
+ @api_debug = ENV['GIZMO_DEBUG'].to_s =~ /^(true|t|yes|y|1)$/i
28
77
  end
29
78
  end
30
79
  end
@@ -0,0 +1,39 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module SurveyGizmo
4
+ class Connection
5
+ TIMEOUT_SECONDS = 300
6
+
7
+ class << self
8
+ delegate :put, :get, :delete, :post, to: :connection
9
+
10
+ def reset!
11
+ @connection = nil
12
+ end
13
+
14
+ private
15
+
16
+ def connection
17
+ options = {
18
+ url: SurveyGizmo.configuration.api_url,
19
+ params: { 'user:md5' => "#{SurveyGizmo.configuration.user}:#{Digest::MD5.hexdigest(SurveyGizmo.configuration.password)}" },
20
+ request: {
21
+ timeout: TIMEOUT_SECONDS,
22
+ open_timeout: TIMEOUT_SECONDS
23
+ }
24
+ }
25
+
26
+ @connection ||= Faraday.new(options) do |connection|
27
+ connection.request :url_encoded
28
+
29
+ connection.response :parse_survey_gizmo_data
30
+ connection.response :pester_survey_gizmo
31
+ connection.response :logger, @logger, bodies: true if SurveyGizmo.configuration.api_debug
32
+ connection.response :json, content_type: /\bjson$/
33
+
34
+ connection.adapter Faraday.default_adapter
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,75 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module SurveyGizmo
4
+ class ParseSurveyGizmo < FaradayMiddleware::ResponseMiddleware
5
+ Faraday::Response.register_middleware(parse_survey_gizmo_data: self)
6
+
7
+ PAGINATION_FIELDS = ['total_count', 'page', 'total_pages', 'results_per_page']
8
+ TIME_FIELDS = ['datesubmitted', 'created_on', 'modified_on', 'datecreated', 'datemodified']
9
+
10
+ def parse_response?(env)
11
+ true
12
+ end
13
+
14
+ define_parser do |body|
15
+ PAGINATION_FIELDS.each { |n| body[n] = body[n].to_i if body[n] }
16
+
17
+ next body unless body['data']
18
+
19
+ # Handle really crappy [] notation in SG API, so far just in SurveyResponse
20
+ Array.wrap(body['data']).compact.each do |datum|
21
+ # SurveyGizmo returns date information in EST but does not provide time zone information.
22
+ # See https://surveygizmov4.helpgizmo.com/help/article/link/date-and-time-submitted
23
+ TIME_FIELDS.each do |time_key|
24
+ datum[time_key] = datum[time_key] + ' EST' unless datum[time_key].blank?
25
+ end
26
+
27
+ datum.keys.grep(/^\[/).each do |key|
28
+ next if datum[key].nil? || datum[key].length == 0
29
+
30
+ parent = find_attribute_parent(key)
31
+ datum[parent] ||= {}
32
+
33
+ case key.downcase
34
+ when /(url|variable.*standard)/
35
+ datum[parent][cleanup_attribute_name(key).to_sym] = datum[key]
36
+ when /variable.*shown/
37
+ datum[parent][cleanup_attribute_name(key).to_i] = datum[key].include?('1')
38
+ when /variable/
39
+ datum[parent][cleanup_attribute_name(key).to_i] = datum[key].to_i
40
+ when /question/
41
+ datum[parent][key] = datum[key]
42
+ end
43
+
44
+ datum.delete(key)
45
+ end
46
+ end
47
+
48
+ body
49
+ end
50
+
51
+ private
52
+
53
+ def self.cleanup_attribute_name(attr)
54
+ attr.downcase.gsub(/[^[:alnum:]]+/, '_')
55
+ .gsub(/(url|variable|standard|shown)/, '')
56
+ .gsub(/_+/, '_')
57
+ .gsub(/^_|_$/, '')
58
+ end
59
+
60
+ def self.find_attribute_parent(attr)
61
+ case attr.downcase
62
+ when /url/
63
+ 'url'
64
+ when /variable.*standard/
65
+ 'meta'
66
+ when /variable.*shown/
67
+ 'shown'
68
+ when /variable/
69
+ 'variable'
70
+ when /question/
71
+ 'answers'
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,18 @@
1
+ module SurveyGizmo
2
+ class RateLimitExceededError < RuntimeError; end
3
+ class BadResponseError < RuntimeError; end
4
+
5
+ class PesterSurveyGizmoMiddleware < Faraday::Middleware
6
+ Faraday::Response.register_middleware(pester_survey_gizmo: self)
7
+
8
+ def call(environment)
9
+ Pester.survey_gizmo_ruby.retry do
10
+ @app.call(environment).on_complete do |response|
11
+ fail RateLimitExceededError if response.status == 429
12
+ fail BadResponseError, "Bad response code #{response.status} in #{response.inspect}" unless response.status == 200
13
+ fail BadResponseError, response.body['message'] unless response.body['result_ok'] && response.body['result_ok'].to_s =~ /^true$/i
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,4 @@
1
- # SurveyGizmo has a bad habit of returning titles in different formats when one is
2
- # requesting all surveys vs. an individual survey.
3
-
1
+ # SurveyGizmo has a bad habit of returning titles in different formats when one is requesting via .all vs .first
4
2
  module SurveyGizmo
5
3
  module MultilingualTitle
6
4
  extend ActiveSupport::Concern
@@ -2,197 +2,193 @@ require 'set'
2
2
  require 'addressable/uri'
3
3
 
4
4
  module SurveyGizmo
5
+ class URLError < RuntimeError; end
6
+
5
7
  module Resource
6
8
  extend ActiveSupport::Concern
7
9
 
8
10
  included do
9
11
  include Virtus.model
10
- instance_variable_set('@paths', {})
12
+ instance_variable_set('@route', nil)
11
13
  SurveyGizmo::Resource.descendants << self
12
14
  end
13
15
 
14
- # @return [Set] Every class that includes SurveyGizmo::Resource
15
16
  def self.descendants
16
17
  @descendants ||= Set.new
17
18
  end
18
19
 
19
20
  # These are methods that every API resource can use to access resources in SurveyGizmo
20
21
  module ClassMethods
21
- # Get an array of resources.
22
- # @param [Hash] options - simple URL params at the top level, and SurveyGizmo "filters" at the :filters key
22
+ attr_accessor :route
23
+
24
+ # Get an enumerator of resources.
25
+ # @param [Hash] conditions - URL and pagination params with SurveyGizmo "filters" at the :filters key
23
26
  #
24
- # example: { page: 2, filters: [{ field: "istestdata", operator: "<>", value: 1 }] }
27
+ # Set all_pages: true if you want the gem to page through all the available responses
25
28
  #
26
- # The top level keys (e.g. page, resultsperpage) get encoded in the url, while the
27
- # contents of the array of hashes passed at the :filters key get turned into the format
28
- # SurveyGizmo expects for its internal filtering, for example:
29
+ # example: { page: 2, filters: { field: "istestdata", operator: "<>", value: 1 } }
29
30
  #
30
- # filter[field][0]=istestdata&filter[operator][0]=<>&filter[value][0]=1
31
+ # The top level keys (e.g. :page, :resultsperpage) get encoded in the url, while the
32
+ # contents of the array of hashes passed at the :filters key get turned into the format
33
+ # SurveyGizmo expects for its internal filtering.
31
34
  #
32
- # Set all_pages: true if you want the gem to page through all the available responses
33
- def all(conditions = {}, _deprecated_filters = {})
34
- conditions = merge_params(conditions, _deprecated_filters)
35
+ # Properties from the conditions hash (e.g. survey_id) will be added to the returned objects
36
+ def all(conditions = {})
35
37
  fail ':all_pages and :page are mutually exclusive' if conditions[:page] && conditions[:all_pages]
38
+ logger.warn('WARNING: Only retrieving first page of results!') if conditions[:page].nil? && conditions[:all_pages].nil?
36
39
 
37
40
  all_pages = conditions.delete(:all_pages)
38
- properties = conditions.dup
39
41
  conditions[:resultsperpage] = SurveyGizmo.configuration.results_per_page unless conditions[:resultsperpage]
40
42
 
41
- request_route = handle_route!(:create, conditions)
42
- response = RestResponse.new(SurveyGizmo.get(request_route + filters_to_query_string(conditions)))
43
- collection = response.data.map { |datum| datum.is_a?(Hash) ? new(datum) : datum }
43
+ Enumerator.new do |yielder|
44
+ response = nil
44
45
 
45
- while all_pages && response.current_page < response.total_pages
46
- paged_filter = filters_to_query_string(conditions.merge(page: response.current_page + 1))
47
- response = RestResponse.new(SurveyGizmo.get(request_route + paged_filter))
48
- collection += response.data.map { |datum| datum.is_a?(Hash) ? new(datum) : datum }
49
- end
46
+ while !response || (all_pages && response['page'] < response['total_pages'])
47
+ conditions[:page] = response ? response['page'] + 1 : 1
48
+ logger.debug("Fetching #{name} page #{conditions} - #{conditions[:page]}#{response ? "/#{response['total_pages']}" : ''}...")
49
+ response = Connection.get(create_route(:create, conditions)).body
50
+ collection = response['data'].map { |datum| datum.is_a?(Hash) ? new(conditions.merge(datum)) : datum }
50
51
 
51
- # Add in the properties from the conditions hash because many of the important ones (like survey_id) are
52
- # not often part of the SurveyGizmo returned data
53
- properties.each do |k,v|
54
- if v && instance_methods.include?(k)
55
- collection.each { |c| c[k] ||= v }
56
- end
57
- end
52
+ # Sub questions are not pulled by default so we have to retrieve them manually. SurveyGizmo
53
+ # claims they will fix this bug and eventually all questions will be returned in one request.
54
+ if self == SurveyGizmo::API::Question
55
+ collection += collection.flat_map { |question| question.sub_questions }
56
+ end
58
57
 
59
- # Sub questions are not pulled by default so we have to retrieve them manually
60
- # SurveyGizmo claims they will fix this bug and eventually all questions will be
61
- # returned in one request.
62
- if self == SurveyGizmo::API::Question
63
- collection += collection.map { |question| question.sub_questions }.flatten
58
+ collection.each { |e| yielder.yield(e) }
59
+ end
64
60
  end
65
-
66
- collection
67
61
  end
68
62
 
69
63
  # Retrieve a single resource. See usage comment on .all
70
- def first(conditions, _deprecated_filters = {})
71
- conditions = merge_params(conditions, _deprecated_filters)
72
- properties = conditions.dup
73
-
74
- response = RestResponse.new(SurveyGizmo.get(handle_route!(:get, conditions) + filters_to_query_string(conditions)))
75
- # Add in the properties from the conditions hash because many of the important ones (like survey_id) are
76
- # not often part of the SurveyGizmo's returned data
77
- new(properties.merge(response.data))
64
+ def first(conditions = {})
65
+ new(conditions.merge(Connection.get(create_route(:get, conditions)).body['data']))
78
66
  end
79
67
 
80
- # Create a new resource. Returns the newly created Resource instance.
68
+ # Create a new resource object locally and save to SurveyGizmo. Returns the newly created Resource instance.
81
69
  def create(attributes = {})
82
- resource = new(attributes)
83
- resource.create_record_in_surveygizmo
84
- resource
70
+ new(attributes).save
85
71
  end
86
72
 
87
73
  # Delete resources
88
74
  def destroy(conditions)
89
- RestResponse.new(SurveyGizmo.delete(handle_route!(:delete, conditions)))
75
+ Connection.delete(create_route(:delete, conditions))
90
76
  end
91
77
 
92
- # Define the path where a resource is located
93
- def route(path, options)
94
- methods = options[:via]
95
- methods = [:get, :create, :update, :delete] if methods == :any
96
- methods.is_a?(Array) ? methods.each { |m| @paths[m] = path } : (@paths[methods] = path)
78
+ # @route is either a hash to be used directly or a string from which standard routes will be built
79
+ def routes
80
+ fail "route not set in #{name}" unless @route
81
+
82
+ return @route if @route.is_a?(Hash)
83
+ routes = { create: @route }
84
+ [:get, :update, :delete].each { |k| routes[k] = @route + '/:id' }
85
+ routes
97
86
  end
98
87
 
99
- # Replaces the :page_id, :survey_id, etc strings defined in each model's URI routes with the
100
- # values being passed in interpolation hash with the same keys.
101
- #
102
- # This method has the SIDE EFFECT of deleting REST path related keys from interpolation_hash!
103
- def handle_route!(key, interpolation_hash)
104
- path = @paths[key]
105
- fail "No routes defined for `#{key}` in #{name}" unless path
106
- fail "User/password hash not setup!" if SurveyGizmo.default_params.empty?
107
-
108
- path.gsub(/:(\w+)/) do |m|
109
- raise(SurveyGizmo::URLError, "Missing RESTful parameters in request: `#{m}`") unless interpolation_hash[$1.to_sym]
110
- interpolation_hash.delete($1.to_sym)
88
+ # Replaces the :page_id, :survey_id, etc strings defined in each model's routes with the
89
+ # values in the params hash
90
+ def create_route(method, params)
91
+ fail "No route defined for #{method} on #{name}" unless routes[method]
92
+
93
+ url_params = params.dup
94
+ rest_path = routes[method].gsub(/:(\w+)/) do |m|
95
+ fail SurveyGizmo::URLError, "Missing RESTful parameters in request: `#{m}`" unless url_params[$1.to_sym]
96
+ url_params.delete($1.to_sym)
111
97
  end
98
+
99
+ SurveyGizmo.configuration.api_version + rest_path + filters_to_query_string(url_params)
112
100
  end
113
101
 
114
102
  private
115
103
 
116
104
  # Convert a [Hash] of params and internal surveygizmo style filters into a query string
117
- def filters_to_query_string(filters = {})
118
- return '' unless filters && filters.size > 0
105
+ #
106
+ # The hashes at the :filters key get turned into URL params like:
107
+ # # filter[field][0]=istestdata&filter[operator][0]=<>&filter[value][0]=1
108
+ def filters_to_query_string(params = {})
109
+ return '' unless params && params.size > 0
119
110
 
120
- params = {}
121
- (filters.delete(:filters) || []).each_with_index do |filter, i|
111
+ params = params.dup
112
+ url_params = {}
113
+
114
+ Array.wrap(params.delete(:filters)).each_with_index do |filter, i|
122
115
  fail "Bad filter params: #{filter}" unless filter.is_a?(Hash) && [:field, :operator, :value].all? { |k| filter[k] }
123
116
 
124
- params["filter[field][#{i}]".to_sym] = "#{filter[:field]}"
125
- params["filter[operator][#{i}]".to_sym] = "#{filter[:operator]}"
126
- params["filter[value][#{i}]".to_sym] = "#{filter[:value]}"
117
+ url_params["filter[field][#{i}]".to_sym] = "#{filter[:field]}"
118
+ url_params["filter[operator][#{i}]".to_sym] = "#{filter[:operator]}"
119
+ url_params["filter[value][#{i}]".to_sym] = "#{filter[:value]}"
127
120
  end
128
121
 
129
- uri = Addressable::URI.new
130
- uri.query_values = params.merge(filters)
122
+ uri = Addressable::URI.new(query_values: url_params.merge(params))
131
123
  "?#{uri.query}"
132
124
  end
133
125
 
134
- def merge_params(conditions, _deprecated_filters)
135
- $stderr.puts('Use of the 2nd hash parameter is deprecated.') unless _deprecated_filters.empty?
136
- conditions.merge(_deprecated_filters || {})
126
+ def logger
127
+ SurveyGizmo.configuration.logger
137
128
  end
138
129
  end
139
130
 
140
- # Save the resource to SurveyGizmo
131
+ ### BELOW HERE ARE INSTANCE METHODS ###
132
+
133
+ # If we have an id, it's an update, because we already know the surveygizmo assigned id
134
+ # Returns itself if successfully saved, but with attributes (like id) added by SurveyGizmo
141
135
  def save
142
- if id
143
- # Then it's an update, because we already know the surveygizmo assigned id
144
- RestResponse.new(SurveyGizmo.post(handle_route(:update), query: attributes_without_blanks))
145
- else
146
- create_record_in_surveygizmo
147
- end
136
+ method, path = id ? [:post, :update] : [:put, :create]
137
+ self.attributes = Connection.send(method, create_route(path), attributes_without_blanks).body['data']
138
+ self
148
139
  end
149
140
 
150
141
  # Repopulate the attributes based on what is on SurveyGizmo's servers
151
142
  def reload
152
- self.attributes = RestResponse.new(SurveyGizmo.get(handle_route(:get))).data
143
+ self.attributes = Connection.get(create_route(:get)).body['data']
153
144
  self
154
145
  end
155
146
 
156
147
  # Delete the Resource from Survey Gizmo
157
148
  def destroy
158
149
  fail "No id; can't delete #{self.inspect}!" unless id
159
- RestResponse.new(SurveyGizmo.delete(handle_route(:delete)))
160
- end
161
-
162
- # Sets the hash that will be used to interpolate values in routes. It needs to be defined per model.
163
- # @return [Hash] a hash of the values needed in routing
164
- def to_param_options
165
- fail "Define #to_param_options in #{self.class.name}"
166
- end
167
-
168
- # Returns itself if successfully saved, but with attributes added by SurveyGizmo
169
- def create_record_in_surveygizmo(attributes = {})
170
- rest_response = RestResponse.new(SurveyGizmo.put(handle_route(:create), query: attributes_without_blanks))
171
- self.attributes = rest_response.data
172
- self
150
+ Connection.delete(create_route(:delete))
173
151
  end
174
152
 
175
153
  def inspect
176
154
  attribute_strings = self.class.attribute_set.map do |attrib|
177
155
  value = self.send(attrib.name)
178
156
  value = value.is_a?(Hash) ? value.inspect : value.to_s
179
-
180
157
  " \"#{attrib.name}\" => \"#{value}\"\n" unless value.strip.blank?
181
158
  end.compact
182
159
 
183
160
  "#<#{self.class.name}:#{self.object_id}>\n#{attribute_strings.join}"
184
161
  end
185
162
 
186
- protected
163
+ private
187
164
 
188
165
  def attributes_without_blanks
189
166
  attributes.reject { |k,v| v.blank? }
190
167
  end
191
168
 
192
- private
169
+ # Extract attributes required for API calls about this object
170
+ def route_params
171
+ params = { id: id }
172
+
173
+ self.class.routes.values.each do |route|
174
+ route.gsub(/:(\w+)/) do |m|
175
+ m = m.delete(':').to_sym
176
+ params[m] = self.send(m)
177
+ end
178
+ end
179
+
180
+ params
181
+ end
182
+
183
+ # Attributes that should be passed down the object hierarchy - e.g. a Question should have a survey_id
184
+ # Also used for loading member objects, e.g. loading Options for a given Question.
185
+ def children_params
186
+ klass_id = self.class.name.split('::').last.downcase + '_id'
187
+ route_params.merge(klass_id.to_sym => id).reject { |k,v| k == :id }
188
+ end
193
189
 
194
- def handle_route(key)
195
- self.class.handle_route!(key, to_param_options)
190
+ def create_route(method)
191
+ self.class.create_route(method, route_params)
196
192
  end
197
193
  end
198
194
  end