surveyor 0.22.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. data/.gitignore +1 -0
  2. data/.rspec +1 -0
  3. data/CHANGELOG.md +83 -0
  4. data/Gemfile.rails_version +7 -1
  5. data/README.md +114 -16
  6. data/Rakefile +15 -9
  7. data/app/inputs/quiet_input.rb +5 -0
  8. data/app/inputs/surveyor_check_boxes_input.rb +35 -0
  9. data/app/inputs/surveyor_radio_input.rb +18 -0
  10. data/app/views/partials/_answer.html.haml +4 -4
  11. data/app/views/partials/_question.html.haml +7 -7
  12. data/app/views/partials/_question_group.html.haml +9 -8
  13. data/app/views/surveyor/export.json.rabl +28 -25
  14. data/app/views/surveyor/new.html.haml +8 -5
  15. data/app/views/surveyor/show.json.rabl +3 -2
  16. data/ci-exec.sh +13 -7
  17. data/cucumber.yml +3 -3
  18. data/doc/REPRESENTATIONS.md +34 -0
  19. data/doc/api_id_schema.json +7 -0
  20. data/doc/response_set_schema.json +54 -0
  21. data/doc/surveyor question combinations.png +0 -0
  22. data/doc/surveyor_timestamp_schema.json +9 -0
  23. data/features/ajax_submissions.feature +140 -0
  24. data/features/export_to_json.feature +182 -34
  25. data/features/no_duplicates.feature +110 -0
  26. data/features/show_survey.feature +1 -1
  27. data/features/step_definitions/parser_steps.rb +25 -2
  28. data/features/step_definitions/surveyor_steps.rb +145 -20
  29. data/features/step_definitions/ui_steps.rb +3 -0
  30. data/features/support/database_cleaner.rb +16 -0
  31. data/features/support/env.rb +21 -17
  32. data/features/support/simultaneous_ajax.rb +101 -0
  33. data/features/support/single_quit_selenium_driver.rb +23 -0
  34. data/features/support/slow_updates.rb +18 -0
  35. data/features/surveyor.feature +174 -44
  36. data/features/surveyor_dependencies.feature +80 -39
  37. data/features/surveyor_parser.feature +114 -20
  38. data/features/z_redcap_parser.feature +0 -1
  39. data/lib/{generators/surveyor/templates/public → assets}/images/surveyor/next.gif +0 -0
  40. data/lib/{generators/surveyor/templates/public → assets}/images/surveyor/prev.gif +0 -0
  41. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  42. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  43. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_65_ffffff_1x400.png +0 -0
  44. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  45. data/lib/assets/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  46. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  47. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-icons_ef8c08_256x240.png +0 -0
  48. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-icons_ffffff_256x240.png +0 -0
  49. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery-ui-timepicker-addon.js +23 -23
  50. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery-ui.js +125 -125
  51. data/lib/assets/javascripts/surveyor/jquery.selectToUISlider.js +240 -0
  52. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery.surveyor.js +52 -57
  53. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery.tools.min.js +7 -7
  54. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/dateinput.css +13 -13
  55. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/jquery-ui-timepicker-addon.css +0 -0
  56. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/jquery-ui.custom.css +17 -17
  57. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/reset.css +1 -1
  58. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/results.css +0 -0
  59. data/lib/assets/stylesheets/surveyor/ui.slider.extras.css +110 -0
  60. data/lib/{generators/surveyor/templates/public/stylesheets/sass → assets/stylesheets}/surveyor.sass +15 -7
  61. data/lib/generators/surveyor/custom_generator.rb +3 -2
  62. data/lib/generators/surveyor/install_generator.rb +59 -17
  63. data/lib/generators/surveyor/templates/app/assets/javascripts/surveyor_all.js +5 -0
  64. data/lib/generators/surveyor/templates/app/assets/stylesheets/surveyor_all.css +9 -0
  65. data/lib/generators/surveyor/templates/app/controllers/surveyor_controller.rb +2 -1
  66. data/lib/generators/surveyor/templates/app/views/layouts/surveyor_custom.html.erb +1 -0
  67. data/lib/generators/surveyor/templates/config/locales/surveyor_es.yml +1 -0
  68. data/lib/generators/surveyor/templates/config/locales/surveyor_he.yml +1 -0
  69. data/lib/generators/surveyor/templates/db/migrate/add_api_id_to_question_groups.rb +1 -0
  70. data/lib/generators/surveyor/templates/db/migrate/add_api_ids.rb +1 -0
  71. data/lib/generators/surveyor/templates/db/migrate/add_api_ids_to_response_sets_and_responses.rb +1 -0
  72. data/lib/generators/surveyor/templates/db/migrate/add_correct_answer_id_to_questions.rb +1 -0
  73. data/lib/generators/surveyor/templates/db/migrate/add_default_value_to_answers.rb +1 -0
  74. data/lib/generators/surveyor/templates/db/migrate/add_display_order_to_surveys.rb +1 -0
  75. data/lib/generators/surveyor/templates/db/migrate/add_display_type_to_answers.rb +1 -0
  76. data/lib/generators/surveyor/templates/db/migrate/add_index_to_response_sets.rb +1 -0
  77. data/lib/generators/surveyor/templates/db/migrate/add_index_to_surveys.rb +1 -0
  78. data/lib/generators/surveyor/templates/db/migrate/add_section_id_to_responses.rb +1 -0
  79. data/lib/generators/surveyor/templates/db/migrate/add_unique_index_on_access_code_and_version_in_surveys.rb +10 -0
  80. data/lib/generators/surveyor/templates/db/migrate/add_unique_indicies.rb +3 -2
  81. data/lib/generators/surveyor/templates/db/migrate/add_version_to_surveys.rb +10 -0
  82. data/lib/generators/surveyor/templates/db/migrate/api_ids_must_be_unique.rb +23 -0
  83. data/lib/generators/surveyor/templates/db/migrate/create_answers.rb +6 -5
  84. data/lib/generators/surveyor/templates/db/migrate/create_dependencies.rb +2 -1
  85. data/lib/generators/surveyor/templates/db/migrate/create_dependency_conditions.rb +2 -1
  86. data/lib/generators/surveyor/templates/db/migrate/create_question_groups.rb +5 -4
  87. data/lib/generators/surveyor/templates/db/migrate/create_questions.rb +4 -3
  88. data/lib/generators/surveyor/templates/db/migrate/create_response_sets.rb +1 -0
  89. data/lib/generators/surveyor/templates/db/migrate/create_responses.rb +10 -9
  90. data/lib/generators/surveyor/templates/db/migrate/create_survey_sections.rb +5 -4
  91. data/lib/generators/surveyor/templates/db/migrate/create_surveys.rb +4 -3
  92. data/lib/generators/surveyor/templates/db/migrate/create_validation_conditions.rb +5 -4
  93. data/lib/generators/surveyor/templates/db/migrate/create_validations.rb +3 -2
  94. data/lib/generators/surveyor/templates/db/migrate/drop_unique_index_on_access_code_in_surveys.rb +10 -0
  95. data/lib/generators/surveyor/templates/db/migrate/update_blank_api_ids_on_question_group.rb +22 -0
  96. data/lib/generators/surveyor/templates/db/migrate/update_blank_versions_on_surveys.rb +13 -0
  97. data/lib/generators/surveyor/templates/surveys/date_survey.rb +1 -0
  98. data/lib/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +54 -24
  99. data/lib/generators/surveyor/templates/surveys/quiz.rb +1 -0
  100. data/lib/generators/surveyor/templates/{public/stylesheets/sass → vendor/assets/stylesheets}/custom.sass +1 -1
  101. data/lib/surveyor/common.rb +16 -31
  102. data/lib/surveyor/engine.rb +2 -4
  103. data/lib/surveyor/helpers/asset_pipeline.rb +13 -0
  104. data/lib/surveyor/helpers/formtastic_custom_input.rb +17 -0
  105. data/lib/surveyor/helpers/surveyor_helper_methods.rb +10 -8
  106. data/lib/surveyor/models/answer_methods.rb +3 -0
  107. data/lib/surveyor/models/dependency_condition_methods.rb +27 -28
  108. data/lib/surveyor/models/dependency_methods.rb +3 -0
  109. data/lib/surveyor/models/question_group_methods.rb +3 -0
  110. data/lib/surveyor/models/question_methods.rb +10 -7
  111. data/lib/surveyor/models/response_methods.rb +16 -0
  112. data/lib/surveyor/models/response_set_methods.rb +71 -64
  113. data/lib/surveyor/models/survey_methods.rb +19 -28
  114. data/lib/surveyor/models/survey_section_methods.rb +3 -0
  115. data/lib/surveyor/models/validation_condition_methods.rb +4 -2
  116. data/lib/surveyor/models/validation_methods.rb +3 -0
  117. data/lib/surveyor/parser.rb +198 -148
  118. data/lib/surveyor/redcap_parser.rb +120 -80
  119. data/lib/surveyor/surveyor_controller_methods.rb +86 -37
  120. data/lib/surveyor/version.rb +2 -2
  121. data/lib/surveyor.rb +5 -6
  122. data/lib/tasks/surveyor_tasks.rake +19 -7
  123. data/spec/controllers/surveyor_controller_spec.rb +166 -92
  124. data/spec/factories.rb +33 -32
  125. data/spec/helpers/formtastic_custom_input_spec.rb +16 -0
  126. data/spec/lib/common_spec.rb +0 -39
  127. data/spec/lib/redcap_parser_spec.rb +24 -24
  128. data/spec/models/answer_spec.rb +12 -0
  129. data/spec/models/dependency_condition_spec.rb +279 -323
  130. data/spec/models/dependency_spec.rb +10 -0
  131. data/spec/models/question_group_spec.rb +12 -0
  132. data/spec/models/question_spec.rb +12 -0
  133. data/spec/models/response_set_spec.rb +189 -139
  134. data/spec/models/response_spec.rb +60 -0
  135. data/spec/models/survey_section_spec.rb +9 -0
  136. data/spec/models/survey_spec.rb +72 -9
  137. data/spec/models/validation_condition_spec.rb +9 -1
  138. data/spec/models/validation_spec.rb +10 -0
  139. data/spec/spec_helper.rb +25 -6
  140. data/surveyor.gemspec +5 -4
  141. metadata +332 -291
  142. data/features/step_definitions/common_steps.rb +0 -3
  143. data/lib/formtastic/surveyor_builder.rb +0 -82
  144. data/lib/generators/surveyor/templates/public/javascripts/surveyor/jquery.blockUI.js +0 -499
