study_engine 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +42 -0
  3. data/app/assets/images/study_engine/spinner.gif +0 -0
  4. data/app/assets/images/study_engine/sprite/delete.png +0 -0
  5. data/app/assets/images/study_engine/sprite/download.png +0 -0
  6. data/app/assets/images/study_engine/sprite/edit.png +0 -0
  7. data/app/assets/images/study_engine/sprite/incomplete.png +0 -0
  8. data/app/assets/images/study_engine/sprite/metrc-sm.png +0 -0
  9. data/app/assets/images/study_engine/sprite/metrc.png +0 -0
  10. data/app/assets/images/study_engine/sprite/new.png +0 -0
  11. data/app/assets/javascripts/study_engine/admin_notes.js.coffee +12 -0
  12. data/app/assets/javascripts/study_engine/assessment_notifications.js.coffee +36 -0
  13. data/app/assets/javascripts/study_engine/assessment_timeout.js.coffee +24 -0
  14. data/app/assets/javascripts/study_engine/disable_buttons_after_click.js.coffee +6 -0
  15. data/app/assets/javascripts/study_engine/fire_click_event_immediately_on_mobile.js.coffee.erb +10 -0
  16. data/app/assets/javascripts/study_engine/response_validation.js.coffee +8 -0
  17. data/app/assets/javascripts/study_engine/restart_assessment_button.js.coffee +3 -0
  18. data/app/assets/javascripts/study_engine/study_engine.js.coffee +9 -0
  19. data/app/assets/javascripts/study_engine/study_id_picker.js.coffee +28 -0
  20. data/app/assets/stylesheets/study_engine.sass +1 -0
  21. data/app/assets/stylesheets/study_engine/_constant.sass +52 -0
  22. data/app/assets/stylesheets/study_engine/_mobile.sass +8 -0
  23. data/app/assets/stylesheets/study_engine/_tablet.sass +36 -0
  24. data/app/assets/stylesheets/study_engine/application.sass +101 -0
  25. data/app/assets/stylesheets/study_engine/base/_form.sass +131 -0
  26. data/app/assets/stylesheets/study_engine/base/_list.sass +38 -0
  27. data/app/assets/stylesheets/study_engine/base/_tables.sass +33 -0
  28. data/app/assets/stylesheets/study_engine/base/_text.sass +78 -0
  29. data/app/assets/stylesheets/study_engine/components/-fields/_login-fields.sass +14 -0
  30. data/app/assets/stylesheets/study_engine/components/-fields/_start-fields.sass +101 -0
  31. data/app/assets/stylesheets/study_engine/components/-nav/_global-nav.sass +19 -0
  32. data/app/assets/stylesheets/study_engine/components/-table/_results-table.sass +75 -0
  33. data/app/assets/stylesheets/study_engine/components/-table/_users-table.sass +11 -0
  34. data/app/assets/stylesheets/study_engine/components/_assessment-questions.sass +30 -0
  35. data/app/assets/stylesheets/study_engine/components/_form-buttons.sass +102 -0
  36. data/app/assets/stylesheets/study_engine/components/_helpt-text.sass +2 -0
  37. data/app/assets/stylesheets/study_engine/components/_method-icons.sass +15 -0
  38. data/app/assets/stylesheets/study_engine/components/_notes-modal.sass +65 -0
  39. data/app/assets/stylesheets/study_engine/components/_progress-bar.sass +25 -0
  40. data/app/assets/stylesheets/study_engine/components/_sortable-columns.sass +26 -0
  41. data/app/assets/stylesheets/study_engine/components/_table-actions.sass +8 -0
  42. data/app/assets/stylesheets/study_engine/components/_table-buttons.sass +17 -0
  43. data/app/assets/stylesheets/study_engine/components/_user-privileges.sass +3 -0
  44. data/app/assets/stylesheets/study_engine/mixins/_actions-table.sass +11 -0
  45. data/app/assets/stylesheets/study_engine/mixins/_buttons.sass +55 -0
  46. data/app/assets/stylesheets/study_engine/mixins/_checkbox-columns.sass +50 -0
  47. data/app/assets/stylesheets/study_engine/mixins/_custom-radios.sass +47 -0
  48. data/app/assets/stylesheets/study_engine/mixins/_icons.sass +38 -0
  49. data/app/assets/stylesheets/study_engine/mixins/_notification-text.sass +23 -0
  50. data/app/controllers/study_engine/admin/application_controller.rb +6 -0
  51. data/app/controllers/study_engine/admin/assessments_controller.rb +79 -0
  52. data/app/controllers/study_engine/admin/notes_controller.rb +17 -0
  53. data/app/controllers/study_engine/application_controller.rb +12 -0
  54. data/app/controllers/study_engine/assessments_controller.rb +94 -0
  55. data/app/helpers/study_engine/helpers.rb +15 -0
  56. data/app/helpers/study_engine/mailer_helpers.rb +11 -0
  57. data/app/mailers/study_engine/mailer.rb +25 -0
  58. data/app/models/study_engine/answer.rb +36 -0
  59. data/app/models/study_engine/assessment.rb +118 -0
  60. data/app/models/study_engine/begin_assessment_form.rb +110 -0
  61. data/app/models/study_engine/existing_assessments_form.rb +99 -0
  62. data/app/models/study_engine/form.rb +59 -0
  63. data/app/models/study_engine/report.rb +69 -0
  64. data/app/models/study_engine/result.rb +46 -0
  65. data/app/models/study_engine/screen_results.rb +54 -0
  66. data/app/models/study_engine/study_id.rb +47 -0
  67. data/app/models/study_engine/study_id/bank.rb +38 -0
  68. data/app/models/study_engine/study_id/sync.rb +52 -0
  69. data/app/models/study_engine/update_assessments_form.rb +53 -0
  70. data/app/views/study_engine/_progress_bar.html.slim +4 -0
  71. data/app/views/study_engine/admin/assessments/_assessment.html.slim +36 -0
  72. data/app/views/study_engine/admin/assessments/domain_results.csv.csvbuilder +17 -0
  73. data/app/views/study_engine/admin/assessments/edit.html.slim +68 -0
  74. data/app/views/study_engine/admin/assessments/index.html.slim +26 -0
  75. data/app/views/study_engine/admin/assessments/item_results.csv.csvbuilder +24 -0
  76. data/app/views/study_engine/admin/assessments/new.html.slim +46 -0
  77. data/app/views/study_engine/admin/assessments/screen_results.csv.csvbuilder +2 -0
  78. data/app/views/study_engine/assessments/complete.html.slim +25 -0
  79. data/app/views/study_engine/assessments/create.html.slim +46 -0
  80. data/app/views/study_engine/assessments/index.html.slim +43 -0
  81. data/app/views/study_engine/assessments/restart.html.slim +28 -0
  82. data/app/views/study_engine/assessments/show.html.slim +60 -0
  83. data/app/views/study_engine/mailer/assessment_recorded_notification.erb +8 -0
  84. data/app/views/study_engine/mailer/assessment_url_notification.erb +6 -0
  85. data/config/routes.rb +15 -0
  86. data/db/migrate/20160422185859_create_tables.rb +52 -0
  87. data/lib/study_engine.rb +5 -0
  88. data/lib/study_engine/config.rb +37 -0
  89. data/lib/study_engine/cucumber.rb +96 -0
  90. data/lib/study_engine/engine.rb +7 -0
  91. data/lib/study_engine/version.rb +3 -0
  92. data/lib/tasks/study_engine_tasks.rake +10 -0
  93. data/spec/ar_helper.rb +30 -0
  94. data/spec/models/study_engine/answer_spec.rb +28 -0
  95. data/spec/models/study_engine/assessment_spec.rb +250 -0
  96. data/spec/models/study_engine/begin_assessment_form_spec.rb +87 -0
  97. data/spec/models/study_engine/existing_assessments_form_spec.rb +171 -0
  98. data/spec/models/study_engine/report_spec.rb +48 -0
  99. data/spec/models/study_engine/result_spec.rb +102 -0
  100. data/spec/models/study_engine/screen_results_spec.rb +69 -0
  101. data/spec/models/study_engine/study_id/bank_spec.rb +72 -0
  102. data/spec/models/study_engine/study_id/sync_spec.rb +61 -0
  103. data/spec/models/study_engine/study_id_spec.rb +7 -0
  104. data/spec/models/study_engine/update_assessments_form_spec.rb +38 -0
  105. data/spec/rails_helper.rb +50 -0
  106. data/spec/schema.rb +65 -0
  107. data/spec/spec_helper.rb +72 -0
  108. data/spec/support/CATEngine3.exe +0 -0
  109. data/spec/support/forms/01. Physical Function Bank.xml +1213 -0
  110. data/spec/support/forms/02. Pain Interference Bank.xml +443 -0
  111. data/spec/support/forms/03. Anxiety Bank.xml +322 -0
  112. data/spec/support/forms/04. Depression Bank.xml +311 -0
  113. data/spec/support/forms/05. Ability to Participate Social bank Version 2.xml +353 -0
  114. data/spec/support/forms/06. Psychosocial Illness Impact Pos Bank.xml +900 -0
  115. data/spec/support/forms/07. Applied Cognition Gen Concerns Bank.xml +377 -0
  116. data/spec/support/forms/08. Satisfaction with Roles and Activities Bank Version 2.xml +443 -0
  117. data/spec/support/forms/09. Sleep Disturbance Bank.xml +300 -0
  118. data/spec/support/forms/10. Emotional Support Bank.xml +163 -0
  119. data/spec/support/forms/A - 07. Psychosocial Illness Impact Pos Bank.xml +471 -0
  120. data/spec/support/forms/A - 08. Pain interference Bank.xml +443 -0
  121. data/spec/support/forms/A - 09. Depression Bank.xml +311 -0
  122. data/spec/support/forms/B - 07. Physical Function Short Form.xml +123 -0
  123. data/spec/support/forms/B - 08. Physical Function Remainder A.xml +543 -0
  124. data/spec/support/forms/B - 09. Ability to Participate Social bank Version 2.xml +353 -0
  125. data/spec/support/forms/C - 07. Physical Function Short Form.xml +123 -0
  126. data/spec/support/forms/C - 08. Physical Function Remainder B.xml +553 -0
  127. data/spec/support/forms/C - 09. Anxiety Bank.xml +322 -0
  128. data/spec/support/parameters/01. Physical Function Bank.xml +682 -0
  129. data/spec/support/parameters/02. Pain Interference Bank.xml +256 -0
  130. data/spec/support/parameters/03. Anxiety Bank.xml +190 -0
  131. data/spec/support/parameters/04. Depression Bank.xml +184 -0
  132. data/spec/support/parameters/05. Ability to Participate Social bank Version 2.xml +226 -0
  133. data/spec/support/parameters/06. Psychosocial Illness Impact Pos Bank.xml +211 -0
  134. data/spec/support/parameters/07. Applied Cognition Gen Concerns Bank.xml +1 -0
  135. data/spec/support/parameters/08. Satisfaction with Roles and Activities Bank Version 2.xml +1 -0
  136. data/spec/support/parameters/09. Sleep Disturbance Bank.xml +1 -0
  137. data/spec/support/parameters/10. Emotional Support Bank.xml +1 -0
  138. metadata +394 -0
