breakpointer-surveyor 0.2.0

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 (198) hide show
  1. data/.gitignore +6 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +20 -0
  4. data/Rakefile +57 -0
  5. data/VERSION +1 -0
  6. data/app/controllers/answers_controller.rb +87 -0
  7. data/app/controllers/dependencies_controller.rb +87 -0
  8. data/app/controllers/dependency_conditions_controller.rb +87 -0
  9. data/app/controllers/questions_controller.rb +85 -0
  10. data/app/controllers/sections_controller.rb +87 -0
  11. data/app/controllers/surveying_controller.rb +139 -0
  12. data/app/controllers/surveys_controller.rb +87 -0
  13. data/app/helpers/answers_helper.rb +2 -0
  14. data/app/helpers/application_helper.rb +3 -0
  15. data/app/helpers/questions_helper.rb +2 -0
  16. data/app/helpers/sections_helper.rb +2 -0
  17. data/app/helpers/survey_form_builder.rb +38 -0
  18. data/app/helpers/survey_importer_helper.rb +2 -0
  19. data/app/helpers/surveying_helper.rb +91 -0
  20. data/app/helpers/surveys_helper.rb +2 -0
  21. data/app/models/answer.rb +18 -0
  22. data/app/models/dependency.rb +43 -0
  23. data/app/models/dependency_condition.rb +76 -0
  24. data/app/models/question.rb +44 -0
  25. data/app/models/question_group.rb +5 -0
  26. data/app/models/response.rb +58 -0
  27. data/app/models/response_set.rb +174 -0
  28. data/app/models/survey.rb +54 -0
  29. data/app/models/survey_section.rb +19 -0
  30. data/app/models/user.rb +5 -0
  31. data/app/views/answer_display_types/_any_answer.html.haml +3 -0
  32. data/app/views/answer_display_types/_any_other_and_string.html.haml +5 -0
  33. data/app/views/answer_display_types/_any_string.html.haml +4 -0
  34. data/app/views/answer_display_types/_date.html.haml +3 -0
  35. data/app/views/answer_display_types/_datetime.html.haml +2 -0
  36. data/app/views/answer_display_types/_default.html.haml +1 -0
  37. data/app/views/answer_display_types/_float.html.haml +5 -0
  38. data/app/views/answer_display_types/_grid_any_answer.html.haml +2 -0
  39. data/app/views/answer_display_types/_grid_default.html.haml +2 -0
  40. data/app/views/answer_display_types/_grid_float.html.haml +2 -0
  41. data/app/views/answer_display_types/_grid_integer.html.haml +2 -0
  42. data/app/views/answer_display_types/_grid_one_answer.html.haml +2 -0
  43. data/app/views/answer_display_types/_grid_string.html.haml +2 -0
  44. data/app/views/answer_display_types/_integer.html.haml +6 -0
  45. data/app/views/answer_display_types/_one_answer.html.haml +3 -0
  46. data/app/views/answer_display_types/_one_string.html.haml +6 -0
  47. data/app/views/answer_display_types/_repeater_integer.html.haml +6 -0
  48. data/app/views/answer_display_types/_repeater_string.html.haml +7 -0
  49. data/app/views/answer_display_types/_string.html.haml +7 -0
  50. data/app/views/answer_display_types/_text.html.haml +6 -0
  51. data/app/views/answer_display_types/_time.html.haml +2 -0
  52. data/app/views/layouts/surveys.html.erb +17 -0
  53. data/app/views/question_display_types/_default.html.haml +12 -0
  54. data/app/views/question_display_types/_dropdown.html.haml +12 -0
  55. data/app/views/question_display_types/_grid_default.html.haml +14 -0
  56. data/app/views/question_display_types/_grid_dropdown.html.haml +15 -0
  57. data/app/views/question_display_types/_group_default.html.haml +8 -0
  58. data/app/views/question_display_types/_group_dropdown.html.haml +6 -0
  59. data/app/views/question_display_types/_image.html.haml +2 -0
  60. data/app/views/question_display_types/_inline.html.haml +12 -0
  61. data/app/views/question_display_types/_label.html.haml +4 -0
  62. data/app/views/question_display_types/_repeater_default.html.haml +10 -0
  63. data/app/views/question_display_types/_repeater_dropdown.html.haml +8 -0
  64. data/app/views/question_display_types/_slider.html.haml +17 -0
  65. data/app/views/question_group_display_types/_default.html.haml +13 -0
  66. data/app/views/question_group_display_types/_grid.html.haml +24 -0
  67. data/app/views/question_group_display_types/_repeater.html.haml +20 -0
  68. data/app/views/surveying/edit.html.haml +38 -0
  69. data/app/views/surveying/finish.html.haml +1 -0
  70. data/app/views/surveying/index.html.erb +19 -0
  71. data/app/views/surveying/new.html.haml +17 -0
  72. data/app/views/surveying/show.html.haml +8 -0
  73. data/config/routes.rb +10 -0
  74. data/generators/surveyor/surveyor_generator.rb +38 -0
  75. data/generators/surveyor/templates/README +9 -0
  76. data/generators/surveyor/templates/assets/images/222222_11x11_icon_arrows_leftright.gif +0 -0
  77. data/generators/surveyor/templates/assets/images/222222_11x11_icon_arrows_updown.gif +0 -0
  78. data/generators/surveyor/templates/assets/images/222222_11x11_icon_close.gif +0 -0
  79. data/generators/surveyor/templates/assets/images/222222_11x11_icon_doc.gif +0 -0
  80. data/generators/surveyor/templates/assets/images/222222_11x11_icon_folder_closed.gif +0 -0
  81. data/generators/surveyor/templates/assets/images/222222_11x11_icon_folder_open.gif +0 -0
  82. data/generators/surveyor/templates/assets/images/222222_11x11_icon_minus.gif +0 -0
  83. data/generators/surveyor/templates/assets/images/222222_11x11_icon_plus.gif +0 -0
  84. data/generators/surveyor/templates/assets/images/222222_11x11_icon_resize_se.gif +0 -0
  85. data/generators/surveyor/templates/assets/images/222222_35x9_colorpicker_indicator.gif.gif +0 -0
  86. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_down.gif +0 -0
  87. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_left.gif +0 -0
  88. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_right.gif +0 -0
  89. data/generators/surveyor/templates/assets/images/222222_7x7_arrow_up.gif +0 -0
  90. data/generators/surveyor/templates/assets/images/454545_11x11_icon_arrows_leftright.gif +0 -0
  91. data/generators/surveyor/templates/assets/images/454545_11x11_icon_arrows_updown.gif +0 -0
  92. data/generators/surveyor/templates/assets/images/454545_11x11_icon_close.gif +0 -0
  93. data/generators/surveyor/templates/assets/images/454545_11x11_icon_doc.gif +0 -0
  94. data/generators/surveyor/templates/assets/images/454545_11x11_icon_folder_closed.gif +0 -0
  95. data/generators/surveyor/templates/assets/images/454545_11x11_icon_folder_open.gif +0 -0
  96. data/generators/surveyor/templates/assets/images/454545_11x11_icon_minus.gif +0 -0
  97. data/generators/surveyor/templates/assets/images/454545_11x11_icon_plus.gif +0 -0
  98. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_down.gif +0 -0
  99. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_left.gif +0 -0
  100. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_right.gif +0 -0
  101. data/generators/surveyor/templates/assets/images/454545_7x7_arrow_up.gif +0 -0
  102. data/generators/surveyor/templates/assets/images/888888_11x11_icon_arrows_leftright.gif +0 -0
  103. data/generators/surveyor/templates/assets/images/888888_11x11_icon_arrows_updown.gif +0 -0
  104. data/generators/surveyor/templates/assets/images/888888_11x11_icon_close.gif +0 -0
  105. data/generators/surveyor/templates/assets/images/888888_11x11_icon_doc.gif +0 -0
  106. data/generators/surveyor/templates/assets/images/888888_11x11_icon_folder_closed.gif +0 -0
  107. data/generators/surveyor/templates/assets/images/888888_11x11_icon_folder_open.gif +0 -0
  108. data/generators/surveyor/templates/assets/images/888888_11x11_icon_minus.gif +0 -0
  109. data/generators/surveyor/templates/assets/images/888888_11x11_icon_plus.gif +0 -0
  110. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_down.gif +0 -0
  111. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_left.gif +0 -0
  112. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_right.gif +0 -0
  113. data/generators/surveyor/templates/assets/images/888888_7x7_arrow_up.gif +0 -0
  114. data/generators/surveyor/templates/assets/images/dadada_40x100_textures_02_glass_75.png +0 -0
  115. data/generators/surveyor/templates/assets/images/e6e6e6_40x100_textures_02_glass_75.png +0 -0
  116. data/generators/surveyor/templates/assets/images/ffffff_40x100_textures_01_flat_0.png +0 -0
  117. data/generators/surveyor/templates/assets/images/ffffff_40x100_textures_02_glass_65.png +0 -0
  118. data/generators/surveyor/templates/assets/javascripts/accessibleUISlider.jQuery.js +201 -0
  119. data/generators/surveyor/templates/assets/javascripts/jquery-1.2.6.js +3549 -0
  120. data/generators/surveyor/templates/assets/javascripts/jquery-ui-personalized-1.5.3.js +7616 -0
  121. data/generators/surveyor/templates/assets/javascripts/jquery.form.js +637 -0
  122. data/generators/surveyor/templates/assets/javascripts/surveyor.js +35 -0
  123. data/generators/surveyor/templates/assets/stylesheets/jquery-ui-slider-additions.css +71 -0
  124. data/generators/surveyor/templates/assets/stylesheets/reset.css +46 -0
  125. data/generators/surveyor/templates/assets/stylesheets/sass/surveyor.sass +321 -0
  126. data/generators/surveyor/templates/assets/stylesheets/surveyor.css +245 -0
  127. data/generators/surveyor/templates/assets/stylesheets/ui.theme.css +851 -0
  128. data/generators/surveyor/templates/migrate/create_answers.rb +46 -0
  129. data/generators/surveyor/templates/migrate/create_dependencies.rb +21 -0
  130. data/generators/surveyor/templates/migrate/create_dependency_conditions.rb +29 -0
  131. data/generators/surveyor/templates/migrate/create_question_groups.rb +18 -0
  132. data/generators/surveyor/templates/migrate/create_questions.rb +33 -0
  133. data/generators/surveyor/templates/migrate/create_response_sets.rb +22 -0
  134. data/generators/surveyor/templates/migrate/create_responses.rb +33 -0
  135. data/generators/surveyor/templates/migrate/create_survey_sections.rb +25 -0
  136. data/generators/surveyor/templates/migrate/create_surveys.rb +25 -0
  137. data/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +201 -0
  138. data/init.rb +1 -0
  139. data/install.rb +1 -0
  140. data/lib/tasks/surveyor_tasks.rake +29 -0
  141. data/lib/tiny_code.rb +58 -0
  142. data/lib/user_manager.rb +9 -0
  143. data/lib/xml_formatter.rb +12 -0
  144. data/script/surveyor/answer.rb +84 -0
  145. data/script/surveyor/columnizer.rb +36 -0
  146. data/script/surveyor/dependency.rb +43 -0
  147. data/script/surveyor/dependency_condition.rb +74 -0
  148. data/script/surveyor/dslparse.rb +66 -0
  149. data/script/surveyor/question.rb +76 -0
  150. data/script/surveyor/question_group.rb +33 -0
  151. data/script/surveyor/specs/answer_spec.rb +66 -0
  152. data/script/surveyor/specs/question_dependency_spec.rb +46 -0
  153. data/script/surveyor/specs/question_group_spec.rb +9 -0
  154. data/script/surveyor/specs/question_spec.rb +111 -0
  155. data/script/surveyor/specs/section_spec.rb +58 -0
  156. data/script/surveyor/survey.rb +108 -0
  157. data/script/surveyor/survey_section.rb +153 -0
  158. data/script/surveyor/whr_dsl.tmproj +244 -0
  159. data/spec/controllers/answers_controller_spec.rb +64 -0
  160. data/spec/controllers/dependencies_controller_spec.rb +63 -0
  161. data/spec/controllers/dependency_conditions_controller_spec.rb +64 -0
  162. data/spec/controllers/questions_controller_spec.rb +64 -0
  163. data/spec/controllers/sections_controller_spec.rb +64 -0
  164. data/spec/controllers/surveying_controller_spec.rb +328 -0
  165. data/spec/controllers/surveying_routing_spec.rb +45 -0
  166. data/spec/controllers/surveys_controller_spec.rb +64 -0
  167. data/spec/fixtures/answers.yml +9 -0
  168. data/spec/fixtures/dependencies.yml +7 -0
  169. data/spec/fixtures/dependency_conditions.yml +27 -0
  170. data/spec/fixtures/question_groups.yml +7 -0
  171. data/spec/fixtures/questions.yml +15 -0
  172. data/spec/fixtures/response_sets.yml +13 -0
  173. data/spec/fixtures/responses.yml +9 -0
  174. data/spec/fixtures/survey_sections.yml +11 -0
  175. data/spec/fixtures/surveys.yml +9 -0
  176. data/spec/fixtures/users.yml +7 -0
  177. data/spec/helpers/survey_importer_helper_spec.rb +11 -0
  178. data/spec/helpers/surveying_helper_spec.rb +11 -0
  179. data/spec/models/answer_spec.rb +62 -0
  180. data/spec/models/dependency_condition_spec.rb +347 -0
  181. data/spec/models/dependency_spec.rb +82 -0
  182. data/spec/models/question_group_spec.rb +11 -0
  183. data/spec/models/question_spec.rb +70 -0
  184. data/spec/models/response_set_spec.rb +172 -0
  185. data/spec/models/response_spec.rb +94 -0
  186. data/spec/models/survey_section_spec.rb +34 -0
  187. data/spec/models/survey_spec.rb +72 -0
  188. data/spec/models/user_spec.rb +11 -0
  189. data/spec/rcov.opts +2 -0
  190. data/spec/spec.opts +4 -0
  191. data/spec/spec_helper.rb +72 -0
  192. data/spec/views/app/edit.html.erb_spec.rb +20 -0
  193. data/spec/views/app/index.html.erb_spec.rb +19 -0
  194. data/spec/views/app/new.html.erb_spec.rb +21 -0
  195. data/spec/views/app/show.html.erb_spec.rb +17 -0
  196. data/surveyor.gemspec +258 -0
  197. data/uninstall.rb +1 -0
  198. metadata +286 -0