@@ -3,42 +3,51 @@ require 'active_support' # for humanize
3
3
  require 'fastercsv'
4
4
  require 'csv'
5
5
  module Surveyor
6
+ class RedcapParserError < StandardError; end
6
7
  class RedcapParser
8
+ class << self; attr_accessor :options end
9
+
7
10
  # Attributes
8
11
  attr_accessor :context
9
12
 
10
13
  # Class methods
11
- def self.parse(str, filename)
12
- puts
14
+ def self.parse(str, filename, options={})
15
+ self.options = options
16
+ Surveyor::RedcapParser.rake_trace "\n"
13
17
  Surveyor::RedcapParser.new.parse(str, filename)
14
- puts
15
- puts
18
+ Surveyor::RedcapParser.rake_trace "\n"
19
+ end
20
+ def self.rake_trace(str)
21
+ self.options ||= {}
22
+ print str if self.options[:trace] == true
16
23
  end
17
-
24
+
18
25
  # Instance methods
19
26
  def initialize
20
27
  self.context = {}
28
+ self.context[:dependency_conditions] = []
21
29
  end
22
30
  def parse(str, filename)
23
31
  csvlib = CSV.const_defined?(:Reader) ? FasterCSV : CSV
24
32
  begin
25
33
  csvlib.parse(str, :headers => :first_row, :return_headers => true, :header_converters => :symbol) do |r|
