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.
- checksums.yaml +7 -0
- data/.gitignore +28 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +198 -0
- data/Gemfile +5 -0
- data/Gemfile.rails_version +27 -0
- data/MIT-LICENSE +20 -0
- data/README.md +166 -0
- data/Rakefile +106 -0
- data/app/controllers/surveyor_controller.rb +5 -0
- data/app/helpers/results_helper.rb +20 -0
- data/app/helpers/survey_form_builder.rb +37 -0
- data/app/helpers/surveyor_helper.rb +3 -0
- 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/models/answer.rb +3 -0
- data/app/models/dependency.rb +3 -0
- data/app/models/dependency_condition.rb +3 -0
- data/app/models/question.rb +3 -0
- data/app/models/question_group.rb +4 -0
- data/app/models/response.rb +4 -0
- data/app/models/response_set.rb +3 -0
- data/app/models/survey.rb +3 -0
- data/app/models/survey_section.rb +4 -0
- data/app/models/survey_section_sweeper.rb +15 -0
- data/app/models/survey_translation.rb +4 -0
- data/app/models/validation.rb +3 -0
- data/app/models/validation_condition.rb +3 -0
- data/app/views/layouts/results.html.erb +13 -0
- data/app/views/layouts/surveyor_default.html.erb +12 -0
- data/app/views/partials/_answer.html.haml +25 -0
- data/app/views/partials/_dependents.html.haml +5 -0
- data/app/views/partials/_question.html.haml +28 -0
- data/app/views/partials/_question_group.html.haml +44 -0
- data/app/views/partials/_section.html.haml +12 -0
- data/app/views/partials/_section_menu.html.haml +12 -0
- data/app/views/surveyor/edit.html.haml +24 -0
- data/app/views/surveyor/export.json.rabl +85 -0
- data/app/views/surveyor/new.html.haml +24 -0
- data/app/views/surveyor/show.html.haml +74 -0
- data/app/views/surveyor/show.json.rabl +15 -0
- data/ci-exec.sh +56 -0
- data/config/routes.rb +8 -0
- data/cucumber.yml +10 -0
- data/doc/REPRESENTATIONS.md +34 -0
- data/doc/api_id_schema.json +7 -0
- data/doc/question types.png +0 -0
- data/doc/response_set_schema.json +54 -0
- data/doc/surveyor question combinations.png +0 -0
- data/doc/surveyor reject or delete decision matrix.png +0 -0
- data/doc/surveyor_models.png +0 -0
- data/doc/surveyor_models2.png +0 -0
- data/doc/surveyor_timestamp_schema.json +9 -0
- data/features/ajax_submissions.feature +140 -0
- data/features/export_to_json.feature +344 -0
- data/features/internationalization.feature +121 -0
- data/features/no_duplicates.feature +110 -0
- data/features/show_survey.feature +71 -0
- data/features/step_definitions/parser_steps.rb +145 -0
- data/features/step_definitions/surveyor_steps.rb +325 -0
- data/features/step_definitions/ui_steps.rb +25 -0
- data/features/step_definitions/web_steps.rb +225 -0
- data/features/support/REDCapDemoDatabase_DataDictionary.csv +127 -0
- data/features/support/database_cleaner.rb +16 -0
- data/features/support/env.rb +56 -0
- data/features/support/hooks.rb +4 -0
- data/features/support/paths.rb +39 -0
- data/features/support/redcap_new_headers.csv +1 -0
- data/features/support/redcap_siblings.csv +1 -0
- data/features/support/redcap_whitespace.csv +1 -0
- data/features/support/selectors.rb +39 -0
- 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 +895 -0
- data/features/surveyor_dependencies.feature +476 -0
- data/features/surveyor_parser.feature +504 -0
- data/features/z_redcap_parser.feature +62 -0
- data/lib/assets/images/surveyor/next.gif +0 -0
- data/lib/assets/images/surveyor/prev.gif +0 -0
- data/lib/assets/images/surveyor/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/lib/assets/images/surveyor/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/lib/assets/images/surveyor/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/lib/assets/images/surveyor/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/lib/assets/images/surveyor/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/lib/assets/images/surveyor/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/lib/assets/images/surveyor/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/lib/assets/images/surveyor/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/lib/assets/images/surveyor/ui-icons_222222_256x240.png +0 -0
- data/lib/assets/images/surveyor/ui-icons_2e83ff_256x240.png +0 -0
- data/lib/assets/images/surveyor/ui-icons_454545_256x240.png +0 -0
- data/lib/assets/images/surveyor/ui-icons_888888_256x240.png +0 -0
- data/lib/assets/images/surveyor/ui-icons_cd0a0a_256x240.png +0 -0
- data/lib/assets/javascripts/surveyor/jquery-1.9.0.js +9555 -0
- data/lib/assets/javascripts/surveyor/jquery-ui-1.10.0.custom.js +14850 -0
- data/lib/assets/javascripts/surveyor/jquery-ui-timepicker-addon.js +1919 -0
- data/lib/assets/javascripts/surveyor/jquery.maskedinput.js +338 -0
- data/lib/assets/javascripts/surveyor/jquery.selectToUISlider.js +240 -0
- data/lib/assets/javascripts/surveyor/jquery.surveyor.js +154 -0
- data/lib/assets/stylesheets/surveyor.sass +132 -0
- data/lib/assets/stylesheets/surveyor/jquery-ui-1.10.0.custom.css +1174 -0
- data/lib/assets/stylesheets/surveyor/jquery-ui-timepicker-addon.css +11 -0
- data/lib/assets/stylesheets/surveyor/reset.css +48 -0
- data/lib/assets/stylesheets/surveyor/results.css +125 -0
- data/lib/assets/stylesheets/surveyor/ui.slider.extras.css +110 -0
- data/lib/generators/surveyor/custom_generator.rb +18 -0
- data/lib/generators/surveyor/install_generator.rb +101 -0
- data/lib/generators/surveyor/templates/app/assets/javascripts/surveyor_all.js +6 -0
- data/lib/generators/surveyor/templates/app/assets/stylesheets/surveyor_all.css +9 -0
- data/lib/generators/surveyor/templates/app/controllers/surveyor_controller.rb +40 -0
- data/lib/generators/surveyor/templates/app/views/layouts/surveyor_custom.html.erb +13 -0
- data/lib/generators/surveyor/templates/config/locales/surveyor_en.yml +19 -0
- data/lib/generators/surveyor/templates/config/locales/surveyor_es.yml +19 -0
- data/lib/generators/surveyor/templates/config/locales/surveyor_he.yml +19 -0
- data/lib/generators/surveyor/templates/config/locales/surveyor_ko.yml +19 -0
- data/lib/generators/surveyor/templates/db/migrate/add_api_id_to_question_groups.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/add_api_ids.rb +14 -0
- data/lib/generators/surveyor/templates/db/migrate/add_api_ids_to_response_sets_and_responses.rb +12 -0
- data/lib/generators/surveyor/templates/db/migrate/add_correct_answer_id_to_questions.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/add_default_value_to_answers.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/add_display_order_to_surveys.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/add_display_type_to_answers.rb +14 -0
- data/lib/generators/surveyor/templates/db/migrate/add_index_to_response_sets.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/add_index_to_surveys.rb +10 -0
- data/lib/generators/surveyor/templates/db/migrate/add_input_mask_attributes_to_answer.rb +12 -0
- data/lib/generators/surveyor/templates/db/migrate/add_section_id_to_responses.rb +12 -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 +18 -0
- 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 +38 -0
- data/lib/generators/surveyor/templates/db/migrate/create_dependencies.rb +23 -0
- data/lib/generators/surveyor/templates/db/migrate/create_dependency_conditions.rb +30 -0
- data/lib/generators/surveyor/templates/db/migrate/create_question_groups.rb +28 -0
- data/lib/generators/surveyor/templates/db/migrate/create_questions.rb +37 -0
- data/lib/generators/surveyor/templates/db/migrate/create_response_sets.rb +23 -0
- data/lib/generators/surveyor/templates/db/migrate/create_responses.rb +34 -0
- data/lib/generators/surveyor/templates/db/migrate/create_survey_sections.rb +30 -0
- data/lib/generators/surveyor/templates/db/migrate/create_survey_translations.rb +19 -0
- data/lib/generators/surveyor/templates/db/migrate/create_surveys.rb +32 -0
- data/lib/generators/surveyor/templates/db/migrate/create_validation_conditions.rb +33 -0
- data/lib/generators/surveyor/templates/db/migrate/create_validations.rb +21 -0
- 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/EXTENDING_SURVEYOR.md +52 -0
- data/lib/generators/surveyor/templates/surveys/MODIFYING_SURVEYOR.md +91 -0
- data/lib/generators/surveyor/templates/surveys/date_survey.rb +16 -0
- data/lib/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +280 -0
- data/lib/generators/surveyor/templates/surveys/languages.rb +14 -0
- data/lib/generators/surveyor/templates/surveys/quiz.rb +11 -0
- data/lib/generators/surveyor/templates/surveys/translations/languages.es.yml +18 -0
- data/lib/generators/surveyor/templates/surveys/translations/languages.he.yml +18 -0
- data/lib/generators/surveyor/templates/surveys/translations/languages.ko.yml +18 -0
- data/lib/generators/surveyor/templates/vendor/assets/stylesheets/custom.sass +5 -0
- data/lib/surveyor.rb +13 -0
- data/lib/surveyor/acts_as_response.rb +17 -0
- data/lib/surveyor/common.rb +59 -0
- data/lib/surveyor/engine.rb +10 -0
- data/lib/surveyor/helpers/asset_pipeline.rb +18 -0
- data/lib/surveyor/helpers/formtastic_custom_input.rb +13 -0
- data/lib/surveyor/helpers/surveyor_helper_methods.rb +103 -0
- data/lib/surveyor/models/answer_methods.rb +83 -0
- data/lib/surveyor/models/dependency_condition_methods.rb +69 -0
- data/lib/surveyor/models/dependency_methods.rb +57 -0
- data/lib/surveyor/models/question_group_methods.rb +58 -0
- data/lib/surveyor/models/question_methods.rb +112 -0
- data/lib/surveyor/models/response_methods.rb +126 -0
- data/lib/surveyor/models/response_set_methods.rb +188 -0
- data/lib/surveyor/models/survey_methods.rb +100 -0
- data/lib/surveyor/models/survey_section_methods.rb +53 -0
- data/lib/surveyor/models/survey_translation_methods.rb +30 -0
- data/lib/surveyor/models/validation_condition_methods.rb +54 -0
- data/lib/surveyor/models/validation_methods.rb +45 -0
- data/lib/surveyor/mustache_context.rb +11 -0
- data/lib/surveyor/parser.rb +427 -0
- data/lib/surveyor/redcap_parser.rb +288 -0
- data/lib/surveyor/surveyor_controller_methods.rb +236 -0
- data/lib/surveyor/unparser.rb +147 -0
- data/lib/surveyor/version.rb +3 -0
- data/lib/tasks/surveyor_tasks.rake +88 -0
- data/rails/init.rb +1 -0
- data/spec/controllers/surveyor_controller_spec.rb +311 -0
- data/spec/factories.rb +166 -0
- data/spec/helpers/formtastic_custom_input_spec.rb +15 -0
- data/spec/helpers/surveyor_helper_spec.rb +117 -0
- data/spec/lib/benchmark_spec.rb +22 -0
- data/spec/lib/chinese_survey.rb +14 -0
- data/spec/lib/common_spec.rb +34 -0
- data/spec/lib/parser_spec.rb +204 -0
- data/spec/lib/rake_kitchen_sink.rb +40 -0
- data/spec/lib/redcap_parser_spec.rb +75 -0
- data/spec/lib/tasks_spec.rake +26 -0
- data/spec/lib/unparser_spec.rb +126 -0
- data/spec/models/answer_spec.rb +174 -0
- data/spec/models/dependency_condition_spec.rb +439 -0
- data/spec/models/dependency_spec.rb +101 -0
- data/spec/models/question_group_spec.rb +93 -0
- data/spec/models/question_spec.rb +206 -0
- data/spec/models/response_set_spec.rb +477 -0
- data/spec/models/response_spec.rb +218 -0
- data/spec/models/survey_section_spec.rb +85 -0
- data/spec/models/survey_spec.rb +191 -0
- data/spec/models/validation_condition_spec.rb +113 -0
- data/spec/models/validation_spec.rb +74 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +56 -0
- data/stacktests.sh +65 -0
- data/surveyor.gemspec +53 -0
- metadata +601 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module Surveyor
|
2
|
+
module Models
|
3
|
+
module DependencyMethods
|
4
|
+
def self.included(base)
|
5
|
+
# Associations
|
6
|
+
base.send :belongs_to, :question
|
7
|
+
base.send :belongs_to, :question_group
|
8
|
+
base.send :has_many, :dependency_conditions, :dependent => :destroy
|
9
|
+
|
10
|
+
@@validations_already_included ||= nil
|
11
|
+
unless @@validations_already_included
|
12
|
+
# Validations
|
13
|
+
base.send :validates_presence_of, :rule
|
14
|
+
base.send :validates_format_of, :rule, :with => /^(?:and|or|\)|\(|[A-Z]|\s)+$/, :multiline => true #TODO properly formed parenthesis etc. # SMELL with :multiline => true Rails reports a security risk
|
15
|
+
base.send :validates_numericality_of, :question_id, :if => Proc.new { |d| d.question_group_id.nil? }
|
16
|
+
base.send :validates_numericality_of, :question_group_id, :if => Proc.new { |d| d.question_id.nil? }
|
17
|
+
|
18
|
+
@@validations_already_included = true
|
19
|
+
end
|
20
|
+
|
21
|
+
# Attribute aliases
|
22
|
+
base.send :alias_attribute, :dependent_question_id, :question_id
|
23
|
+
end
|
24
|
+
|
25
|
+
# Instance Methods
|
26
|
+
def question_group_id=(i)
|
27
|
+
write_attribute(:question_id, nil) unless i.nil?
|
28
|
+
write_attribute(:question_group_id, i)
|
29
|
+
end
|
30
|
+
|
31
|
+
def question_id=(i)
|
32
|
+
write_attribute(:question_group_id, nil) unless i.nil?
|
33
|
+
write_attribute(:question_id, i)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Has this dependency has been met in the context of response_set?
|
37
|
+
# Substitutes the conditions hash into the rule and evaluates it
|
38
|
+
def is_met?(response_set)
|
39
|
+
ch = conditions_hash(response_set)
|
40
|
+
return false if ch.blank?
|
41
|
+
# logger.debug "rule: #{self.rule.inspect}"
|
42
|
+
# logger.debug "rexp: #{rgx.inspect}"
|
43
|
+
# logger.debug "keyp: #{ch.inspect}"
|
44
|
+
# logger.debug "subd: #{self.rule.gsub(rgx){|m| ch[m.to_sym]}}"
|
45
|
+
rgx = Regexp.new(self.dependency_conditions.map{|dc| ["a","o"].include?(dc.rule_key) ? "\\b#{dc.rule_key}(?!nd|r)\\b" : "\\b#{dc.rule_key}\\b"}.join("|")) # exclude and, or
|
46
|
+
eval(self.rule.gsub(rgx){|m| ch[m.to_sym]})
|
47
|
+
end
|
48
|
+
|
49
|
+
# A hash of the conditions (keyed by rule_key) and their evaluation (boolean) in the context of response_set
|
50
|
+
def conditions_hash(response_set)
|
51
|
+
hash = {}
|
52
|
+
self.dependency_conditions.each{|dc| hash.merge!(dc.to_hash(response_set))}
|
53
|
+
return hash
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'surveyor/common'
|
2
|
+
|
3
|
+
module Surveyor
|
4
|
+
module Models
|
5
|
+
module QuestionGroupMethods
|
6
|
+
def self.included(base)
|
7
|
+
# Associations
|
8
|
+
base.send :has_many, :questions
|
9
|
+
base.send :has_one, :dependency
|
10
|
+
end
|
11
|
+
|
12
|
+
include MustacheContext
|
13
|
+
|
14
|
+
# Instance Methods
|
15
|
+
def initialize(*args)
|
16
|
+
super(*args)
|
17
|
+
default_args
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_args
|
21
|
+
self.display_type ||= "inline"
|
22
|
+
self.api_id ||= Surveyor::Common.generate_api_id
|
23
|
+
end
|
24
|
+
|
25
|
+
def renderer
|
26
|
+
display_type.blank? ? :default : display_type.to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
def display_type=(val)
|
30
|
+
write_attribute(:display_type, val.nil? ? nil : val.to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
def dependent?
|
34
|
+
self.dependency != nil
|
35
|
+
end
|
36
|
+
def triggered?(response_set)
|
37
|
+
dependent? ? self.dependency.is_met?(response_set) : true
|
38
|
+
end
|
39
|
+
def css_class(response_set)
|
40
|
+
[(dependent? ? "g_dependent" : nil), (triggered?(response_set) ? nil : "g_hidden"), custom_class].compact.join(" ")
|
41
|
+
end
|
42
|
+
|
43
|
+
def text_for(context = nil, locale = nil)
|
44
|
+
return "" if display_type == "hidden_label"
|
45
|
+
in_context(translation(locale)[:text], context)
|
46
|
+
end
|
47
|
+
def help_text_for(context = nil, locale = nil)
|
48
|
+
in_context(translation(locale)[:help_text], context)
|
49
|
+
end
|
50
|
+
|
51
|
+
def translation(locale)
|
52
|
+
{:text => self.text, :help_text => self.help_text}.with_indifferent_access.merge(
|
53
|
+
(self.questions.first.survey_section.survey.translation(locale)[:question_groups] || {})[self.reference_identifier] || {}
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'surveyor/common'
|
2
|
+
|
3
|
+
module Surveyor
|
4
|
+
module Models
|
5
|
+
module QuestionMethods
|
6
|
+
def self.included(base)
|
7
|
+
# Associations
|
8
|
+
base.send :belongs_to, :survey_section
|
9
|
+
base.send :belongs_to, :question_group, :dependent => :destroy
|
10
|
+
base.send :has_many, :answers, :dependent => :destroy # it might not always have answers
|
11
|
+
base.send :has_one, :dependency, :dependent => :destroy
|
12
|
+
base.send :belongs_to, :correct_answer, :class_name => "Answer", :dependent => :destroy
|
13
|
+
|
14
|
+
# Scopes
|
15
|
+
base.instance_eval {default_scope ->{order "#{base.quoted_table_name}.display_order ASC"}}
|
16
|
+
|
17
|
+
# Mustache
|
18
|
+
base.send :include, MustacheContext
|
19
|
+
|
20
|
+
@@validations_already_included ||= nil
|
21
|
+
unless @@validations_already_included
|
22
|
+
# Validations
|
23
|
+
base.send :validates_presence_of, :text, :display_order
|
24
|
+
# this causes issues with building and saving
|
25
|
+
#, :survey_section_id
|
26
|
+
base.send :validates_inclusion_of, :is_mandatory, :in => [true, false]
|
27
|
+
|
28
|
+
@@validations_already_included = true
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Instance Methods
|
34
|
+
def initialize(*args)
|
35
|
+
super(*args)
|
36
|
+
default_args
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_args
|
40
|
+
self.is_mandatory ||= false
|
41
|
+
self.display_type ||= "default"
|
42
|
+
self.pick ||= "none"
|
43
|
+
self.data_export_identifier ||= Surveyor::Common.normalize(text)
|
44
|
+
self.short_text ||= text
|
45
|
+
self.api_id ||= Surveyor::Common.generate_api_id
|
46
|
+
end
|
47
|
+
|
48
|
+
def pick=(val)
|
49
|
+
write_attribute(:pick, val.nil? ? nil : val.to_s)
|
50
|
+
end
|
51
|
+
def display_type=(val)
|
52
|
+
write_attribute(:display_type, val.nil? ? nil : val.to_s)
|
53
|
+
end
|
54
|
+
|
55
|
+
def mandatory?
|
56
|
+
self.is_mandatory == true
|
57
|
+
end
|
58
|
+
|
59
|
+
def dependent?
|
60
|
+
self.dependency != nil
|
61
|
+
end
|
62
|
+
def triggered?(response_set)
|
63
|
+
dependent? ? self.dependency.is_met?(response_set) : true
|
64
|
+
end
|
65
|
+
def css_class(response_set)
|
66
|
+
[(dependent? ? "q_dependent" : nil), (triggered?(response_set) ? nil : "q_hidden"), custom_class].compact.join(" ")
|
67
|
+
end
|
68
|
+
|
69
|
+
def part_of_group?
|
70
|
+
!self.question_group.nil?
|
71
|
+
end
|
72
|
+
def solo?
|
73
|
+
self.question_group.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
def text_for(position = nil, context = nil, locale = nil)
|
77
|
+
return "" if display_type == "hidden_label"
|
78
|
+
imaged(split(in_context(translation(locale)[:text], context), position))
|
79
|
+
end
|
80
|
+
def help_text_for(context = nil, locale = nil)
|
81
|
+
in_context(translation(locale)[:help_text], context)
|
82
|
+
end
|
83
|
+
def split(text, position=nil)
|
84
|
+
case position
|
85
|
+
when :pre
|
86
|
+
text.split("|",2)[0]
|
87
|
+
when :post
|
88
|
+
text.split("|",2)[1]
|
89
|
+
else
|
90
|
+
text
|
91
|
+
end.to_s
|
92
|
+
end
|
93
|
+
def renderer(g = question_group)
|
94
|
+
r = [g ? g.renderer.to_s : nil, display_type].compact.join("_")
|
95
|
+
r.blank? ? :default : r.to_sym
|
96
|
+
end
|
97
|
+
def translation(locale)
|
98
|
+
{:text => self.text, :help_text => self.help_text}.with_indifferent_access.merge(
|
99
|
+
(self.survey_section.survey.translation(locale)[:questions] || {})[self.reference_identifier] || {}
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
|
106
|
+
def imaged(text)
|
107
|
+
self.display_type == "image" && !text.blank? ? ActionController::Base.helpers.image_tag(text) : text
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Surveyor
|
2
|
+
module Models
|
3
|
+
module ResponseMethods
|
4
|
+
def self.included(base)
|
5
|
+
# Associations
|
6
|
+
base.send :belongs_to, :response_set
|
7
|
+
base.send :belongs_to, :question
|
8
|
+
base.send :belongs_to, :answer
|
9
|
+
|
10
|
+
@@validations_already_included ||= nil
|
11
|
+
unless @@validations_already_included
|
12
|
+
# Validations
|
13
|
+
base.send :validates_presence_of, :question_id, :answer_id
|
14
|
+
|
15
|
+
@@validations_already_included = true
|
16
|
+
end
|
17
|
+
base.send :include, Surveyor::ActsAsResponse # includes "as" instance method
|
18
|
+
|
19
|
+
# Class methods
|
20
|
+
base.instance_eval do
|
21
|
+
def applicable_attributes(attrs)
|
22
|
+
result = HashWithIndifferentAccess.new(attrs)
|
23
|
+
answer_id = result[:answer_id].is_a?(Array) ? result[:answer_id].last : result[:answer_id] # checkboxes are arrays / radio buttons are not arrays
|
24
|
+
if result[:string_value] && !answer_id.blank? && Answer.exists?(answer_id)
|
25
|
+
answer = Answer.find(answer_id)
|
26
|
+
result.delete(:string_value) unless answer.response_class && answer.response_class.to_sym == :string
|
27
|
+
end
|
28
|
+
result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Instance Methods
|
34
|
+
def initialize(*args)
|
35
|
+
super(*args)
|
36
|
+
default_args
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_args
|
40
|
+
self.api_id ||= Surveyor::Common.generate_api_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def answer_id=(val)
|
44
|
+
write_attribute :answer_id, (val.is_a?(Array) ? val.detect{|x| !x.to_s.blank?} : val)
|
45
|
+
end
|
46
|
+
def correct?
|
47
|
+
question.correct_answer.nil? or self.answer.response_class != "answer" or (question.correct_answer.id.to_i == answer.id.to_i)
|
48
|
+
end
|
49
|
+
|
50
|
+
def time_value
|
51
|
+
read_attribute(:datetime_value).strftime( time_format ) unless read_attribute(:datetime_value).blank?
|
52
|
+
end
|
53
|
+
|
54
|
+
def time_value=(val)
|
55
|
+
self.datetime_value =
|
56
|
+
if val && time = Time.zone.parse("#{Date.today.to_s} #{val}")
|
57
|
+
time.to_datetime
|
58
|
+
else
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def date_value
|
64
|
+
read_attribute(:datetime_value).strftime( date_format ) unless read_attribute(:datetime_value).blank?
|
65
|
+
end
|
66
|
+
|
67
|
+
def date_value=(val)
|
68
|
+
self.datetime_value =
|
69
|
+
if val && time = Time.zone.parse(val)
|
70
|
+
time.to_datetime
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def time_format
|
77
|
+
'%H:%M'
|
78
|
+
end
|
79
|
+
|
80
|
+
def date_format
|
81
|
+
'%Y-%m-%d'
|
82
|
+
end
|
83
|
+
|
84
|
+
def datetime_format
|
85
|
+
'%Y-%m-%d %H:%M:%S'
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_formatted_s
|
89
|
+
return "" if answer.nil? || answer.response_class.nil?
|
90
|
+
return case t = answer.response_class.to_sym
|
91
|
+
when :string, :text, :integer, :float
|
92
|
+
send("#{t}_value".to_sym).to_s
|
93
|
+
when :date
|
94
|
+
date_value
|
95
|
+
when :time
|
96
|
+
time_value
|
97
|
+
when :datetime
|
98
|
+
(read_attribute(:datetime_value).strftime( datetime_format ) unless read_attribute(:datetime_value).blank?) || ''
|
99
|
+
else
|
100
|
+
to_s
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s # used in dependency_explanation_helper
|
105
|
+
if self.answer.response_class == "answer" and self.answer_id
|
106
|
+
return self.answer.text
|
107
|
+
else
|
108
|
+
return "#{(self.string_value || self.text_value || self.integer_value || self.float_value || nil).to_s}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def json_value
|
113
|
+
return nil if answer.response_class == "answer"
|
114
|
+
|
115
|
+
formats = {
|
116
|
+
'datetime' => '%Y-%m-%dT%H:%M%:z',
|
117
|
+
'date' => '%Y-%m-%d',
|
118
|
+
'time' => '%H:%M'
|
119
|
+
}
|
120
|
+
|
121
|
+
found = formats[answer.response_class]
|
122
|
+
found ? datetime_value.try{|d| d.utc.strftime(found)} : as(answer.response_class)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'rabl'
|
2
|
+
|
3
|
+
module Surveyor
|
4
|
+
module Models
|
5
|
+
module ResponseSetMethods
|
6
|
+
def self.included(base)
|
7
|
+
# Associations
|
8
|
+
base.send :belongs_to, :survey
|
9
|
+
base.send :belongs_to, :user
|
10
|
+
base.instance_eval {has_many :responses, ->{order "#{Response.quoted_table_name}.created_at ASC"}, :dependent => :destroy}
|
11
|
+
base.send :accepts_nested_attributes_for, :responses, :allow_destroy => true
|
12
|
+
|
13
|
+
@@validations_already_included ||= nil
|
14
|
+
unless @@validations_already_included
|
15
|
+
# Validations
|
16
|
+
base.send :validates_presence_of, :survey_id
|
17
|
+
base.send :validates_associated, :responses
|
18
|
+
base.send :validates_uniqueness_of, :access_code
|
19
|
+
|
20
|
+
@@validations_already_included = true
|
21
|
+
end
|
22
|
+
|
23
|
+
base.send :before_create, :ensure_start_timestamp
|
24
|
+
base.send :before_create, :ensure_identifiers
|
25
|
+
|
26
|
+
# Class methods
|
27
|
+
base.instance_eval do
|
28
|
+
def has_blank_value?(hash)
|
29
|
+
return true if hash["answer_id"].blank?
|
30
|
+
return false if (q = Question.find_by_id(hash["question_id"])) and q.pick == "one"
|
31
|
+
hash.any?{|k,v| v.is_a?(Array) ? v.all?{|x| x.to_s.blank?} : v.to_s.blank?}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def ensure_start_timestamp
|
37
|
+
self.started_at ||= Time.now
|
38
|
+
end
|
39
|
+
|
40
|
+
def ensure_identifiers
|
41
|
+
self.access_code ||= Surveyor::Common.make_tiny_code
|
42
|
+
self.api_id ||= Surveyor::Common.generate_api_id
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_csv(access_code = false, print_header = true)
|
46
|
+
qcols = Question.content_columns.map(&:name) - %w(created_at updated_at)
|
47
|
+
acols = Answer.content_columns.map(&:name) - %w(created_at updated_at)
|
48
|
+
rcols = Response.content_columns.map(&:name)
|
49
|
+
result = Surveyor::Common.csv_impl.generate do |csv|
|
50
|
+
if print_header
|
51
|
+
csv << (access_code ? ["response set access code"] : []) +
|
52
|
+
qcols.map{|qcol| "question.#{qcol}"} +
|
53
|
+
acols.map{|acol| "answer.#{acol}"} +
|
54
|
+
rcols.map{|rcol| "response.#{rcol}"}
|
55
|
+
end
|
56
|
+
responses.each do |response|
|
57
|
+
csv << (access_code ? [self.access_code] : []) +
|
58
|
+
qcols.map{|qcol| response.question.send(qcol)} +
|
59
|
+
acols.map{|acol| response.answer.send(acol)} +
|
60
|
+
rcols.map{|rcol| response.send(rcol)}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def as_json(options = nil)
|
67
|
+
template_paths = ActionController::Base.view_paths.collect(&:to_path)
|
68
|
+
Rabl.render(self, 'surveyor/show.json', :view_path => template_paths, :format => "hash")
|
69
|
+
end
|
70
|
+
|
71
|
+
def complete!
|
72
|
+
self.completed_at = Time.now
|
73
|
+
end
|
74
|
+
|
75
|
+
def complete?
|
76
|
+
!completed_at.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
def correct?
|
80
|
+
responses.all?(&:correct?)
|
81
|
+
end
|
82
|
+
def correctness_hash
|
83
|
+
{ :questions => survey.sections_with_questions.map(&:questions).flatten.compact.size,
|
84
|
+
:responses => responses.compact.size,
|
85
|
+
:correct => responses.find_all(&:correct?).compact.size
|
86
|
+
}
|
87
|
+
end
|
88
|
+
def mandatory_questions_complete?
|
89
|
+
progress_hash[:triggered_mandatory] == progress_hash[:triggered_mandatory_completed]
|
90
|
+
end
|
91
|
+
def progress_hash
|
92
|
+
qs = survey.sections_with_questions.map(&:questions).flatten
|
93
|
+
ds = dependencies(qs.map(&:id))
|
94
|
+
triggered = qs - ds.select{|d| !d.is_met?(self)}.map(&:question)
|
95
|
+
{ :questions => qs.compact.size,
|
96
|
+
:triggered => triggered.compact.size,
|
97
|
+
:triggered_mandatory => triggered.select{|q| q.mandatory?}.compact.size,
|
98
|
+
:triggered_mandatory_completed => triggered.select{|q| q.mandatory? and is_answered?(q)}.compact.size
|
99
|
+
}
|
100
|
+
end
|
101
|
+
def is_answered?(question)
|
102
|
+
%w(label image).include?(question.display_type) or !is_unanswered?(question)
|
103
|
+
end
|
104
|
+
def is_unanswered?(question)
|
105
|
+
self.responses.detect{|r| r.question_id == question.id}.nil?
|
106
|
+
end
|
107
|
+
def is_group_unanswered?(group)
|
108
|
+
group.questions.any?{|question| is_unanswered?(question)}
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the number of response groups (count of group responses enterted) for this question group
|
112
|
+
def count_group_responses(questions)
|
113
|
+
questions.map { |q|
|
114
|
+
responses.select { |r|
|
115
|
+
(r.question_id.to_i == q.id.to_i) && !r.response_group.nil?
|
116
|
+
}.group_by(&:response_group).size
|
117
|
+
}.max
|
118
|
+
end
|
119
|
+
|
120
|
+
def unanswered_dependencies
|
121
|
+
unanswered_question_dependencies + unanswered_question_group_dependencies
|
122
|
+
end
|
123
|
+
|
124
|
+
def unanswered_question_dependencies
|
125
|
+
dependencies.select{ |d| d.question && self.is_unanswered?(d.question) && d.is_met?(self) }.map(&:question)
|
126
|
+
end
|
127
|
+
|
128
|
+
def unanswered_question_group_dependencies
|
129
|
+
dependencies.
|
130
|
+
select{ |d| d.question_group && self.is_group_unanswered?(d.question_group) && d.is_met?(self) }.
|
131
|
+
map(&:question_group)
|
132
|
+
end
|
133
|
+
|
134
|
+
def all_dependencies(question_ids = nil)
|
135
|
+
arr = dependencies(question_ids).partition{|d| d.is_met?(self) }
|
136
|
+
{
|
137
|
+
:show => arr[0].map{|d| d.question_group_id.nil? ? "q_#{d.question_id}" : "g_#{d.question_group_id}"},
|
138
|
+
:hide => arr[1].map{|d| d.question_group_id.nil? ? "q_#{d.question_id}" : "g_#{d.question_group_id}"}
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check existence of responses to questions from a given survey_section
|
143
|
+
def no_responses_for_section?(section)
|
144
|
+
!responses.any?{|r| r.survey_section_id == section.id}
|
145
|
+
end
|
146
|
+
|
147
|
+
def update_from_ui_hash(ui_hash)
|
148
|
+
transaction do
|
149
|
+
ui_hash.each do |ord, response_hash|
|
150
|
+
api_id = response_hash['api_id']
|
151
|
+
fail "api_id missing from response #{ord}" unless api_id
|
152
|
+
|
153
|
+
existing = Response.where(:api_id => api_id).first
|
154
|
+
updateable_attributes = response_hash.reject { |k, v| k == 'api_id' }
|
155
|
+
|
156
|
+
if self.class.has_blank_value?(response_hash)
|
157
|
+
existing.destroy if existing
|
158
|
+
elsif existing
|
159
|
+
if existing.question_id.to_s != updateable_attributes['question_id']
|
160
|
+
fail "Illegal attempt to change question for response #{api_id}."
|
161
|
+
end
|
162
|
+
|
163
|
+
existing.update_attributes(updateable_attributes)
|
164
|
+
else
|
165
|
+
responses.build(updateable_attributes).tap do |r|
|
166
|
+
r.api_id = api_id
|
167
|
+
r.save!
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
def dependencies(question_ids = nil)
|
178
|
+
question_ids = survey.sections.map(&:questions).flatten.map(&:id) if responses.blank? and question_ids.blank?
|
179
|
+
deps = Dependency.includes(:dependency_conditions).
|
180
|
+
where({:dependency_conditions => {:question_id => question_ids || responses.map(&:question_id)}})
|
181
|
+
# this is a work around for a bug in active_record in rails 2.3 which incorrectly eager-loads associatins when a
|
182
|
+
# condition clause includes an association limiter
|
183
|
+
deps.each{|d| d.dependency_conditions.reload}
|
184
|
+
deps
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|