surveyor 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. data/.gitignore +6 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +104 -0
  4. data/Rakefile +50 -0
  5. data/VERSION +1 -0
  6. data/app/controllers/surveyor_controller.rb +117 -0
  7. data/app/helpers/survey_form_builder.rb +37 -0
  8. data/app/helpers/surveyor_helper.rb +66 -0
  9. data/app/models/answer.rb +21 -0
  10. data/app/models/dependency.rb +46 -0
  11. data/app/models/dependency_condition.rb +79 -0
  12. data/app/models/question.rb +54 -0
  13. data/app/models/question_group.rb +18 -0
  14. data/app/models/response.rb +47 -0
  15. data/app/models/response_set.rb +185 -0
  16. data/app/models/survey.rb +57 -0
  17. data/app/models/survey_section.rb +15 -0
  18. data/app/views/layouts/surveyor_default.html.erb +13 -0
  19. data/app/views/partials/_answer.html.haml +49 -0
  20. data/app/views/partials/_question.html.haml +73 -0
  21. data/app/views/partials/_question_group.html.haml +41 -0
  22. data/app/views/surveyor/edit.html.haml +32 -0
  23. data/app/views/surveyor/new.html.haml +17 -0
  24. data/app/views/surveyor/show.html.haml +12 -0
  25. data/config/routes.rb +10 -0
  26. data/generators/surveyor/EXTENDING_SURVEYOR +12 -0
  27. data/generators/surveyor/extend_surveyor_generator.rb +22 -0
  28. data/generators/surveyor/surveyor_generator.rb +60 -0
  29. data/generators/surveyor/templates/README +10 -0
  30. data/generators/surveyor/templates/assets/images/222222_11x11_icon_arrows_leftright.gif +0 -0
  31. data/generators/surveyor/templates/assets/images/222222_11x11_icon_arrows_updown.gif +0 -0
  32. data/generators/surveyor/templates/assets/images/222222_11x11_icon_close.gif +0 -0
  33. data/generators/surveyor/templates/assets/images/222222_11x11_icon_doc.gif +0 -0
  34. data/generators/surveyor/templates/assets/images/222222_11x11_icon_folder_closed.gif +0 -0
  35. data/generators/surveyor/templates/assets/images/222222_11x11_icon_folder_open.gif +0 -0
  36. data/generators/surveyor/templates/assets/images/222222_11x11_icon_minus.gif +0 -0
  37. data/generators/surveyor/templates/assets/images/222222_11x11_icon_plus.gif +0 -0
  38. data/generators/surveyor/templates/assets/images/222222_11x11_icon_resize_se.gif +0 -0
  39. data/generators/surveyor/templates/assets/images/222222_35x9_colorpicker_indicator.gif.gif +0 -0
  40. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_down.gif +0 -0
  41. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_left.gif +0 -0
  42. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_right.gif +0 -0
  43. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_up.gif +0 -0
  44. data/generators/surveyor/templates/assets/images/454545_11x11_icon_arrows_leftright.gif +0 -0
  45. data/generators/surveyor/templates/assets/images/454545_11x11_icon_arrows_updown.gif +0 -0
  46. data/generators/surveyor/templates/assets/images/454545_11x11_icon_close.gif +0 -0
  47. data/generators/surveyor/templates/assets/images/454545_11x11_icon_doc.gif +0 -0
  48. data/generators/surveyor/templates/assets/images/454545_11x11_icon_folder_closed.gif +0 -0
  49. data/generators/surveyor/templates/assets/images/454545_11x11_icon_folder_open.gif +0 -0
  50. data/generators/surveyor/templates/assets/images/454545_11x11_icon_minus.gif +0 -0
  51. data/generators/surveyor/templates/assets/images/454545_11x11_icon_plus.gif +0 -0
  52. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_down.gif +0 -0
  53. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_left.gif +0 -0
  54. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_right.gif +0 -0
  55. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_up.gif +0 -0
  56. data/generators/surveyor/templates/assets/images/888888_11x11_icon_arrows_leftright.gif +0 -0
  57. data/generators/surveyor/templates/assets/images/888888_11x11_icon_arrows_updown.gif +0 -0
  58. data/generators/surveyor/templates/assets/images/888888_11x11_icon_close.gif +0 -0
  59. data/generators/surveyor/templates/assets/images/888888_11x11_icon_doc.gif +0 -0
  60. data/generators/surveyor/templates/assets/images/888888_11x11_icon_folder_closed.gif +0 -0
  61. data/generators/surveyor/templates/assets/images/888888_11x11_icon_folder_open.gif +0 -0
  62. data/generators/surveyor/templates/assets/images/888888_11x11_icon_minus.gif +0 -0
  63. data/generators/surveyor/templates/assets/images/888888_11x11_icon_plus.gif +0 -0
  64. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_down.gif +0 -0
  65. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_left.gif +0 -0
  66. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_right.gif +0 -0
  67. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_up.gif +0 -0
  68. data/generators/surveyor/templates/assets/images/dadada_40x100_textures_02_glass_75.png +0 -0
  69. data/generators/surveyor/templates/assets/images/e6e6e6_40x100_textures_02_glass_75.png +0 -0
  70. data/generators/surveyor/templates/assets/images/ffffff_40x100_textures_01_flat_0.png +0 -0
  71. data/generators/surveyor/templates/assets/images/ffffff_40x100_textures_02_glass_65.png +0 -0
  72. data/generators/surveyor/templates/assets/javascripts/accessibleUISlider.jQuery.js +201 -0
  73. data/generators/surveyor/templates/assets/javascripts/jquery-1.2.6.js +3549 -0
  74. data/generators/surveyor/templates/assets/javascripts/jquery-ui-personalized-1.5.3.js +7616 -0
  75. data/generators/surveyor/templates/assets/javascripts/jquery.form.js +637 -0
  76. data/generators/surveyor/templates/assets/javascripts/surveyor.js +28 -0
  77. data/generators/surveyor/templates/assets/stylesheets/jquery-ui-slider-additions.css +71 -0
  78. data/generators/surveyor/templates/assets/stylesheets/reset.css +46 -0
  79. data/generators/surveyor/templates/assets/stylesheets/sass/surveyor.sass +245 -0
  80. data/generators/surveyor/templates/assets/stylesheets/surveyor.css +231 -0
  81. data/generators/surveyor/templates/assets/stylesheets/ui.theme.css +851 -0
  82. data/generators/surveyor/templates/extensions/survey_extensions.rb +18 -0
  83. data/generators/surveyor/templates/extensions/surveyor_controller_extensions.rb +28 -0
  84. data/generators/surveyor/templates/extensions/surveyor_custom.html.erb +13 -0
  85. data/generators/surveyor/templates/extensions/surveyor_helper_extensions.rb +18 -0
  86. data/generators/surveyor/templates/initializers/haml.rb +8 -0
  87. data/generators/surveyor/templates/initializers/surveyor.rb +11 -0
  88. data/generators/surveyor/templates/migrate/create_answers.rb +45 -0
  89. data/generators/surveyor/templates/migrate/create_dependencies.rb +21 -0
  90. data/generators/surveyor/templates/migrate/create_dependency_conditions.rb +29 -0
  91. data/generators/surveyor/templates/migrate/create_question_groups.rb +18 -0
  92. data/generators/surveyor/templates/migrate/create_questions.rb +33 -0
  93. data/generators/surveyor/templates/migrate/create_response_sets.rb +22 -0
  94. data/generators/surveyor/templates/migrate/create_responses.rb +33 -0
  95. data/generators/surveyor/templates/migrate/create_survey_sections.rb +25 -0
  96. data/generators/surveyor/templates/migrate/create_surveys.rb +25 -0
  97. data/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +201 -0
  98. data/generators/surveyor/templates/tasks/surveyor.rb +4 -0
  99. data/init.rb +1 -0
  100. data/install.rb +1 -0
  101. data/lib/surveyor.rb +7 -0
  102. data/lib/surveyor/config.rb +45 -0
  103. data/lib/tasks/surveyor_tasks.rake +33 -0
  104. data/lib/tiny_code.rb +58 -0
  105. data/lib/xml_formatter.rb +12 -0
  106. data/script/surveyor/answer.rb +84 -0
  107. data/script/surveyor/columnizer.rb +36 -0
  108. data/script/surveyor/dependency.rb +43 -0
  109. data/script/surveyor/dependency_condition.rb +74 -0
  110. data/script/surveyor/question.rb +76 -0
  111. data/script/surveyor/question_group.rb +33 -0
  112. data/script/surveyor/specs/answer_spec.rb +66 -0
  113. data/script/surveyor/specs/question_dependency_spec.rb +46 -0
  114. data/script/surveyor/specs/question_group_spec.rb +9 -0
  115. data/script/surveyor/specs/question_spec.rb +111 -0
  116. data/script/surveyor/specs/section_spec.rb +58 -0
  117. data/script/surveyor/survey.rb +108 -0
  118. data/script/surveyor/survey_parser.rb +64 -0
  119. data/script/surveyor/survey_section.rb +153 -0
  120. data/script/surveyor/whr_dsl.tmproj +244 -0
  121. data/spec/controllers/surveyor_controller_spec.rb +328 -0
  122. data/spec/factories.rb +107 -0
  123. data/spec/models/answer_spec.rb +29 -0
  124. data/spec/models/dependency_condition_spec.rb +338 -0
  125. data/spec/models/dependency_spec.rb +82 -0
  126. data/spec/models/question_group_spec.rb +11 -0
  127. data/spec/models/question_spec.rb +75 -0
  128. data/spec/models/response_set_spec.rb +153 -0
  129. data/spec/models/response_spec.rb +94 -0
  130. data/spec/models/survey_section_spec.rb +34 -0
  131. data/spec/models/survey_spec.rb +72 -0
  132. data/spec/rcov.opts +2 -0
  133. data/spec/spec.opts +4 -0
  134. data/spec/spec_helper.rb +14 -0
  135. data/surveyor.gemspec +187 -0
  136. data/uninstall.rb +1 -0
  137. metadata +211 -0