26
34
  if r.header_row? # header row
27
- return puts "Missing headers: #{missing_columns(r.headers).inspect}\n\n" unless missing_columns(r.headers).blank?
35
+ return Surveyor::RedcapParser.rake_trace "Missing headers: #{missing_columns(r.headers).inspect}\n\n" unless missing_columns(r.headers).blank?
28
36
  context[:survey] = Survey.new(:title => filename)
29
- print "survey_#{context[:survey].access_code} "
37
+ Surveyor::RedcapParser.rake_trace "survey_#{context[:survey].access_code} "
30
38
  else # non-header rows
31
- SurveySection.build_or_set(context, r)
32
- Question.build_and_set(context, r)
33
- Answer.build_and_set(context, r)
34
- Validation.build_and_set(context, r)
35
- Dependency.build_and_set(context, r)
39
+ SurveySection.new.extend(SurveyorRedcapParserSurveySectionMethods).build_or_set(context, r)
40
+ Question.new.extend(SurveyorRedcapParserQuestionMethods).build_and_set(context, r)
41
+ Answer.new.extend(SurveyorRedcapParserAnswerMethods).build_and_set(context, r)
42
+ Validation.new.extend(SurveyorRedcapParserValidationMethods).build_and_set(context, r)
43
+ Dependency.new.extend(SurveyorRedcapParserDependencyMethods).build_and_set(context, r)
36
44
  end
37
45
  end
38
- print context[:survey].save ? "saved. " : " not saved! #{context[:survey].errors.full_messages.join(", ")} "
39
- # print context[:survey].sections.map(&:questions).flatten.map(&:answers).flatten.map{|x| x.errors.each_full{|y| y}.join}.join
46
+ resolve_references
47
+ Surveyor::RedcapParser.rake_trace context[:survey].save ? "saved. " : " not saved! #{context[:survey].errors.full_messages.join(", ")} "
48
+ # Surveyor::RedcapParser.rake_trace context[:survey].sections.map(&:questions).flatten.map(&:answers).flatten.map{|x| x.errors.each_full{|y| y}.join}.join
40
49
  rescue csvlib::MalformedCSVError
41
- puts = "Oops. Not a valid CSV file."
50
+ raise Surveyor::RedcapParserError, "Oops. Not a valid CSV file."
42
51
  # ensure
43
52
  end
44
53
  return context[:survey]
@@ -46,42 +55,61 @@ module Surveyor
46
55
  def missing_columns(r)
47
56
  missing = []
48
57
  missing << "choices_or_calculations" unless r.map(&:to_s).include?("choices_or_calculations") or r.map(&:to_s).include?("choices_calculations_or_slider_labels")
49
- missing << "text_validation_type" unless r.map(&:to_s).include?("text_validation_type") or r.map(&:to_s).include?("text_validation_type_or_show_slider_number")
58
+ missing << "text_validation_type" unless r.map(&:to_s).include?("text_validation_type") or r.map(&:to_s).include?("text_validation_type_or_show_slider_number")
50
59
  missing += (static_required_columns - r.map(&:to_s))
51
60
  end
52
61
  def static_required_columns
53
62
  # no longer requiring field_units
54
63
  %w(variable__field_name form_name section_header field_type field_label field_note text_validation_min text_validation_max identifier branching_logic_show_field_only_if required_field)
55
64
  end
65
+ def resolve_references
66
+ context[:dependency_conditions].each do |dc|
67
+ return unless dc.lookup_reference
68
+ Surveyor::RedcapParser.rake_trace "resolve(#{dc.question_reference},#{dc.answer_reference})"
69
+ if dc.answer_reference.blank? and (row = dc.lookup_reference.find{|r| r[0] == dc.question_reference and r[1] == nil}) and row[2].answers.size == 1
70
+ Surveyor::RedcapParser.rake_trace "...found "
71
+ dc.question = row[2]
72
+ dc.answer = dc.question.answers.first
73
+ elsif row = dc.lookup_reference.find{|r| r[0] == dc.question_reference and r[1] == dc.answer_reference}
74
+ Surveyor::RedcapParser.rake_trace "...found "
75
+ dc.answer = row[2]
76
+ dc.question = dc.answer.question
77
+ else
78
+ Surveyor::RedcapParser.rake_trace "\n!!! failed lookup for dependency_condition q: #{question_reference} a: #{question_reference}"
79
+ end
80
+ end
81
+ end
56
82
  end
57
83
  end
58
84
 
59
85
  # Surveyor models with extra parsing methods
