hssc_surveyor 1.4.1.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. data/.gitignore +28 -0
  2. data/.rspec +1 -0
  3. data/CHANGELOG.md +198 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.rails_version +19 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +186 -0
  8. data/Rakefile +105 -0
  9. data/app/controllers/surveyor_controller.rb +6 -0
  10. data/app/helpers/results_helper.rb +20 -0
  11. data/app/helpers/survey_form_builder.rb +37 -0
  12. data/app/helpers/surveyor_helper.rb +3 -0
  13. data/app/inputs/quiet_input.rb +5 -0
  14. data/app/inputs/surveyor_check_boxes_input.rb +35 -0
  15. data/app/inputs/surveyor_radio_input.rb +18 -0
  16. data/app/models/answer.rb +4 -0
  17. data/app/models/dependency.rb +4 -0
  18. data/app/models/dependency_condition.rb +4 -0
  19. data/app/models/question.rb +4 -0
  20. data/app/models/question_group.rb +5 -0
  21. data/app/models/response.rb +5 -0
  22. data/app/models/response_set.rb +4 -0
  23. data/app/models/survey.rb +4 -0
  24. data/app/models/survey_section.rb +5 -0
  25. data/app/models/survey_section_sweeper.rb +15 -0
  26. data/app/models/survey_translation.rb +5 -0
  27. data/app/models/validation.rb +4 -0
  28. data/app/models/validation_condition.rb +4 -0
  29. data/app/views/layouts/results.html.erb +13 -0
  30. data/app/views/layouts/surveyor_default.html.erb +12 -0
  31. data/app/views/partials/_answer.html.haml +25 -0
  32. data/app/views/partials/_dependents.html.haml +5 -0
  33. data/app/views/partials/_question.html.haml +41 -0
  34. data/app/views/partials/_question_group.html.haml +44 -0
  35. data/app/views/partials/_section.html.haml +12 -0
  36. data/app/views/partials/_section_menu.html.haml +12 -0
  37. data/app/views/surveyor/edit.html.haml +24 -0
  38. data/app/views/surveyor/export.json.rabl +85 -0
  39. data/app/views/surveyor/new.html.haml +24 -0
  40. data/app/views/surveyor/show.html.haml +74 -0
  41. data/app/views/surveyor/show.json.rabl +15 -0
  42. data/ci-exec.sh +56 -0
  43. data/config/routes.rb +8 -0
  44. data/cucumber.yml +10 -0
  45. data/doc/REPRESENTATIONS.md +34 -0
  46. data/doc/api_id_schema.json +7 -0
  47. data/doc/question types.png +0 -0
  48. data/doc/response_set_schema.json +54 -0
  49. data/doc/surveyor question combinations.png +0 -0
  50. data/doc/surveyor reject or delete decision matrix.png +0 -0
  51. data/doc/surveyor_models.png +0 -0
  52. data/doc/surveyor_models2.png +0 -0
  53. data/doc/surveyor_timestamp_schema.json +9 -0
  54. data/features/ajax_submissions.feature +140 -0
  55. data/features/export_to_json.feature +344 -0
  56. data/features/internationalization.feature +121 -0
  57. data/features/no_duplicates.feature +110 -0
  58. data/features/show_survey.feature +71 -0
  59. data/features/step_definitions/parser_steps.rb +145 -0
  60. data/features/step_definitions/surveyor_steps.rb +325 -0
  61. data/features/step_definitions/ui_steps.rb +25 -0
  62. data/features/step_definitions/web_steps.rb +225 -0
  63. data/features/support/REDCapDemoDatabase_DataDictionary.csv +127 -0
  64. data/features/support/database_cleaner.rb +16 -0
  65. data/features/support/env.rb +56 -0
  66. data/features/support/hooks.rb +4 -0
  67. data/features/support/paths.rb +39 -0
  68. data/features/support/redcap_new_headers.csv +1 -0
  69. data/features/support/redcap_siblings.csv +1 -0
  70. data/features/support/redcap_whitespace.csv +1 -0
  71. data/features/support/selectors.rb +39 -0
  72. data/features/support/simultaneous_ajax.rb +101 -0
  73. data/features/support/single_quit_selenium_driver.rb +23 -0
  74. data/features/support/slow_updates.rb +18 -0
  75. data/features/surveyor.feature +895 -0
  76. data/features/surveyor_dependencies.feature +476 -0
  77. data/features/surveyor_parser.feature +504 -0
  78. data/features/z_redcap_parser.feature +62 -0
  79. data/lib/assets/images/surveyor/next.gif +0 -0
  80. data/lib/assets/images/surveyor/prev.gif +0 -0
  81. data/lib/assets/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  82. data/lib/assets/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  83. data/lib/assets/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  84. data/lib/assets/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  85. data/lib/assets/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  86. data/lib/assets/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  87. data/lib/assets/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  88. data/lib/assets/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  89. data/lib/assets/images/ui-icons_222222_256x240.png +0 -0
  90. data/lib/assets/images/ui-icons_2e83ff_256x240.png +0 -0
  91. data/lib/assets/images/ui-icons_454545_256x240.png +0 -0
  92. data/lib/assets/images/ui-icons_888888_256x240.png +0 -0
  93. data/lib/assets/images/ui-icons_cd0a0a_256x240.png +0 -0
  94. data/lib/assets/javascripts/surveyor/jquery-1.9.0.js +9555 -0
  95. data/lib/assets/javascripts/surveyor/jquery-ui-1.10.0.custom.js +14850 -0
  96. data/lib/assets/javascripts/surveyor/jquery-ui-timepicker-addon.js +1919 -0
  97. data/lib/assets/javascripts/surveyor/jquery.maskedinput.js +338 -0
  98. data/lib/assets/javascripts/surveyor/jquery.selectToUISlider.js +240 -0
  99. data/lib/assets/javascripts/surveyor/jquery.surveyor.js +156 -0
  100. data/lib/assets/stylesheets/surveyor/jquery-ui-1.10.0.custom.css +1174 -0
  101. data/lib/assets/stylesheets/surveyor/jquery-ui-timepicker-addon.css +11 -0
  102. data/lib/assets/stylesheets/surveyor/reset.css +48 -0
  103. data/lib/assets/stylesheets/surveyor/results.css +125 -0
  104. data/lib/assets/stylesheets/surveyor/ui.slider.extras.css +110 -0
  105. data/lib/assets/stylesheets/surveyor.sass +132 -0
  106. data/lib/generators/surveyor/custom_generator.rb +18 -0
  107. data/lib/generators/surveyor/install_generator.rb +102 -0
  108. data/lib/generators/surveyor/templates/app/assets/javascripts/surveyor_all.js +6 -0
  109. data/lib/generators/surveyor/templates/app/assets/stylesheets/surveyor_all.css +9 -0
  110. data/lib/generators/surveyor/templates/app/controllers/surveyor_controller.rb +40 -0
  111. data/lib/generators/surveyor/templates/app/views/layouts/surveyor_custom.html.erb +13 -0
  112. data/lib/generators/surveyor/templates/config/locales/surveyor_en.yml +19 -0
  113. data/lib/generators/surveyor/templates/config/locales/surveyor_es.yml +19 -0
  114. data/lib/generators/surveyor/templates/config/locales/surveyor_he.yml +19 -0
  115. data/lib/generators/surveyor/templates/config/locales/surveyor_ko.yml +19 -0
  116. data/lib/generators/surveyor/templates/db/migrate/add_api_id_to_question_groups.rb +10 -0
  117. data/lib/generators/surveyor/templates/db/migrate/add_api_ids.rb +14 -0
  118. data/lib/generators/surveyor/templates/db/migrate/add_api_ids_to_response_sets_and_responses.rb +12 -0
  119. data/lib/generators/surveyor/templates/db/migrate/add_attachment_to_response.rb +5 -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 +284 -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/acts_as_response.rb +17 -0
  158. data/lib/surveyor/common.rb +59 -0
  159. data/lib/surveyor/engine.rb +10 -0
  160. data/lib/surveyor/helpers/asset_pipeline.rb +13 -0
  161. data/lib/surveyor/helpers/formtastic_custom_input.rb +13 -0
  162. data/lib/surveyor/helpers/surveyor_helper_methods.rb +103 -0
  163. data/lib/surveyor/models/answer_methods.rb +86 -0
  164. data/lib/surveyor/models/dependency_condition_methods.rb +72 -0
  165. data/lib/surveyor/models/dependency_methods.rb +60 -0
  166. data/lib/surveyor/models/question_group_methods.rb +61 -0
  167. data/lib/surveyor/models/question_methods.rb +115 -0
  168. data/lib/surveyor/models/response_methods.rb +132 -0
  169. data/lib/surveyor/models/response_set_methods.rb +196 -0
  170. data/lib/surveyor/models/survey_methods.rb +103 -0
  171. data/lib/surveyor/models/survey_section_methods.rb +56 -0
  172. data/lib/surveyor/models/survey_translation_methods.rb +33 -0
  173. data/lib/surveyor/models/validation_condition_methods.rb +56 -0
  174. data/lib/surveyor/models/validation_methods.rb +48 -0
  175. data/lib/surveyor/mustache_context.rb +11 -0
  176. data/lib/surveyor/parser.rb +428 -0
  177. data/lib/surveyor/redcap_parser.rb +289 -0
  178. data/lib/surveyor/surveyor_controller_methods.rb +237 -0
  179. data/lib/surveyor/unparser.rb +147 -0
  180. data/lib/surveyor/version.rb +3 -0
  181. data/lib/surveyor.rb +13 -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 +307 -0
  185. data/spec/factories.rb +161 -0
  186. data/spec/helpers/formtastic_custom_input_spec.rb +15 -0
  187. data/spec/helpers/surveyor_helper_spec.rb +118 -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 +175 -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 +207 -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 +45 -0
  212. metadata +657 -0