@@ -0,0 +1,2 @@
1
+ module SurveysHelper
2
+ end
@@ -0,0 +1,18 @@
1
+ class Answer < ActiveRecord::Base
2
+
3
+ # Associations
4
+ belongs_to :question
5
+ has_many :responses
6
+
7
+ # Validations
8
+ validates_presence_of :text
9
+ validates_numericality_of :question_id, :allow_nil => false, :only_integer => true
10
+ #validates_uniqueness_of :reference_identifier
11
+
12
+ # Methods
13
+ def partial_name
14
+ [(self.question.pick == "none")? nil : self.question.pick.downcase, self.response_class.downcase].compact.join("_")
15
+ end
16
+
17
+
18
+ end
@@ -0,0 +1,43 @@
1
+ class Dependency < ActiveRecord::Base
2
+ # Associations
3
+ belongs_to :question
4
+ has_many :dependency_conditions
5
+
6
+ # Validations
7
+ validates_presence_of :rule
8
+ validates_format_of :rule, :with => /^(?:and|or|\)|\(|\d|\s)+$/ #TODO properly formed parenthesis etc.
9
+ validates_numericality_of :question_id
10
+
11
+ # Attribute aliases
12
+ alias_attribute :dependent_question_id, :question_id
13
+
14
+ # Is the method that determines if this dependency has been met within
15
+ # the provided response set
16
+ def met?(response_set)
17
+ if keyed_pairs = keyed_conditions(response_set)
18
+ return(rule_evaluation(keyed_pairs))
19
+ else
20
+ return(false)
21
+ end
22
+ end
23
+
24
+ # Pairs up the substitution key with the evaluated condition result for substitution into the rule
25
+ # Example: If you have two dependency conditions with rule keys "A" and "B" in the rule "A or B"
26
+ # calling keyed_condition_pairs will return {:A => true, :B => false}
27
+ def keyed_conditions(response_set)
28
+ keyed_pairs = {}
29
+ self.dependency_conditions.each do |dc|
30
+ keyed_pairs.merge!(dc.to_evaluation_hash(response_set))
31
+ end
32
+ return(keyed_pairs)
33
+ end
34
+
35
+ # Does the substiution and evaluation of the dependency rule with the keyed pairs
36
+ def rule_evaluation(keyed_pairs)
37
+ # subtitute into rule for evaluation
38
+ rgx = Regexp.new(self.dependency_conditions.map{|dc| dc.rule_key}.join("|")) # Making a regexp to only look for the keys used in the child conditions
39
+ #logger.debug("rexp: #{rgx.inspect} FOO: #{keyed_pairs.inspect} --- subbed rules: #{rule.gsub(rgx){|m| keyed_pairs[m.to_sym]}} --> #{eval(self.rule.gsub(rgx){|m| keyed_pairs[m.to_sym]})}")
40
+ eval(self.rule.gsub(rgx){|m| keyed_pairs[m.to_sym]}) # returns the evaluation of the rule and the conditions
41
+ end
42
+
43
+ end
@@ -0,0 +1,76 @@
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
+ # Evaluates the condition on the response_set
22
+ def evaluation_of(response_set)
23
+ 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
24
+ return(response and self.is_satisfied_by?(response))
25
+ end
26
+
27
+ # Checks to see if the response passed in satisfies the dependency condition
28
+ def is_satisfied_by?(response)
29
+ response_class = response.answer.response_class
30
+ return case self.operator
31
+ when "=="
32
+ response.as(response_class) == self.as(response_class)
33
+ when "!="
34
+ response.as(response_class) != self.as(response_class)
35
+ when "<"
36
+ response.as(response_class) < self.as(response_class)
37
+ when ">"
38
+ response.as(response_class) > self.as(response_class)
39
+ when "<="
40
+ response.as(response_class) <= self.as(response_class)
41
+ when ">="
42
+ response.as(response_class) >= self.as(response_class)
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ # Method that returns the dependency as a particular response_class type
49
+ def as(type_symbol)
50
+ return case type_symbol.to_sym
51
+ when :string
52
+ self.string_value
53
+ when :text
54
+ self.text_value
55
+ when :integer
56
+ self.integer_value
57
+ when :float
58
+ self.float_value
59
+ when :answer
60
+ self.answer_id
61
+ else
62
+ self.answer_id
63
+ end
64
+ end
65
+
66
+ # The hash used in the dependency parent object to evaluate its rule string
67
+ def to_evaluation_hash(response_set)
68
+ {self.symbol_key => self.evaluation_of(response_set)}
69
+ end
70
+
71
+ # Returns the rule key to be used in the subsitution string in the parent depenedency object
72
+ def symbol_key
73
+ self.rule_key.to_sym
74
+ end
75
+
76
+ end
@@ -0,0 +1,44 @@
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
+ # Validations
10
+ validates_presence_of :text, :survey_section_id, :display_order
11
+ validates_inclusion_of :is_mandatory, :in => [true, false]
12
+
13
+ # Instance Methods
14
+ def initialize(*args)
15
+ super(*args)
16
+ default_args
17
+ end
18
+
19
+ def default_args
20
+ # self.is_active ||= false
21
+ self.is_mandatory ||= true
22
+ end
23
+
24
+ def mandatory?
25
+ self.is_mandatory == true
26
+ end
27
+
28
+ def display_type
29
+ super || "default"
30
+ end
31
+
32
+ def has_dependency?
33
+ self.dependency != nil
34
+ end
35
+
36
+ def dependency_satisfied?(response_set)
37
+ self.has_dependency? and self.dependency.met?(response_set)
38
+ end
39
+
40
+ def part_of_group?
41
+ !self.question_group.nil?
42
+ end
43
+
44
+ end
@@ -0,0 +1,5 @@
1
+ class QuestionGroup < ActiveRecord::Base
2
+
3
+ has_many :questions
4
+
5
+ end
@@ -0,0 +1,58 @@
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
29
+ self.string_value
30
+ when :text
31
+ self.text_value
32
+ when :integer
33
+ self.integer_value
34
+ when :float
35
+ self.float_value
36
+ when :date
37
+ self.datetime_value.nil? ? nil : self.datetime_value.to_date
38
+ when :time
39
+ self.datetime_value.nil? ? nil : self.datetime_value.to_time
40
+ when :datetime
41
+ self.datetime_value
42
+ when :answer
43
+ self.answer_id
44
+ else
45
+ self.answer_id
46
+ end
47
+ end
48
+
49
+ def to_s
50
+ if self.answer_id
51
+ self.answer.text
52
+ else
53
+ "#{(self.string_value || self.text_value || self.integer_value || self.float_value).to_s}"
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,174 @@
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, :user_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, response_group = nil)
32
+ found = responses.find_by_question_id_and_answer_id_and_response_group(question_id, answer_id, response_group)
33
+ found.blank? ? responses.new(:question_id => question_id, :answer_id => answer_id, :response_group => response_group) : found
34
+ end
35
+
36
+ # "responses"=>{
37
+ #string "6"=>{"question_id"=>"6", "20"=>{"string_value"=>"saf"}},
38
+ #text "7"=>{"question_id"=>"7", "21"=>{"text_value"=>""}},
39
+ #radio+txt "1"=>{"question_id"=>"1", "answer_id"=>"1", "4"=>{"string_value"=>""}},
40
+ #radio "2"=>{"answer_id"=>"6"},
41
+ #radio "3"=>{"answer_id"=>"10"},
42
+ #check "4"=>{"question_id"=>"4", "answer_id"=>"15"},
43
+ #check+txt "5"=>{"question_id"=>"5", "16"=>{"selected"=>"1"}, "19"=>{"string_value"=>""}}
44
+ # },
45
+ # "survey_code"=>"test_survey",
46
+ # "commit"=>"Next Section (Utensiles and you!) >>",
47
+ # "authenticity_token"=>"8bee21081eea820ab1c658358c0baaa2e46de5d1",
48
+ # "_method"=>"put",
49
+ # "action"=>"update",
50
+ # "controller"=>"app",
51
+ # "response_set_code"=>"T2x8HhCQej",
52
+ # "section"=>"2"
53
+
54
+ def response_attributes=(response_attributes)
55
+ response_attributes.each do |question_id, responses_hash|
56
+ Response.delete_all(["response_set_id =? AND question_id =?", self.id, question_id])
57
+ if (answer_id = responses_hash[:answer_id])
58
+ if (!responses_hash[:answer_id].empty?) # Dropdowns return answer id but have an empty value if they are not set... ignoring those.
59
+ #radio or dropdown - only one response
60
+ responses.build({:question_id => question_id, :answer_id => answer_id}.merge(responses_hash[answer_id] || {}))
61
+ end
62
+ else
63
+ #possibly multiples responses - unresponded radios end up here too
64
+ # we use the variable question_id, not the "question_id" in the response_hash
65
+ responses_hash.delete_if{|k,v| k == "question_id"}.each do |answer_id, response_hash|
66
+ unless response_hash.delete_if{|k,v| v.blank?}.empty?
67
+ responses.build({:question_id => question_id, :answer_id => answer_id}.merge(response_hash))
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ # "24"=>{
75
+ # "0"=>{"response_group"=>"0", "question_id"=>"24", "answer_id"=>"172"}, "1"=>{"response_group"=>"1", "question_id"=>"24", "answer_id"=>"173"},
76
+ # "2"=>{"response_group"=>"2", "question_id"=>"24", "answer_id"=>""}, "3"=>{"response_group"=>"3", "question_id"=>"24", "answer_id"=>""},
77
+ # "4"=>{"response_group"=>"4", "question_id"=>"24", "answer_id"=>""}},
78
+ # where "24" is the question id
79
+
80
+ # Some other examples:
81
+ # "25"=>{
82
+ # "0"=>{"response_group"=>"0", "question_id"=>"25", "179"=>{"string_value"=>"camry"}},
83
+ # "1"=>{"response_group"=>"1", "question_id"=>"25", "179"=>{"string_value"=>"f150"}},
84
+ # "2"=>{"response_group"=>"2", "question_id"=>"25", "179"=>{"string_value"=>""}},
85
+ # "3"=>{"response_group"=>"3", "question_id"=>"25", "179"=>{"string_value"=>""}},
86
+ # "4"=>{"response_group"=>"4", "question_id"=>"25", "179"=>{"string_value"=>""}}},
87
+ #
88
+ # "26"=>{
89
+ # "0"=>{"response_group"=>"0", "question_id"=>"26", "180"=>{"string_value"=>"1999"}},
90
+ # "1"=>{"response_group"=>"1", "question_id"=>"26", "180"=>{"string_value"=>"2004"}},
91
+ # "2"=>{"response_group"=>"2", "question_id"=>"26", "180"=>{"string_value"=>""}},
92
+ # "3"=>{"response_group"=>"3", "question_id"=>"26", "180"=>{"string_value"=>""}},
93
+ # "4"=>{"response_group"=>"4", "question_id"=>"26", "180"=>{"string_value"=>""}}},
94
+ #
95
+ # "27"=>{
96
+ # "0"=>{"182"=>{"integer_value"=>""}, "response_group"=>"0", "question_id"=>"27", "181"=>{"string_value"=>""}},
97
+ # "1"=>{"182"=>{"integer_value"=>""}, "response_group"=>"1", "question_id"=>"27", "181"=>{"string_value"=>""}},
98
+ # "2"=>{"182"=>{"integer_value"=>""}, "response_group"=>"2", "question_id"=>"27", "181"=>{"string_value"=>""}},
99
+ # "3"=>{"182"=>{"integer_value"=>""}, "response_group"=>"3", "question_id"=>"27", "181"=>{"string_value"=>""}},
100
+ # "4"=>{"182"=>{"integer_value"=>""}, "response_group"=>"4", "question_id"=>"27", "181"=>{"string_value"=>""}}}},
101
+
102
+ # 0,1,2,3,4 are the response group numbers
103
+ # and anything else in the response group hash is handled normally
104
+
105
+ # method to process responses in response groups
106
+ def response_group_attributes=(response_attributes)
107
+ response_attributes.each do |question_id, responses_group_hash|
108
+ Response.delete_all(["response_set_id =? AND question_id =?", self.id, question_id])
109
+ responses_group_hash.each do |response_group_number, group_hash|
110
+ if (answer_id = group_hash[:answer_id]) # if group_hash has an answer_id key we treat it differently
111
+ if (!group_hash[:answer_id].empty?) # dropdowns return empty values in answer_ids if they are not selected
112
+ #radio or dropdown - only one response
113
+ responses.build({:question_id => question_id, :answer_id => answer_id, :response_group => response_group_number}.merge(group_hash[answer_id] || {}))
114
+ end
115
+ else
116
+ #possibly multiples responses - unresponded radios end up here too
117
+ # we use the variable question_id in the key, not the "question_id" in the response_hash... same with response_group key
118
+ group_hash.delete_if{|k,v| (k == "question_id") or (k == "response_group")}.each do |answer_id, inner_hash|
119
+ unless inner_hash.delete_if{|k,v| v.blank?}.empty?
120
+ responses.build({:question_id => question_id, :answer_id => answer_id, :response_group => response_group_number}.merge(inner_hash))
121
+ end
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+ end
128
+
129
+
130
+ def save_responses
131
+ responses.each do |response|
132
+ response.save(false)
133
+ end
134
+ end
135
+
136
+ # ResponseSet has an awareness of its internal state
137
+ def empty?
138
+ self.responses.empty?
139
+ end
140
+
141
+ def complete!
142
+ self.completed_at = Time.now
143
+ end
144
+
145
+ def has_answered_question?(question)
146
+ !has_not_answered_question?(question)
147
+ end
148
+
149
+ def has_not_answered_question?(question)
150
+ self.responses.find_all_by_question_id(question.id).empty?
151
+ end
152
+
153
+ # ResponseSet knows if certain answers are contained in this response set
154
+ # This method acts as an interface for the dependency analysis
155
+ def find_response(answer_id)
156
+ self.responses.find_by_answer_id(answer_id)
157
+ end
158
+
159
+ # Counts the number of responses for the current user for this question
160
+ def count_question_responses(question)
161
+ Response.count(:conditions => ["response_set_id =? AND question_id=?", self.id, question.id])
162
+ end
163
+
164
+ # Returns the number of response groups (count of group responses enterted) for this question group
165
+ def count_group_responses(group)
166
+ counts = []
167
+ group.questions.each do |question|
168
+ counts << Response.count("response_group",:conditions => ["response_set_id =? AND question_id=? AND response_group IS NOT NULL", self.id, question.id], :distinct => true)
169
+ end
170
+
171
+ counts.max #since response groups can be partially filled, such that for one response group the user may have answered only one of the questions in the group. We want to still count the partially complete group response.
172
+ end
173
+
174
+ end
@@ -0,0 +1,54 @@
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
+ # Validations
10
+ validates_presence_of :title
11
+
12
+ # Class methods
13
+ def self.to_normalized_string(value)
14
+ # replace non-alphanumeric with "-". remove repeat "-"s. don't start or end with "-"
15
+ value.to_s.downcase.gsub(/[^a-z0-9]/,"-").gsub(/-+/,"-").gsub(/-$|^-/,"")
16
+ end
17
+
18
+ # Instance methods
19
+ def initialize(*args)
20
+ super(*args)
21
+ default_args
22
+ end
23
+
24
+ def default_args
25
+ self.inactive_at ||= DateTime.now
26
+ end
27
+
28
+ def title=(value)
29
+ self.access_code = Survey.to_normalized_string(value)
30
+ super
31
+ end
32
+
33
+ def active?
34
+ self.active_as_of?(DateTime.now)
35
+ end
36
+ def active_as_of?(datetime)
37
+ (self.active_at.nil? or self.active_at < datetime) and (self.inactive_at.nil? or self.inactive_at > datetime)
38
+ end
39
+ def activate!
40
+ self.active_at = DateTime.now
41
+ end
42
+ def deactivate!
43
+ self.inactive_at = DateTime.now
44
+ end
45
+ def active_at=(datetime)
46
+ self.inactive_at = nil if !datetime.nil? and !self.inactive_at.nil? and self.inactive_at < datetime
47
+ super(datetime)
48
+ end
49
+ def inactive_at=(datetime)
50
+ self.active_at = nil if !datetime.nil? and !self.active_at.nil? and self.active_at > datetime
51
+ super(datetime)
52
+ end
53
+
54
+ end