60
- class Survey < ActiveRecord::Base
61
- end
62
- class SurveySection < ActiveRecord::Base
63
- def self.build_or_set(context, r)
86
+
87
+ # SurveySection model
88
+ module SurveyorRedcapParserSurveySectionMethods
89
+ def build_or_set(context, r)
64
90
  unless context[:survey_section] && context[:survey_section].reference_identifier == r[:form_name]
65
91
  if match = context[:survey].sections.detect{|ss| ss.reference_identifier == r[:form_name]}
66
92
  context[:current_survey_section] = match
67
93
  else
68
- context[:survey_section] = context[:survey].sections.build({:title => r[:form_name].to_s.humanize,
69
- :reference_identifier => r[:form_name],
70
- :display_order => context[:survey].sections.size })
71
- print "survey_section_#{context[:survey_section].reference_identifier} "
94
+ self.attributes = (
95
+ {:title => r[:form_name].to_s.humanize,
96
+ :reference_identifier => r[:form_name],
97
+ :display_order => context[:survey].sections.size })
98
+ context[:survey].sections << context[:survey_section] = self
99
+ Surveyor::RedcapParser.rake_trace "survey_section_#{context[:survey_section].reference_identifier} "
72
100
  end
73
101
  end
74
102
  end
75
103
  end
76
- class QuestionGroup < ActiveRecord::Base
77
- end
78
- class Question < ActiveRecord::Base
79
- def self.build_and_set(context, r)
104
+
105
+ # Question model
106
+ module SurveyorRedcapParserQuestionMethods
107
+ def build_and_set(context, r)
80
108
  if !r[:section_header].blank?
81
109
  context[:survey_section].questions.build({:display_type => "label", :text => r[:section_header], :display_order => context[:survey_section].questions.size})
82
- print "label_ "
110
+ Surveyor::RedcapParser.rake_trace "label_ "
83
111
  end
