affectiva-surveyor 1.5.0.pre.disco.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (212) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +28 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG.md +198 -0
  5. data/Gemfile +5 -0
  6. data/Gemfile.rails_version +27 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +166 -0
  9. data/Rakefile +106 -0
  10. data/app/controllers/surveyor_controller.rb +5 -0
  11. data/app/helpers/results_helper.rb +20 -0
  12. data/app/helpers/survey_form_builder.rb +37 -0
  13. data/app/helpers/surveyor_helper.rb +3 -0
  14. data/app/inputs/quiet_input.rb +5 -0
  15. data/app/inputs/surveyor_check_boxes_input.rb +35 -0
  16. data/app/inputs/surveyor_radio_input.rb +18 -0
  17. data/app/models/answer.rb +3 -0
  18. data/app/models/dependency.rb +3 -0
  19. data/app/models/dependency_condition.rb +3 -0
  20. data/app/models/question.rb +3 -0
  21. data/app/models/question_group.rb +4 -0
  22. data/app/models/response.rb +4 -0
  23. data/app/models/response_set.rb +3 -0
  24. data/app/models/survey.rb +3 -0
  25. data/app/models/survey_section.rb +4 -0
  26. data/app/models/survey_section_sweeper.rb +15 -0
  27. data/app/models/survey_translation.rb +4 -0
  28. data/app/models/validation.rb +3 -0
  29. data/app/models/validation_condition.rb +3 -0
  30. data/app/views/layouts/results.html.erb +13 -0
  31. data/app/views/layouts/surveyor_default.html.erb +12 -0
  32. data/app/views/partials/_answer.html.haml +25 -0
  33. data/app/views/partials/_dependents.html.haml +5 -0
  34. data/app/views/partials/_question.html.haml +28 -0
  35. data/app/views/partials/_question_group.html.haml +44 -0
  36. data/app/views/partials/_section.html.haml +12 -0
  37. data/app/views/partials/_section_menu.html.haml +12 -0
  38. data/app/views/surveyor/edit.html.haml +24 -0
  39. data/app/views/surveyor/export.json.rabl +85 -0
  40. data/app/views/surveyor/new.html.haml +24 -0
  41. data/app/views/surveyor/show.html.haml +74 -0
  42. data/app/views/surveyor/show.json.rabl +15 -0
  43. data/ci-exec.sh +56 -0
  44. data/config/routes.rb +8 -0
  45. data/cucumber.yml +10 -0
  46. data/doc/REPRESENTATIONS.md +34 -0
  47. data/doc/api_id_schema.json +7 -0
  48. data/doc/question types.png +0 -0
  49. data/doc/response_set_schema.json +54 -0
  50. data/doc/surveyor question combinations.png +0 -0
  51. data/doc/surveyor reject or delete decision matrix.png +0 -0
  52. data/doc/surveyor_models.png +0 -0
  53. data/doc/surveyor_models2.png +0 -0
  54. data/doc/surveyor_timestamp_schema.json +9 -0
  55. data/features/ajax_submissions.feature +140 -0
  56. data/features/export_to_json.feature +344 -0
  57. data/features/internationalization.feature +121 -0
  58. data/features/no_duplicates.feature +110 -0
  59. data/features/show_survey.feature +71 -0
  60. data/features/step_definitions/parser_steps.rb +145 -0
  61. data/features/step_definitions/surveyor_steps.rb +325 -0
  62. data/features/step_definitions/ui_steps.rb +25 -0
  63. data/features/step_definitions/web_steps.rb +225 -0
  64. data/features/support/REDCapDemoDatabase_DataDictionary.csv +127 -0
  65. data/features/support/database_cleaner.rb +16 -0
  66. data/features/support/env.rb +56 -0
  67. data/features/support/hooks.rb +4 -0
  68. data/features/support/paths.rb +39 -0
  69. data/features/support/redcap_new_headers.csv +1 -0
  70. data/features/support/redcap_siblings.csv +1 -0
  71. data/features/support/redcap_whitespace.csv +1 -0
  72. data/features/support/selectors.rb +39 -0
  73. data/features/support/simultaneous_ajax.rb +101 -0
  74. data/features/support/single_quit_selenium_driver.rb +23 -0
  75. data/features/support/slow_updates.rb +18 -0
  76. data/features/surveyor.feature +895 -0
  77. data/features/surveyor_dependencies.feature +476 -0
  78. data/features/surveyor_parser.feature +504 -0
  79. data/features/z_redcap_parser.feature +62 -0
  80. data/lib/assets/images/surveyor/next.gif +0 -0
  81. data/lib/assets/images/surveyor/prev.gif +0 -0
  82. data/lib/assets/images/surveyor/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  83. data/lib/assets/images/surveyor/ui-bg_flat_75_ffffff_40x100.png +0 -0
  84. data/lib/assets/images/surveyor/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  85. data/lib/assets/images/surveyor/ui-bg_glass_65_ffffff_1x400.png +0 -0
  86. data/lib/assets/images/surveyor/ui-bg_glass_75_dadada_1x400.png +0 -0
  87. data/lib/assets/images/surveyor/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  88. data/lib/assets/images/surveyor/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  89. data/lib/assets/images/surveyor/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  90. data/lib/assets/images/surveyor/ui-icons_222222_256x240.png +0 -0
  91. data/lib/assets/images/surveyor/ui-icons_2e83ff_256x240.png +0 -0
  92. data/lib/assets/images/surveyor/ui-icons_454545_256x240.png +0 -0
  93. data/lib/assets/images/surveyor/ui-icons_888888_256x240.png +0 -0
  94. data/lib/assets/images/surveyor/ui-icons_cd0a0a_256x240.png +0 -0
  95. data/lib/assets/javascripts/surveyor/jquery-1.9.0.js +9555 -0
  96. data/lib/assets/javascripts/surveyor/jquery-ui-1.10.0.custom.js +14850 -0
  97. data/lib/assets/javascripts/surveyor/jquery-ui-timepicker-addon.js +1919 -0
  98. data/lib/assets/javascripts/surveyor/jquery.maskedinput.js +338 -0
  99. data/lib/assets/javascripts/surveyor/jquery.selectToUISlider.js +240 -0
  100. data/lib/assets/javascripts/surveyor/jquery.surveyor.js +154 -0
  101. data/lib/assets/stylesheets/surveyor.sass +132 -0
  102. data/lib/assets/stylesheets/surveyor/jquery-ui-1.10.0.custom.css +1174 -0
  103. data/lib/assets/stylesheets/surveyor/jquery-ui-timepicker-addon.css +11 -0
  104. data/lib/assets/stylesheets/surveyor/reset.css +48 -0
  105. data/lib/assets/stylesheets/surveyor/results.css +125 -0
  106. data/lib/assets/stylesheets/surveyor/ui.slider.extras.css +110 -0
  107. data/lib/generators/surveyor/custom_generator.rb +18 -0
  108. data/lib/generators/surveyor/install_generator.rb +101 -0
  109. data/lib/generators/surveyor/templates/app/assets/javascripts/surveyor_all.js +6 -0
  110. data/lib/generators/surveyor/templates/app/assets/stylesheets/surveyor_all.css +9 -0
  111. data/lib/generators/surveyor/templates/app/controllers/surveyor_controller.rb +40 -0
  112. data/lib/generators/surveyor/templates/app/views/layouts/surveyor_custom.html.erb +13 -0
  113. data/lib/generators/surveyor/templates/config/locales/surveyor_en.yml +19 -0
  114. data/lib/generators/surveyor/templates/config/locales/surveyor_es.yml +19 -0
  115. data/lib/generators/surveyor/templates/config/locales/surveyor_he.yml +19 -0
  116. data/lib/generators/surveyor/templates/config/locales/surveyor_ko.yml +19 -0
  117. data/lib/generators/surveyor/templates/db/migrate/add_api_id_to_question_groups.rb +10 -0
  118. data/lib/generators/surveyor/templates/db/migrate/add_api_ids.rb +14 -0
  119. data/lib/generators/surveyor/templates/db/migrate/add_api_ids_to_response_sets_and_responses.rb +12 -0
  120. data/lib/generators/surveyor/templates/db/migrate/add_correct_answer_id_to_questions.rb +10 -0
  121. data/lib/generators/surveyor/templates/db/migrate/add_default_value_to_answers.rb +10 -0
  122. data/lib/generators/surveyor/templates/db/migrate/add_display_order_to_surveys.rb +10 -0
  123. data/lib/generators/surveyor/templates/db/migrate/add_display_type_to_answers.rb +14 -0
  124. data/lib/generators/surveyor/templates/db/migrate/add_index_to_response_sets.rb +10 -0
  125. data/lib/generators/surveyor/templates/db/migrate/add_index_to_surveys.rb +10 -0
  126. data/lib/generators/surveyor/templates/db/migrate/add_input_mask_attributes_to_answer.rb +12 -0
  127. data/lib/generators/surveyor/templates/db/migrate/add_section_id_to_responses.rb +12 -0
  128. data/lib/generators/surveyor/templates/db/migrate/add_unique_index_on_access_code_and_version_in_surveys.rb +10 -0
  129. data/lib/generators/surveyor/templates/db/migrate/add_unique_indicies.rb +18 -0
  130. data/lib/generators/surveyor/templates/db/migrate/add_version_to_surveys.rb +10 -0
  131. data/lib/generators/surveyor/templates/db/migrate/api_ids_must_be_unique.rb +23 -0
  132. data/lib/generators/surveyor/templates/db/migrate/create_answers.rb +38 -0
  133. data/lib/generators/surveyor/templates/db/migrate/create_dependencies.rb +23 -0
  134. data/lib/generators/surveyor/templates/db/migrate/create_dependency_conditions.rb +30 -0
  135. data/lib/generators/surveyor/templates/db/migrate/create_question_groups.rb +28 -0
  136. data/lib/generators/surveyor/templates/db/migrate/create_questions.rb +37 -0
  137. data/lib/generators/surveyor/templates/db/migrate/create_response_sets.rb +23 -0
  138. data/lib/generators/surveyor/templates/db/migrate/create_responses.rb +34 -0
  139. data/lib/generators/surveyor/templates/db/migrate/create_survey_sections.rb +30 -0
  140. data/lib/generators/surveyor/templates/db/migrate/create_survey_translations.rb +19 -0
  141. data/lib/generators/surveyor/templates/db/migrate/create_surveys.rb +32 -0
  142. data/lib/generators/surveyor/templates/db/migrate/create_validation_conditions.rb +33 -0
  143. data/lib/generators/surveyor/templates/db/migrate/create_validations.rb +21 -0
  144. data/lib/generators/surveyor/templates/db/migrate/drop_unique_index_on_access_code_in_surveys.rb +10 -0
  145. data/lib/generators/surveyor/templates/db/migrate/update_blank_api_ids_on_question_group.rb +22 -0
  146. data/lib/generators/surveyor/templates/db/migrate/update_blank_versions_on_surveys.rb +13 -0
  147. data/lib/generators/surveyor/templates/surveys/EXTENDING_SURVEYOR.md +52 -0
  148. data/lib/generators/surveyor/templates/surveys/MODIFYING_SURVEYOR.md +91 -0
  149. data/lib/generators/surveyor/templates/surveys/date_survey.rb +16 -0
  150. data/lib/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +280 -0
  151. data/lib/generators/surveyor/templates/surveys/languages.rb +14 -0
  152. data/lib/generators/surveyor/templates/surveys/quiz.rb +11 -0
  153. data/lib/generators/surveyor/templates/surveys/translations/languages.es.yml +18 -0
  154. data/lib/generators/surveyor/templates/surveys/translations/languages.he.yml +18 -0
  155. data/lib/generators/surveyor/templates/surveys/translations/languages.ko.yml +18 -0
  156. data/lib/generators/surveyor/templates/vendor/assets/stylesheets/custom.sass +5 -0
  157. data/lib/surveyor.rb +13 -0
  158. data/lib/surveyor/acts_as_response.rb +17 -0
  159. data/lib/surveyor/common.rb +59 -0
  160. data/lib/surveyor/engine.rb +10 -0
  161. data/lib/surveyor/helpers/asset_pipeline.rb +18 -0
  162. data/lib/surveyor/helpers/formtastic_custom_input.rb +13 -0
  163. data/lib/surveyor/helpers/surveyor_helper_methods.rb +103 -0
  164. data/lib/surveyor/models/answer_methods.rb +83 -0
  165. data/lib/surveyor/models/dependency_condition_methods.rb +69 -0
  166. data/lib/surveyor/models/dependency_methods.rb +57 -0
  167. data/lib/surveyor/models/question_group_methods.rb +58 -0
  168. data/lib/surveyor/models/question_methods.rb +112 -0
  169. data/lib/surveyor/models/response_methods.rb +126 -0
  170. data/lib/surveyor/models/response_set_methods.rb +188 -0
  171. data/lib/surveyor/models/survey_methods.rb +100 -0
  172. data/lib/surveyor/models/survey_section_methods.rb +53 -0
  173. data/lib/surveyor/models/survey_translation_methods.rb +30 -0
  174. data/lib/surveyor/models/validation_condition_methods.rb +54 -0
  175. data/lib/surveyor/models/validation_methods.rb +45 -0
  176. data/lib/surveyor/mustache_context.rb +11 -0
  177. data/lib/surveyor/parser.rb +427 -0
  178. data/lib/surveyor/redcap_parser.rb +288 -0
  179. data/lib/surveyor/surveyor_controller_methods.rb +236 -0
  180. data/lib/surveyor/unparser.rb +147 -0
  181. data/lib/surveyor/version.rb +3 -0
  182. data/lib/tasks/surveyor_tasks.rake +88 -0
  183. data/rails/init.rb +1 -0
  184. data/spec/controllers/surveyor_controller_spec.rb +311 -0
  185. data/spec/factories.rb +166 -0
  186. data/spec/helpers/formtastic_custom_input_spec.rb +15 -0
  187. data/spec/helpers/surveyor_helper_spec.rb +117 -0
  188. data/spec/lib/benchmark_spec.rb +22 -0
  189. data/spec/lib/chinese_survey.rb +14 -0
  190. data/spec/lib/common_spec.rb +34 -0
  191. data/spec/lib/parser_spec.rb +204 -0
  192. data/spec/lib/rake_kitchen_sink.rb +40 -0
  193. data/spec/lib/redcap_parser_spec.rb +75 -0
  194. data/spec/lib/tasks_spec.rake +26 -0
  195. data/spec/lib/unparser_spec.rb +126 -0
  196. data/spec/models/answer_spec.rb +174 -0
  197. data/spec/models/dependency_condition_spec.rb +439 -0
  198. data/spec/models/dependency_spec.rb +101 -0
  199. data/spec/models/question_group_spec.rb +93 -0
  200. data/spec/models/question_spec.rb +206 -0
  201. data/spec/models/response_set_spec.rb +477 -0
  202. data/spec/models/response_spec.rb +218 -0
  203. data/spec/models/survey_section_spec.rb +85 -0
  204. data/spec/models/survey_spec.rb +191 -0
  205. data/spec/models/validation_condition_spec.rb +113 -0
  206. data/spec/models/validation_spec.rb +74 -0
  207. data/spec/rcov.opts +2 -0
  208. data/spec/spec.opts +4 -0
  209. data/spec/spec_helper.rb +56 -0
  210. data/stacktests.sh +65 -0
  211. data/surveyor.gemspec +53 -0
  212. metadata +601 -0
