affectiva-surveyor 1.5.0.pre.disco.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (212) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +28 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG.md +198 -0
  5. data/Gemfile +5 -0
  6. data/Gemfile.rails_version +27 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +166 -0
  9. data/Rakefile +106 -0
  10. data/app/controllers/surveyor_controller.rb +5 -0
  11. data/app/helpers/results_helper.rb +20 -0
  12. data/app/helpers/survey_form_builder.rb +37 -0
  13. data/app/helpers/surveyor_helper.rb +3 -0
  14. data/app/inputs/quiet_input.rb +5 -0
  15. data/app/inputs/surveyor_check_boxes_input.rb +35 -0
  16. data/app/inputs/surveyor_radio_input.rb +18 -0
  17. data/app/models/answer.rb +3 -0
  18. data/app/models/dependency.rb +3 -0
  19. data/app/models/dependency_condition.rb +3 -0
  20. data/app/models/question.rb +3 -0
  21. data/app/models/question_group.rb +4 -0
  22. data/app/models/response.rb +4 -0
  23. data/app/models/response_set.rb +3 -0
  24. data/app/models/survey.rb +3 -0
  25. data/app/models/survey_section.rb +4 -0
  26. data/app/models/survey_section_sweeper.rb +15 -0
  27. data/app/models/survey_translation.rb +4 -0
  28. data/app/models/validation.rb +3 -0
  29. data/app/models/validation_condition.rb +3 -0
  30. data/app/views/layouts/results.html.erb +13 -0
  31. data/app/views/layouts/surveyor_default.html.erb +12 -0
  32. data/app/views/partials/_answer.html.haml +25 -0
  33. data/app/views/partials/_dependents.html.haml +5 -0
  34. data/app/views/partials/_question.html.haml +28 -0
  35. data/app/views/partials/_question_group.html.haml +44 -0
  36. data/app/views/partials/_section.html.haml +12 -0
  37. data/app/views/partials/_section_menu.html.haml +12 -0
  38. data/app/views/surveyor/edit.html.haml +24 -0
  39. data/app/views/surveyor/export.json.rabl +85 -0
  40. data/app/views/surveyor/new.html.haml +24 -0
  41. data/app/views/surveyor/show.html.haml +74 -0
  42. data/app/views/surveyor/show.json.rabl +15 -0
  43. data/ci-exec.sh +56 -0
  44. data/config/routes.rb +8 -0
  45. data/cucumber.yml +10 -0
  46. data/doc/REPRESENTATIONS.md +34 -0
  47. data/doc/api_id_schema.json +7 -0
  48. data/doc/question types.png +0 -0
  49. data/doc/response_set_schema.json +54 -0
  50. data/doc/surveyor question combinations.png +0 -0
  51. data/doc/surveyor reject or delete decision matrix.png +0 -0
  52. data/doc/surveyor_models.png +0 -0
  53. data/doc/surveyor_models2.png +0 -0
  54. data/doc/surveyor_timestamp_schema.json +9 -0
  55. data/features/ajax_submissions.feature +140 -0
  56. data/features/export_to_json.feature +344 -0
  57. data/features/internationalization.feature +121 -0
  58. data/features/no_duplicates.feature +110 -0
  59. data/features/show_survey.feature +71 -0
  60. data/features/step_definitions/parser_steps.rb +145 -0
  61. data/features/step_definitions/surveyor_steps.rb +325 -0
  62. data/features/step_definitions/ui_steps.rb +25 -0
  63. data/features/step_definitions/web_steps.rb +225 -0
  64. data/features/support/REDCapDemoDatabase_DataDictionary.csv +127 -0
  65. data/features/support/database_cleaner.rb +16 -0
  66. data/features/support/env.rb +56 -0
  67. data/features/support/hooks.rb +4 -0
  68. data/features/support/paths.rb +39 -0
  69. data/features/support/redcap_new_headers.csv +1 -0
  70. data/features/support/redcap_siblings.csv +1 -0
  71. data/features/support/redcap_whitespace.csv +1 -0
  72. data/features/support/selectors.rb +39 -0
  73. data/features/support/simultaneous_ajax.rb +101 -0
  74. data/features/support/single_quit_selenium_driver.rb +23 -0
  75. data/features/support/slow_updates.rb +18 -0
  76. data/features/surveyor.feature +895 -0
  77. data/features/surveyor_dependencies.feature +476 -0
  78. data/features/surveyor_parser.feature +504 -0
  79. data/features/z_redcap_parser.feature +62 -0
  80. data/lib/assets/images/surveyor/next.gif +0 -0
  81. data/lib/assets/images/surveyor/prev.gif +0 -0
  82. data/lib/assets/images/surveyor/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  83. data/lib/assets/images/surveyor/ui-bg_flat_75_ffffff_40x100.png +0 -0
  84. data/lib/assets/images/surveyor/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  85. data/lib/assets/images/surveyor/ui-bg_glass_65_ffffff_1x400.png +0 -0
  86. data/lib/assets/images/surveyor/ui-bg_glass_75_dadada_1x400.png +0 -0
  87. data/lib/assets/images/surveyor/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  88. data/lib/assets/images/surveyor/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  89. data/lib/assets/images/surveyor/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  90. data/lib/assets/images/surveyor/ui-icons_222222_256x240.png +0 -0
  91. data/lib/assets/images/surveyor/ui-icons_2e83ff_256x240.png +0 -0
  92. data/lib/assets/images/surveyor/ui-icons_454545_256x240.png +0 -0
  93. data/lib/assets/images/surveyor/ui-icons_888888_256x240.png +0 -0
  94. data/lib/assets/images/surveyor/ui-icons_cd0a0a_256x240.png +0 -0
  95. data/lib/assets/javascripts/surveyor/jquery-1.9.0.js +9555 -0
  96. data/lib/assets/javascripts/surveyor/jquery-ui-1.10.0.custom.js +14850 -0
  97. data/lib/assets/javascripts/surveyor/jquery-ui-timepicker-addon.js +1919 -0
  98. data/lib/assets/javascripts/surveyor/jquery.maskedinput.js +338 -0
  99. data/lib/assets/javascripts/surveyor/jquery.selectToUISlider.js +240 -0
  100. data/lib/assets/javascripts/surveyor/jquery.surveyor.js +154 -0
  101. data/lib/assets/stylesheets/surveyor.sass +132 -0
  102. data/lib/assets/stylesheets/surveyor/jquery-ui-1.10.0.custom.css +1174 -0
  103. data/lib/assets/stylesheets/surveyor/jquery-ui-timepicker-addon.css +11 -0
  104. data/lib/assets/stylesheets/surveyor/reset.css +48 -0
  105. data/lib/assets/stylesheets/surveyor/results.css +125 -0
  106. data/lib/assets/stylesheets/surveyor/ui.slider.extras.css +110 -0
  107. data/lib/generators/surveyor/custom_generator.rb +18 -0
  108. data/lib/generators/surveyor/install_generator.rb +101 -0
  109. data/lib/generators/surveyor/templates/app/assets/javascripts/surveyor_all.js +6 -0
  110. data/lib/generators/surveyor/templates/app/assets/stylesheets/surveyor_all.css +9 -0
  111. data/lib/generators/surveyor/templates/app/controllers/surveyor_controller.rb +40 -0
  112. data/lib/generators/surveyor/templates/app/views/layouts/surveyor_custom.html.erb +13 -0
  113. data/lib/generators/surveyor/templates/config/locales/surveyor_en.yml +19 -0
  114. data/lib/generators/surveyor/templates/config/locales/surveyor_es.yml +19 -0
  115. data/lib/generators/surveyor/templates/config/locales/surveyor_he.yml +19 -0
  116. data/lib/generators/surveyor/templates/config/locales/surveyor_ko.yml +19 -0
  117. data/lib/generators/surveyor/templates/db/migrate/add_api_id_to_question_groups.rb +10 -0
  118. data/lib/generators/surveyor/templates/db/migrate/add_api_ids.rb +14 -0
  119. data/lib/generators/surveyor/templates/db/migrate/add_api_ids_to_response_sets_and_responses.rb +12 -0
  120. data/lib/generators/surveyor/templates/db/migrate/add_correct_answer_id_to_questions.rb +10 -0
  121. data/lib/generators/surveyor/templates/db/migrate/add_default_value_to_answers.rb +10 -0
  122. data/lib/generators/surveyor/templates/db/migrate/add_display_order_to_surveys.rb +10 -0
  123. data/lib/generators/surveyor/templates/db/migrate/add_display_type_to_answers.rb +14 -0
  124. data/lib/generators/surveyor/templates/db/migrate/add_index_to_response_sets.rb +10 -0
  125. data/lib/generators/surveyor/templates/db/migrate/add_index_to_surveys.rb +10 -0
  126. data/lib/generators/surveyor/templates/db/migrate/add_input_mask_attributes_to_answer.rb +12 -0
  127. data/lib/generators/surveyor/templates/db/migrate/add_section_id_to_responses.rb +12 -0
  128. data/lib/generators/surveyor/templates/db/migrate/add_unique_index_on_access_code_and_version_in_surveys.rb +10 -0
  129. data/lib/generators/surveyor/templates/db/migrate/add_unique_indicies.rb +18 -0
  130. data/lib/generators/surveyor/templates/db/migrate/add_version_to_surveys.rb +10 -0
  131. data/lib/generators/surveyor/templates/db/migrate/api_ids_must_be_unique.rb +23 -0
  132. data/lib/generators/surveyor/templates/db/migrate/create_answers.rb +38 -0
  133. data/lib/generators/surveyor/templates/db/migrate/create_dependencies.rb +23 -0
  134. data/lib/generators/surveyor/templates/db/migrate/create_dependency_conditions.rb +30 -0
  135. data/lib/generators/surveyor/templates/db/migrate/create_question_groups.rb +28 -0
  136. data/lib/generators/surveyor/templates/db/migrate/create_questions.rb +37 -0
  137. data/lib/generators/surveyor/templates/db/migrate/create_response_sets.rb +23 -0
  138. data/lib/generators/surveyor/templates/db/migrate/create_responses.rb +34 -0
  139. data/lib/generators/surveyor/templates/db/migrate/create_survey_sections.rb +30 -0
  140. data/lib/generators/surveyor/templates/db/migrate/create_survey_translations.rb +19 -0
  141. data/lib/generators/surveyor/templates/db/migrate/create_surveys.rb +32 -0
  142. data/lib/generators/surveyor/templates/db/migrate/create_validation_conditions.rb +33 -0
  143. data/lib/generators/surveyor/templates/db/migrate/create_validations.rb +21 -0
  144. data/lib/generators/surveyor/templates/db/migrate/drop_unique_index_on_access_code_in_surveys.rb +10 -0
  145. data/lib/generators/surveyor/templates/db/migrate/update_blank_api_ids_on_question_group.rb +22 -0
  146. data/lib/generators/surveyor/templates/db/migrate/update_blank_versions_on_surveys.rb +13 -0
  147. data/lib/generators/surveyor/templates/surveys/EXTENDING_SURVEYOR.md +52 -0
  148. data/lib/generators/surveyor/templates/surveys/MODIFYING_SURVEYOR.md +91 -0
  149. data/lib/generators/surveyor/templates/surveys/date_survey.rb +16 -0
  150. data/lib/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +280 -0
  151. data/lib/generators/surveyor/templates/surveys/languages.rb +14 -0
  152. data/lib/generators/surveyor/templates/surveys/quiz.rb +11 -0
  153. data/lib/generators/surveyor/templates/surveys/translations/languages.es.yml +18 -0
  154. data/lib/generators/surveyor/templates/surveys/translations/languages.he.yml +18 -0
  155. data/lib/generators/surveyor/templates/surveys/translations/languages.ko.yml +18 -0
  156. data/lib/generators/surveyor/templates/vendor/assets/stylesheets/custom.sass +5 -0
  157. data/lib/surveyor.rb +13 -0
  158. data/lib/surveyor/acts_as_response.rb +17 -0
  159. data/lib/surveyor/common.rb +59 -0
  160. data/lib/surveyor/engine.rb +10 -0
  161. data/lib/surveyor/helpers/asset_pipeline.rb +18 -0
  162. data/lib/surveyor/helpers/formtastic_custom_input.rb +13 -0
  163. data/lib/surveyor/helpers/surveyor_helper_methods.rb +103 -0
  164. data/lib/surveyor/models/answer_methods.rb +83 -0
  165. data/lib/surveyor/models/dependency_condition_methods.rb +69 -0
  166. data/lib/surveyor/models/dependency_methods.rb +57 -0
  167. data/lib/surveyor/models/question_group_methods.rb +58 -0
  168. data/lib/surveyor/models/question_methods.rb +112 -0
  169. data/lib/surveyor/models/response_methods.rb +126 -0
  170. data/lib/surveyor/models/response_set_methods.rb +188 -0
  171. data/lib/surveyor/models/survey_methods.rb +100 -0
  172. data/lib/surveyor/models/survey_section_methods.rb +53 -0
  173. data/lib/surveyor/models/survey_translation_methods.rb +30 -0
  174. data/lib/surveyor/models/validation_condition_methods.rb +54 -0
  175. data/lib/surveyor/models/validation_methods.rb +45 -0
  176. data/lib/surveyor/mustache_context.rb +11 -0
  177. data/lib/surveyor/parser.rb +427 -0
  178. data/lib/surveyor/redcap_parser.rb +288 -0
  179. data/lib/surveyor/surveyor_controller_methods.rb +236 -0
  180. data/lib/surveyor/unparser.rb +147 -0
  181. data/lib/surveyor/version.rb +3 -0
  182. data/lib/tasks/surveyor_tasks.rake +88 -0
  183. data/rails/init.rb +1 -0
  184. data/spec/controllers/surveyor_controller_spec.rb +311 -0
  185. data/spec/factories.rb +166 -0
  186. data/spec/helpers/formtastic_custom_input_spec.rb +15 -0
  187. data/spec/helpers/surveyor_helper_spec.rb +117 -0
  188. data/spec/lib/benchmark_spec.rb +22 -0
  189. data/spec/lib/chinese_survey.rb +14 -0
  190. data/spec/lib/common_spec.rb +34 -0
  191. data/spec/lib/parser_spec.rb +204 -0
  192. data/spec/lib/rake_kitchen_sink.rb +40 -0
  193. data/spec/lib/redcap_parser_spec.rb +75 -0
  194. data/spec/lib/tasks_spec.rake +26 -0
  195. data/spec/lib/unparser_spec.rb +126 -0
  196. data/spec/models/answer_spec.rb +174 -0
  197. data/spec/models/dependency_condition_spec.rb +439 -0
  198. data/spec/models/dependency_spec.rb +101 -0
  199. data/spec/models/question_group_spec.rb +93 -0
  200. data/spec/models/question_spec.rb +206 -0
  201. data/spec/models/response_set_spec.rb +477 -0
  202. data/spec/models/response_spec.rb +218 -0
  203. data/spec/models/survey_section_spec.rb +85 -0
  204. data/spec/models/survey_spec.rb +191 -0
  205. data/spec/models/validation_condition_spec.rb +113 -0
  206. data/spec/models/validation_spec.rb +74 -0
  207. data/spec/rcov.opts +2 -0
  208. data/spec/spec.opts +4 -0
  209. data/spec/spec_helper.rb +56 -0
  210. data/stacktests.sh +65 -0
  211. data/surveyor.gemspec +53 -0
  212. metadata +601 -0