84
- context[:question] = context[:survey_section].questions.build({
112
+ self.attributes = ({
85
113
  :reference_identifier => r[:variable__field_name],
86
114
  :text => r[:field_label],
87
115
  :help_text => r[:field_note],
@@ -90,33 +118,38 @@ class Question < ActiveRecord::Base
90
118
  :display_type => display_type_from_field_type(r[:field_type]),
91
119
  :display_order => context[:survey_section].questions.size
92
120
  })
121
+ context[:survey_section].questions << context[:question] = self
93
122
  unless context[:question].reference_identifier.blank?
94
123
  context[:lookup] ||= []
95
124
  context[:lookup] << [context[:question].reference_identifier, nil, context[:question]]
96
- end
97
- print "question_#{context[:question].reference_identifier} "
125
+ end
126
+ Surveyor::RedcapParser.rake_trace "question_#{context[:question].reference_identifier} "
98
127
  end
99
- def self.pick_from_field_type(ft)
128
+ def pick_from_field_type(ft)
100
129
  {"checkbox" => :any, "radio" => :one}[ft] || :none
101
130
  end
102
- def self.display_type_from_field_type(ft)
131
+ def display_type_from_field_type(ft)
103
132
  {"text" => :string, "dropdown" => :dropdown, "notes" => :text}[ft]
104
133
  end
105
134
  end
106
- class Dependency < ActiveRecord::Base
107
- def self.build_and_set(context, r)
135
+
136
+ # Dependency model
137
+ module SurveyorRedcapParserDependencyMethods
138
+ def build_and_set(context, r)
108
139
  unless (bl = r[:branching_logic_show_field_only_if]).blank?
109
140
  # TODO: forgot to tie rule key to component, counting on the sequence of components
110
141
  letters = ('A'..'Z').to_a
111
142
  hash = decompose_rule(bl)
112
- context[:dependency] = context[:question].build_dependency(:rule => hash[:rule])
143
+ self.attributes = {:rule => hash[:rule]}
144
+ context[:question].dependency = context[:dependency] = self
113
145
  hash[:components].each do |component|
114
- context[:dependency].dependency_conditions.build(decompose_component(component).merge(:lookup_reference => context[:lookup], :rule_key => letters.shift))
146
+ dc = context[:dependency].dependency_conditions.build(decompose_component(component).merge(:lookup_reference => context[:lookup], :rule_key => letters.shift))
147
+ context[:dependency_conditions] << dc
115
148
  end
116
- print "dependency(#{hash[:rule]}) "
149
+ Surveyor::RedcapParser.rake_trace "dependency(#{hash[:rule]}) "
117
150
  end
118
151
  end
119
- def self.decompose_component(str)
152
+ def decompose_component(str)
120
153
  # [initial_52] = "1" or [f1_q15] = '' or [f1_q15] = '-2' or [hi_event1_type] <> ''
121
154
  if match = str.match(/^\[(\w+)\] ?([!=><]+) ?['"](-?\w*)['"]$/)
122
155
  {:question_reference => match[1], :operator => match[2].gsub(/^=$/, "==").gsub(/^<>$/, "!="), :answer_reference => match[3]}
@@ -127,10 +160,10 @@ class Dependency < ActiveRecord::Base
127
160
  elsif match = str.match(/^\[(\w+)\] ?([!=><]+) ?(-?\d+)$/)
128
161
  {:question_reference => match[1], :operator => match[2].gsub(/^=$/, "==").gsub(/^<>$/, "!="), :integer_value => match[3]}
129
162
  else
130
- puts "\n!!! skipping dependency_condition #{str}"
131
- end
163
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping dependency_condition #{str}"
164
+ end
132
165
  end
133
- def self.decompose_rule(str)
166
+ def decompose_rule(str)
134
167
  # see spec/lib/redcap_parser_spec.rb for examples
135
168
  letters = ('A'..'Z').to_a
136
169
  rule = str
@@ -149,7 +182,7 @@ class Dependency < ActiveRecord::Base
149
182
  # sub in rule key
150
183
  rule = rule.gsub(part, "(#{nums.map{letters.shift}.join(' and ')})")
151
184
  else
152
- # 'or' on the right of the operator
185
+ # 'or' on the right of the operator
153
186
  components[i] = components[i-1].gsub(/"(\d+)"/, part) if part.match(/^"(\d+)"$/) && i != 0
154
187
  # sub in rule key
155
188
  rule = rule.gsub(part){letters.shift}
@@ -158,60 +191,66 @@ class Dependency < ActiveRecord::Base
158
191
  {:rule => rule, :components => components.flatten}
159
192
  end
160
193
  end
161
- class DependencyCondition < ActiveRecord::Base
162
- attr_accessor :question_reference, :answer_reference, :lookup_reference
163
- before_save :resolve_references
164
- def resolve_references
165
- return unless lookup_reference
166
- print "resolve(#{question_reference},#{answer_reference})"
167
- if answer_reference.blank? and (row = lookup_reference.find{|r| r[0] == question_reference and r[1] == nil}) and row[2].answers.size == 1
168
- print "...found "
169
- self.question = row[2]
170
- self.answer = self.question.answers.first
171
- elsif row = lookup_reference.find{|r| r[0] == question_reference and r[1] == answer_reference}
172
- print "...found "
173
- self.answer = row[2]
174
- self.question = self.answer.question
175
- else
176
- puts "\n!!! failed lookup for dependency_condition q: #{question_reference} a: #{question_reference}"
177
- end
194
+
195
+ # DependencyCondition model
196
+ module SurveyorRedcapParserDependencyConditionMethods
197
+ DependencyCondition.instance_eval do
198
+ attr_accessor :question_reference, :answer_reference, :lookup_reference
199
+ attr_accessible :question_reference, :answer_reference, :lookup_reference
178
200
  end
179
201
  end
180
- class Answer < ActiveRecord::Base
181
- def self.build_and_set(context, r)
202
+
203
+ # Answer model
204
+ module SurveyorRedcapParserAnswerMethods
205
+ def build_and_set(context, r)
182
206
  case r[:field_type]
183
207
  when "text"
184
- context[:answer] = context[:question].answers.build(:response_class => "string", :text => "Text", :display_order => context[:question].answers.size)
208
+ self.attributes = {
209
+ :response_class => "string",
210
+ :text => "Text",
211
+ :display_order => context[:question].answers.size }
212
+ context[:question].answers << context[:answer] = self
185
213
  when "notes"
186
- context[:answer] = context[:question].answers.build(:response_class => "text", :text => "Notes", :display_order => context[:question].answers.size)
214
+ self.attributes = {
215
+ :response_class => "text",
216
+ :text => "Notes",
217
+ :display_order => context[:question].answers.size }
218
+ context[:question].answers << context[:answer] = self
187
219
  when "file"
188
- puts "\n!!! skipping answer: file"
220
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping answer: file"
189
221
  end
190
222
  (r[:choices_or_calculations] || r[:choices_calculations_or_slider_labels]).to_s.split("|").each do |pair|
191
223
  aref, atext = pair.split(",").map(&:strip)
192
224
  if aref.blank? or atext.blank? or (aref.to_i.to_s != aref)
193
- puts "\n!!! skipping answer #{pair}"
225
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping answer #{pair}"
194
226
  else
195
- context[:answer] = context[:question].answers.build(:reference_identifier => aref, :text => atext, :display_order => context[:question].answers.size)
227
+ a = Answer.new({
228
+ :reference_identifier => aref,
229
+ :text => atext,
230
+ :display_order => context[:question].answers.size })
231
+ context[:question].answers << context[:answer] = a
196
232
  unless context[:question].reference_identifier.blank? or aref.blank? or !context[:answer].valid?
197
233
  context[:lookup] ||= []
198
234
  context[:lookup] << [context[:question].reference_identifier, aref, context[:answer]]
199
235
  end
200
- puts "#{context[:answer].errors.full_messages}, #{context[:answer].inspect}" unless context[:answer].valid?
201
- print "answer_#{context[:answer].reference_identifier} "
236
+ Surveyor::RedcapParser.rake_trace "#{context[:answer].errors.full_messages}, #{context[:answer].inspect}" unless context[:answer].valid?
237
+ Surveyor::RedcapParser.rake_trace "answer_#{context[:answer].reference_identifier} "
202
238
  end
203
239
  end
204
240
  end
205
241
  end
206
- class Validation < ActiveRecord::Base
207
- def self.build_and_set(context, r)
242
+
243
+ # Validation model
244
+ module SurveyorRedcapParserValidationMethods
245
+ def build_and_set(context, r)
208
246
  # text_validation_type text_validation_min text_validation_max
209
247
  min = r[:text_validation_min].to_s.blank? ? nil : r[:text_validation_min].to_s
210
248
  max = r[:text_validation_max].to_s.blank? ? nil : r[:text_validation_max].to_s
211
249
  type = r[:text_validation_type].to_s.blank? ? nil : r[:text_validation_type].to_s
212
250
  if min or max
213
251
  context[:question].answers.each do |a|
214
- context[:validation] = a.validations.build(:rule => min ? max ? "A and B" : "A" : "B")
252
+ self.rule = (min ? max ? "A and B" : "A" : "B")
253
+ a.validations << context[:validation] = self
215
254
  context[:validation].validation_conditions.build(:rule_key => "A", :operator => ">=", :integer_value => min) if min
216
255
  context[:validation].validation_conditions.build(:rule_key => "B", :operator => "<=", :integer_value => max) if max
217
256
  end
@@ -222,30 +261,31 @@ class Validation < ActiveRecord::Base
222
261
  context[:question].display_type = :date if context[:question].display_type == :string
223
262
  when "email"
224
263
  context[:question].answers.each do |a|
225
- context[:validation] = a.validations.build(:rule => "A")
264
+ self.rule = "A"
265
+ a.validations << context[:validation] = self
226
266
  context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$")
227
267
  end
228
268
  when "integer"
229
269
  context[:question].display_type = :integer if context[:question].display_type == :string
230
270
  context[:question].answers.each do |a|
231
- context[:validation] = a.validations.build(:rule => "A")
271
+ self.rule = "A"
272
+ a.validations << context[:validation] = self
232
273
  context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "\d+")
233
274
  end
234
275
  when "number"
235
276
  context[:question].display_type = :float if context[:question].display_type == :string
236
277
  context[:question].answers.each do |a|
237
- context[:validation] = a.validations.build(:rule => "A")
278
+ self.rule = "A"
279
+ a.validations << context[:validation] = self
238
280
  context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "^\d*(,\d{3})*(\.\d*)?$")
239
281
  end
240
282
  when "phone"
241
283
  context[:question].answers.each do |a|
242
- context[:validation] = a.validations.build(:rule => "A")
284
+ self.rule = "A"
285
+ a.validations << context[:validation] = self
243
286
  context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "\d{3}.*\d{4}")
244
287
  end
245
288
  end
246
289
  end
247
290
  end
248
-
249
- end
250
- class ValidationCondition < ActiveRecord::Base
251
291
  end
@@ -1,43 +1,58 @@
1
1
  require 'rabl'
2
2
  Rabl.register!
3
+ Rabl.configure {|config| config.include_child_root = false }
3
4
  Rabl.configure {|config| config.include_json_root = false }
4
5
  module Surveyor
5
6
  module SurveyorControllerMethods
6
7
  def self.included(base)
7
8
  base.send :before_filter, :get_current_user, :only => [:new, :create]
8
9
  base.send :before_filter, :determine_if_javascript_is_enabled, :only => [:create, :update]
9
- base.send :before_filter, :set_render_context, :only => [:edit]
10
+ base.send :before_filter, :set_response_set_and_render_context, :only => [:edit, :show]
10
11
  base.send :layout, 'surveyor_default'
11
12
  end
12
13
 
13
14
  # Actions
14
15
  def new
15
16
  @surveys = Survey.find(:all)
17
+ @codes = @surveys.inject({}) do |codes,s|
18
+ codes[s.access_code] ||= {}
19
+ codes[s.access_code][:title] = s.title
20
+ codes[s.access_code][:survey_versions] ||= []
21
+ codes[s.access_code][:survey_versions] << s.survey_version
22
+ codes
23
+ end
16
24
  @title = "You can take these surveys"
17
25
  redirect_to surveyor_index unless surveyor_index == available_surveys_path
18
26
  end
19
27
 
20
28
  def create
21
- @survey = Survey.find_by_access_code(params[:survey_code])
22
- @response_set = ResponseSet.create(:survey => @survey, :user_id => (@current_user.nil? ? @current_user : @current_user.id))
29
+ surveys = Survey.where(:access_code => params[:survey_code]).order("survey_version DESC")
30
+ if params[:survey_version].blank?
31
+ @survey = surveys.first
32
+ else
33
+ @survey = surveys.where(:survey_version => params[:survey_version]).first
34
+ end
35
+ @response_set = ResponseSet.
36
+ create(:survey => @survey, :user_id => (@current_user.nil? ? @current_user : @current_user.id))
23
37
  if (@survey && @response_set)
24
38
  flash[:notice] = t('surveyor.survey_started_success')
25
- redirect_to(edit_my_survey_path(:survey_code => @survey.access_code, :response_set_code => @response_set.access_code))
39
+ redirect_to(edit_my_survey_path(
40
+ :survey_code => @survey.access_code, :response_set_code => @response_set.access_code))
26
41
  else
27
42
  flash[:notice] = t('surveyor.Unable_to_find_that_survey')
28
43
  redirect_to surveyor_index
29
44
  end
30
45
  end
31
46
 
32
-
33
47
  def show
34
- @response_set = ResponseSet.find_by_access_code(params[:response_set_code], :include => {:responses => [:question, :answer]})
48
+ # @response_set is set in before_filter - set_response_set_and_render_context
35
49
  if @response_set
36
50
  @survey = @response_set.survey
37
51
  respond_to do |format|
38
52
  format.html #{render :action => :show}
39
53
  format.csv {
40
- send_data(@response_set.to_csv, :type => 'text/csv; charset=utf-8; header=present',:filename => "#{@response_set.updated_at.strftime('%Y-%m-%d')}_#{@response_set.access_code}.csv")
54
+ send_data(@response_set.to_csv, :type => 'text/csv; charset=utf-8; header=present',
55
+ :filename => "#{@response_set.updated_at.strftime('%Y-%m-%d')}_#{@response_set.access_code}.csv")
41
56
  }
42
57
  format.json
43
58
  end
@@ -48,7 +63,7 @@ module Surveyor
48
63
  end
49
64
 
50
65
  def edit
51
- @response_set = ResponseSet.find_by_access_code(params[:response_set_code], :include => {:responses => [:question, :answer]})
66
+ # @response_set is set in before_filter - set_response_set_and_render_context
52
67
  if @response_set
53
68
  @survey = Survey.with_sections.find_by_id(@response_set.survey_id)
54
69
  @sections = @survey.sections
@@ -65,45 +80,76 @@ module Surveyor
65
80
  end
66
81
 
67
82
  def update
68
- saved = false
69
- ActiveRecord::Base.transaction do
70
- @response_set = ResponseSet.find_by_access_code(params[:response_set_code], :include => {:responses => :answer}, :lock => true)
71
- unless @response_set.blank?
72
- saved = @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable(params[:r]))
73
- @response_set.complete! if saved && params[:finish]
74
- saved &= @response_set.save
75
- end
76
- end
83
+ saved = load_and_update_response_set_with_retries
84
+
77
85
  return redirect_with_message(surveyor_finish, :notice, t('surveyor.completed_survey')) if saved && params[:finish]