@@ -0,0 +1,100 @@
1
+ require 'surveyor/common'
2
+ require 'rabl'
3
+
4
+ module Surveyor
5
+ module Models
6
+ module SurveyMethods
7
+ def self.included(base)
8
+ # Associations
9
+ base.instance_eval {has_many :sections, ->{order "#{SurveySection.quoted_table_name}.display_order"}, :class_name => "SurveySection", :dependent => :destroy}
10
+ base.instance_eval {has_many :sections_with_questions, ->{includes(:questions).order("#{SurveySection.quoted_table_name}.display_order")}, :class_name => "SurveySection"}
11
+ base.send :has_many, :response_sets
12
+ base.send :has_many, :translations, :class_name => "SurveyTranslation"
13
+
14
+ # Scopes
15
+ base.instance_eval {scope :with_sections, ->{includes :sections}}
16
+
17
+ @@validations_already_included ||= nil
18
+ unless @@validations_already_included
19
+ # Validations
20
+ base.send :validates_presence_of, :title
21
+ base.send :validates_uniqueness_of, :survey_version, :scope => :access_code, :message => "survey with matching access code and version already exists"
22
+
23
+ @@validations_already_included = true
24
+ end
25
+
26
+ # Derived attributes
27
+ base.send :before_save, :generate_access_code
28
+ base.send :before_save, :increment_version
29
+
30
+ # Class methods
31
+ base.instance_eval do
32
+ def to_normalized_string(value)
33
+ # replace non-alphanumeric with "-". remove repeat "-"s. don't start or end with "-"
34
+ value.to_s.downcase.gsub(/[^a-z0-9]/,"-").gsub(/-+/,"-").gsub(/-$|^-/,"")
35
+ end
36
+ end
37
+ end
38
+
39
+ # Instance methods
40
+ def initialize(*args)
41
+ super(*args)
42
+ default_args
43
+ end
44
+
45
+ def default_args
46
+ self.api_id ||= Surveyor::Common.generate_api_id
47
+ self.display_order ||= Survey.count
48
+ end
49
+
50
+ def active?
51
+ self.active_as_of?(DateTime.now)
52
+ end
53
+ def active_as_of?(date)
54
+ (active_at && active_at < date && (!inactive_at || inactive_at > date)) ? true : false
55
+ end
56
+ def activate!
57
+ self.active_at = DateTime.now
58
+ self.inactive_at = nil
59
+ end
60
+ def deactivate!
61
+ self.inactive_at = DateTime.now
62
+ self.active_at = nil
63
+ end
64
+
65
+ def as_json(options = nil)
66
+ template_paths = ActionController::Base.view_paths.collect(&:to_path)
67
+ Rabl.render(filtered_for_json, 'surveyor/export.json', :view_path => template_paths, :format => "hash")
68
+ end
69
+
70
+ ##
71
+ # A hook that allows the survey object to be modified before it is
72
+ # serialized by the #as_json method.
73
+ def filtered_for_json
74
+ self
75
+ end
76
+
77
+ def default_access_code
78
+ self.class.to_normalized_string(title)
79
+ end
80
+
81
+ def generate_access_code
82
+ self.access_code ||= default_access_code
83
+ end
84
+
85
+ def increment_version
86
+ surveys = self.class.select(:survey_version).where(:access_code => access_code).order("survey_version DESC")
87
+ next_version = surveys.any? ? surveys.first.survey_version.to_i + 1 : 0
88
+
89
+ self.survey_version = next_version
90
+ end
91
+
92
+ def translation(locale_symbol)
93
+ t = self.translations.where(:locale => locale_symbol.to_s).first
94
+ {:title => self.title, :description => self.description}.with_indifferent_access.merge(
95
+ t ? YAML.load(t.translation || "{}").with_indifferent_access : {}
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ module Surveyor
2
+ module Models
3
+ module SurveySectionMethods
4
+ def self.included(base)
5
+ # Associations
6
+ base.send :has_many, :questions, :dependent => :destroy
7
+ base.send :belongs_to, :survey
8
+
9
+ # Scopes
10
+ base.instance_eval {scope :with_includes, ->{ includes :questions => [:answers, :question_group, {:dependency => :dependency_conditions}]}}
11
+
12
+ @@validations_already_included ||= nil
13
+ unless @@validations_already_included
14
+ # Validations
15
+ base.send :validates_presence_of, :title, :display_order
16
+ # this causes issues with building and saving
17
+ #, :survey
18
+
19
+ @@validations_already_included = true
20
+ end
21
+ end
22
+
23
+ # Instance Methods
24
+ def initialize(*args)
25
+ super(*args)
26
+ default_args
27
+ end
28
+
29
+ def default_args
30
+ self.data_export_identifier ||= Surveyor::Common.normalize(title)
31
+ end
32
+
33
+ def questions_and_groups
34
+ qs = []
35
+ questions.each_with_index.map do |q,i|
36
+ if q.part_of_group?
37
+ if (i+1 >= questions.size) or (q.question_group_id != questions[i+1].question_group_id)
38
+ q.question_group
39
+ end
40
+ else
41
+ q
42
+ end
43
+ end.compact
44
+ end
45
+
46
+ def translation(locale)
47
+ {:title => self.title, :description => self.description}.with_indifferent_access.merge(
48
+ (self.survey.translation(locale)[:survey_sections] || {})[self.reference_identifier] || {}
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ module Surveyor
2
+ module Models
3
+ module SurveyTranslationMethods
4
+ def self.included(base)
5
+ # Associations
6
+ base.send :belongs_to, :survey
7
+
8
+ @@validations_already_included ||= nil
9
+ unless @@validations_already_included
10
+ # Validations
11
+ base.send :validates_presence_of, :locale, :translation
12
+ base.send :validates_uniqueness_of, :locale, :scope => :survey_id
13
+ # this causes issues with building and saving
14
+ #, :survey
15
+
16
+ @@validations_already_included = true
17
+ end
18
+ end
19
+
20
+ # Instance Methods
21
+ def initialize(*args)
22
+ super(*args)
23
+ default_args
24
+ end
25
+
26
+ def default_args
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ module Surveyor
2
+ module Models
3
+ module ValidationConditionMethods
4
+ def self.included(base)
5
+ # Associations
6
+ base.send :belongs_to, :validation
7
+
8
+ # Scopes
9
+ @@validations_already_included ||= nil
10
+ unless @@validations_already_included
11
+ # Validations
12
+ base.send :validates_presence_of, :operator, :rule_key
13
+ base.send :validates_inclusion_of, :operator, :in => Surveyor::Common::OPERATORS
14
+ base.send :validates_uniqueness_of, :rule_key, :scope => :validation_id
15
+ # this causes issues with building and saving
16
+ # base.send :validates_numericality_of, :validation_id #, :question_id, :answer_id
17
+
18
+ @@validations_already_included = true
19
+ end
20
+
21
+ base.send :include, Surveyor::ActsAsResponse # includes "as" instance method
22
+
23
+ # Class methods
24
+ base.instance_eval do
25
+ def operators
26
+ Surveyor::Common::OPERATORS
27
+ end
28
+ end
29
+ end
30
+
31
+ # Instance Methods
32
+ def to_hash(response)
33
+ {rule_key.to_sym => (response.nil? ? false : self.is_valid?(response))}
34
+ end
35
+
36
+ def is_valid?(response)
37
+ klass = response.answer.response_class
38
+ compare_to = Response.find_by_question_id_and_answer_id(self.question_id, self.answer_id) || self
39
+ case self.operator
40
+ when "==", "<", ">", "<=", ">="
41
+ response.as(klass).send(self.operator, compare_to.as(klass))
42
+ when "!="
43
+ !(response.as(klass) == compare_to.as(klass))
44
+ when "=~"
45
+ return false if compare_to != self
46
+ # SMELL Eval regex!!
47
+ !(response.as(klass).to_s =~ Regexp.new(eval(self.regexp) || "")).nil?
48
+ else
49
+ false
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ module Surveyor
2
+ module Models
3
+ module ValidationMethods
4
+ def self.included(base)
5
+ # Associations
6
+ base.send :belongs_to, :answer
7
+ base.send :has_many, :validation_conditions, :dependent => :destroy
8
+
9
+ # Scopes
10
+
11
+ @@validations_already_included ||= nil
12
+ unless @@validations_already_included
13
+ # Validations
14
+ base.send :validates_presence_of, :rule
15
+ base.send :validates_format_of, :rule, :with => /^(?:and|or|\)|\(|[A-Z]|\s)+$/, :multiline => true # SMELL with :multiline => true Rails reports a security risk
16
+ # this causes issues with building and saving
17
+ # base.send :validates_numericality_of, :answer_id
18
+
19
+ @@validations_already_included = true
20
+ end
21
+ end
22
+
23
+ # Instance Methods
24
+ def is_valid?(response_set)
25
+ ch = conditions_hash(response_set)
26
+ rgx = Regexp.new(self.validation_conditions.map{|vc| ["a","o"].include?(vc.rule_key) ? "#{vc.rule_key}(?!nd|r)" : vc.rule_key}.join("|")) # exclude and, or
27
+ # logger.debug "v: #{self.inspect}"
28
+ # logger.debug "rule: #{self.rule.inspect}"
29
+ # logger.debug "rexp: #{rgx.inspect}"
30
+ # logger.debug "keyp: #{ch.inspect}"
31
+ # logger.debug "subd: #{self.rule.gsub(rgx){|m| ch[m.to_sym]}}"
32
+ eval(self.rule.gsub(rgx){|m| ch[m.to_sym]})
33
+ end
34
+
35
+ # A hash of the conditions (keyed by rule_key) and their evaluation (boolean) in the context of response_set
36
+ def conditions_hash(response_set)
37
+ hash = {}
38
+ response = response_set.responses.detect{|r| r.answer_id.to_i == self.answer_id.to_i}
39
+ # logger.debug "r: #{response.inspect}"
40
+ self.validation_conditions.each{|vc| hash.merge!(vc.to_hash(response))}
41
+ return hash
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ module Surveyor
2
+ module MustacheContext
3
+ def in_context(text, context=nil)
4
+ case context
5
+ when NilClass then text
6
+ when Hash then Mustache.render(text, context)
7
+ else context.render(text)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,427 @@
1
+ %w(survey survey_translation survey_section question_group question dependency dependency_condition answer validation validation_condition).each {|model| require model }
2
+
3
+ require 'yaml'
4
+
5
+ module Surveyor
6
+ class ParserError < StandardError; end
7
+ class Parser
8
+ class << self; attr_accessor :options, :log end
9
+
10
+ # Attributes
11
+ attr_accessor :context
12
+
13
+ # Class methods
14
+ def self.parse_file(filename, options={})
15
+ self.parse(File.read(filename),{:filename => filename}.merge(options))
16
+ end
17
+ def self.parse(str, options={})
18
+ self.ensure_attrs
19
+ self.options = options
20
+ self.log[:source] = str
21
+ Surveyor::Parser.rake_trace "\n"
22
+ Surveyor::Parser.new.parse(str)
23
+ Surveyor::Parser.rake_trace "\n"
24
+ end
25
+ def self.ensure_attrs
26
+ self.options ||= {}
27
+ self.log ||= {}
28
+ self.log[:indent] ||= 0
29
+ self.log[:source] ||= ""
30
+ end
31
+ def self.rake_trace(str, indent_increment=0)
32
+ self.ensure_attrs
33
+ unless str.blank?
34
+ puts "#{' ' * self.log[:indent]}#{str}" if self.options[:trace] == true
35
+ end
36
+ self.log[:indent] += indent_increment
37
+ end
38
+ # from https://github.com/rails/rails/blob/master/actionpack/lib/action_view/template/error.rb#L81
39
+ def self.source_extract(line)
40
+ source_code = self.log[:source].split("\n")
41
+ radius = 3
42
+ start_on_line = [ line - radius - 1, 0 ].max
43
+ end_on_line = [ line + radius - 1, source_code.length].min
44
+
45
+ return unless source_code = source_code[start_on_line..end_on_line]
46
+ line_counter = start_on_line
47
+ source_code.sum do |line|
48
+ line_counter += 1
49
+ "#{line_counter}: #{line}\n"
50
+ end
51
+ end
52
+ def self.raise_error(str, skip_trace = false)
53
+ self.ensure_attrs
54
+ line = caller[1].split('/').last.split(':')[1].to_i
55
+
56
+ raise Surveyor::ParserError, "#{str}\n" if skip_trace
57
+ raise Surveyor::ParserError, "#{self.source_extract(line)}\nline \##{line}: #{str}\n"
58
+ end
59
+
60
+ # Instance methods
61
+ def initialize
62
+ self.context = {}
63
+ end
64
+ def parse(str)
65
+ instance_eval(str)
66
+ return context[:survey]
67
+ end
68
+ # This method_missing does all the heavy lifting for the DSL
69
+ def method_missing(missing_method, *args, &block)
70
+ method_name, reference_identifier = missing_method.to_s.split("_", 2)
71
+ type = full(method_name)
72
+ Surveyor::Parser.raise_error( "\"#{type}\" is not a surveyor method." )if !%w(survey survey_translation survey_section question_group question dependency dependency_condition answer validation validation_condition).include?(type)
73
+
74
+ Surveyor::Parser.rake_trace(reference_identifier.blank? ? "#{type} #{args.map(&:inspect).join ', '}" : "#{type}_#{reference_identifier} #{args.map(&:inspect).join ', '}",
75
+ block_models.include?(type) ? 2 : 0)
76
+
77
+ # check for blocks
78
+ Surveyor::Parser.raise_error "A #{type.humanize.downcase} should take a block" if block_models.include?(type) && !block_given?
79
+ Surveyor::Parser.raise_error "A #{type.humanize.downcase} doesn't take a block" if !block_models.include?(type) && block_given?
80
+
81
+ # parse and build
82
+ type.classify.constantize.new.extend("SurveyorParser#{type.classify}Methods".constantize).parse_and_build(context, args, method_name, reference_identifier)
83
+
84
+ # evaluate and clear context for block models
85
+ if block_models.include?(type)
86
+ self.instance_eval(&block)
87
+ if type == 'survey'
88
+ resolve_dependency_condition_references
89
+ resolve_question_correct_answers
90
+ report_lost_and_duplicate_references
91
+ report_missing_default_locale
92
+ Surveyor::Parser.rake_trace("", -2)
93
+ if context[:survey].save
94
+ Surveyor::Parser.rake_trace "Survey saved."
95
+ else
96
+ Surveyor::Parser.raise_error "Survey not saved: #{context[:survey].errors.full_messages.join(", ")}"
97
+ end
98
+ else
99
+ Surveyor::Parser.rake_trace("", -2)
100
+ context[type.to_sym].clear(context)
101
+ end
102
+ end
103
+ end
104
+
105
+ # Private methods
106
+ private
107
+
108
+ def full(method_name)
109
+ case method_name.to_s
110
+ when /^translations$/; "survey_translation"
111
+ when /^section$/; "survey_section"
112
+ when /^g$|^grid$|^group$|^repeater$/; "question_group"
113
+ when /^q$|^label$|^image$/; "question"
114
+ when /^a$/; "answer"
115
+ when /^d$/; "dependency"
116
+ when /^c(ondition)?$/; context[:validation] ? "validation_condition" : "dependency_condition"
117
+ when /^v$/; "validation"
118
+ when /^dc(ondition)?$/; "dependency_condition"
119
+ when /^vc(ondition)?$/; "validation_condition"
120
+ else method_name
121
+ end
122
+ end
123
+ def block_models
124
+ %w(survey survey_section question_group)
125
+ end
126
+ def report_missing_default_locale
127
+ if !self.context[:survey].translations.empty? && self.context[:survey].translations.select{|t|YAML::load(t.translation)=={}}.empty?
128
+ Surveyor::Parser.raise_error("No default locale specified for translations.",true)
129
+ end
130
+ end
131
+ def report_lost_and_duplicate_references
132
+ Surveyor::Parser.raise_error("Bad references: #{self.context[:bad_references].join("; ")}", true) unless self.context[:bad_references].empty?
133
+ Surveyor::Parser.raise_error("Duplicate references: #{self.context[:duplicate_references].join("; ")}", true) unless self.context[:duplicate_references].empty?
134
+ end
135
+ def resolve_question_correct_answers
136
+ self.context[:questions_with_correct_answers].each do |question_reference_idenitifer, correct_answer_reference|
137
+ # Looking up references for quiz answers
138
+ if self.context[:answer_references][question_reference_idenitifer] &&
139
+ (a = self.context[:answer_references][question_reference_idenitifer][correct_answer_reference]) &&
140
+ a.save
141
+ self.context[:question_references][question_reference_idenitifer].correct_answer_id = a.id
142
+ else
143
+ self.context[:bad_references].push "q_#{question_reference_idenitifer}.correct => a_#{correct_answer_reference}"
144
+ end
145
+ end
146
+ end
147
+ def resolve_dependency_condition_references
148
+ self.context[:dependency_conditions].each do |dc|
149
+ # Looking up references to questions and answers for linking the dependency objects
150
+ self.context[:bad_references].push "q_#{dc.question_reference}" unless (dc.question = self.context[:question_references][dc.question_reference])
151
+ self.context[:answer_references][dc.question_reference] ||= {}
152
+ self.context[:bad_references].push "q_#{dc.question_reference}, a_#{dc.answer_reference}" if !dc.answer_reference.blank? and (dc.answer = self.context[:answer_references][dc.question_reference][dc.answer_reference]).nil?
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ # Surveyor models with extra parsing methods
159
+
160
+ # Survey model
161
+ module SurveyorParserSurveyMethods
162
+ def parse_and_build(context, args, original_method, reference_identifier)
163
+ # clear context
164
+ clear(context)
165
+
166
+ # build and set context
167
+ title = args[0]
168
+ args[1] ||= {}
169
+ context[:default_mandatory] = args[1].delete(:default_mandatory) || false
170
+ self.attributes = ({
171
+ :title => title,
172
+ :reference_identifier => reference_identifier }.merge(args[1]))
173
+ context[:survey] = self
174
+ end
175
+ def clear(context)
176
+ context.delete_if{|k,v| true}
177
+ context.merge!({
178
+ :question_references => {},
179
+ :answer_references => {},
180
+ :bad_references => [],
181
+ :duplicate_references => [],
182
+ :dependency_conditions => [],
183
+ :questions_with_correct_answers => {},
184
+ :default_mandatory => false
185
+ })
186
+ end
187
+ end
188
+
189
+ # SurveySection model
190
+ module SurveyorParserSurveyTranslationMethods
191
+ def parse_and_build(context, args, original_method, reference_identifier)
192
+ dir = Surveyor::Parser.options[:filename].nil? ? Dir.pwd : File.dirname(Surveyor::Parser.options[:filename])
193
+ # build, no change in context
194
+ args[0].each do |k,v|
195
+ case v
196
+ when Hash
197
+ trans = YAML::dump(v)
198
+ when String
199
+ trans = File.read(File.join(dir,v))
200
+ when :default
201
+ trans = YAML::dump({})
202
+ end
203
+ context[:survey].translations << self.class.new(:locale => k.to_s, :translation => trans)
204
+ end
205
+ end
206
+ end
207
+
208
+ # SurveySection model
209
+ module SurveyorParserSurveySectionMethods
210
+ def parse_and_build(context, args, original_method, reference_identifier)
211
+ # clear context
212
+ clear(context)
213
+
214
+ # build and set context
215
+ title = args[0]
216
+ self.attributes = ({
217
+ :title => title,
218
+ :reference_identifier => reference_identifier,
219
+ :display_order => context[:survey].sections.size }.merge(args[1] || {}))
220
+ context[:survey].sections << context[:survey_section] = self
221
+ end
222
+ def clear(context)
223
+ [ :survey_section,
224
+ :question_group,
225
+ :grid_answers,
226
+ :question,
227
+ :dependency,
228
+ :dependency_condition,
229
+ :answer,
230
+ :validation,
231
+ :validation_condition ].each{|k| context.delete k}
232
+ end
233
+ end
234
+
235
+ # QuestionGroup model
236
+ module SurveyorParserQuestionGroupMethods
237
+ def parse_and_build(context, args, original_method, reference_identifier)
238
+ # clear context
239
+ clear(context)
240
+
241
+ # build and set context
242
+ self.attributes = ({
243
+ :text => args[0] || "Question Group",
244
+ :reference_identifier => reference_identifier,
245
+ :display_type => (original_method =~ /grid|repeater/ ? original_method : "default")}.merge(args[1] || {}))
246
+ context[:question_group] = self
247
+ end
248
+ def clear(context)
249
+ [ :question_group,
250
+ :grid_answers,
251
+ :question,
252
+ :dependency,
253
+ :dependency_condition,
254
+ :answer,
255
+ :validation,
256
+ :validation_condition ].each{|k| context.delete k}
257
+ context[:grid_answers] = []
258
+ end
259
+ end
260
+
261
+ # Question model
262
+ module SurveyorParserQuestionMethods
263
+ def parse_and_build(context, args, original_method, reference_identifier)
264
+ # clear context
265
+ [ :question,
266
+ :dependency,
267
+ :dependency_condition,
268
+ :answer,
269
+ :validation,
270
+ :validation_condition ].each{|k| context.delete k}
271
+
272
+ # build and set context
273
+ text = args[0] || "Question"
274
+ hash_args = args[1] || {}
275
+ correct = hash_args.delete :correct
276
+ self.attributes = ({
277
+ :question_group => context[:question_group],
278
+ :reference_identifier => reference_identifier,
279
+ :is_mandatory => context[:default_mandatory],
280
+ :text => text,
281
+ :display_type => (original_method =~ /label|image/ ? original_method : "default"),
282
+ :display_order => context[:survey_section].questions.size }.merge(hash_args))
283
+ context[:survey_section].questions << context[:question] = self
284
+
285
+ # keep reference for correct answers
286
+ context[:questions_with_correct_answers][self.reference_identifier] = correct unless self.reference_identifier.blank? or correct.blank?
287
+
288
+ # keep reference for dependencies
289
+ unless self.reference_identifier.blank?
290
+ context[:duplicate_references].push "q_#{self.reference_identifier}" if context[:question_references].has_key?(self.reference_identifier)
291
+ context[:question_references][self.reference_identifier] = context[:question]
292
+ end
293
+
294
+ # add grid answers
295
+ if context[:question_group] && context[:question_group].display_type == "grid"
296
+ (context[:grid_answers] || []).each do |grid_answer|
297
+ a = context[:question].answers.build(grid_answer.attributes.reject{|k,v| %w(id api_id created_at updated_at).include?(k)})
298
+ context[:answer_references][self.reference_identifier] ||= {} unless self.reference_identifier.blank?
299
+ context[:answer_references][self.reference_identifier][grid_answer.reference_identifier] = a unless self.reference_identifier.blank? or grid_answer.reference_identifier.blank?
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ # Dependency model
306
+ module SurveyorParserDependencyMethods
307
+ def parse_and_build(context, args, original_method, reference_identifier)
308
+ # clear context
309
+ [ :dependency,
310
+ :dependency_condition ].each{|k| context.delete k}
311
+
312
+ # build and set context
313
+ self.attributes = (args[0] || {})
314
+ if context[:question]
315
+ context[:dependency] = context[:question].dependency = self
316
+ elsif context[:question_group]
317
+ context[:dependency] = context[:question_group].dependency = self
318
+ end
319
+ end
320
+ end
321
+
322
+ # DependencyCondition model
323
+ module SurveyorParserDependencyConditionMethods
324
+ DependencyCondition.instance_eval do
325
+ attr_accessor :question_reference, :answer_reference
326
+ end
327
+ def parse_and_build(context, args, original_method, reference_identifier)
328
+ # clear context
329
+ context.delete :dependency_condition
330
+
331
+ # build and set context
332
+ a0, a1, a2 = args
333
+ self.attributes = ({
334
+ :operator => a1 || "==",
335
+ :question_reference => a0.to_s.gsub(/^q_|^question_/, ""),
336
+ :rule_key => reference_identifier
337
+ }.merge( a2.is_a?(Hash) ? a2 : { :answer_reference => a2.to_s.gsub(/^a_|^answer_/, "") }))
338
+ context[:dependency].dependency_conditions << context[:dependency_condition] = self
339
+ context[:dependency_conditions] << self
340
+ end
341
+ end
342
+
343
+ # Answer model
344
+ module SurveyorParserAnswerMethods
345
+ def parse_and_build(context, args, original_method, reference_identifier)
346
+ # clear context
347
+ [ :answer,
348
+ :validation,
349
+ :validation_condition ].each{|k| context.delete k}
350
+ attrs = { :reference_identifier => reference_identifier }.merge(parse_args(args))
351
+
352
+ # add answers to grid
353
+ if context[:question_group] && context[:question_group].display_type == "grid"
354
+ self.attributes = ({:display_order => context[:grid_answers].size}.merge(attrs))
355
+ context[:grid_answers] << context[:answer] = self
356
+ else
357
+ self.attributes = ({:display_order => context[:question].answers.size}.merge(attrs))
358
+ context[:question].answers << context[:answer] = self
359
+ # keep reference for dependencies
360
+ unless context[:question].reference_identifier.blank? or self.reference_identifier.blank?
361
+ context[:answer_references][context[:question].reference_identifier] ||= {}
362
+ context[:duplicate_references].push "q_#{context[:question].reference_identifier}, a_#{self.reference_identifier}" if context[:answer_references][context[:question].reference_identifier].has_key?(self.reference_identifier)
363
+ context[:answer_references][context[:question].reference_identifier][self.reference_identifier] = context[:answer]
364
+ end
365
+ end
366
+ end
367
+ def parse_args(args)
368
+ case args[0]
369
+ when Hash # Hash
370
+ text_args(args[0][:text]).merge(args[0])
371
+ when String # (String, Hash) or (String, Symbol, Hash)
372
+ text_args(args[0]).merge(hash_from args[1]).merge(args[2] || {})
373
+ when Symbol # (Symbol, Hash) or (Symbol, Symbol, Hash)
374
+ symbol_args(args[0]).merge(hash_from args[1]).merge(args[2] || {})
375
+ else
376
+ text_args(nil)
377
+ end
378
+ end
379
+ def text_args(text = "Answer")
380
+ {:text => text.to_s}
381
+ end
382
+ def hash_from(arg)
383
+ arg.is_a?(Symbol) ? {:response_class => arg.to_s} : arg.is_a?(Hash) ? arg : {}
384
+ end
385
+ def symbol_args(arg)
386
+ case arg
387
+ when :other
388
+ text_args("Other")
389
+ when :other_and_string
390
+ text_args("Other").merge({:response_class => "string"})
391
+ when :none, :omit # is_exclusive erases and disables other checkboxes and input elements
392
+ text_args(arg.to_s.humanize).merge({:is_exclusive => true})
393
+ when :integer, :float, :date, :time, :datetime, :text, :datetime, :string
394
+ text_args(arg.to_s.humanize).merge({:response_class => arg.to_s, :display_type => "hidden_label"})
395
+ end
396
+ end
397
+ end
398
+
399
+ # Validation model
400
+ module SurveyorParserValidationMethods
401
+ def parse_and_build(context, args, original_method, reference_identifier)
402
+ # clear context
403
+ [ :validation,
404
+ :validation_condition ].each{|k| context.delete k}
405
+
406
+ context.delete_if{|k,v| %w(validation validation_condition).map(&:to_sym).include? k}
407
+
408
+ # build and set context
409
+ self.attributes = ({:rule => "A"}.merge(args[0] || {}))
410
+ context[:answer].validations << context[:validation] = self
411
+ end
412
+ end
413
+
414
+ # ValidationCondition model
415
+ module SurveyorParserValidationConditionMethods
416
+ def parse_and_build(context, args, original_method, reference_identifier)
417
+ # clear context
418
+ context.delete :validation_condition
419
+
420
+ # build and set context
421
+ a0, a1 = args
422
+ self.attributes = ({
423
+ :operator => a0 || "==",
424
+ :rule_key => reference_identifier}.merge(a1 || {}))
425
+ context[:validation].validation_conditions << context[:validation_condition] = self
426
+ end
427
+ end