@@ -0,0 +1,289 @@
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
+ attr_accessible :question_reference, :answer_reference
197
+ end
198
+ end
199
+
200
+ # Answer model
201
+ module SurveyorRedcapParserAnswerMethods
202
+ def build_and_set(context, r)
203
+ case r[:field_type]
204
+ when "text"
205
+ self.attributes = {
206
+ :response_class => "string",
207
+ :text => "Text",
208
+ :display_order => context[:question].answers.size }
209
+ context[:question].answers << context[:answer] = self
210
+ when "notes"
211
+ self.attributes = {
212
+ :response_class => "text",
213
+ :text => "Notes",
214
+ :display_order => context[:question].answers.size }
215
+ context[:question].answers << context[:answer] = self
216
+ when "file"
217
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping answer: file"
218
+ end
219
+ (r[:choices_or_calculations] || r[:choices_calculations_or_slider_labels]).to_s.split("|").each do |pair|
220
+ aref, atext = pair.split(",").map(&:strip)
221
+ if aref.blank? or atext.blank? or (aref.to_i.to_s != aref)
222
+ Surveyor::RedcapParser.rake_trace "\n!!! skipping answer #{pair}"
223
+ else
224
+ a = Answer.new({
225
+ :reference_identifier => aref,
226
+ :text => atext,
227
+ :display_order => context[:question].answers.size })
228
+ context[:question].answers << context[:answer] = a
229
+ unless context[:question].reference_identifier.blank? or aref.blank? or !context[:answer].valid?
230
+ context[:answer_references] ||= {}
231
+ context[:answer_references][context[:question].reference_identifier] ||= {}
232
+ context[:answer_references][context[:question].reference_identifier][aref] = context[:answer]
233
+ end
234
+ Surveyor::RedcapParser.rake_trace "#{context[:answer].errors.full_messages}, #{context[:answer].inspect}" unless context[:answer].valid?
235
+ Surveyor::RedcapParser.rake_trace "answer_#{context[:answer].reference_identifier} "
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ # Validation model
242
+ module SurveyorRedcapParserValidationMethods
243
+ def build_and_set(context, r)
244
+ # text_validation_type text_validation_min text_validation_max
245
+ min = r[:text_validation_min].to_s.blank? ? nil : r[:text_validation_min].to_s
246
+ max = r[:text_validation_max].to_s.blank? ? nil : r[:text_validation_max].to_s
247
+ type = r[:text_validation_type].to_s.blank? ? nil : r[:text_validation_type].to_s
248
+ if min or max
249
+ context[:question].answers.each do |a|
250
+ self.rule = (min ? max ? "A and B" : "A" : "B")
251
+ a.validations << context[:validation] = self
252
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => ">=", :integer_value => min) if min
253
+ context[:validation].validation_conditions.build(:rule_key => "B", :operator => "<=", :integer_value => max) if max
254
+ end
255
+ elsif type
256
+ # date email integer number phone
257
+ case r[:text_validation_type]
258
+ when "date"
259
+ context[:question].display_type = :date if context[:question].display_type == :string
260
+ when "email"
261
+ context[:question].answers.each do |a|
262
+ self.rule = "A"
263
+ a.validations << context[:validation] = self
264
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$")
265
+ end
266
+ when "integer"
267
+ context[:question].display_type = :integer if context[:question].display_type == :string
268
+ context[:question].answers.each do |a|
269
+ self.rule = "A"
270
+ a.validations << context[:validation] = self
271
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "\d+")
272
+ end
273
+ when "number"
274
+ context[:question].display_type = :float if context[:question].display_type == :string
275
+ context[:question].answers.each do |a|
276
+ self.rule = "A"
277
+ a.validations << context[:validation] = self
278
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "^\d*(,\d{3})*(\.\d*)?$")
279
+ end
280
+ when "phone"
281
+ context[:question].answers.each do |a|
282
+ self.rule = "A"
283
+ a.validations << context[:validation] = self
284
+ context[:validation].validation_conditions.build(:rule_key => "A", :operator => "=~", :regexp => "\d{3}.*\d{4}")
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,237 @@
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").all.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 = @sections.with_includes.find(section_id_from(params) || :first) || @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.
112
+ find_by_access_code(params[:response_set_code], :include => {:responses => :answer})
113
+ if @response_set
114
+ saved = true
115
+ if params[:r]
116
+ @response_set.update_from_ui_hash(params[:r])
117
+ end
118
+ if params[:finish]
119
+ @response_set.complete!
120
+ saved &= @response_set.save
121
+ end
122
+ saved
123
+ else
124
+ false
125
+ end
126
+ end
127
+ end
128
+ private :load_and_update_response_set
129
+
130
+ def export
131
+ surveys = Survey.where(:access_code => params[:survey_code]).order("survey_version DESC")
132
+ s = params[:survey_version].blank? ? surveys.first : surveys.where(:survey_version => params[:survey_version]).first
133
+ render_404 and return if s.blank?
134
+ @survey = s.filtered_for_json
135
+ end
136
+
137
+ def render_404
138
+ head :status => 404
139
+ true
140
+ end
141
+
142
+ def url_options
143
+ ((I18n.locale == I18n.default_locale) ? {} : {:locale => I18n.locale}).merge(super)
144
+ end
145
+
146
+ private
147
+
148
+ # This is a hook method for surveyor-using applications to override and provide the context object
149
+ def render_context
150
+ nil
151
+ end
152
+
153
+ # Filters
154
+ def get_current_user
155
+ @current_user = self.respond_to?(:current_user) ? self.current_user : nil
156
+ end
157
+
158
+ def set_response_set_and_render_context
159
+ @response_set = ResponseSet.
160
+ find_by_access_code(params[:response_set_code], :include => {:responses => [:question, :answer]})
161
+ @render_context = render_context
162
+ end
163
+
164
+ def set_locale
165
+ if params[:new_locale]
166
+ I18n.locale = params[:new_locale]
167
+ elsif params[:locale]
168
+ I18n.locale = params[:locale]
169
+ else
170
+ I18n.locale = I18n.default_locale
171
+ end
172
+ end
173
+
174
+ # Params: the name of some submit buttons store the section we'd like to go
175
+ # to. for repeater questions, an anchor to the repeater group is also stored
176
+ # e.g. params[:section] = {"1"=>{"question_group_1"=>"<= add row"}}
177
+ def section_id_from(p = {})
178
+ if p[:section] && p[:section].respond_to?(:keys)
179
+ p[:section].keys.first
180
+ elsif p[:section]
181
+ p[:section]
182
+ elsif p[:current_section]
183
+ p[:current_section]
184
+ end
185
+ end
186
+
187
+ def anchor_from(p)
188
+ p.respond_to?(:keys) && p[p.keys.first].respond_to?(:keys) ? p[p.keys.first].keys.first : nil
189
+ end
190
+
191
+ def surveyor_index
192
+ surveyor.available_surveys_path
193
+ end
194
+ def surveyor_finish
195
+ surveyor.available_surveys_path
196
+ end
197
+
198
+ def redirect_with_message(path, message_type, message)
199
+ respond_to do |format|
200
+ format.html do
201
+ flash[message_type] = message if !message.blank? and !message_type.blank?
202
+ redirect_to path
203
+ end
204
+ format.js do
205
+ render :text => message, :status => 403
206
+ end
207
+ end
208
+ end
209
+
210
+ ##
211
+ # @dependents are necessary in case the client does not have javascript enabled
212
+ # Whether or not javascript is enabled is determined by a hidden field set in the surveyor/edit.html form
213
+ def set_dependents
214
+ if session[:surveyor_javascript] && session[:surveyor_javascript] == "enabled"
215
+ @dependents = []
216
+ else
217
+ @dependents = get_unanswered_dependencies_minus_section_questions
218
+ end
219
+ end
220
+
221
+ def get_unanswered_dependencies_minus_section_questions
222
+ @response_set.unanswered_dependencies - @section.questions || []
223
+ end
224
+
225
+ ##
226
+ # If the hidden field surveyor_javascript_enabled is set to true
227
+ # cf. surveyor/edit.html.haml
228
+ # the set the session variable [:surveyor_javascript] to "enabled"
229
+ def determine_if_javascript_is_enabled
230
+ if params[:surveyor_javascript_enabled] && params[:surveyor_javascript_enabled].to_s == "true"
231
+ session[:surveyor_javascript] = "enabled"
232
+ else
233
+ session[:surveyor_javascript] = "not_enabled"
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,147 @@
1
+ %w(survey survey_section question_group question dependency dependency_condition answer validation validation_condition).each {|model| require model }
2
+ module Surveyor
3
+ class Unparser
4
+ # Class methods
5
+ def self.unparse(survey)
6
+ survey.unparse(dsl = "")
7
+ dsl
8
+ end
9
+ end
10
+ end
11
+
12
+ # Surveyor models with extra parsing methods
13
+ class Survey < ActiveRecord::Base
14
+ # block
15
+
16
+ def unparse(dsl)
17
+ attrs = (self.attributes.diff Survey.new(:title => title).attributes).delete_if{|k,v| %w(created_at updated_at inactive_at id title access_code api_id).include? k}.symbolize_keys!
18
+ dsl << "survey \"#{title}\""
19
+ dsl << (attrs.blank? ? " do\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")} do\n")
20
+ sections.each{|section| section.unparse(dsl)}
21
+ dsl << "end\n"
22
+ end
23
+ end
24
+ class SurveySection < ActiveRecord::Base
25
+ # block
26
+
27
+ def unparse(dsl)
28
+ attrs = (self.attributes.diff SurveySection.new(:title => title).attributes).delete_if{|k,v| %w(created_at updated_at id survey_id).include? k}.symbolize_keys!
29
+ group_questions = []
30
+ dsl << " section \"#{title}\""
31
+ dsl << (attrs.blank? ? " do\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")} do\n")
32
+ questions.each_with_index do |question, index|
33
+ if question.solo?
34
+ question.unparse(dsl)
35
+ else # gather up the group questions
36
+ group_questions << question
37
+ if (index + 1 >= questions.size) or (question.question_group != questions[index + 1].question_group)
38
+ # this is the last question of the section, or the group
39
+ question.question_group.unparse(dsl)
40
+ end
41
+ group_questions = []
42
+ end
43
+ end
44
+ dsl << " end\n"
45
+ end
46
+ end
47
+ class QuestionGroup < ActiveRecord::Base
48
+ # block
49
+
50
+ def unparse(dsl)
51
+ attrs = (self.attributes.diff QuestionGroup.new(:text => text).attributes).delete_if{|k,v| %w(created_at updated_at id api_id).include?(k) or (k == "display_type" && %w(grid repeater default).include?(v))}.symbolize_keys!
52
+ method = (%w(grid repeater).include?(display_type) ? display_type : "group")
53
+ dsl << "\n"
54
+ dsl << " #{method} \"#{text}\""
55
+ dsl << (attrs.blank? ? " do\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")} do\n")
56
+ questions.first.answers.each{|answer| answer.unparse(dsl)} if display_type == "grid"
57
+ questions.each{|question| question.unparse(dsl)}
58
+ dsl << " end\n"
59
+ end
60
+ end
61
+ class Question < ActiveRecord::Base
62
+ # nonblock
63
+
64
+ def unparse(dsl)
65
+ attrs = (self.attributes.diff Question.new(:text => text).attributes).delete_if{|k,v| %w(created_at updated_at reference_identifier id survey_section_id question_group_id api_id).include?(k) or (k == "display_type" && v == "label")}.symbolize_keys!
66
+ dsl << (solo? ? "\n" : " ")
67
+ if display_type == "label"
68
+ dsl << " label"
69
+ else
70
+ dsl << " q"
71
+ end
72
+ dsl << "_#{reference_identifier}" unless reference_identifier.blank?
73
+ dsl << " \"#{text}\""
74
+ dsl << (attrs.blank? ? "\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")}\n")
75
+ if solo? or question_group.display_type != "grid"
76
+ answers.each{|answer| answer.unparse(dsl)}
77
+ end
78
+ dependency.unparse(dsl) if dependency
79
+ end
80
+ end
81
+ class Dependency < ActiveRecord::Base
82
+ # nonblock
83
+
84
+ def unparse(dsl)
85
+ attrs = (self.attributes.diff Dependency.new.attributes).delete_if{|k,v| %w(created_at updated_at id question_id).include?(k) }.symbolize_keys!
86
+ dsl << " " if question.part_of_group?
87
+ dsl << " dependency"
88
+ dsl << (attrs.blank? ? "\n" : " #{attrs.inspect.gsub(/\{|\}/, "")}\n")
89
+ dependency_conditions.each{|dependency_condition| dependency_condition.unparse(dsl)}
90
+ end
91
+ end
92
+ class DependencyCondition < ActiveRecord::Base
93
+ # nonblock
94
+
95
+ def unparse(dsl)
96
+ attrs = (self.attributes.diff Dependency.new.attributes).delete_if{|k,v| %w(created_at updated_at question_id question_group_id rule_key rule operator id dependency_id answer_id).include? k}.symbolize_keys!
97
+ dsl << " " if dependency.question.part_of_group?
98
+ dsl << " condition"
99
+ dsl << "_#{rule_key}" unless rule_key.blank?
100
+ dsl << " :q_#{question.reference_identifier}, \"#{operator}\""
101
+ dsl << (attrs.blank? ? ", {:answer_reference=>\"#{answer && answer.reference_identifier}\"}\n" : ", {#{attrs.inspect.gsub(/\{|\}/, "")}, :answer_reference=>\"#{answer && answer.reference_identifier}\"}\n")
102
+ end
103
+ end
104
+ class Answer < ActiveRecord::Base
105
+ # nonblock
106
+
107
+ def unparse(dsl)
108
+ attrs = (self.attributes.diff Answer.new(:text => text).attributes).delete_if{|k,v| %w(created_at updated_at reference_identifier response_class id question_id api_id).include? k}.symbolize_keys!
109
+ attrs.delete(:is_exclusive) if text == "Omit" && is_exclusive == true
110
+ attrs.merge!({:is_exclusive => false}) if text == "Omit" && is_exclusive == false
111
+ dsl << " " if question.part_of_group?
112
+ dsl << " a"
113
+ dsl << "_#{reference_identifier}" unless reference_identifier.blank?
114
+ if response_class.to_s.titlecase == text && attrs == {:display_type => "hidden_label"}
115
+ dsl << " :#{response_class}"
116
+ else
117
+ dsl << [ text.blank? ? nil : text == "Other" ? " :other" : text == "Omit" ? " :omit" : " \"#{text}\"",
118
+ (response_class.blank? or response_class == "answer") ? nil : " #{response_class.to_sym.inspect}",
119
+ attrs.blank? ? nil : " #{attrs.inspect.gsub(/\{|\}/, "")}\n"].compact.join(",")
120
+ end
121
+ dsl << "\n"
122
+ validations.each{|validation| validation.unparse(dsl)}
123
+ end
124
+ end
125
+ class Validation < ActiveRecord::Base
126
+ # nonblock
127
+
128
+ def unparse(dsl)
129
+ attrs = (self.attributes.diff Validation.new.attributes).delete_if{|k,v| %w(created_at updated_at id answer_id).include?(k) }.symbolize_keys!
130
+ dsl << " " if answer.question.part_of_group?
131
+ dsl << " validation"
132
+ dsl << (attrs.blank? ? "\n" : " #{attrs.inspect.gsub(/\{|\}/, "")}\n")
133
+ validation_conditions.each{|validation_condition| validation_condition.unparse(dsl)}
134
+ end
135
+ end
136
+ class ValidationCondition < ActiveRecord::Base
137
+ # nonblock
138
+
139
+ def unparse(dsl)
140
+ attrs = (self.attributes.diff ValidationCondition.new.attributes).delete_if{|k,v| %w(created_at updated_at operator rule_key id validation_id).include? k}.symbolize_keys!
141
+ dsl << " " if validation.answer.question.part_of_group?
142
+ dsl << " condition"
143
+ dsl << "_#{rule_key}" unless rule_key.blank?
144
+ dsl << " \"#{operator}\""
145
+ dsl << (attrs.blank? ? "\n" : ", #{attrs.inspect.gsub(/\{|\}/, "")}\n")
146
+ end
147
+ end
@@ -0,0 +1,3 @@
1
+ module Surveyor
2
+ VERSION = '1.4.1.pre'
3
+ end