@@ -0,0 +1,99 @@
1
+ require "study_engine/assessment"
2
+ require "study_engine/config"
3
+
4
+ module StudyEngine
5
+ class ExistingAssessmentsForm
6
+ include ActiveModel::Validations
7
+ extend ActiveModel::Naming
8
+
9
+ def self.model_name
10
+ ActiveModel::Name.new(Assessment)
11
+ end
12
+
13
+ validate :format_of_study_id
14
+ validate :presence_of_some_event
15
+ validate :presence_of_previous_events
16
+
17
+ def initialize attributes = {}
18
+ self.study_id = StudyID.new
19
+ self.event_datetimes ||= []
20
+ attributes.each do |name, value|
21
+ send("#{name}=", value)
22
+ end
23
+ end
24
+
25
+ def to_model
26
+ self
27
+ end
28
+
29
+ def to_key
30
+ end
31
+
32
+ def persisted?
33
+ false
34
+ end
35
+
36
+ attr_accessor :study_id, :method_applied, :event_datetimes
37
+
38
+ delegate :study, :site, :id, to: :study_id, prefix: true
39
+ delegate :study=, :site=, :id=, to: :study_id, prefix: true
40
+
41
+ def method_applied
42
+ @method_applied || "In-person"
43
+ end
44
+
45
+ def study_options
46
+ StudyID::Bank.studies
47
+ end
48
+
49
+ def save
50
+ if valid?
51
+ base = Assessment.by_study_id(study_id)
52
+ event_datetimes.each.with_index do |datetime, index|
53
+ next if datetime.blank?
54
+ event = StudyEngine.events[index]
55
+ assessment = base.by_event(event).create!(created_at: datetime, assessment_updated_at: datetime, method_applied: method_applied)
56
+ assessment.update_column :group_index, nil
57
+ end
58
+ true
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def format_of_study_id
65
+ if study_id.present?
66
+ unless study_id.valid?
67
+ errors.add :base, "METRC study ID must be in the following format: XXX-XXX-####"
68
+ end
69
+ else
70
+ errors.add :base, "METRC study ID can not be blank. Enter ID in the following format: XXX-XXX-####"
71
+ end
72
+ end
73
+
74
+ def presence_of_some_event
75
+ if event_datetimes.all?(&:blank?)
76
+ events = StudyEngine.events.dup
77
+ events.map!(&:downcase)
78
+ events.last.prepend("or ")
79
+ events = events.join(", ")
80
+ errors.add :base, "A #{events} timestamp must be present"
81
+ end
82
+ end
83
+
84
+ def presence_of_previous_events
85
+ event_datetimes.each.with_index do |datetime, index|
86
+ next if datetime.blank? || index.nil? || index.zero?
87
+ event = StudyEngine.events[index]
88
+ previous_events = StudyEngine.events[0...index]
89
+
90
+ previous_events.each.with_index do |previous_event, index|
91
+ if event_datetimes[index].blank? && Assessment.by_study_id(study_id).by_event(previous_event).none?
92
+ errors.add :base, "Cannot add a #{event.downcase} assessment when #{previous_event.downcase} assessment does not exist"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
@@ -0,0 +1,59 @@
1
+ require "active_support/core_ext/module/delegation"
2
+
3
+ module StudyEngine
4
+ class Form
5
+ include ActiveModel::Validations
6
+ extend ActiveModel::Naming
7
+
8
+ def self.wraps(model)
9
+ @wraps = model
10
+ end
11
+
12
+ def self.model_name
13
+ ActiveModel::Name.new(wrapped_class)
14
+ end
15
+
16
+ def self.wrapped_class
17
+ @wraps.to_s.classify.constantize
18
+ end
19
+
20
+ def self.lookup_ancestors
21
+ [self]
22
+ end
23
+
24
+ cattr_accessor(:attributes_list) { [] }
25
+
26
+ def self.attributes *attributes
27
+ attributes.each do |attribute|
28
+ attributes_list << attribute
29
+ attr_accessor attribute
30
+ end
31
+ end
32
+
33
+ def self.new_from_model model
34
+ attributes = model.attributes.slice(*attributes_list.map(&:to_s))
35
+ new attributes
36
+ end
37
+
38
+ def initialize attributes = {}, model = self.class.wrapped_class.new
39
+ @model = model
40
+ attributes.each do |name, value|
41
+ send("#{name}=", value)
42
+ end
43
+ end
44
+
45
+ def attributes
46
+ attributes_list.reduce({}) do |hash, key|
47
+ hash[key] = send(key)
48
+ hash
49
+ end
50
+ end
51
+
52
+ def save
53
+ @model.update_attributes(attributes) if valid?
54
+ end
55
+
56
+ delegate :to_model, :to_key, :to_param, :to_partial_path, :persisted?, :new_record?, to: :@model
57
+ end
58
+ end
59
+
@@ -0,0 +1,69 @@
1
+ require "study_engine/config"
2
+
3
+ module StudyEngine
4
+ class Report < Struct.new(:assessments, :field, :direction)
5
+ def self.event_fields
6
+ StudyEngine.events.map.with_index { |_, index| :"event_#{index}" }
7
+ end
8
+
9
+ def self.group_class
10
+ Struct.new(:study_id, *event_fields) do
11
+ def each(&block)
12
+ Report.event_fields.map { |field| send(field) }.each(&block)
13
+ end
14
+
15
+ def max_count
16
+ Report.event_fields.map { |field| send(field).count }.max
17
+ end
18
+
19
+ def == other
20
+ values == other.values
21
+ end
22
+ end
23
+ end
24
+
25
+ def groups
26
+ sort build_groups
27
+ end
28
+
29
+ def build_groups
30
+ assessments.reduce({}) do |groups, assessment|
31
+ group = groups[assessment.study_id.to_s] || new_empty_group(assessment.study_id)
32
+
33
+ field = field_map[assessment.event]
34
+
35
+ group.send(field).push assessment
36
+ group.send(field).sort_by! { |a| a.completed_at ? -1 : 1 }
37
+
38
+ groups.merge(assessment.study_id.to_s => group)
39
+ end.values
40
+ end
41
+
42
+ def sort groups
43
+ groups.sort do |a, b|
44
+ a = a.send(field)
45
+ b = b.send(field)
46
+ ascending? ? a <=> b : b <=> a
47
+ end
48
+ end
49
+
50
+ def ascending?
51
+ direction == "ASC"
52
+ end
53
+
54
+ private
55
+
56
+ def field_map
57
+ StudyEngine.events.inject({}) do |hash, event|
58
+ index = StudyEngine.events.index(event)
59
+ hash.merge event => "event_#{index}"
60
+ end
61
+ end
62
+
63
+ def new_empty_group(study_id)
64
+ empty_groups = StudyEngine.events.map { |_| [] }
65
+ self.class.group_class.new(study_id, *empty_groups)
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,46 @@
1
+ require "cat_engine/bank"
2
+ require "cat_engine/engine"
3
+
4
+ module StudyEngine
5
+ class Result < ActiveRecord::Base
6
+ belongs_to :assessment, touch: true
7
+
8
+ has_many :answers
9
+
10
+ delegate :current_question, to: :bank
11
+
12
+ def bank
13
+ CatEngine::Bank.new(bank_id, history, assessment.full_history)
14
+ end
15
+
16
+ def name
17
+ bank_id.sub(".flat","")
18
+ end
19
+
20
+ def history
21
+ answers.pluck(:response_id).join(" ")
22
+ end
23
+
24
+ def save_answer answer
25
+ wrapper do # race condition could bypass validity check
26
+ if bank.valid_answer?(answer.response_id)
27
+ answers << answer
28
+ update_attributes bank.to_h
29
+ save!
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def wrapper
37
+ # UGH HACK HACK HACK why can't FlatEngine handle being wrapped in a lock?
38
+ if bank.send(:engine).is_a? CatEngine::Engine
39
+ with_lock { yield }
40
+ else
41
+ yield
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,54 @@
1
+ require "study_engine/config"
2
+
3
+ module StudyEngine
4
+ class ScreenResults < Struct.new(:groups)
5
+ def each &block
6
+ block.call header
7
+ body block
8
+ end
9
+
10
+ private
11
+
12
+ def header
13
+ fields = ["Study ID"]
14
+ StudyEngine.events.each do |event|
15
+ fields += ["#{event} incomplete", "#{event} timestamp", "#{event} method", "#{event} coordinator", "#{event} note"]
16
+ fields += ["#{event} set"] if StudyEngine.groups[event].present?
17
+ end
18
+ fields.flatten
19
+ end
20
+
21
+ def body block
22
+ groups.each do |group|
23
+ group.max_count.times do |count|
24
+ col = [group.study_id.to_s]
25
+
26
+ col = group.each_with_index.inject(col) do |col, (assessments, index)|
27
+ col + assessment_segment(assessments[count], index)
28
+ end
29
+
30
+ block.call col
31
+ end
32
+ end
33
+ end
34
+
35
+ def assessment_segment assessment, index
36
+ assessment ? existing_assessment(assessment) : empty_assessment(index)
37
+ end
38
+
39
+ def existing_assessment assessment
40
+ summary = assessment.incomplete? && !assessment.by_mail? ? assessment.results_summary : ""
41
+ col = [summary, assessment.assessment_updated_at.to_s, assessment.method_applied, assessment.coordinator.try(:email), assessment.notes.to_s]
42
+ col << assessment.group_index_name if assessment.group_index
43
+ col
44
+ end
45
+
46
+ def empty_assessment index
47
+ col = ["", "", "", "", ""]
48
+ event = StudyEngine.events[index]
49
+ col << "" if StudyEngine.groups[event].present?
50
+ col
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,47 @@
1
+ require "equalizer"
2
+
3
+ module StudyEngine
4
+ class StudyID
5
+ def self.load slug
6
+ return unless slug
7
+ new slug
8
+ end
9
+
10
+ def self.dump study_id
11
+ study_id.to_s
12
+ end
13
+
14
+ include Equalizer.new(:study, :site, :id)
15
+
16
+ attr_accessor :study, :site, :id
17
+
18
+ def initialize slug=nil
19
+ @study, @site, @id = slug.split("-") if slug
20
+ end
21
+
22
+ def study= value
23
+ @study = value.try(:upcase)
24
+ end
25
+
26
+ def site= value
27
+ @site = value.try(:upcase)
28
+ end
29
+
30
+ def to_s
31
+ [study, site, id].join("-")
32
+ end
33
+
34
+ def <=> other
35
+ to_s <=> other.to_s
36
+ end
37
+
38
+ def present?
39
+ [@study, @site, @id].all?(&:present?)
40
+ end
41
+
42
+ def valid?
43
+ to_s =~ %r{\A[A-Z]{3}-[A-Z]{3}-[0-9]{4}\Z}
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,38 @@
1
+ module StudyEngine
2
+ class StudyID
3
+ class Bank < ActiveRecord::Base
4
+ self.table_name = "study_engine_study_ids"
5
+
6
+ def self.find_or_create_from_full_study_id study_id
7
+ study_id_study, study_id_site, study_id_id = study_id.split("-")
8
+ find_or_create_by study_id_study: study_id_study, study_id_site: study_id_site, study_id_id: study_id_id
9
+ end
10
+
11
+ def self.studies
12
+ pluck(:study_id_study).uniq.sort
13
+ end
14
+
15
+ def self.sites
16
+ pluck(:study_id_site).uniq.sort
17
+ end
18
+
19
+ def self.ids_grouped_by_study_and_site
20
+ all.reduce(Hash.new([])) do |hash, study_id|
21
+ hash[study_id.study_id_study + study_id.study_id_site] += [study_id.study_id_id]
22
+ hash
23
+ end
24
+ end
25
+
26
+ def self.studies_grouped_by_site
27
+ all.reduce(Hash.new(Set.new)) do |hash, study_id|
28
+ hash[study_id.study_id_site] += [study_id.study_id_study]
29
+ hash
30
+ end
31
+ end
32
+
33
+ validates :study_id_study, :study_id_site, format: /\A[A-Z]{3}\Z/
34
+ validates :study_id_id, format: /\A[0-9]{4}\Z/
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,52 @@
1
+ require "faraday"
2
+ require "csv"
3
+ require "study_engine/config"
4
+
5
+ module StudyEngine
6
+ class StudyID
7
+ class Sync
8
+ def self.call
9
+ new.call
10
+ end
11
+
12
+ def call
13
+ raise "Cannot sync with REDCap without setting StudyEngine.redcap_token" if StudyEngine.redcap_token.blank?
14
+ CSV.parse(response, headers: true) do |row|
15
+ process row unless row.empty?
16
+ end
17
+ StudyID::Bank.where(["updated_at < ?", 30.minutes.ago]).delete_all
18
+ end
19
+
20
+ private
21
+
22
+ def process row
23
+ study_id = StudyID::Bank.find_or_create_from_full_study_id(row["study_id"])
24
+ study_id.touch unless study_id.new_record?
25
+ end
26
+
27
+ def response
28
+ conn = Faraday.new(url: "https://metrcdata.org")
29
+ resp = conn.post "/redcap/api/",
30
+ token: StudyEngine.redcap_token,
31
+ content: "record",
32
+ format: "csv"
33
+ resp.body
34
+ end
35
+
36
+ prepend Module.new {
37
+ def response
38
+ retries = 0
39
+ super
40
+ rescue Faraday::TimeoutError
41
+ retries += 1
42
+ if retries < 5
43
+ retry
44
+ else
45
+ raise
46
+ end
47
+ end
48
+ }
49
+ end
50
+ end
51
+ end
52
+