jakewendt-surveyor 0.11.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (160) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +285 -0
  3. data/Rakefile +54 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/surveyor_controller.rb +142 -0
  6. data/app/helpers/survey_form_builder.rb +37 -0
  7. data/app/helpers/surveyor_helper.rb +67 -0
  8. data/app/models/answer.rb +23 -0
  9. data/app/models/dependency.rb +50 -0
  10. data/app/models/dependency_condition.rb +47 -0
  11. data/app/models/question.rb +54 -0
  12. data/app/models/question_group.rb +33 -0
  13. data/app/models/response.rb +94 -0
  14. data/app/models/response_set.rb +316 -0
  15. data/app/models/survey.rb +172 -0
  16. data/app/models/survey_section.rb +18 -0
  17. data/app/models/validation.rb +38 -0
  18. data/app/models/validation_condition.rb +47 -0
  19. data/app/views/layouts/surveyor_default.html.erb +13 -0
  20. data/app/views/partials/_answer.html.haml +58 -0
  21. data/app/views/partials/_question.html.haml +80 -0
  22. data/app/views/partials/_question_group.html.haml +39 -0
  23. data/app/views/surveyor/edit.html.haml +38 -0
  24. data/app/views/surveyor/new.html.haml +16 -0
  25. data/app/views/surveyor/show.html.haml +12 -0
  26. data/config/routes.rb +11 -0
  27. data/features/step_definitions/surveyor_steps.rb +7 -0
  28. data/features/step_definitions/web_steps.rb +273 -0
  29. data/features/support/env.rb +57 -0
  30. data/features/support/paths.rb +25 -0
  31. data/features/surveyor.feature +11 -0
  32. data/generators/extend_surveyor/extend_surveyor_generator.rb +22 -0
  33. data/generators/extend_surveyor/templates/EXTENDING_SURVEYOR +17 -0
  34. data/generators/extend_surveyor/templates/extensions/survey_extensions.rb +24 -0
  35. data/generators/extend_surveyor/templates/extensions/surveyor_controller_extensions.rb +28 -0
  36. data/generators/extend_surveyor/templates/extensions/surveyor_custom.html.erb +13 -0
  37. data/generators/extend_surveyor/templates/extensions/surveyor_helper_extensions.rb +17 -0
  38. data/generators/surveyor/surveyor_generator.rb +67 -0
  39. data/generators/surveyor/templates/README +10 -0
  40. data/generators/surveyor/templates/assets/images/222222_11x11_icon_arrows_leftright.gif +0 -0
  41. data/generators/surveyor/templates/assets/images/222222_11x11_icon_arrows_updown.gif +0 -0
  42. data/generators/surveyor/templates/assets/images/222222_11x11_icon_close.gif +0 -0
  43. data/generators/surveyor/templates/assets/images/222222_11x11_icon_doc.gif +0 -0
  44. data/generators/surveyor/templates/assets/images/222222_11x11_icon_folder_closed.gif +0 -0
  45. data/generators/surveyor/templates/assets/images/222222_11x11_icon_folder_open.gif +0 -0
  46. data/generators/surveyor/templates/assets/images/222222_11x11_icon_minus.gif +0 -0
  47. data/generators/surveyor/templates/assets/images/222222_11x11_icon_plus.gif +0 -0
  48. data/generators/surveyor/templates/assets/images/222222_11x11_icon_resize_se.gif +0 -0
  49. data/generators/surveyor/templates/assets/images/222222_35x9_colorpicker_indicator.gif.gif +0 -0
  50. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_down.gif +0 -0
  51. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_left.gif +0 -0
  52. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_right.gif +0 -0
  53. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_up.gif +0 -0
  54. data/generators/surveyor/templates/assets/images/454545_11x11_icon_arrows_leftright.gif +0 -0
  55. data/generators/surveyor/templates/assets/images/454545_11x11_icon_arrows_updown.gif +0 -0
  56. data/generators/surveyor/templates/assets/images/454545_11x11_icon_close.gif +0 -0
  57. data/generators/surveyor/templates/assets/images/454545_11x11_icon_doc.gif +0 -0
  58. data/generators/surveyor/templates/assets/images/454545_11x11_icon_folder_closed.gif +0 -0
  59. data/generators/surveyor/templates/assets/images/454545_11x11_icon_folder_open.gif +0 -0
  60. data/generators/surveyor/templates/assets/images/454545_11x11_icon_minus.gif +0 -0
  61. data/generators/surveyor/templates/assets/images/454545_11x11_icon_plus.gif +0 -0
  62. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_down.gif +0 -0
  63. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_left.gif +0 -0
  64. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_right.gif +0 -0
  65. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_up.gif +0 -0
  66. data/generators/surveyor/templates/assets/images/888888_11x11_icon_arrows_leftright.gif +0 -0
  67. data/generators/surveyor/templates/assets/images/888888_11x11_icon_arrows_updown.gif +0 -0
  68. data/generators/surveyor/templates/assets/images/888888_11x11_icon_close.gif +0 -0
  69. data/generators/surveyor/templates/assets/images/888888_11x11_icon_doc.gif +0 -0
  70. data/generators/surveyor/templates/assets/images/888888_11x11_icon_folder_closed.gif +0 -0
  71. data/generators/surveyor/templates/assets/images/888888_11x11_icon_folder_open.gif +0 -0
  72. data/generators/surveyor/templates/assets/images/888888_11x11_icon_minus.gif +0 -0
  73. data/generators/surveyor/templates/assets/images/888888_11x11_icon_plus.gif +0 -0
  74. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_down.gif +0 -0
  75. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_left.gif +0 -0
  76. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_right.gif +0 -0
  77. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_up.gif +0 -0
  78. data/generators/surveyor/templates/assets/images/dadada_40x100_textures_02_glass_75.png +0 -0
  79. data/generators/surveyor/templates/assets/images/e6e6e6_40x100_textures_02_glass_75.png +0 -0
  80. data/generators/surveyor/templates/assets/images/ffffff_40x100_textures_01_flat_0.png +0 -0
  81. data/generators/surveyor/templates/assets/images/ffffff_40x100_textures_02_glass_65.png +0 -0
  82. data/generators/surveyor/templates/assets/javascripts/accessibleUISlider.jQuery.js +201 -0
  83. data/generators/surveyor/templates/assets/javascripts/jquery-1.2.6.js +3549 -0
  84. data/generators/surveyor/templates/assets/javascripts/jquery-ui-personalized-1.5.3.js +7616 -0
  85. data/generators/surveyor/templates/assets/javascripts/jquery.form.js +637 -0
  86. data/generators/surveyor/templates/assets/javascripts/surveyor.js +47 -0
  87. data/generators/surveyor/templates/assets/stylesheets/jquery-ui-slider-additions.css +71 -0
  88. data/generators/surveyor/templates/assets/stylesheets/reset.css +50 -0
  89. data/generators/surveyor/templates/assets/stylesheets/sass/surveyor.sass +243 -0
  90. data/generators/surveyor/templates/assets/stylesheets/surveyor.css +235 -0
  91. data/generators/surveyor/templates/assets/stylesheets/ui.theme.css +851 -0
  92. data/generators/surveyor/templates/initializers/haml.rb +8 -0
  93. data/generators/surveyor/templates/initializers/surveyor.rb +10 -0
  94. data/generators/surveyor/templates/migrate/add_correct_answer_id_to_questions.rb +9 -0
  95. data/generators/surveyor/templates/migrate/add_display_order_to_surveys.rb +9 -0
  96. data/generators/surveyor/templates/migrate/add_index_to_response_sets.rb +9 -0
  97. data/generators/surveyor/templates/migrate/add_index_to_surveys.rb +9 -0
  98. data/generators/surveyor/templates/migrate/add_manual_numbering.rb +11 -0
  99. data/generators/surveyor/templates/migrate/add_unique_indicies.rb +17 -0
  100. data/generators/surveyor/templates/migrate/create_answers.rb +37 -0
  101. data/generators/surveyor/templates/migrate/create_dependencies.rb +22 -0
  102. data/generators/surveyor/templates/migrate/create_dependency_conditions.rb +29 -0
  103. data/generators/surveyor/templates/migrate/create_question_groups.rb +27 -0
  104. data/generators/surveyor/templates/migrate/create_questions.rb +36 -0
  105. data/generators/surveyor/templates/migrate/create_response_sets.rb +22 -0
  106. data/generators/surveyor/templates/migrate/create_responses.rb +33 -0
  107. data/generators/surveyor/templates/migrate/create_survey_sections.rb +29 -0
  108. data/generators/surveyor/templates/migrate/create_surveys.rb +31 -0
  109. data/generators/surveyor/templates/migrate/create_validation_conditions.rb +32 -0
  110. data/generators/surveyor/templates/migrate/create_validations.rb +20 -0
  111. data/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +218 -0
  112. data/generators/surveyor/templates/tasks/surveyor.rb +4 -0
  113. data/generators/test_surveyor/templates/TESTING_SURVEYOR +0 -0
  114. data/generators/test_surveyor/templates/environments/cucumber.rb +21 -0
  115. data/generators/test_surveyor/test_surveyor_generator.rb +15 -0
  116. data/jakewendt-surveyor.gemspec +212 -0
  117. data/lib/fixtures_extensions.rb +6 -0
  118. data/lib/jakewendt-surveyor.rb +1 -0
  119. data/lib/surveyor.rb +44 -0
  120. data/lib/surveyor/acts_as_response.rb +33 -0
  121. data/lib/surveyor/config.rb +45 -0
  122. data/lib/tasks/surveyor_tasks.rake +33 -0
  123. data/lib/xml_formatter.rb +12 -0
  124. data/rails/init.rb +1 -0
  125. data/script/surveyor/answer.rb +54 -0
  126. data/script/surveyor/base.rb +77 -0
  127. data/script/surveyor/dependency.rb +13 -0
  128. data/script/surveyor/dependency_condition.rb +40 -0
  129. data/script/surveyor/parser.rb +207 -0
  130. data/script/surveyor/question.rb +37 -0
  131. data/script/surveyor/question_group.rb +26 -0
  132. data/script/surveyor/specs/answer_spec.rb +29 -0
  133. data/script/surveyor/specs/question_spec.rb +63 -0
  134. data/script/surveyor/specs/spec_helper.rb +7 -0
  135. data/script/surveyor/specs/survey_section_spec.rb +23 -0
  136. data/script/surveyor/specs/validation_condition_spec.rb +20 -0
  137. data/script/surveyor/specs/validation_spec.rb +20 -0
  138. data/script/surveyor/survey.rb +35 -0
  139. data/script/surveyor/survey_section.rb +21 -0
  140. data/script/surveyor/validation.rb +21 -0
  141. data/script/surveyor/validation_condition.rb +21 -0
  142. data/script/surveyor/whr_dsl.tmproj +244 -0
  143. data/spec/controllers/surveyor_controller_spec.rb +193 -0
  144. data/spec/factories.rb +145 -0
  145. data/spec/lib/surveyor_spec.rb +44 -0
  146. data/spec/models/answer_spec.rb +29 -0
  147. data/spec/models/dependency_condition_spec.rb +321 -0
  148. data/spec/models/dependency_spec.rb +81 -0
  149. data/spec/models/question_group_spec.rb +35 -0
  150. data/spec/models/question_spec.rb +75 -0
  151. data/spec/models/response_set_spec.rb +245 -0
  152. data/spec/models/response_spec.rb +76 -0
  153. data/spec/models/survey_section_spec.rb +32 -0
  154. data/spec/models/survey_spec.rb +71 -0
  155. data/spec/models/validation_condition_spec.rb +105 -0
  156. data/spec/models/validation_spec.rb +59 -0
  157. data/spec/rcov.opts +2 -0
  158. data/spec/spec.opts +4 -0
  159. data/spec/spec_helper.rb +12 -0
  160. metadata +254 -0