@@ -0,0 +1,79 @@
1
+ class DependencyCondition < ActiveRecord::Base
2
+ # Constants
3
+ OPERATORS = %w(== != < > <= >=) # CONSTANT or @@class_variable when validations listed before class method
4
+
5
+ # Associations
6
+ belongs_to :answer
7
+ belongs_to :dependency
8
+ belongs_to :dependent_question, :foreign_key => :question_id, :class_name => :question
9
+
10
+ # Validations
11
+ validates_numericality_of :dependency_id, :question_id, :answer_id
12
+ validates_presence_of :operator, :rule_key
13
+ validates_inclusion_of :operator, :in => OPERATORS
14
+ validates_uniqueness_of :rule_key, :scope => :dependency_id
15
+
16
+ # Class methods
17
+ def self.operators
18
+ OPERATORS
19
+ end
20
+
21
+ # Instance methods
22
+
23
+ # The hash used in the dependency parent object to evaluate its rule string
24
+ def to_evaluation_hash(response_set)
25
+ x = {rule_key.to_sym => self.evaluation_of(response_set)}
26
+ # logger.warn "to_evaluation_hash #{x.inspect}"
27
+ return x
28
+ end
29
+
30
+ # # Evaluates the condition on the response_set
31
+ # def evaluation_of(response_set)
32
+ # response = response_set.responses.detect{|r| r.answer_id == answer_id} || false # turns out eval("nil and false") => nil so we need to return false if no response is found
33
+ # return(response and self.is_satisfied_by?(response))
34
+ # end
35
+ # Evaluates the condition on the response_set
36
+ def evaluation_of(response_set)
37
+ # response = response_set.find_response(self.answer_id) || false # turns out eval("nil and false") => nil so we need to return false if no response is found
38
+ response = response_set.responses.detect{|r| r.answer_id.to_i == self.answer_id.to_i} || false # turns out eval("nil and false") => nil so we need to return false if no response is found
39
+ # logger.warn "evaluation_of_response #{response.inspect}"
40
+ return(response and self.is_satisfied_by?(response))
41
+ end
42
+
43
+ # Checks to see if the response passed in satisfies the dependency condition
44
+ def is_satisfied_by?(response)
45
+ response_class = response.answer.response_class
46
+ # response.as(response_class).send(operator.to_sym, self.as(response_class))
47
+ return case self.operator
48
+ when "=="
49
+ response.as(response_class) == self.as(response_class)
50
+ when "!="
51
+ response.as(response_class) != self.as(response_class)
52
+ when "<"
53
+ response.as(response_class) < self.as(response_class)
54
+ when ">"
55
+ response.as(response_class) > self.as(response_class)
56
+ when "<="
57
+ response.as(response_class) <= self.as(response_class)
58
+ when ">="
59
+ response.as(response_class) >= self.as(response_class)
60
+ else
61
+ false
62
+ end
63
+ end
64
+
65
+ # Method that returns the dependency as a particular response_class type
66
+ def as(type_symbol)
67
+ return case type_symbol.to_sym
68
+ when :string, :text, :integer, :float, :datetime
69
+ self.send("#{type_symbol}_value".to_sym)
70
+ when :date
71
+ self.datetime_value.nil? ? nil : self.datetime_value.to_date
72
+ when :time
73
+ self.datetime_value.nil? ? nil : self.datetime_value.to_time
74
+ else # :answer_id
75
+ self.answer_id
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,54 @@
1
+ class Question < ActiveRecord::Base
2
+
3
+ # Associations
4
+ belongs_to :survey_section
5
+ belongs_to :question_group
6
+ has_many :answers # it might not always have answers
7
+ has_one :dependency
8
+
9
+ # Scopes
10
+ default_scope :order => "display_order ASC"
11
+
12
+ # Validations
13
+ validates_presence_of :text, :survey_section_id, :display_order
14
+ validates_inclusion_of :is_mandatory, :in => [true, false]
15
+
16
+ # Instance Methods
17
+ def initialize(*args)
18
+ super(*args)
19
+ default_args
20
+ end
21
+
22
+ def default_args
23
+ self.is_mandatory ||= true
24
+ self.display_type = "default"
25
+ self.pick = "none"
26
+ end
27
+
28
+ def mandatory?
29
+ self.is_mandatory == true
30
+ end
31
+
32
+ def dependent?
33
+ self.dependency != nil
34
+ end
35
+ def triggered?(response_set)
36
+ dependent? ? self.dependency.met?(response_set) : true
37
+ end
38
+ def dep_class(response_set)
39
+ dependent? ? triggered?(response_set) ? "dependent" : "hidden dependent" : nil
40
+ end
41
+ def dependency_satisfied?(response_set)
42
+ self.has_dependency? and self.dependency.met?(response_set)
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,18 @@
1
+ class QuestionGroup < ActiveRecord::Base
2
+
3
+ has_many :questions
4
+
5
+ # Instance Methods
6
+ def initialize(*args)
7
+ super(*args)
8
+ default_args
9
+ end
10
+
11
+ def default_args
12
+ self.display_type = "inline"
13
+ end
14
+
15
+ def renderer
16
+ display_type.blank? ? :default : display_type.to_sym
17
+ end
18
+ end
@@ -0,0 +1,47 @@
1
+ class Response < ActiveRecord::Base
2
+ include ActionView::Helpers::SanitizeHelper
3
+
4
+ # Associations
5
+ belongs_to :response_set
6
+ belongs_to :question
7
+ belongs_to :answer
8
+
9
+ # Validations
10
+ validates_presence_of :response_set_id, :question_id, :answer_id
11
+
12
+ # Named scopes
13
+ named_scope :in_section, lambda {|section_id| {:include => :question, :conditions => ['questions.survey_section_id =?', section_id.to_i ]}}
14
+
15
+ def selected
16
+ !self.new_record?
17
+ end
18
+
19
+ alias_method :selected?, :selected
20
+
21
+ def selected=(value)
22
+ true
23
+ end
24
+
25
+ #Method that returns the response as a particular response_class type
26
+ def as(type_symbol)
27
+ return case type_symbol.to_sym
28
+ when :string, :text, :integer, :float, :datetime
29
+ self.send("#{type_symbol}_value".to_sym)
30
+ when :date
31
+ self.datetime_value.nil? ? nil : self.datetime_value.to_date
32
+ when :time
33
+ self.datetime_value.nil? ? nil : self.datetime_value.to_time
34
+ else # :answer_id
35
+ self.answer_id
36
+ end
37
+ end
38
+
39
+ def to_s
40
+ if self.answer.response_class == "answer" and self.answer_id
41
+ return self.answer.text
42
+ else
43
+ return "#{(self.string_value || self.text_value || self.integer_value || self.float_value || nil).to_s}"
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,185 @@
1
+ class ResponseSet < ActiveRecord::Base
2
+ include TinyCode
3
+
4
+ # Associations
5
+ belongs_to :survey
6
+ belongs_to :user
7
+ has_many :responses, :dependent => :destroy
8
+
9
+ # Validations
10
+ validates_presence_of :survey_id
11
+ validates_associated :responses
12
+
13
+ # Attributes
14
+ attr_protected :completed_at
15
+ attr_accessor :current_section_id
16
+
17
+ # Callbacks
18
+ after_update :save_responses
19
+
20
+ # Instance methods
21
+ def initialize(*args)
22
+ super(*args)
23
+ default_args
24
+ end
25
+
26
+ def default_args
27
+ self.started_at ||= Time.now
28
+ self.access_code = ResponseSet.make_tiny_code
29
+ end
30
+
31
+ def response_for(question_id, answer_id, group = nil)
32
+ response_group = group.nil? ? nil : group.to_s # for PostgreSql 8.3 compatibility - response_group is a string
33
+ # found = responses.find_by_question_id_and_answer_id_and_response_group(question_id, answer_id, response_group)
34
+ found = responses.detect{|r| r.question_id == question_id && r.answer_id == answer_id && r.response_group == response_group}
35
+ found.blank? ? responses.new(:question_id => question_id, :answer_id => answer_id, :response_group => response_group) : found
36
+ end
37
+
38
+ def clear_responses
39
+ question_ids = Question.find_all_by_survey_section_id(current_section_id).map(&:id)
40
+ # logger.warn "responses: #{responses.inspect}\n"
41
+ responses.select{|r| question_ids.include? r.question_id}.map(&:destroy)
42
+ responses.reload
43
+ # logger.warn "responses: #{responses.inspect}\n"
44
+ end
45
+
46
+ # "responses"=>{
47
+ #string "6"=>{"question_id"=>"6", "20"=>{"string_value"=>"saf"}},
48
+ #text "7"=>{"question_id"=>"7", "21"=>{"text_value"=>""}},
49
+ #radio+txt "1"=>{"question_id"=>"1", "answer_id"=>"1", "4"=>{"string_value"=>""}},
50
+ #radio "2"=>{"answer_id"=>"6"},
51
+ #radio "3"=>{"answer_id"=>"10"},
52
+ #check "4"=>{"question_id"=>"4", "answer_id"=>"15"},
53
+ #check+txt "5"=>{"question_id"=>"5", "16"=>{"selected"=>"1"}, "19"=>{"string_value"=>""}}
54
+ # },
55
+ # "survey_code"=>"test_survey",
56
+ # "commit"=>"Next Section (Utensiles and you!) >>",
57
+ # "authenticity_token"=>"8bee21081eea820ab1c658358c0baaa2e46de5d1",
58
+ # "_method"=>"put",
59
+ # "action"=>"update",
60
+ # "controller"=>"app",
61
+ # "response_set_code"=>"T2x8HhCQej",
62
+ # "section"=>"2"
63
+
64
+ def response_attributes=(response_attributes)
65
+ response_attributes.each do |question_id, responses_hash|
66
+ # Response.delete_all(["response_set_id =? AND question_id =?", self.id, question_id])
67
+ if (answer_id = responses_hash[:answer_id])
68
+ if (!responses_hash[:answer_id].empty?) # Dropdowns return answer id but have an empty value if they are not set... ignoring those.
69
+ #radio or dropdown - only one response
70
+ responses.build({:question_id => question_id, :answer_id => answer_id}.merge(responses_hash[answer_id] || {}))
71
+ end
72
+ else
73
+ #possibly multiples responses - unresponded radios end up here too
74
+ # we use the variable question_id, not the "question_id" in the response_hash
75
+ responses_hash.delete_if{|k,v| k == "question_id"}.each do |answer_id, response_hash|
76
+ unless response_hash.delete_if{|k,v| v.blank?}.empty?
77
+ responses.build({:question_id => question_id, :answer_id => answer_id}.merge(response_hash))
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ # "24"=>{
85
+ # "0"=>{"response_group"=>"0", "question_id"=>"24", "answer_id"=>"172"}, "1"=>{"response_group"=>"1", "question_id"=>"24", "answer_id"=>"173"},
86
+ # "2"=>{"response_group"=>"2", "question_id"=>"24", "answer_id"=>""}, "3"=>{"response_group"=>"3", "question_id"=>"24", "answer_id"=>""},
87
+ # "4"=>{"response_group"=>"4", "question_id"=>"24", "answer_id"=>""}},
88
+ # where "24" is the question id
89
+
90
+ # Some other examples:
91
+ # "25"=>{
92
+ # "0"=>{"response_group"=>"0", "question_id"=>"25", "179"=>{"string_value"=>"camry"}},
93
+ # "1"=>{"response_group"=>"1", "question_id"=>"25", "179"=>{"string_value"=>"f150"}},
94
+ # "2"=>{"response_group"=>"2", "question_id"=>"25", "179"=>{"string_value"=>""}},
95
+ # "3"=>{"response_group"=>"3", "question_id"=>"25", "179"=>{"string_value"=>""}},
96
+ # "4"=>{"response_group"=>"4", "question_id"=>"25", "179"=>{"string_value"=>""}}},
97
+ #
98
+ # "26"=>{
99
+ # "0"=>{"response_group"=>"0", "question_id"=>"26", "180"=>{"string_value"=>"1999"}},
100
+ # "1"=>{"response_group"=>"1", "question_id"=>"26", "180"=>{"string_value"=>"2004"}},
101
+ # "2"=>{"response_group"=>"2", "question_id"=>"26", "180"=>{"string_value"=>""}},
102
+ # "3"=>{"response_group"=>"3", "question_id"=>"26", "180"=>{"string_value"=>""}},
103
+ # "4"=>{"response_group"=>"4", "question_id"=>"26", "180"=>{"string_value"=>""}}},
104
+ #
105
+ # "27"=>{
106
+ # "0"=>{"182"=>{"integer_value"=>""}, "response_group"=>"0", "question_id"=>"27", "181"=>{"string_value"=>""}},
107
+ # "1"=>{"182"=>{"integer_value"=>""}, "response_group"=>"1", "question_id"=>"27", "181"=>{"string_value"=>""}},
108
+ # "2"=>{"182"=>{"integer_value"=>""}, "response_group"=>"2", "question_id"=>"27", "181"=>{"string_value"=>""}},
109
+ # "3"=>{"182"=>{"integer_value"=>""}, "response_group"=>"3", "question_id"=>"27", "181"=>{"string_value"=>""}},
110
+ # "4"=>{"182"=>{"integer_value"=>""}, "response_group"=>"4", "question_id"=>"27", "181"=>{"string_value"=>""}}}},
111
+
112
+ # 0,1,2,3,4 are the response group numbers
113
+ # and anything else in the response group hash is handled normally
114
+
115
+ # method to process responses in response groups
116
+ def response_group_attributes=(response_attributes)
117
+ response_attributes.each do |question_id, responses_group_hash|
118
+ # Response.delete_all(["response_set_id =? AND question_id =?", self.id, question_id])
119
+ responses_group_hash.each do |response_group_number, group_hash|
120
+ if (answer_id = group_hash[:answer_id]) # if group_hash has an answer_id key we treat it differently
121
+ if (!group_hash[:answer_id].empty?) # dropdowns return empty values in answer_ids if they are not selected
122
+ #radio or dropdown - only one response
123
+ responses.build({:question_id => question_id, :answer_id => answer_id, :response_group => response_group_number}.merge(group_hash[answer_id] || {}))
124
+ end
125
+ else
126
+ #possibly multiples responses - unresponded radios end up here too
127
+ # we use the variable question_id in the key, not the "question_id" in the response_hash... same with response_group key
128
+ group_hash.delete_if{|k,v| (k == "question_id") or (k == "response_group")}.each do |answer_id, inner_hash|
129
+ unless inner_hash.delete_if{|k,v| v.blank?}.empty?
130
+ responses.build({:question_id => question_id, :answer_id => answer_id, :response_group => response_group_number}.merge(inner_hash))
131
+ end
132
+ end
133
+ end
134
+
135
+ end
136
+ end
137
+ end
138
+
139
+ def find_response(answer_id)
140
+ self.responses.find_by_answer_id(answer_id)
141
+ end
142
+
143
+ def save_responses
144
+ responses.each do |response|
145
+ response.save(false)
146
+ end
147
+ end
148
+
149
+ # ResponseSet has an awareness of its internal state
150
+ def empty?
151
+ self.responses.empty?
152
+ end
153
+
154
+ def complete!
155
+ self.completed_at = Time.now
156
+ end
157
+
158
+ def has_answered_question?(question)
159
+ !has_not_answered_question?(question)
160
+ end
161
+
162
+ def has_not_answered_question?(question)
163
+ self.responses.find_all_by_question_id(question.id).empty?
164
+ end
165
+
166
+ # Returns the number of response groups (count of group responses enterted) for this question group
167
+ def count_group_responses(questions)
168
+ questions.map{|q| responses.select{|r| r.question_id == q.id && !r.response_group.nil?}.size }.max
169
+ end
170
+
171
+ def unanswered_dependencies
172
+ dependencies.select{|d| d.met?(self) and self.has_not_answered_question?(d.question)}.map(&:question)
173
+ end
174
+ def all_dependencies
175
+ arr = dependencies.partition{|d| d.met?(self) }
176
+ {:show => arr[0].map{|d| "question_#{d.question_id}"}, :hide => arr[1].map{|d| "question_#{d.question_id}"}}
177
+ end
178
+
179
+ protected
180
+
181
+ def dependencies
182
+ question_ids = Question.find_all_by_survey_section_id(current_section_id).map(&:id)
183
+ Dependency.depending_on_questions(question_ids)
184
+ end
185
+ end
@@ -0,0 +1,57 @@
1
+ class Survey < ActiveRecord::Base
2
+ include TinyCode
3
+
4
+ # Associations
5
+ has_many :sections, :class_name => "SurveySection", :order => 'display_order'
6
+ has_many :sections_with_questions, :include => :questions, :class_name => "SurveySection", :order => 'display_order'
7
+ has_many :response_sets
8
+
9
+ # Scopes
10
+ named_scope :with_sections, {:include => :sections}
11
+
12
+ # Validations
13
+ validates_presence_of :title
14
+
15
+ # Class methods
16
+ def self.to_normalized_string(value)
17
+ # replace non-alphanumeric with "-". remove repeat "-"s. don't start or end with "-"
18
+ value.to_s.downcase.gsub(/[^a-z0-9]/,"-").gsub(/-+/,"-").gsub(/-$|^-/,"")
19
+ end
20
+
21
+ # Instance methods
22
+ def initialize(*args)
23
+ super(*args)
24
+ default_args
25
+ end
26
+
27
+ def default_args
28
+ self.inactive_at ||= DateTime.now
29
+ end
30
+
31
+ def title=(value)
32
+ self.access_code = Survey.to_normalized_string(value)
33
+ super
34
+ end
35
+
36
+ def active?
37
+ self.active_as_of?(DateTime.now)
38
+ end
39
+ def active_as_of?(datetime)
40
+ (self.active_at.nil? or self.active_at < datetime) and (self.inactive_at.nil? or self.inactive_at > datetime)
41
+ end
42
+ def activate!
43
+ self.active_at = DateTime.now
44
+ end
45
+ def deactivate!
46
+ self.inactive_at = DateTime.now
47
+ end
48
+ def active_at=(datetime)
49
+ self.inactive_at = nil if !datetime.nil? and !self.inactive_at.nil? and self.inactive_at < datetime
50
+ super(datetime)
51
+ end
52
+ def inactive_at=(datetime)
53
+ self.active_at = nil if !datetime.nil? and !self.active_at.nil? and self.active_at > datetime
54
+ super(datetime)
55
+ end
56
+
57
+ end
@@ -0,0 +1,15 @@
1
+ class SurveySection < ActiveRecord::Base
2
+
3
+ # Associations
4
+ has_many :questions
5
+ belongs_to :survey
6
+
7
+ # Scopes
8
+ default_scope :order => "display_order ASC"
9
+ named_scope :with_includes, { :include => {:questions => [:answers, :question_group, {:dependency => :dependency_conditions}]}}
10
+
11
+ # Validations
12
+ validates_presence_of :title, :survey, :display_order
13
+
14
+ end
15
+