78
86
 
79
87
  respond_to do |format|
80
88
  format.html do
81
- if @response_set.blank?
89
+ if @response_set.nil?
82
90
  return redirect_with_message(available_surveys_path, :notice, t('surveyor.unable_to_find_your_responses'))
83
91
  else
84
92
  flash[:notice] = t('surveyor.unable_to_update_survey') unless saved
85
- redirect_to edit_my_survey_path(:anchor => anchor_from(params[:section]), :section => section_id_from(params[:section]))
93
+ redirect_to edit_my_survey_path(
94
+ :anchor => anchor_from(params[:section]), :section => section_id_from(params[:section]))
86
95
  end
87
96
  end
88
97
  format.js do
89
- ids, remove, question_ids = {}, {}, []
90
- ResponseSet.trim_for_lookups(params[:r]).each do |k,v|
91
- v[:answer_id].reject!(&:blank?) if v[:answer_id].is_a?(Array)
92
- ids[k] = @response_set.responses.find(:first, :conditions => v, :order => "created_at DESC").id if !v.has_key?("id")
93
- remove[k] = v["id"] if v.has_key?("id") && v.has_key?("_destroy")
94
- question_ids << v["question_id"]
98
+ if @response_set
99
+ render :json => @response_set.reload.all_dependencies
100
+ else
101
+ render :text => "No response set #{params[:response_set_code]}",
102
+ :status => 404
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def load_and_update_response_set_with_retries(remaining=2)
109
+ begin
110
+ load_and_update_response_set
111
+ rescue ActiveRecord::StatementInvalid => e
112
+ if remaining > 0
113
+ load_and_update_response_set_with_retries(remaining - 1)
114
+ else
115
+ raise e
116
+ end
117
+ end
118
+ end
119
+
120
+ def load_and_update_response_set
121
+ ResponseSet.transaction do
122
+ @response_set = ResponseSet.
123
+ find_by_access_code(params[:response_set_code], :include => {:responses => :answer})
124
+ if @response_set
125
+ saved = true
126
+ if params[:r]
127
+ @response_set.update_from_ui_hash(params[:r])
95
128
  end