@@ -0,0 +1,37 @@
1
+ class SurveyFormBuilder < ActionView::Helpers::FormBuilder
2
+ def survey_check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
3
+ fields = @template.survey_check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value)
4
+ fields[1]
5
+ end
6
+ end
7
+
8
+ module ActionView
9
+ module Helpers
10
+ module FormHelper
11
+ def survey_check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
12
+ if (Rails::VERSION::STRING.to_f > 2.1)
13
+ InstanceTag.new(object_name, method, self, options.delete(:object)).to_survey_check_box_tag(options, checked_value, unchecked_value)
14
+ else
15
+ InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_survey_check_box_tag(options, checked_value, unchecked_value)
16
+ end
17
+ end
18
+ end
19
+
20
+ class InstanceTag
21
+ def to_survey_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
22
+ options = options.stringify_keys
23
+ options["type"] = "checkbox"
24
+ options["value"] = checked_value
25
+ if options.has_key?("checked")
26
+ cv = options.delete "checked"
27
+ checked = cv == true || cv == "checked"
28
+ else
29
+ checked = self.class.check_box_checked?(value(object), checked_value)
30
+ end
31
+ options["checked"] = "checked" if checked
32
+ add_default_name_and_id(options)
33
+ [tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value), tag("input", options)]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,67 @@
1
+ module SurveyorHelper
2
+
3
+ # Extending surveyor
4
+ include SurveyorHelperExtensions if Surveyor::Config['extend'].include?("surveyor_helper")
5
+
6
+ # Configuration
7
+ def surveyor_config
8
+ Surveyor::Config
9
+ end
10
+
11
+ # Layout: stylsheets and javascripts
12
+ def surveyor_includes
13
+ surveyor_stylsheets + surveyor_javascripts
14
+ end
15
+ def surveyor_stylsheets
16
+ stylesheet_link_tag 'surveyor/reset', 'surveyor/surveyor', 'surveyor/ui.theme.css','surveyor/jquery-ui-slider-additions'
17
+ end
18
+ def surveyor_javascripts
19
+ javascript_include_tag 'surveyor/jquery-1.2.6.js', 'surveyor/jquery-ui-personalized-1.5.3.js', 'surveyor/accessibleUISlider.jQuery.js','surveyor/jquery.form.js', 'surveyor/surveyor.js'
20
+ end
21
+
22
+ # Section: dependencies, menu, previous and next
23
+ def dependency_explanation_helper(question,response_set)
24
+ # Attempts to explain why this dependent question needs to be answered by referenced the dependent question and users response
25
+ trigger_responses = []
26
+ dependent_questions = Question.find_all_by_id(question.dependency.dependency_conditions.map(&:question_id)).uniq
27
+ response_set.responses.find_all_by_question_id(dependent_questions.map(&:id)).uniq.each do |resp|
28
+ trigger_responses << resp.to_s
29
+ end
30
+ "&nbsp;&nbsp;You answered &quot;#{trigger_responses.join("&quot; and &quot;")}&quot; to the question &quot;#{dependent_questions.map(&:text).join("&quot;,&quot;")}&quot;"
31
+ end
32
+ def menu_button_for(section)
33
+ submit_tag(section.title, :name => "section[#{section.id}]")
34
+ end
35
+ def previous_section
36
+ # submit_tag("&laquo; Previous section", :name => "section[#{@section.previous.id}]") unless @section.previous.nil?
37
+ # refactored to use copy in memory instead of making extra db calls
38
+ submit_tag("&laquo; Previous section", :name => "section[#{@sections[@sections.index(@section)-1].id}]") unless @sections.first == @section
39
+ end
40
+ def next_section
41
+ # @section.next.nil? ? submit_tag("Click here to finish", :name => "finish") : submit_tag("Next section &raquo;", :name => "section[#{@section.next.id}]")
42
+ # refactored to use copy in memory instead of making extra db calls
43
+ @sections.last == @section ? submit_tag("Click here to finish", :name => "finish") : submit_tag("Next section &raquo;", :name => "section[#{@sections[@sections.index(@section)+1].id}]")
44
+ end
45
+
46
+ # Questions
47
+ def next_number
48
+ @n ||= 0
49
+ "#{@n += 1}<span style='padding-left:0.1em;'>)</span>"
50
+ end
51
+ def split_text(text = "") # Split text into with "|" delimiter - parts to go before/after input element
52
+ {:prefix => text.split("|")[0].blank? ? "&nbsp;" : text.split("|")[0], :postfix => text.split("|")[1] || "&nbsp;"}
53
+ end
54
+ def question_help_helper(question)
55
+ question.help_text.blank? ? "" : %Q(<span class="question-help">#{question.help_text}</span>)
56
+ end
57
+
58
+ # Answers
59
+ def fields_for_response(response, response_group = nil, &block)
60
+ name = response_group.nil? ? "responses[#{response.question_id}][#{response.answer_id}]" : "response_groups[#{response.question_id}][#{response_group}][#{response.answer_id}]"
61
+ fields_for(name, response, :builder => SurveyFormBuilder, &block)
62
+ end
63
+ def fields_for_radio(response, &block)
64
+ fields_for("responses[#{response.question_id}]", response, :builder => SurveyFormBuilder, &block)
65
+ end
66
+
67
+ end
@@ -0,0 +1,23 @@
1
+ class Answer < ActiveRecord::Base
2
+
3
+ # Extending surveyor
4
+ include "#{self.name}Extensions".constantize if Surveyor::Config['extend'].include?(self.name.underscore)
5
+
6
+ # Associations
7
+ belongs_to :question
8
+ has_many :responses
9
+
10
+ # Scopes
11
+ default_scope :order => "display_order ASC"
12
+
13
+ # Validations
14
+ validates_presence_of :text
15
+ validates_numericality_of :question_id, :allow_nil => false, :only_integer => true
16
+
17
+ # Methods
18
+ def renderer(q = question)
19
+ r = [q.pick.to_s, self.response_class].compact.map(&:downcase).join("_")
20
+ r.blank? ? :default : r.to_sym
21
+ end
22
+
23
+ end
@@ -0,0 +1,50 @@
1
+ class Dependency < ActiveRecord::Base
2
+
3
+ # Extending surveyor
4
+ include "#{self.name}Extensions".constantize if Surveyor::Config['extend'].include?(self.name.underscore)
5
+
6
+ # Associations
7
+ belongs_to :question
8
+ belongs_to :question_group
9
+ has_many :dependency_conditions
10
+
11
+ # Validations
12
+ validates_presence_of :rule
13
+ validates_format_of :rule, :with => /^(?:and|or|\)|\(|[A-Z]|\s)+$/ #TODO properly formed parenthesis etc.
14
+ validates_numericality_of :question_id, :if => Proc.new { |d| d.question_group_id.nil? }
15
+ validates_numericality_of :question_group_id, :if => Proc.new { |d| d.question_id.nil? }
16
+
17
+ # Attribute aliases
18
+ alias_attribute :dependent_question_id, :question_id
19
+
20
+ def question_group_id=(i)
21
+ write_attribute(:question_id, nil) unless i.nil?
22
+ write_attribute(:question_group_id, i)
23
+ end
24
+
25
+ def question_id=(i)
26
+ write_attribute(:question_group_id, nil) unless i.nil?
27
+ write_attribute(:question_id, i)
28
+ end
29
+
30
+ # Has this dependency has been met in the context of response_set?
31
+ # Substitutes the conditions hash into the rule and evaluates it
32
+ def is_met?(response_set)
33
+ ch = conditions_hash(response_set)
34
+ return false if ch.blank?
35
+ # logger.debug "rule: #{self.rule.inspect}"
36
+ # logger.debug "rexp: #{rgx.inspect}"
37
+ # logger.debug "keyp: #{ch.inspect}"
38
+ # logger.debug "subd: #{self.rule.gsub(rgx){|m| ch[m.to_sym]}}"
39
+ rgx = Regexp.new(self.dependency_conditions.map{|dc| ["a","o"].include?(dc.rule_key) ? "#{dc.rule_key}(?!nd|r)" : dc.rule_key}.join("|")) # exclude and, or
40
+ eval(self.rule.gsub(rgx){|m| ch[m.to_sym]})
41
+ end
42
+
43
+ # A hash of the conditions (keyed by rule_key) and their evaluation (boolean) in the context of response_set
44
+ def conditions_hash(response_set)
45
+ hash = {}
46
+ self.dependency_conditions.each{|dc| hash.merge!(dc.to_hash(response_set))}
47
+ return hash
48
+ end
49
+
50
+ end
@@ -0,0 +1,47 @@
1
+ class DependencyCondition < ActiveRecord::Base
2
+
3
+ # Extending surveyor
4
+ include "#{self.name}Extensions".constantize if Surveyor::Config['extend'].include?(self.name.underscore)
5
+
6
+ # Constants
7
+ OPERATORS = %w(== != < > <= >=) # CONSTANT or @@class_variable when validations listed before class method
8
+
9
+ # Associations
10
+ belongs_to :answer
11
+ belongs_to :dependency
12
+ belongs_to :dependent_question, :foreign_key => :question_id, :class_name => :question
13
+ belongs_to :question
14
+
15
+ # Validations
16
+ validates_numericality_of :dependency_id, :question_id, :answer_id
17
+ validates_presence_of :operator, :rule_key
18
+ validates_inclusion_of :operator, :in => OPERATORS
19
+ validates_uniqueness_of :rule_key, :scope => :dependency_id
20
+
21
+ acts_as_response # includes "as" instance method
22
+
23
+ # Class methods
24
+ def self.operators
25
+ OPERATORS
26
+ end
27
+
28
+ # Instance methods
29
+ def to_hash(response_set)
30
+ response = response_set.responses.detect{|r| r.answer_id.to_i == self.answer_id.to_i} || false # eval("nil and false") => nil so return false if no response is found
31
+ {rule_key.to_sym => (response and self.is_met?(response))}
32
+ end
33
+
34
+ # Checks to see if the response passed in meets the dependency condition
35
+ def is_met?(response)
36
+ klass = response.answer.response_class
37
+ return case self.operator
38
+ when "==", "<", ">", "<=", ">="
39
+ response.as(klass).send(self.operator, self.as(klass))
40
+ when "!="
41
+ !(response.as(klass) == self.as(klass))
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,54 @@
1
+ class Question < ActiveRecord::Base
2
+
3
+ # Extending surveyor
4
+ include "#{self.name}Extensions".constantize if Surveyor::Config['extend'].include?(self.name.underscore)
5
+
6
+ # Associations
7
+ belongs_to :survey_section
8
+ belongs_to :question_group
9
+ has_many :answers, :order => "display_order ASC" # it might not always have answers
10
+ has_one :dependency
11
+
12
+ # Scopes
13
+ default_scope :order => "display_order ASC"
14
+
15
+ # Validations
16
+ validates_presence_of :text, :survey_section_id, :display_order
17
+ validates_inclusion_of :is_mandatory, :in => [true, false]
18
+
19
+ # Instance Methods
20
+ def initialize(*args)
21
+ super(*args)
22
+ default_args
23
+ end
24
+
25
+ def default_args
26
+ self.is_mandatory ||= true
27
+ self.display_type ||= "default"
28
+ self.pick ||= "none"
29
+ end
30
+
31
+ def mandatory?
32
+ self.is_mandatory == true
33
+ end
34
+
35
+ def dependent?
36
+ self.dependency != nil
37
+ end
38
+ def triggered?(response_set)
39
+ dependent? ? self.dependency.is_met?(response_set) : true
40
+ end
41
+ def css_class(response_set)
42
+ [(dependent? ? "dependent" : nil), (triggered?(response_set) ? nil : "hidden"), custom_class].compact.join(" ")
43
+ end
44
+
45
+ def part_of_group?
46
+ !self.question_group.nil?
47
+ end
48
+
49
+ def renderer(g = question_group)
50
+ r = [g ? g.renderer.to_s : nil, display_type].compact.join("_")
51
+ r.blank? ? :default : r.to_sym
52
+ end
53
+
54
+ end
@@ -0,0 +1,33 @@
1
+ class QuestionGroup < ActiveRecord::Base
2
+
3
+ # Extending surveyor
4
+ include "#{self.name}Extensions".constantize if Surveyor::Config['extend'].include?(self.name.underscore)
5
+
6
+ has_many :questions
7
+ has_one :dependency
8
+
9
+ # Instance Methods
10
+ def initialize(*args)
11
+ super(*args)
12
+ default_args
13
+ end
14
+
15
+ def default_args
16
+ self.display_type ||= "inline"
17
+ end
18
+
19
+ def renderer
20
+ display_type.blank? ? :default : display_type.to_sym
21
+ end
22
+
23
+ def dependent?
24
+ self.dependency != nil
25
+ end
26
+ def triggered?(response_set)
27
+ dependent? ? self.dependency.is_met?(response_set) : true
28
+ end
29
+ def css_class(response_set)
30
+ [(dependent? ? "dependent" : nil), (triggered?(response_set) ? nil : "hidden"), custom_class].compact.join(" ")
31
+ end
32
+
33
+ end
@@ -0,0 +1,94 @@
1
+ class Response < ActiveRecord::Base
2
+
3
+ # Extending surveyor
4
+ include "#{self.name}Extensions".constantize if Surveyor::Config['extend'].include?(self.name.underscore)
5
+
6
+ include ActionView::Helpers::SanitizeHelper
7
+
8
+ # Associations
9
+ belongs_to :response_set
10
+ belongs_to :question
11
+ belongs_to :answer
12
+
13
+ # Validations
14
+ validates_presence_of :response_set_id, :question_id, :answer_id
15
+
16
+ acts_as_response # includes "as" instance method
17
+
18
+ def selected
19
+ !self.new_record?
20
+ end
21
+
22
+ alias_method :selected?, :selected
23
+
24
+ def selected=(value)
25
+ true
26
+ end
27
+
28
+ def correct?
29
+ question.correct_answer_id.nil? or self.answer.response_class != "answer" or (question.correct_answer_id.to_i == answer_id.to_i)
30
+ end
31
+
32
+ def to_s # used in dependency_explanation_helper
33
+ if self.answer.response_class == "answer" and self.answer_id
34
+ return self.answer.text
35
+ else
36
+ return "#{(self.string_value || self.text_value || self.integer_value || self.float_value || nil).to_s}"
37
+ end
38
+ end
39
+
40
+
41
+ # Error for when answer's response_class is not in
42
+ # ( answer string integer float text date time datetime )
43
+ # Actually, date and time aren't available anymore.
44
+ class InvalidResponseClass < StandardError #:nodoc:
45
+ end
46
+
47
+ # Return an individual response's question and
48
+ # answer coded for Home Exposure questionnaire.
49
+ def q_and_a_codes
50
+ q_code = self.question.data_export_identifier
51
+
52
+ unless %w( answer string integer float
53
+ text datetime
54
+ ).include?(self.answer.response_class)
55
+ raise InvalidResponseClass
56
+ end
57
+
58
+ a_code = if self.answer.response_class == "answer"
59
+ self.answer.data_export_identifier
60
+ else
61
+ self.send("#{self.answer.response_class}_value")
62
+ end
63
+ [ q_code, a_code ]
64
+ end
65
+
66
+ def q_and_a_codes_and_text
67
+ q_code = self.question.data_export_identifier
68
+
69
+ unless %w( answer string integer float
70
+ text datetime
71
+ ).include?(self.answer.response_class)
72
+ raise InvalidResponseClass
73
+ end
74
+
75
+ a_code = if self.answer.response_class == "answer"
76
+ self.answer.data_export_identifier
77
+ else
78
+ self.send("#{self.answer.response_class}_value")
79
+ end
80
+
81
+ a_text = if self.answer.response_class == "answer"
82
+ self.answer.text
83
+ else
84
+ self.send("#{self.answer.response_class}_value")
85
+ end
86
+
87
+ q_text = self.question.text
88
+
89
+ { q_code => { :a_code => a_code, :a_text => a_text, :q_text => q_text }}
90
+ end
91
+ alias_method :codes_and_text, :q_and_a_codes_and_text
92
+
93
+
94
+ end
@@ -0,0 +1,316 @@
1
+ class ResponseSet < ActiveRecord::Base
2
+
3
+ # Extending surveyor
4
+ include "#{self.name}Extensions".constantize if Surveyor::Config['extend'].include?(self.name.underscore)
5
+
6
+ # Associations
7
+ belongs_to :survey
8
+ belongs_to :user
9
+ has_many :responses, :dependent => :destroy
10
+
11
+ # Validations
12
+ validates_presence_of :survey_id
13
+ validates_associated :responses
14
+ validates_uniqueness_of :access_code
15
+
16
+ # Attributes
17
+ attr_protected :completed_at
18
+ attr_accessor :current_section_id
19
+
20
+ # Callbacks
21
+ after_update :save_responses
22
+
23
+ # Instance methods
24
+ def initialize(*args)
25
+ super(*args)
26
+ default_args
27
+ end
28
+
29
+ def default_args
30
+ self.started_at ||= Time.now
31
+ self.access_code = Surveyor.make_tiny_code
32
+ end
33
+
34
+ def access_code=(val)
35
+ while ResponseSet.find_by_access_code(val)
36
+ val = Surveyor.make_tiny_code
37
+ end
38
+ super
39
+ end
40
+
41
+ def to_csv
42
+ qcols = Question.content_columns.map(&:name) - %w(created_at updated_at)
43
+ acols = Answer.content_columns.map(&:name) - %w(created_at updated_at)
44
+ rcols = Response.content_columns.map(&:name)
45
+ require 'fastercsv'
46
+ FCSV(result = "") do |csv|
47
+ csv << qcols.map{|qcol| "question.#{qcol}"} + acols.map{|acol| "answer.#{acol}"} + rcols.map{|rcol| "response.#{rcol}"}
48
+ responses.each do |response|
49
+ csv << qcols.map{|qcol| response.question.send(qcol)} + acols.map{|acol| response.answer.send(acol)} + rcols.map{|rcol| response.send(rcol)}
50
+ end
51
+ end
52
+ result
53
+ end
54
+
55
+ def response_for(question_id, answer_id, group = nil)
56
+ found = responses.detect{|r| r.question_id == question_id && r.answer_id == answer_id && r.response_group.to_s == group.to_s}
57
+ found.blank? ? responses.new(:question_id => question_id, :answer_id => answer_id, :response_group => group) : found
58
+ end
59
+
60
+ def clear_responses
61
+ question_ids = Question.find_all_by_survey_section_id(current_section_id).map(&:id)
62
+ responses.select{|r| question_ids.include? r.question_id}.map(&:destroy)
63
+ responses.reload
64
+ end
65
+
66
+ def response_attributes=(response_attributes)
67
+ response_attributes.each do |question_id, responses_hash|
68
+ # Response.delete_all(["response_set_id =? AND question_id =?", self.id, question_id])
69
+ if (answer_id = responses_hash[:answer_id])
70
+ if (!responses_hash[:answer_id].empty?) # Dropdowns return answer id but have an empty value if they are not set... ignoring those.
71
+ #radio or dropdown - only one response
72
+ responses.build({:question_id => question_id, :answer_id => answer_id}.merge(responses_hash[answer_id] || {}))
73
+ end
74
+ else
75
+ #possibly multiples responses - unresponded radios end up here too
76
+ # we use the variable question_id, not the "question_id" in the response_hash
77
+ responses_hash.delete_if{|k,v| k == "question_id"}.each do |answer_id, response_hash|
78
+ unless response_hash.delete_if{|k,v| v.blank?}.empty?
79
+ responses.build({:question_id => question_id, :answer_id => answer_id}.merge(response_hash))
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def response_group_attributes=(response_attributes)
87
+ response_attributes.each do |question_id, responses_group_hash|
88
+ # Response.delete_all(["response_set_id =? AND question_id =?", self.id, question_id])
89
+ responses_group_hash.each do |response_group_number, group_hash|
90
+ if (answer_id = group_hash[:answer_id]) # if group_hash has an answer_id key we treat it differently
91
+ if (!group_hash[:answer_id].empty?) # dropdowns return empty values in answer_ids if they are not selected
92
+ #radio or dropdown - only one response
93
+ responses.build({:question_id => question_id, :answer_id => answer_id, :response_group => response_group_number}.merge(group_hash[answer_id] || {}))
94
+ end
95
+ else
96
+ #possibly multiples responses - unresponded radios end up here too
97
+ # we use the variable question_id in the key, not the "question_id" in the response_hash... same with response_group key
98
+ group_hash.delete_if{|k,v| (k == "question_id") or (k == "response_group")}.each do |answer_id, inner_hash|
99
+ unless inner_hash.delete_if{|k,v| v.blank?}.empty?
100
+ responses.build({:question_id => question_id, :answer_id => answer_id, :response_group => response_group_number}.merge(inner_hash))
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+ end
108
+
109
+ def save_responses
110
+ responses.each{|response| response.save(false)}
111
+ end
112
+
113
+ def complete!
114
+ self.completed_at = Time.now
115
+ end
116
+
117
+ def correct?
118
+ responses.all?(&:correct?)
119
+ end
120
+ def correctness_hash
121
+ { :questions => survey.sections_with_questions.map(&:questions).flatten.compact.size,
122
+ :responses => responses.compact.size,
123
+ :correct => responses.find_all(&:correct?).compact.size
124
+ }
125
+ end
126
+ def mandatory_questions_complete?
127
+ progress_hash[:triggered_mandatory] == progress_hash[:triggered_mandatory_completed]
128
+ end
129
+ def progress_hash
130
+ qs = survey.sections_with_questions.map(&:questions).flatten
131
+ ds = dependencies(qs.map(&:id))
132
+ triggered = qs - ds.select{|d| !d.is_met?(self)}.map(&:question)
133
+ { :questions => qs.compact.size,
134
+ :triggered => triggered.compact.size,
135
+ :triggered_mandatory => triggered.select{|q| q.mandatory?}.compact.size,
136
+ :triggered_mandatory_completed => triggered.select{|q| q.mandatory? and is_answered?(q)}.compact.size
137
+ }
138
+ end
139
+ def is_answered?(question)
140
+ %w(label image).include?(question.display_type) or !is_unanswered?(question)
141
+ end
142
+ def is_unanswered?(question)
143
+ self.responses.detect{|r| r.question_id == question.id}.nil?
144
+ end
145
+
146
+ # Returns the number of response groups (count of group responses enterted) for this question group
147
+ def count_group_responses(questions)
148
+ questions.map{|q| responses.select{|r| (r.question_id.to_i == q.id.to_i) && !r.response_group.nil?}.group_by(&:response_group).size }.max
149
+ end
150
+
151
+ def unanswered_dependencies
152
+ dependencies.select{|d| d.is_met?(self) and self.is_unanswered?(d.question)}.map(&:question)
153
+ end
154
+
155
+
156
+ # Why return nil, when you can return a real value.
157
+ # This is untested so there may be some consequences.
158
+ # I only had it for use in the console. Commenting.
159
+ # def current_section_id
160
+ # # Do all surveys have a first section???
161
+ # @current_section_id || self.survey.sections.first.id
162
+ # end
163
+
164
+ def all_validations
165
+ arr = validations.partition{|v| v.is_valid?(self) }
166
+ { :valid => arr[0].map{|v| "question_#{v.answer.question_id}"},
167
+ :invalid => arr[1].map{|v| "question_#{v.answer.question_id}"} }
168
+ end
169
+
170
+ def all_things_hash
171
+ self.all_dependencies.merge(self.all_validations)
172
+ end
173
+
174
+ def all_dependencies
175
+ arr = dependencies.partition{|d| d.is_met?(self) }
176
+ {:show => arr[0].map{|d| d.question_group_id.nil? ? "question_#{d.question_id}" : "question_group_#{d.question_group_id}"}, :hide => arr[1].map{|d| d.question_group_id.nil? ? "question_#{d.question_id}" : "question_group_#{d.question_group_id}"}}
177
+ end
178
+
179
+
180
+
181
+ # Collect all of the question and answers coded for
182
+ # the Home Exposures questionnaire.
183
+ #
184
+ # >> ResponseSet.last.q_and_a_codes
185
+ #
186
+ # => [["how_often_vacuumed_12mos", "1"],
187
+ # ["freq_grilled_meat_outside_12mos", "2"],
188
+ # ["other_pest_community_sprayed", "dogs"],
189
+ # ["year_home_built", 1900],
190
+ # ["number_of_rooms_in_home", 5]]
191
+ def q_and_a_codes
192
+ self.responses.collect(&:q_and_a_codes)
193
+ end
194
+
195
+ # Collect all of the question and answers coded for
196
+ # the Home Exposures questionnaire.
197
+ #
198
+ # >> ResponseSet.last.q_and_a_codes_as_attributes
199
+ #
200
+ # => {"doneness_of_meat_exterior_12mos"=>"3",
201
+ # "number_of_rooms_in_home"=>5,
202
+ # "year_home_built"=>1900,
203
+ # "cmty_sprayed_other_pest_12mos"=>"1",
204
+ # "other_pest_community_sprayed"=>"dogs"}
205
+ #
206
+ # >> HomeExposureResponse.create(
207
+ # ResponseSet.find(7).q_and_a_codes_as_attributes)
208
+ def q_and_a_codes_as_attributes
209
+ Hash[*self.responses.collect(&:q_and_a_codes).flatten]
210
+ end
211
+
212
+ # Return a good chunk of info used when merging
213
+ # response sets into one home exposure response
214
+ def q_and_a_codes_and_text_as_attributes
215
+ h=Hash.new({:a_code => '', :a_text => '(no answer)', :q_text => ''})
216
+ # h.merge keeps h's defaults!!! woohoo!
217
+ h.merge(self.responses.collect(&:codes_and_text).inject(:merge)||{})
218
+ end
219
+ alias_method :codes_and_text, :q_and_a_codes_and_text_as_attributes
220
+
221
+ # Compare the Q and A codes of 2 response sets
222
+ # and return boolean.
223
+ def is_the_same_as?(another_response_set)
224
+ ars = ResponseSet.find(another_response_set)
225
+ (self.q_and_a_codes_as_attributes.diff(
226
+ ars.q_and_a_codes_as_attributes)).blank?
227
+ end
228
+
229
+ # Compare the Q and A codes of 2 response sets
230
+ # and return hash of differences.
231
+ def diff(another_response_set)
232
+ ars = ResponseSet.find(another_response_set)
233
+ self.q_and_a_codes_as_attributes.diff(ars.q_and_a_codes_as_attributes)
234
+ end
235
+
236
+ def is_complete?
237
+ # eventually return the is_complete column value
238
+ # false
239
+ !self.completed_at.nil?
240
+ end
241
+
242
+
243
+
244
+
245
+
246
+ protected
247
+
248
+ def validations(question_ids = nil)
249
+ question_ids ||= Question.find_all_by_survey_section_id(
250
+ current_section_id).map(&:id)
251
+ answer_ids = Answer.all(
252
+ :conditions => {:question_id => question_ids }).map(&:id)
253
+ Validation.all(:conditions => {:answer_id => answer_ids},
254
+ :include => :validation_conditions)
255
+ end
256
+
257
+ def dependencies(question_ids = nil)
258
+ question_ids ||= Question.find_all_by_survey_section_id(current_section_id).map(&:id)
259
+ depdendecy_ids = DependencyCondition.all(:conditions => {:question_id => question_ids}).map(&:dependency_id)
260
+ Dependency.find(depdendecy_ids, :include => :dependency_conditions)
261
+ end
262
+
263
+ end
264
+
265
+ # responses
266
+
267
+ # "responses"=>{
268
+ #string "6"=>{"question_id"=>"6", "20"=>{"string_value"=>"saf"}},
269
+ #text "7"=>{"question_id"=>"7", "21"=>{"text_value"=>""}},
270
+ #radio+txt "1"=>{"question_id"=>"1", "answer_id"=>"1", "4"=>{"string_value"=>""}},
271
+ #radio "2"=>{"answer_id"=>"6"},
272
+ #radio "3"=>{"answer_id"=>"10"},
273
+ #check "4"=>{"question_id"=>"4", "answer_id"=>"15"},
274
+ #check+txt "5"=>{"question_id"=>"5", "16"=>{"selected"=>"1"}, "19"=>{"string_value"=>""}}
275
+ # },
276
+ # "survey_code"=>"test_survey",
277
+ # "commit"=>"Next Section (Utensiles and you!) >>",
278
+ # "authenticity_token"=>"8bee21081eea820ab1c658358c0baaa2e46de5d1",
279
+ # "_method"=>"put",
280
+ # "action"=>"update",
281
+ # "controller"=>"app",
282
+ # "response_set_code"=>"T2x8HhCQej",
283
+ # "section"=>"2"
284
+
285
+ # response groups
286
+
287
+ # "24"=>{
288
+ # "0"=>{"response_group"=>"0", "question_id"=>"24", "answer_id"=>"172"}, "1"=>{"response_group"=>"1", "question_id"=>"24", "answer_id"=>"173"},
289
+ # "2"=>{"response_group"=>"2", "question_id"=>"24", "answer_id"=>""}, "3"=>{"response_group"=>"3", "question_id"=>"24", "answer_id"=>""},
290
+ # "4"=>{"response_group"=>"4", "question_id"=>"24", "answer_id"=>""}},
291
+ # where "24" is the question id
292
+
293
+ # Some other examples:
294
+ # "25"=>{
295
+ # "0"=>{"response_group"=>"0", "question_id"=>"25", "179"=>{"string_value"=>"camry"}},
296
+ # "1"=>{"response_group"=>"1", "question_id"=>"25", "179"=>{"string_value"=>"f150"}},
297
+ # "2"=>{"response_group"=>"2", "question_id"=>"25", "179"=>{"string_value"=>""}},
298
+ # "3"=>{"response_group"=>"3", "question_id"=>"25", "179"=>{"string_value"=>""}},
299
+ # "4"=>{"response_group"=>"4", "question_id"=>"25", "179"=>{"string_value"=>""}}},
300
+ #
301
+ # "26"=>{
302
+ # "0"=>{"response_group"=>"0", "question_id"=>"26", "180"=>{"string_value"=>"1999"}},
303
+ # "1"=>{"response_group"=>"1", "question_id"=>"26", "180"=>{"string_value"=>"2004"}},
304
+ # "2"=>{"response_group"=>"2", "question_id"=>"26", "180"=>{"string_value"=>""}},
305
+ # "3"=>{"response_group"=>"3", "question_id"=>"26", "180"=>{"string_value"=>""}},
306
+ # "4"=>{"response_group"=>"4", "question_id"=>"26", "180"=>{"string_value"=>""}}},
307
+ #
308
+ # "27"=>{
309
+ # "0"=>{"182"=>{"integer_value"=>""}, "response_group"=>"0", "question_id"=>"27", "181"=>{"string_value"=>""}},
310
+ # "1"=>{"182"=>{"integer_value"=>""}, "response_group"=>"1", "question_id"=>"27", "181"=>{"string_value"=>""}},
311
+ # "2"=>{"182"=>{"integer_value"=>""}, "response_group"=>"2", "question_id"=>"27", "181"=>{"string_value"=>""}},
312
+ # "3"=>{"182"=>{"integer_value"=>""}, "response_group"=>"3", "question_id"=>"27", "181"=>{"string_value"=>""}},
313
+ # "4"=>{"182"=>{"integer_value"=>""}, "response_group"=>"4", "question_id"=>"27", "181"=>{"string_value"=>""}}}},
314
+
315
+ # 0,1,2,3,4 are the response group numbers
316
+ # and anything else in the response group hash is handled normally