@@ -0,0 +1,288 @@
1
+ %w(survey survey_section question_group question dependency dependency_condition answer validation validation_condition).each {|model| require model }
2
+ require 'active_support' # for humanize
3
+ module Surveyor
4
+ class RedcapParserError < StandardError; end
5
+ class RedcapParser
6
+ class << self; attr_accessor :options end
7
+
8
+ # Attributes
9
+ attr_accessor :context
10
+
11
+ # Class methods
12
+ def self.parse(str, filename, options={})
13
+ self.options = options
14
+ Surveyor::RedcapParser.rake_trace "\n"
15
+ Surveyor::RedcapParser.new.parse(str, filename)
16
+ Surveyor::RedcapParser.rake_trace "\n"
17
+ end
18
+ def self.rake_trace(str)
19
+ self.options ||= {}
20
+ print str if self.options[:trace] == true
21
+ end
22
+
23
+ # Instance methods
24
+ def initialize
25
+ self.context = {}
26
+ self.context[:dependency_conditions] = []
27
+ end
28
+ def parse(str, filename)
29
+ csvlib = Surveyor::Common.csv_impl
30
+ begin
31
+ csvlib.parse(str, :headers => :first_row, :return_headers => true, :header_converters => :symbol) do |r|
32
+ if r.header_row? # header row
33
+ return Surveyor::RedcapParser.rake_trace "Missing headers: #{missing_columns(r.headers).inspect}\n\n" unless missing_columns(r.headers).blank?
34
+ context[:survey] = Survey.new(:title => filename)
35
+ Surveyor::RedcapParser.rake_trace "survey_#{context[:survey].access_code} "
36
+ else # non-header rows
37
+ SurveySection.new.extend(SurveyorRedcapParserSurveySectionMethods).build_or_set(context, r)
38
+ Question.new.extend(SurveyorRedcapParserQuestionMethods).build_and_set(context, r)
39
+ Answer.new.extend(SurveyorRedcapParserAnswerMethods).build_and_set(context, r)
40
+ Validation.new.extend(SurveyorRedcapParserValidationMethods).build_and_set(context, r)
41
+ Dependency.new.extend(SurveyorRedcapParserDependencyMethods).build_and_set(context, r)
42
+ end
43
+ end
44
+ resolve_references
45
+ Surveyor::RedcapParser.rake_trace context[:survey].save ? "saved. " : " not saved! #{context[:survey].errors.full_messages.join(", ")} "
46
+ # Surveyor::RedcapParser.rake_trace context[:survey].sections.map(&:questions).flatten.map(&:answers).flatten.map{|x| x.errors.each_full{|y| y}.join}.join
47
+ rescue csvlib::MalformedCSVError
48
+ raise Surveyor::RedcapParserError, "Oops. Not a valid CSV file."
49
+ # ensure
50
+ end
51
+ return context[:survey]
52
+ end
53
+ def missing_columns(r)
54
+ missing = []
55
+ missing << "choices_or_calculations" unless r.map(&:to_s).include?("choices_or_calculations") or r.map(&:to_s).include?("choices_calculations_or_slider_labels")
56
+ 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")
57
+ missing += (static_required_columns - r.map(&:to_s))
58
+ end
59
+ def static_required_columns
60
+ # no longer requiring field_units
61
+ %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)
62
+ end
63
+ def resolve_references
64
+ context[:dependency_conditions].each do |dc|
65
+ Surveyor::RedcapParser.rake_trace "resolve(#{dc.question_reference},#{dc.answer_reference})"
66
+ if dc.answer_reference.blank? and (context[:question_references][dc.question_reference].answers.size == 1)
67
+ Surveyor::RedcapParser.rake_trace "...found "
68
+ dc.question = context[:question_references][dc.question_reference]
69
+ dc.answer = dc.question.answers.first
70
+ elsif answer = context[:answer_references][dc.question_reference][dc.answer_reference]
71
+ Surveyor::RedcapParser.rake_trace "...found "
72
+ dc.answer = answer
73
+ dc.question = context[:question_references][dc.question_reference]
74
+ else
75
+ Surveyor::RedcapParser.rake_trace "\n!!! failed lookup for dependency_condition q: #{question_reference} a: #{question_reference}"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Surveyor models with extra parsing methods
83
+
84
+ # SurveySection model
85
+ module SurveyorRedcapParserSurveySectionMethods
86
+ def build_or_set(context, r)
87
+ unless context[:survey_section] && context[:survey_section].reference_identifier == r[:form_name]
88
+ if match = context[:survey].sections.detect{|ss| ss.reference_identifier == r[:form_name]}
89
+ context[:current_survey_section] = match
90
+ else
91
+ self.attributes = (
92
+ {:title => r[:form_name].to_s.humanize,
93
+ :reference_identifier => r[:form_name],
94
+ :display_order => context[:survey].sections.size })
95
+ context[:survey].sections << context[:survey_section] = self
96
+ Surveyor::RedcapParser.rake_trace "survey_section_#{context[:survey_section].reference_identifier} "
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ # Question model
103
+ module SurveyorRedcapParserQuestionMethods
104
+ def build_and_set(context, r)
105
+ if !r[:section_header].blank?
106
+ context[:survey_section].questions.build({:display_type => "label", :text => r[:section_header], :display_order => context[:survey_section].questions.size})
107
+ Surveyor::RedcapParser.rake_trace "label_ "
108
+ end
109
+ self.attributes = ({
110
+ :reference_identifier => r[:variable__field_name],
111
+ :text => r[:field_label],
112
+ :help_text => r[:field_note],
113
+ :is_mandatory => (/^y/i.match r[:required_field]) ? true : false,
114
+ :pick => pick_from_field_type(r[:field_type]),
115
+ :display_type => display_type_from_field_type(r[:field_type]),
116
+ :display_order => context[:survey_section].questions.size
117
+ })
118
+ context[:survey_section].questions << context[:question] = self
119
+ unless context[:question].reference_identifier.blank?
120
+ context[:question_references] ||= {}
121
+ context[:question_references][context[:question].reference_identifier] = context[:question]
122
+ end
123
+ Surveyor::RedcapParser.rake_trace "question_#{context[:question].reference_identifier} "
124
+ end
125
+ def pick_from_field_type(ft)
126
+ {"checkbox" => :any, "radio" => :one}[ft] || :none
127
+ end
128
+ def display_type_from_field_type(ft)
129
+ {"text" => :string, "dropdown" => :dropdown, "notes" => :text}[ft]
130
+ end
131
+ end
132
+
133
+ # Dependency model
134
+ module SurveyorRedcapParserDependencyMethods
135
+ def build_and_set(context, r)
136
+ unless (bl = r[:branching_logic_show_field_only_if]).blank?
137
+ # TODO: forgot to tie rule key to component, counting on the sequence of components
138
+ letters = ('A'..'Z').to_a
139
+ hash = decompose_rule(bl)
140
+ self.attributes = {:rule => hash[:rule]}
141
+ context[:question].dependency = context[:dependency] = self
142
+ hash[:components].each do |component|
143
+ dc = context[:dependency].dependency_conditions.build(decompose_component(component).merge({ :rule_key => letters.shift } ))
144
+ context[:dependency_conditions] << dc
145
+ end
146
+ Surveyor::RedcapParser.rake_trace "dependency(#{hash[:rule]}) "
147
+ end
148
+ end
149
+ def decompose_component(str)
150
+ # [initial_52] = "1" or [f1_q15] = '' or [f1_q15] = '-2' or [hi_event1_type] <> ''
151
+ if match = str.match(/^\[(\w+)\] ?([!=><]+) ?['"](-?\w*)['"]$/)
152
+ {:question_reference => match[1], :operator => match[2].gsub(/^=$/, "==").gsub(/^<>$/, "!="), :answer_reference => match[3]}
153
+ # [initial_119(2)] = "1" or [hiprep_heat2(97)] = '1'
154
+ elsif match = str.match(/^\[(\w+)\((\w+)\)\] ?([!=><]+) ?['"]1['"]$/)
155
+ {:question_reference => match[1], :operator => match[3].gsub(/^=$/, "==").gsub(/^<>$/, "!="), :answer_reference => match[2]}
156
+ # [f1_q15] >= 21 or [f1_q15] >= -21
157
+ elsif match = str.match(/^\[(\w+)\] ?([!=><]+) ?(-?\d+)$/)
158
+ {:question_reference => match[1], :operator => match[2].gsub(/^=$/, "==").gsub(/^<>$/, "!="), :integer_value => match[3]}
159
+ else
160
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping dependency_condition #{str}"
161
+ end
162
+ end
163
+ def decompose_rule(str)
164
+ # see spec/lib/redcap_parser_spec.rb for examples
165
+ letters = ('A'..'Z').to_a
166
+ rule = str
167
+ components = str.split(/\band\b|\bor\b|\((?!\d)|\)(?!\(|\])/).reject(&:blank?).map(&:strip)
168
+ components.each_with_index do |part, i|
169
+ # internal commas on the right side of the operator e.g. '[initial_189] = "1, 2, 3"'
170
+ if match = part.match(/^(\[[^\]]+\][^\"]+)"([0-9 ]+,[0-9 ,]+)"$/)
171
+ nums = match[2].split(",").map(&:strip)
172
+ components[i] = nums.map{|x| "#{match[1]}\"#{x}\""}
173
+ # sub in rule key
174
+ rule = rule.gsub(part, "(#{nums.map{letters.shift}.join(' and ')})")
175
+ # multiple internal parenthesis on the left e.g. '[initial_119(1)(2)(3)(4)(6)] = "1"'
176
+ elsif match = part.match(/^\[(\w+)(\(\d+\)\([\d\(\)]+)\]([^\"]+"\d+")$/)
177
+ nums = match[2].split(/\(|\)/).reject(&:blank?).map(&:strip)
178
+ components[i] = nums.map{|x| "[#{match[1]}(#{x})]#{match[3]}"}
179
+ # sub in rule key
180
+ rule = rule.gsub(part, "(#{nums.map{letters.shift}.join(' and ')})")
181
+ else
182
+ # 'or' on the right of the operator
183
+ components[i] = components[i-1].gsub(/"(\d+)"/, part) if part.match(/^"(\d+)"$/) && i != 0
184
+ # sub in rule key
185
+ rule = rule.gsub(part){letters.shift}
186
+ end
187
+ end
188
+ {:rule => rule, :components => components.flatten}
189
+ end
190
+ end
191
+
192
+ # DependencyCondition model
193
+ module SurveyorRedcapParserDependencyConditionMethods
194
+ DependencyCondition.instance_eval do
195
+ attr_accessor :question_reference, :answer_reference
196
+ end
197
+ end
198
+
199
+ # Answer model
200
+ module SurveyorRedcapParserAnswerMethods
201
+ def build_and_set(context, r)
202
+ case r[:field_type]
203
+ when "text"
204
+ self.attributes = {
205
+ :response_class => "string",
206
+ :text => "Text",
207
+ :display_order => context[:question].answers.size }
208
+ context[:question].answers << context[:answer] = self
209
+ when "notes"
210
+ self.attributes = {
211
+ :response_class => "text",
212
+ :text => "Notes",
213
+ :display_order => context[:question].answers.size }
214
+ context[:question].answers << context[:answer] = self
215
+ when "file"
216
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping answer: file"
217
+ end
218
+ (r[:choices_or_calculations] || r[:choices_calculations_or_slider_labels]).to_s.split("|").each do |pair|
219
+ aref, atext = pair.split(",").map(&:strip)
220
+ if aref.blank? or atext.blank? or (aref.to_i.to_s != aref)
221
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping answer #{pair}"
222
+ else
223
+ a = Answer.new({
224
+ :reference_identifier => aref,
225
+ :text => atext,
226
+ :display_order => context[:question].answers.size })
227
+ context[:question].answers << context[:answer] = a
228
+ unless context[:question].reference_identifier.blank? or aref.blank? or !context[:answer].valid?
229
+ context[:answer_references] ||= {}
230
+ context[:answer_references][context[:question].reference_identifier] ||= {}
231
+ context[:answer_references][context[:question].reference_identifier][aref] = context[:answer]
232
+ end
233
+ Surveyor::RedcapParser.rake_trace "#{context[:answer].errors.full_messages}, #{context[:answer].inspect}" unless context[:answer].valid?
234
+ Surveyor::RedcapParser.rake_trace "answer_#{context[:answer].reference_identifier} "
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ # Validation model
241
+ module SurveyorRedcapParserValidationMethods
242
+ def build_and_set(context, r)
243
+ # text_validation_type text_validation_min text_validation_max
244
+ min = r[:text_validation_min].to_s.blank? ? nil : r[:text_validation_min].to_s
245
+ max = r[:text_validation_max].to_s.blank? ? nil : r[:text_validation_max].to_s
246
+ type = r[:text_validation_type].to_s.blank? ? nil : r[:text_validation_type].to_s
247
+ if min or max
248
+ context[:question].answers.each do |a|
249
+ self.rule = (min ? max ? "A and B" : "A" : "B")
250
+ a.validations << context[:validation] = self
251
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => ">=", :integer_value => min) if min
252
+ context[:validation].validation_conditions.build(:rule_key => "B", :operator => "<=", :integer_value => max) if max
253
+ end
254
+ elsif type
255
+ # date email integer number phone
256
+ case r[:text_validation_type]
257
+ when "date"
258
+ context[:question].display_type = :date if context[:question].display_type == :string
259
+ when "email"
260
+ context[:question].answers.each do |a|
261
+ self.rule = "A"
262
+ a.validations << context[:validation] = self
263
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$")
264
+ end
265
+ when "integer"
266
+ context[:question].display_type = :integer if context[:question].display_type == :string
267
+ context[:question].answers.each do |a|
268
+ self.rule = "A"
269
+ a.validations << context[:validation] = self
270
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "\d+")
271
+ end
272
+ when "number"
273
+ context[:question].display_type = :float if context[:question].display_type == :string
274
+ context[:question].answers.each do |a|
275
+ self.rule = "A"
276
+ a.validations << context[:validation] = self
277
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "^\d*(,\d{3})*(\.\d*)?$")
278
+ end
279
+ when "phone"
280
+ context[:question].answers.each do |a|
281
+ self.rule = "A"
282
+ a.validations << context[:validation] = self
283
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "\d{3}.*\d{4}")
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,236 @@
1
+ require 'rabl'
2
+ Rabl.register!
3
+ Rabl.configure {|config| config.include_child_root = false }
4
+ Rabl.configure {|config| config.include_json_root = false }
5
+ module Surveyor
6
+ module SurveyorControllerMethods
7
+ def self.included(base)
8
+ base.send :before_filter, :get_current_user, :only => [:new, :create]
9
+ base.send :before_filter, :determine_if_javascript_is_enabled, :only => [:create, :update]
10
+ base.send :before_filter, :set_response_set_and_render_context, :only => [:edit, :show]
11
+ base.send :layout, 'surveyor_default'
12
+ base.send :before_filter, :set_locale
13
+ end
14
+
15
+ # Actions
16
+ def new
17
+ @surveys_by_access_code = Survey.order("created_at DESC, survey_version DESC").group_by(&:access_code)
18
+ redirect_to surveyor_index unless surveyor_index == surveyor.available_surveys_path
19
+ end
20
+
21
+ def create
22
+ surveys = Survey.where(:access_code => params[:survey_code]).order("survey_version DESC")
23
+ if params[:survey_version].blank?
24
+ @survey = surveys.first
25
+ else
26
+ @survey = surveys.where(:survey_version => params[:survey_version]).first
27
+ end
28
+ @response_set = ResponseSet.
29
+ create(:survey => @survey, :user_id => (@current_user.nil? ? @current_user : @current_user.id))
30
+ if (@survey && @response_set)
31
+ flash[:notice] = t('surveyor.survey_started_success')
32
+ redirect_to(surveyor.edit_my_survey_path(
33
+ :survey_code => @survey.access_code, :response_set_code => @response_set.access_code))
34
+ else
35
+ flash[:notice] = t('surveyor.Unable_to_find_that_survey')
36
+ redirect_to surveyor_index
37
+ end
38
+ end
39
+
40
+ def show
41
+ # @response_set is set in before_filter - set_response_set_and_render_context
42
+ if @response_set
43
+ @survey = @response_set.survey
44
+ respond_to do |format|
45
+ format.html #{render :action => :show}
46
+ format.csv {
47
+ send_data(@response_set.to_csv, :type => 'text/csv; charset=utf-8; header=present',
48
+ :filename => "#{@response_set.updated_at.strftime('%Y-%m-%d')}_#{@response_set.access_code}.csv")
49
+ }
50
+ format.json
51
+ end
52
+ else
53
+ flash[:notice] = t('surveyor.unable_to_find_your_responses')
54
+ redirect_to surveyor_index
55
+ end
56
+ end
57
+
58
+ def edit
59
+ # @response_set is set in before_filter - set_response_set_and_render_context
60
+ if @response_set
61
+ @survey = Survey.with_sections.find_by_id(@response_set.survey_id)
62
+ @sections = @survey.sections
63
+ @section = section_id_from(params) ? @sections.with_includes.find(section_id_from(params)) : @sections.with_includes.first
64
+ set_dependents
65
+ else
66
+ flash[:notice] = t('surveyor.unable_to_find_your_responses')
67
+ redirect_to surveyor_index
68
+ end
69
+ end
70
+
71
+ def update
72
+ question_ids_for_dependencies = (params[:r] || []).map{|k,v| v["question_id"] }.compact.uniq
73
+ saved = load_and_update_response_set_with_retries
74
+
75
+ return redirect_with_message(surveyor_finish, :notice, t('surveyor.completed_survey')) if saved && params[:finish]
76
+
77
+ respond_to do |format|
78
+ format.html do
79
+ if @response_set.nil?
80
+ return redirect_with_message(surveyor.available_surveys_path, :notice, t('surveyor.unable_to_find_your_responses'))
81
+ else
82
+ flash[:notice] = t('surveyor.unable_to_update_survey') unless saved
83
+ redirect_to surveyor.edit_my_survey_path(:anchor => anchor_from(params[:section]), :section => section_id_from(params))
84
+ end
85
+ end
86
+ format.js do
87
+ if @response_set
88
+ render :json => @response_set.reload.all_dependencies(question_ids_for_dependencies)
89
+ else
90
+ render :text => "No response set #{params[:response_set_code]}",
91
+ :status => 404
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def load_and_update_response_set_with_retries(remaining=2)
98
+ begin
99
+ load_and_update_response_set
100
+ rescue ActiveRecord::StatementInvalid => e
101
+ if remaining > 0
102
+ load_and_update_response_set_with_retries(remaining - 1)
103
+ else
104
+ raise e
105
+ end
106
+ end
107
+ end
108
+
109
+ def load_and_update_response_set
110
+ ResponseSet.transaction do
111
+ @response_set = ResponseSet.includes(:responses => :answer).find_by(:access_code => params[:response_set_code])
112
+ if @response_set
113
+ saved = true
114
+ if params[:r]
115
+ @response_set.update_from_ui_hash(params[:r])
116
+ end
117
+ if params[:finish]
118
+ @response_set.complete!
119
+ saved &= @response_set.save
120
+ end
121
+ saved
122
+ else
123
+ false
124
+ end
125
+ end
126
+ end
127
+ private :load_and_update_response_set
128
+
129
+ def export
130
+ surveys = Survey.where(:access_code => params[:survey_code]).order("survey_version DESC")
131
+ s = params[:survey_version].blank? ? surveys.first : surveys.where(:survey_version => params[:survey_version]).first
132
+ render_404 and return if s.blank?
133
+ @survey = s.filtered_for_json
134
+ end
135
+
136
+ def render_404
137
+ head :status => 404
138
+ true
139
+ end
140
+
141
+ def url_options
142
+ ((I18n.locale == I18n.default_locale) ? {} : {:locale => I18n.locale}).merge(super)
143
+ end
144
+
145
+ private
146
+
147
+ # This is a hook method for surveyor-using applications to override and provide the context object
148
+ def render_context
149
+ nil
150
+ end
151
+
152
+ # Filters
153
+ def get_current_user
154
+ @current_user = self.respond_to?(:current_user) ? self.current_user : nil
155
+ end
156
+
157
+ def set_response_set_and_render_context
158
+ @response_set = ResponseSet.includes(:responses => [:question, :answer])
159
+ .find_by(:access_code => params[:response_set_code])
160
+ @render_context = render_context
161
+ end
162
+
163
+ def set_locale
164
+ if params[:new_locale]
165
+ I18n.locale = params[:new_locale]
166
+ elsif params[:locale]
167
+ I18n.locale = params[:locale]
168
+ else
169
+ I18n.locale = I18n.default_locale
170
+ end
171
+ end
172
+
173
+ # Params: the name of some submit buttons store the section we'd like to go
174
+ # to. for repeater questions, an anchor to the repeater group is also stored
175
+ # e.g. params[:section] = {"1"=>{"question_group_1"=>"<= add row"}}
176
+ def section_id_from(p = {})
177
+ if p[:section] && p[:section].respond_to?(:keys)
178
+ p[:section].keys.first
179
+ elsif p[:section]
180
+ p[:section]
181
+ elsif p[:current_section]
182
+ p[:current_section]
183
+ end
184
+ end
185
+
186
+ def anchor_from(p)
187
+ p.respond_to?(:keys) && p[p.keys.first].respond_to?(:keys) ? p[p.keys.first].keys.first : nil
188
+ end
189
+
190
+ def surveyor_index
191
+ surveyor.available_surveys_path
192
+ end
193
+ def surveyor_finish
194
+ surveyor.available_surveys_path
195
+ end
196
+
197
+ def redirect_with_message(path, message_type, message)
198
+ respond_to do |format|
199
+ format.html do
200
+ flash[message_type] = message if !message.blank? and !message_type.blank?
201
+ redirect_to path
202
+ end
203
+ format.js do
204
+ render :text => message, :status => 403
205
+ end
206
+ end
207
+ end
208
+
209
+ ##
210
+ # @dependents are necessary in case the client does not have javascript enabled
211
+ # Whether or not javascript is enabled is determined by a hidden field set in the surveyor/edit.html form
212
+ def set_dependents
213
+ if session[:surveyor_javascript] && session[:surveyor_javascript] == "enabled"
214
+ @dependents = []
215
+ else
216
+ @dependents = get_unanswered_dependencies_minus_section_questions
217
+ end
218
+ end
219
+
220
+ def get_unanswered_dependencies_minus_section_questions
221
+ @response_set.unanswered_dependencies - @section.questions || []
222
+ end
223
+
224
+ ##
225
+ # If the hidden field surveyor_javascript_enabled is set to true
226
+ # cf. surveyor/edit.html.haml
227
+ # the set the session variable [:surveyor_javascript] to "enabled"
228
+ def determine_if_javascript_is_enabled
229
+ if params[:surveyor_javascript_enabled] && params[:surveyor_javascript_enabled].to_s == "true"
230
+ session[:surveyor_javascript] = "enabled"
231
+ else
232
+ session[:surveyor_javascript] = "not_enabled"
233
+ end
234
+ end
235
+ end
236
+ end