96
- render :json => {"ids" => ids, "remove" => remove}.merge(@response_set.reload.all_dependencies(question_ids))
129
+ if params[:finish]
130
+ @response_set.complete!
131
+ saved &= @response_set.save
132
+ end
133
+ saved
134
+ else
135
+ false
97
136
  end
98
137
  end
99
138
  end
100
-
139
+ private :load_and_update_response_set
140
+
101
141
  def export
102
- @survey = Survey.find_by_access_code(params[:survey_code])
142
+ surveys = Survey.where(:access_code => params[:survey_code]).order("survey_version DESC")
143
+ if params[:survey_version].blank?
144
+ @survey = surveys.first
145
+ else
146
+ @survey = surveys.where(:survey_version => params[:survey_version]).first
147
+ end
103
148
  end
149
+
104
150
  private
105
151
 
106
- # This is a hoock method for surveyor-using applications to override and provide the context object
152
+ # This is a hook method for surveyor-using applications to override and provide the context object
107
153
  def render_context
108
154
  nil
109
155
  end
@@ -113,11 +159,14 @@ module Surveyor
113
159
  @current_user = self.respond_to?(:current_user) ? self.current_user : nil
114
160
  end
115
161
 
116
- def set_render_context
162
+ def set_response_set_and_render_context
163
+ @response_set = ResponseSet.
164
+ find_by_access_code(params[:response_set_code], :include => {:responses => [:question, :answer]})
117
165
  @render_context = render_context
118
166
  end
119
167
 
120
- # Params: the name of some submit buttons store the section we'd like to go to. for repeater questions, an anchor to the repeater group is also stored
168
+ # Params: the name of some submit buttons store the section we'd like to go
169
+ # to. for repeater questions, an anchor to the repeater group is also stored
121
170
  # e.g. params[:section] = {"1"=>{"question_group_1"=>"<= add row"}}
122
171
  def section_id_from(p)
123
172
  p.respond_to?(:keys) ? p.keys.first : p
@@ -145,10 +194,10 @@ module Surveyor
145
194
  end
146
195
  end
147
196
  end
148
-
197
+
149
198
  ##
150
199
  # @dependents are necessary in case the client does not have javascript enabled
