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.
- checksums.yaml +4 -4
- data/README.md +108 -50
- data/lib/survey-gizmo-ruby.rb +12 -20
- data/lib/survey_gizmo/api/account_teams.rb +1 -6
- data/lib/survey_gizmo/api/answer.rb +59 -0
- data/lib/survey_gizmo/api/{survey_campaign.rb → campaign.rb} +4 -5
- data/lib/survey_gizmo/api/contact.rb +2 -7
- data/lib/survey_gizmo/api/email_message.rb +1 -6
- data/lib/survey_gizmo/api/option.rb +2 -7
- data/lib/survey_gizmo/api/page.rb +12 -8
- data/lib/survey_gizmo/api/question.rb +24 -12
- data/lib/survey_gizmo/api/response.rb +14 -5
- data/lib/survey_gizmo/api/survey.rb +29 -13
- data/lib/survey_gizmo/configuration.rb +61 -12
- data/lib/survey_gizmo/connection.rb +39 -0
- data/lib/survey_gizmo/faraday_middleware/parse_survey_gizmo.rb +75 -0
- data/lib/survey_gizmo/faraday_middleware/pester_survey_gizmo.rb +18 -0
- data/lib/survey_gizmo/multilingual_title.rb +1 -3
- data/lib/survey_gizmo/resource.rb +100 -104
- data/lib/survey_gizmo/version.rb +1 -2
- data/spec/configuration_spec.rb +1 -8
- data/spec/resource_spec.rb +114 -6
- data/spec/spec_helper.rb +7 -1
- data/spec/support/spec_shared_api_object.rb +11 -35
- data/spec/support/spec_shared_object_with_errors.rb +1 -1
- data/spec/support/test_resource_classes.rb +6 -7
- data/survey-gizmo-ruby.gemspec +5 -3
- metadata +44 -16
- data/lib/survey_gizmo/rest_response.rb +0 -91
- data/lib/survey_gizmo/survey_gizmo.rb +0 -15
- data/spec/survey-gizmo-ruby_spec.rb +0 -7
@@ -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
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
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
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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'
|
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
|
40
|
-
|
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
|
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
|
-
|
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
|
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.
|
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(
|
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 ||=
|
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
|
-
|
4
|
-
end
|
3
|
+
attr_writer :configuration
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
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
|
-
|
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
|
-
@
|
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('@
|
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
|
-
|
22
|
-
|
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
|
-
#
|
27
|
+
# Set all_pages: true if you want the gem to page through all the available responses
|
25
28
|
#
|
26
|
-
#
|
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
|
-
#
|
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
|
-
#
|
33
|
-
def all(conditions = {}
|
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
|
-
|
42
|
-
|
43
|
-
collection = response.data.map { |datum| datum.is_a?(Hash) ? new(datum) : datum }
|
43
|
+
Enumerator.new do |yielder|
|
44
|
+
response = nil
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
60
|
-
|
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
|
71
|
-
conditions
|
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
|
-
|
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
|
-
|
75
|
+
Connection.delete(create_route(:delete, conditions))
|
90
76
|
end
|
91
77
|
|
92
|
-
#
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
100
|
-
# values
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
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
|
135
|
-
|
136
|
-
conditions.merge(_deprecated_filters || {})
|
126
|
+
def logger
|
127
|
+
SurveyGizmo.configuration.logger
|
137
128
|
end
|
138
129
|
end
|
139
130
|
|
140
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
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 =
|
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
|
-
|
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
|
-
|
163
|
+
private
|
187
164
|
|
188
165
|
def attributes_without_blanks
|
189
166
|
attributes.reject { |k,v| v.blank? }
|
190
167
|
end
|
191
168
|
|
192
|
-
|
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
|
195
|
-
self.class.
|
190
|
+
def create_route(method)
|
191
|
+
self.class.create_route(method, route_params)
|
196
192
|
end
|
197
193
|
end
|
198
194
|
end
|