surveyor 0.22.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +83 -0
- data/Gemfile.rails_version +7 -1
- data/README.md +114 -16
- data/Rakefile +15 -9
- data/app/inputs/quiet_input.rb +5 -0
- data/app/inputs/surveyor_check_boxes_input.rb +35 -0
- data/app/inputs/surveyor_radio_input.rb +18 -0
- data/app/views/partials/_answer.html.haml +4 -4
- data/app/views/partials/_question.html.haml +7 -7
- data/app/views/partials/_question_group.html.haml +9 -8
- data/app/views/surveyor/export.json.rabl +28 -25
- data/app/views/surveyor/new.html.haml +8 -5
- data/app/views/surveyor/show.json.rabl +3 -2
- data/ci-exec.sh +13 -7
- data/cucumber.yml +3 -3
- data/doc/REPRESENTATIONS.md +34 -0
- data/doc/api_id_schema.json +7 -0
- data/doc/response_set_schema.json +54 -0
- data/doc/surveyor question combinations.png +0 -0
- data/doc/surveyor_timestamp_schema.json +9 -0
- data/features/ajax_submissions.feature +140 -0
- data/features/export_to_json.feature +182 -34
- data/features/no_duplicates.feature +110 -0
- data/features/show_survey.feature +1 -1
- data/features/step_definitions/parser_steps.rb +25 -2
- data/features/step_definitions/surveyor_steps.rb +145 -20
- data/features/step_definitions/ui_steps.rb +3 -0
- data/features/support/database_cleaner.rb +16 -0
- data/features/support/env.rb +21 -17
- data/features/support/simultaneous_ajax.rb +101 -0
- data/features/support/single_quit_selenium_driver.rb +23 -0
- data/features/support/slow_updates.rb +18 -0
- data/features/surveyor.feature +174 -44
- data/features/surveyor_dependencies.feature +80 -39
- data/features/surveyor_parser.feature +114 -20
- data/features/z_redcap_parser.feature +0 -1
- data/lib/{generators/surveyor/templates/public → assets}/images/surveyor/next.gif +0 -0
- data/lib/{generators/surveyor/templates/public → assets}/images/surveyor/prev.gif +0 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- data/lib/assets/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-icons_ef8c08_256x240.png +0 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-icons_ffffff_256x240.png +0 -0
- data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery-ui-timepicker-addon.js +23 -23
- data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery-ui.js +125 -125
- data/lib/assets/javascripts/surveyor/jquery.selectToUISlider.js +240 -0
- data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery.surveyor.js +52 -57
- data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery.tools.min.js +7 -7
- data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/dateinput.css +13 -13
- data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/jquery-ui-timepicker-addon.css +0 -0
- data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/jquery-ui.custom.css +17 -17
- data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/reset.css +1 -1
- data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/results.css +0 -0
- data/lib/assets/stylesheets/surveyor/ui.slider.extras.css +110 -0
- data/lib/{generators/surveyor/templates/public/stylesheets/sass → assets/stylesheets}/surveyor.sass +15 -7
- data/lib/generators/surveyor/custom_generator.rb +3 -2
- data/lib/generators/surveyor/install_generator.rb +59 -17
- data/lib/generators/surveyor/templates/app/assets/javascripts/surveyor_all.js +5 -0
- data/lib/generators/surveyor/templates/app/assets/stylesheets/surveyor_all.css +9 -0
- data/lib/generators/surveyor/templates/app/controllers/surveyor_controller.rb +2 -1
- data/lib/generators/surveyor/templates/app/views/layouts/surveyor_custom.html.erb +1 -0
- data/lib/generators/surveyor/templates/config/locales/surveyor_es.yml +1 -0
- data/lib/generators/surveyor/templates/config/locales/surveyor_he.yml +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_api_id_to_question_groups.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_api_ids.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_api_ids_to_response_sets_and_responses.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_correct_answer_id_to_questions.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_default_value_to_answers.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_display_order_to_surveys.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_display_type_to_answers.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_index_to_response_sets.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_index_to_surveys.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_section_id_to_responses.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/add_unique_index_on_access_code_and_version_in_surveys.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/add_unique_indicies.rb +3 -2
- data/lib/generators/surveyor/templates/db/migrate/add_version_to_surveys.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/api_ids_must_be_unique.rb +23 -0
- data/lib/generators/surveyor/templates/db/migrate/create_answers.rb +6 -5
- data/lib/generators/surveyor/templates/db/migrate/create_dependencies.rb +2 -1
- data/lib/generators/surveyor/templates/db/migrate/create_dependency_conditions.rb +2 -1
- data/lib/generators/surveyor/templates/db/migrate/create_question_groups.rb +5 -4
- data/lib/generators/surveyor/templates/db/migrate/create_questions.rb +4 -3
- data/lib/generators/surveyor/templates/db/migrate/create_response_sets.rb +1 -0
- data/lib/generators/surveyor/templates/db/migrate/create_responses.rb +10 -9
- data/lib/generators/surveyor/templates/db/migrate/create_survey_sections.rb +5 -4
- data/lib/generators/surveyor/templates/db/migrate/create_surveys.rb +4 -3
- data/lib/generators/surveyor/templates/db/migrate/create_validation_conditions.rb +5 -4
- data/lib/generators/surveyor/templates/db/migrate/create_validations.rb +3 -2
- data/lib/generators/surveyor/templates/db/migrate/drop_unique_index_on_access_code_in_surveys.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/update_blank_api_ids_on_question_group.rb +22 -0
- data/lib/generators/surveyor/templates/db/migrate/update_blank_versions_on_surveys.rb +13 -0
- data/lib/generators/surveyor/templates/surveys/date_survey.rb +1 -0
- data/lib/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +54 -24
- data/lib/generators/surveyor/templates/surveys/quiz.rb +1 -0
- data/lib/generators/surveyor/templates/{public/stylesheets/sass → vendor/assets/stylesheets}/custom.sass +1 -1
- data/lib/surveyor/common.rb +16 -31
- data/lib/surveyor/engine.rb +2 -4
- data/lib/surveyor/helpers/asset_pipeline.rb +13 -0
- data/lib/surveyor/helpers/formtastic_custom_input.rb +17 -0
- data/lib/surveyor/helpers/surveyor_helper_methods.rb +10 -8
- data/lib/surveyor/models/answer_methods.rb +3 -0
- data/lib/surveyor/models/dependency_condition_methods.rb +27 -28
- data/lib/surveyor/models/dependency_methods.rb +3 -0
- data/lib/surveyor/models/question_group_methods.rb +3 -0
- data/lib/surveyor/models/question_methods.rb +10 -7
- data/lib/surveyor/models/response_methods.rb +16 -0
- data/lib/surveyor/models/response_set_methods.rb +71 -64
- data/lib/surveyor/models/survey_methods.rb +19 -28
- data/lib/surveyor/models/survey_section_methods.rb +3 -0
- data/lib/surveyor/models/validation_condition_methods.rb +4 -2
- data/lib/surveyor/models/validation_methods.rb +3 -0
- data/lib/surveyor/parser.rb +198 -148
- data/lib/surveyor/redcap_parser.rb +120 -80
- data/lib/surveyor/surveyor_controller_methods.rb +86 -37
- data/lib/surveyor/version.rb +2 -2
- data/lib/surveyor.rb +5 -6
- data/lib/tasks/surveyor_tasks.rake +19 -7
- data/spec/controllers/surveyor_controller_spec.rb +166 -92
- data/spec/factories.rb +33 -32
- data/spec/helpers/formtastic_custom_input_spec.rb +16 -0
- data/spec/lib/common_spec.rb +0 -39
- data/spec/lib/redcap_parser_spec.rb +24 -24
- data/spec/models/answer_spec.rb +12 -0
- data/spec/models/dependency_condition_spec.rb +279 -323
- data/spec/models/dependency_spec.rb +10 -0
- data/spec/models/question_group_spec.rb +12 -0
- data/spec/models/question_spec.rb +12 -0
- data/spec/models/response_set_spec.rb +189 -139
- data/spec/models/response_spec.rb +60 -0
- data/spec/models/survey_section_spec.rb +9 -0
- data/spec/models/survey_spec.rb +72 -9
- data/spec/models/validation_condition_spec.rb +9 -1
- data/spec/models/validation_spec.rb +10 -0
- data/spec/spec_helper.rb +25 -6
- data/surveyor.gemspec +5 -4
- metadata +332 -291
- data/features/step_definitions/common_steps.rb +0 -3
- data/lib/formtastic/surveyor_builder.rb +0 -82
- 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
|
-
|
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
|
-
|
15
|
-
|
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
|
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
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
def
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
def
|
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
|
-
|
110
|
+
Surveyor::RedcapParser.rake_trace "label_ "
|
83
111
|
end
|
84
|
-
|
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
|
-
|
125
|
+
end
|
126
|
+
Surveyor::RedcapParser.rake_trace "question_#{context[:question].reference_identifier} "
|
98
127
|
end
|
99
|
-
def
|
128
|
+
def pick_from_field_type(ft)
|
100
129
|
{"checkbox" => :any, "radio" => :one}[ft] || :none
|
101
130
|
end
|
102
|
-
def
|
131
|
+
def display_type_from_field_type(ft)
|
103
132
|
{"text" => :string, "dropdown" => :dropdown, "notes" => :text}[ft]
|
104
133
|
end
|
105
134
|
end
|
106
|
-
|
107
|
-
|
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
|
-
|
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
|
-
|
149
|
+
Surveyor::RedcapParser.rake_trace "dependency(#{hash[:rule]}) "
|
117
150
|
end
|
118
151
|
end
|
119
|
-
def
|
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
|
-
|
131
|
-
end
|
163
|
+
Surveyor::RedcapParser.rake_trace "\n!!! skipping dependency_condition #{str}"
|
164
|
+
end
|
132
165
|
end
|
133
|
-
def
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
181
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
225
|
+
Surveyor::RedcapParser.rake_trace "\n!!! skipping answer #{pair}"
|
194
226
|
else
|
195
|
-
|
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
|
-
|
201
|
-
|
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
|
-
|
207
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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, :
|
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
|
-
|
22
|
-
|
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(
|
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
|
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'
|
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
|
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 =
|
69
|
-
|
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.
|
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(
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
data/lib/surveyor/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Surveyor
|
2
|
-
VERSION = '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
|
-
|
12
|
-
Formtastic::
|
13
|
-
Formtastic::
|
14
|
-
Formtastic::
|
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
|
-
|
67
|
-
|
68
|
-
|
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
|