151
- # Whether or not javascript is enabled is determined by a hidden field set in the surveyor/edit.html form
200
+ # Whether or not javascript is enabled is determined by a hidden field set in the surveyor/edit.html form
152
201
  def set_dependents
153
202
  if session[:surveyor_javascript] && session[:surveyor_javascript] == "enabled"
154
203
  @dependents = []
@@ -156,11 +205,11 @@ module Surveyor
156
205
  @dependents = get_unanswered_dependencies_minus_section_questions
157
206
  end
158
207
  end
159
-
208
+
160
209
  def get_unanswered_dependencies_minus_section_questions
161
210
  @response_set.unanswered_dependencies - @section.questions || []
162
211
  end
163
-
212
+
164
213
  ##
165
214
  # If the hidden field surveyor_javascript_enabled is set to true
166
215
  # cf. surveyor/edit.html.haml
@@ -172,6 +221,6 @@ module Surveyor
172
221
  session[:surveyor_javascript] = "not_enabled"
173
222
  end
174
223
  end
175
-
224
+
176
225
  end
177
- end
226
+ end
@@ -1,3 +1,3 @@
1
1
  module Surveyor
2
- VERSION = '0.22.0'
3
- end
2
+ VERSION = '1.0.0'
3
+ end
data/lib/surveyor.rb CHANGED
@@ -1,14 +1,13 @@
1
1
  module Surveyor
2
2
  require 'surveyor/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
3
-
4
3
  autoload :VERSION, 'surveyor/version'
4
+ autoload :ParserError, 'surveyor/parser'
5
5
  end
6
6
  require 'surveyor/common'
7
7
  require 'surveyor/acts_as_response'
8
- require 'formtastic/surveyor_builder'
9
8
  # require 'surveyor/surveyor_controller_methods'
10
9
  # require 'surveyor/models/survey_methods'
11
- Formtastic::SemanticFormHelper.builder = Formtastic::SurveyorBuilder
12
- Formtastic::SemanticFormBuilder.default_text_area_height = 5
13
- Formtastic::SemanticFormBuilder.default_text_area_width = 50
14
- Formtastic::SemanticFormBuilder.all_fields_required_by_default = false
10
+ require 'formtastic'
11
+ Formtastic::FormBuilder.default_text_area_height = 5
12
+ Formtastic::FormBuilder.default_text_area_width = 50
13
+ Formtastic::FormBuilder.all_fields_required_by_default = false
@@ -16,7 +16,7 @@ namespace :surveyor do
16
16
  file = File.join(Rails.root, ENV["FILE"])
17
17
  raise "File does not exist: #{file}" unless FileTest.exists?(file)
18
18
  puts "--- Parsing #{file} ---"
19
- Surveyor::RedcapParser.parse File.read(file), File.basename(file, ".csv")
19
+ Surveyor::RedcapParser.parse File.read(file), File.basename(file, ".csv"), {:trace => Rake.application.options.trace}
20
20
  puts "--- Done #{file} ---"
21
21
  end
22
22
  desc "generate a surveyor DSL file from a survey"
@@ -37,7 +37,7 @@ namespace :surveyor do
37
37
  puts "not found"
38
38
  end
39
39
  else
40
- puts "There are no surveys available"
40
+ puts "There are no surveys available"
41
41
  end
42
42
  end
43
43
  desc "remove surveys (that don't have response sets)"
@@ -57,18 +57,30 @@ namespace :surveyor do
57
57
  put "not found"
58
58
  end
59
59
  else
60
- puts "There are no surveys without response sets"
60
+ puts "There are no surveys without response sets"
61
61
  end
62
62
  end
63
63
  desc "dump all responses to a given survey"
64
64
  task :dump => :environment do
65
65
  require 'fileutils.rb'
66
- raise "USAGE: rake surveyor:dump SURVEY_ACCESS_CODE=<access_code> [OUTPUT_DIR=<dir>]" unless ENV["SURVEY_ACCESS_CODE"]
67
- survey = Survey.find_by_access_code(ENV["SURVEY_ACCESS_CODE"])
68
- raise "No Survey found with code " + ENV["SURVEY_ACCESS_CODE"] unless survey
66
+ survey_version = ENV["SURVEY_VERSION"]
67
+ access_code = ENV["SURVEY_ACCESS_CODE"]
68
+
69
+ raise "USAGE: rake surveyor:dump SURVEY_ACCESS_CODE=<access_code> [OUTPUT_DIR=<dir>] [SURVEY_VERSION=<survey_version>]" unless access_code
70
+ params_string = "code #{access_code}"
71
+
72
+ surveys = Survey.where(:access_code => access_code).order("survey_version ASC")
73
+ if survey_version.blank?
74
+ survey = surveys.last
75
+ else
76
+ params_string += " and survey_version #{survey_version}"
77
+ survey = surveys.where(:survey_version => survey_version).first
78
+ end
79
+
80
+ raise "No Survey found with #{params_string}" unless survey
69
81
  dir = ENV["OUTPUT_DIR"] || Rails.root
70
82
  mkpath(dir) # Create all non-existent directories
71
- full_path = File.join(dir,"#{survey.access_code}_#{Time.now.to_i}.csv")
83
+ full_path = File.join(dir,"#{survey.access_code}_v#{survey.survey_version}_#{Time.now.to_i}.csv")
72
84
  File.open(full_path, 'w') do |f|
73
85
  survey.response_sets.each_with_index{|r,i| f.write(r.to_csv(true, i == 0)) } # print access code every time, print_header first time
74
86
  end