effective_questions 0.0.2 → 0.0.4

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/admin/responses_controller.rb +13 -0
  3. data/app/datatables/admin/effective_questions_datatable.rb +7 -2
  4. data/app/datatables/admin/effective_responses_datatable.rb +24 -0
  5. data/app/models/concerns/acts_as_questionable.rb +4 -0
  6. data/app/models/concerns/acts_as_responsable.rb +14 -0
  7. data/app/models/effective/question.rb +67 -2
  8. data/app/models/effective/question_answer.rb +204 -0
  9. data/app/models/effective/question_option.rb +2 -0
  10. data/app/models/effective/response.rb +78 -7
  11. data/app/models/effective/response_option.rb +5 -0
  12. data/app/views/admin/questions/_fields_category.html.haml +76 -0
  13. data/app/views/admin/questions/_fields_scored.html.haml +131 -0
  14. data/app/views/admin/questions/_form.html.haml +7 -86
  15. data/app/views/effective/questionable/results/_question_option.html.haml +1 -1
  16. data/app/views/effective/responsable/_results.html.haml +27 -1
  17. data/app/views/effective/responses/fields/_decimal.html.haml +2 -0
  18. data/app/views/effective/responses/fields/_percentage.html.haml +2 -0
  19. data/app/views/effective/responses/fields/_price.html.haml +2 -0
  20. data/app/views/effective/responses/responses/_choose_one.html.haml +1 -1
  21. data/app/views/effective/responses/responses/_decimal.html.haml +1 -0
  22. data/app/views/effective/responses/responses/_percentage.html.haml +5 -0
  23. data/app/views/effective/responses/responses/_price.html.haml +1 -0
  24. data/app/views/effective/responses/responses/_select_all_that_apply.html.haml +2 -2
  25. data/app/views/effective/responses/responses/_select_up_to_1.html.haml +1 -1
  26. data/app/views/effective/responses/responses/_select_up_to_2.html.haml +2 -2
  27. data/app/views/effective/responses/responses/_select_up_to_3.html.haml +2 -2
  28. data/app/views/effective/responses/responses/_select_up_to_4.html.haml +2 -2
  29. data/app/views/effective/responses/responses/_select_up_to_5.html.haml +2 -2
  30. data/config/routes.rb +1 -0
  31. data/db/migrate/101_create_effective_questions.rb +44 -0
  32. data/lib/effective_questions/version.rb +1 -1
  33. metadata +13 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d948f3c70d98667858558ab90fc1da83bb053636c058c3684dad93f7ccdab507
4
- data.tar.gz: 479a4faf525f5d1f31a5fcc4d2039b0d23783eb736d1cc0338a711316e7cf3bb
3
+ metadata.gz: 7b1be795579cdb74c263712b86c74f5b5fe34e2e1d87fbbb57fc8cef6fc07510
4
+ data.tar.gz: 1acfbae757e1852f801024ee6679a9d9c1661cd8fd3f5e4c37d24807dad2b0f4
5
5
  SHA512:
6
- metadata.gz: af99111296b23031caa4325bc8fe7a85460d54defc6344efb7627b65d5f9d55e4f60909d9d10cdece24b041c1aad36155e03759f2f77bc6949c5a8624eb6936d
7
- data.tar.gz: 65db1d73024c9a9457da21febc75789578e7ca28a4efd607aabd443692a017536e512e7455fc7ffabf46221d1bff6671c9a76712df78a97800d8ceb97810919f
6
+ metadata.gz: 16c8a5452a08adf9fe820e809c5a5847a460f01040a3356b1dc8448b50f9e268e3ea4e18a747fa842550898c5856cf44081fe86fb19763dc146af3f65bb76cdb
7
+ data.tar.gz: 7cddf83480381e353009cbbf263c3fc7c9c902e0d9c447c3ef7125d61d1dde8d76cc89a27ddd87ba3204726d4ad1fc43873f4c703bc728bef563d5a4dfe6bd93
@@ -0,0 +1,13 @@
1
+ module Admin
2
+ class ResponsesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_questions) }
5
+
6
+ include Effective::CrudController
7
+
8
+ def permitted_params
9
+ params.require(:effective_response).permit!
10
+ end
11
+
12
+ end
13
+ end
@@ -12,12 +12,14 @@ class Admin::EffectiveQuestionsDatatable < Effective::Datatable
12
12
  col :questionable
13
13
  end
14
14
 
15
- col :position, visible: false do |question|
15
+ col :position, visible: attributes[:follow_up].blank? do |question|
16
16
  question.position.to_i + 1
17
17
  end
18
18
 
19
19
  col :title
20
- col :body, as: :text, visible: !attributes[:follow_up]
20
+ col :task
21
+ col :points
22
+ col :body, as: :text, visible: false
21
23
  col :required
22
24
 
23
25
  col :category, label: 'Type'
@@ -27,6 +29,9 @@ class Admin::EffectiveQuestionsDatatable < Effective::Datatable
27
29
  col :follow_up_questions, action: false, label: 'Follow up questions'
28
30
  end
29
31
 
32
+ col :scored
33
+ col :answer_to_s, label: 'Answer'
34
+
30
35
  actions_col
31
36
  end
32
37
 
@@ -0,0 +1,24 @@
1
+ class Admin::EffectiveResponsesDatatable < Effective::Datatable
2
+ datatable do
3
+ col :updated_at, visible: false
4
+ col :created_at, visible: false
5
+ col :id, visible: false
6
+
7
+ col :responsable
8
+ col :questionable
9
+ col :question
10
+
11
+ col(:response) do |response|
12
+ render('effective/responses/response', response: response)
13
+ end
14
+
15
+ col :correct?, as: :boolean
16
+
17
+ actions_col
18
+ end
19
+
20
+ collection do
21
+ Effective::Response.all.deep
22
+ end
23
+
24
+ end
@@ -27,4 +27,8 @@ module ActsAsQuestionable
27
27
  responses.select { |response| response.completed? && response.question_id == question.id }
28
28
  end
29
29
 
30
+ def questionable_scored?
31
+ questions.any?(&:scored?)
32
+ end
33
+
30
34
  end
@@ -27,4 +27,18 @@ module ActsAsResponsable
27
27
  response ||= responses.build(questionable: question.questionable, question: question)
28
28
  end
29
29
 
30
+ def responsable_completed?
31
+ try(:done?) == true || try(:completed?) == true
32
+ end
33
+
34
+ def scored_responses
35
+ responses.select { |r| r.question&.scored? }
36
+ end
37
+
38
+ def responsable_score
39
+ scored = scored_responses()
40
+ return 0 unless scored.present?
41
+
42
+ ((scored.count(&:correct?).to_f / scored.length) * 100_000).round
43
+ end
30
44
  end
@@ -4,11 +4,13 @@ module Effective
4
4
 
5
5
  belongs_to :question, optional: true # Present when I'm a follow up question
6
6
  belongs_to :question_option, optional: true # Might be present when I'm a follow up question
7
+ has_many :follow_up_questions, -> { order(:position) }, class_name: 'Effective::Question', foreign_key: :question_id, dependent: :destroy
7
8
 
8
9
  has_many :question_options, -> { order(:position) }, inverse_of: :question, dependent: :delete_all
9
- accepts_nested_attributes_for :question_options, reject_if: :all_blank, allow_destroy: true
10
+ accepts_nested_attributes_for :question_options, reject_if: -> (atts) { atts['title'].blank? }, allow_destroy: true
10
11
 
11
- has_many :follow_up_questions, -> { order(:position) }, class_name: 'Effective::Question', foreign_key: :question_id, dependent: :destroy
12
+ has_many :question_answers, dependent: :delete_all
13
+ accepts_nested_attributes_for :question_answers, reject_if: :all_blank, allow_destroy: true
12
14
 
13
15
  has_rich_text :body
14
16
  log_changes(to: :questionable) if respond_to?(:log_changes)
@@ -24,8 +26,11 @@ module Effective
24
26
  'Select up to 4',
25
27
  'Select up to 5',
26
28
  'Date', # Date Field
29
+ 'Decimal', # Decimal Field
27
30
  'Email', # Email Field
28
31
  'Number', # Numeric Field
32
+ 'Percentage', # Percentage Field
33
+ 'Price', # Price Field
29
34
  'Long Answer', # Text Area
30
35
  'Short Answer', # Text Field
31
36
  'Upload File' # File field
@@ -53,6 +58,11 @@ module Effective
53
58
  follow_up :boolean
54
59
  follow_up_value :string
55
60
 
61
+ scored :boolean
62
+
63
+ task :string
64
+ points :decimal
65
+
56
66
  timestamps
57
67
  end
58
68
 
@@ -81,6 +91,30 @@ module Effective
81
91
  validates :question_option, presence: true, if: -> { follow_up? && question.try(:question_option?) }
82
92
  validates :follow_up_value, presence: true, if: -> { follow_up? && !question.try(:question_option?) }
83
93
 
94
+ validates :scored, inclusion: { in: [false], message: 'is not supported' }, if: -> { category == 'Upload File' }
95
+
96
+ validates :question_answer, presence: true, if: -> { scored? }
97
+
98
+ validate(if: -> { scored? && (choose_one? || select_up_to_1? || select_all_that_apply?) }) do
99
+ errors.add(:base, 'must have at least one correct answer option') if answer_options.count < 1
100
+ end
101
+
102
+ validate(if: -> { scored? && select_up_to_2? }) do
103
+ errors.add(:base, 'must have two or more correct answer options') if answer_options.count < 2
104
+ end
105
+
106
+ validate(if: -> { scored? && select_up_to_3? }) do
107
+ errors.add(:base, 'must have three or more correct answer options') if answer_options.count < 3
108
+ end
109
+
110
+ validate(if: -> { scored? && select_up_to_4? }) do
111
+ errors.add(:base, 'must have four or more correct answer options') if answer_options.count < 4
112
+ end
113
+
114
+ validate(if: -> { scored? && select_up_to_5? }) do
115
+ errors.add(:base, 'must have five or more correct answer options') if answer_options.count < 5
116
+ end
117
+
84
118
  # Create choose_one? and select_all_that_apply? methods for each category
85
119
  CATEGORIES.each do |category|
86
120
  define_method(category.parameterize.underscore + '?') { self.category == category }
@@ -95,8 +129,11 @@ module Effective
95
129
 
96
130
  case category
97
131
  when 'Date' then :date
132
+ when 'Decimal' then :decimal
98
133
  when 'Email' then :email
99
134
  when 'Number' then :number
135
+ when 'Percentage' then :percentage
136
+ when 'Price' then :price
100
137
  when 'Long Answer' then :long_answer
101
138
  when 'Short Answer' then :short_answer
102
139
  when 'Upload File' then :upload_file
@@ -104,6 +141,26 @@ module Effective
104
141
  end
105
142
  end
106
143
 
144
+ def answer
145
+ return unless scored?
146
+ question_option? ? answer_options : question_answer.try(:answer)
147
+ end
148
+
149
+ def answer_to_s
150
+ return '' unless scored?
151
+
152
+ case category
153
+ when 'Choose one' then "One of: #{answer_options.join(', ')}"
154
+ when 'Select all that apply' then "All of: #{answer_options.join(', ')}"
155
+ when 'Select up to 1' then "One of: #{answer_options.join(', ')}"
156
+ when 'Select up to 2' then "Two of: #{answer_options.join(', ')}"
157
+ when 'Select up to 3' then "Three of: #{answer_options.join(', ')}"
158
+ when 'Select up to 4' then "Four of: #{answer_options.join(', ')}"
159
+ when 'Select up to 5' then "Five of: #{answer_options.join(', ')}"
160
+ else question_answer.try(:to_s)
161
+ end
162
+ end
163
+
107
164
  def show_if_value
108
165
  question.try(:question_option?) ? question_option_id : follow_up_value
109
166
  end
@@ -116,9 +173,17 @@ module Effective
116
173
  WITH_OPTIONS_CATEGORIES.include?(category)
117
174
  end
118
175
 
176
+ def answer_options
177
+ question_options.reject(&:marked_for_destruction?).select(&:answer?)
178
+ end
179
+
119
180
  def category_partial
120
181
  category.to_s.parameterize.underscore
121
182
  end
122
183
 
184
+ def question_answer
185
+ question_answers.first || question_answers.build()
186
+ end
187
+
123
188
  end
124
189
  end
@@ -0,0 +1,204 @@
1
+ module Effective
2
+ class QuestionAnswer < ActiveRecord::Base
3
+ belongs_to :question
4
+
5
+ OPERATIONS = [
6
+ 'Equal to',
7
+ 'Within range',
8
+ 'Less than',
9
+ 'Less than or equal to',
10
+ 'Greater than',
11
+ 'Greater than or equal to',
12
+ 'Contains',
13
+ 'Does not contain'
14
+ ]
15
+
16
+ effective_resource do
17
+ operation :string
18
+
19
+ # The answer
20
+ date :date
21
+ date_begin :date
22
+ date_end :date
23
+
24
+ decimal :decimal
25
+ decimal_begin :decimal
26
+ decimal_end :decimal
27
+
28
+ email :string
29
+
30
+ number :integer
31
+ number_begin :integer
32
+ number_end :integer
33
+
34
+ percentage :integer
35
+ percentage_begin :integer
36
+ percentage_end :integer
37
+
38
+ price :integer
39
+ price_begin :integer
40
+ price_end :integer
41
+
42
+ long_answer :text
43
+ short_answer :text
44
+
45
+ timestamps
46
+ end
47
+
48
+ scope :deep, -> { includes(:question) }
49
+
50
+ with_options(unless: -> { question.blank? }) do
51
+ validate(if: -> { question.category.present? }) do
52
+ supported = operations(question.category)
53
+ errors.add(:operation, 'is not supported for this question') if supported.present? && supported.exclude?(operation)
54
+ end
55
+
56
+ validates :date, presence: true, if: -> { question.date? && (equals? || gteq? || lteq? || lt? || gt?) }
57
+ validates :date_begin, presence: true, if: -> { question.date? && within_range? }
58
+ validates :date_end, presence: true, if: -> { question.date? && within_range? }
59
+
60
+ validates :decimal, presence: true, if: -> { question.decimal? && (equals? || gteq? || lteq? || lt? || gt?) }
61
+ validates :decimal_begin, presence: true, if: -> { question.decimal? && within_range? }
62
+ validates :decimal_end, presence: true, if: -> { question.decimal? && within_range? }
63
+
64
+ validates :email, presence: true, if: -> { question.email? && (equals? || contains? || does_not_contain?) }
65
+ validates :email, email: true, if: -> { question.email? && equals? }
66
+
67
+ validates :number, presence: true, if: -> { question.number? && (equals? || gteq? || lteq? || lt? || gt?) }
68
+ validates :number_begin, presence: true, if: -> { question.number? && within_range? }
69
+ validates :number_end, presence: true, if: -> { question.number? && within_range? }
70
+
71
+ validates :percentage, presence: true, if: -> { question.percentage? && (equals? || gteq? || lteq? || lt? || gt?) }
72
+ validates :percentage_begin, presence: true, if: -> { question.percentage? && within_range? }
73
+ validates :percentage_end, presence: true, if: -> { question.percentage? && within_range? }
74
+
75
+ validates :price, presence: true, if: -> { question.price? && (equals? || gteq? || lteq? || lt? || gt?) }
76
+ validates :price_begin, presence: true, if: -> { question.price? && within_range? }
77
+ validates :price_end, presence: true, if: -> { question.price? && within_range? }
78
+
79
+ validates :long_answer, presence: true, if: -> { question.long_answer? }
80
+ validates :short_answer, presence: true, if: -> { question.short_answer? }
81
+ end
82
+
83
+ def to_s
84
+ return '' unless question.present?
85
+ return '-' unless operations(question.category).present?
86
+
87
+ case operation
88
+ when 'Equal to'
89
+ "Equal to #{format_value(answer)}"
90
+ when 'Within range'
91
+ "Between #{format_value(answer.begin)} and #{format_value(answer.end)}"
92
+ when 'Less than'
93
+ "Less than #{format_value(answer)}"
94
+ when 'Less than or equal to'
95
+ "Less than or equal to #{format_value(answer)}"
96
+ when 'Greater than'
97
+ "Greater than #{format_value(answer)}"
98
+ when 'Greater than or equal to'
99
+ "Greater than or equal to #{format_value(answer)}"
100
+ when 'Contains'
101
+ "Contains #{format_value(answer)}"
102
+ when 'Does not contain'
103
+ "Does not contain #{format_value(answer)}"
104
+ else
105
+ raise("unknown operation: #{operation}")
106
+ end
107
+ end
108
+
109
+ def operations(category)
110
+ case category
111
+ when 'Choose one' then nil
112
+ when 'Select all that apply' then nil
113
+ when 'Select up to 1' then nil
114
+ when 'Select up to 2' then nil
115
+ when 'Select up to 3' then nil
116
+ when 'Select up to 4' then nil
117
+ when 'Select up to 5' then nil
118
+ when 'Upload File' then nil
119
+ when 'Date' then OPERATIONS - ['Contains', 'Does not contain']
120
+ when 'Decimal' then OPERATIONS - ['Contains', 'Does not contain']
121
+ when 'Number' then OPERATIONS - ['Contains', 'Does not contain']
122
+ when 'Percentage' then OPERATIONS - ['Contains', 'Does not contain']
123
+ when 'Price' then OPERATIONS - ['Contains', 'Does not contain']
124
+ when 'Email' then ['Equal to', 'Contains', 'Does not contain']
125
+ when 'Long Answer' then ['Equal to', 'Contains', 'Does not contain']
126
+ when 'Short Answer' then ['Equal to', 'Contains', 'Does not contain']
127
+ else
128
+ raise("unknown operations for category: #{category}")
129
+ end
130
+ end
131
+
132
+ def answer
133
+ if operation == 'Within range'
134
+ return (date_begin..date_end) if question.date?
135
+ return (decimal_begin..decimal_end) if question.decimal?
136
+ return (number_begin..number_end) if question.number?
137
+ return (percentage_begin..percentage_end) if question.percentage?
138
+ return (price_begin..price_end) if question.price?
139
+ else
140
+ return date if question.date?
141
+ return decimal if question.decimal?
142
+ return email if question.email?
143
+ return number if question.number?
144
+ return percentage if question.percentage?
145
+ return price if question.price?
146
+ return long_answer if question.long_answer?
147
+ return short_answer if question.short_answer?
148
+ end
149
+
150
+ raise("unknown operation: #{operation}")
151
+ end
152
+
153
+ def equals?
154
+ operation == 'Equal to'
155
+ end
156
+
157
+ def contains?
158
+ operation == 'Contains'
159
+ end
160
+
161
+ def does_not_contain?
162
+ operation == 'Does not contain'
163
+ end
164
+
165
+ def within_range?
166
+ operation == 'Within range'
167
+ end
168
+
169
+ def lt?
170
+ operation == 'Less than'
171
+ end
172
+
173
+ def gt?
174
+ operation == 'Greater than'
175
+ end
176
+
177
+ def lteq?
178
+ operation == 'Less than or equal to'
179
+ end
180
+
181
+ def gteq?
182
+ operation == 'Greater than or equal to'
183
+ end
184
+
185
+ private
186
+
187
+ def format_value(val)
188
+ return '' if val.blank?
189
+
190
+ if question.price?
191
+ "$#{'%0.2f' % (val / 100.0)}"
192
+ elsif question.percentage?
193
+ precision = (val % 1000).zero? ? 0 : 3
194
+ "#{format("%.#{precision}f", val.to_f / 1000)}%"
195
+ elsif question.date?
196
+ val.strftime('%Y-%m-%d')
197
+ elsif val.is_a?(Numeric)
198
+ val.to_s
199
+ else
200
+ "\"#{val}\""
201
+ end
202
+ end
203
+ end
204
+ end
@@ -6,6 +6,8 @@ module Effective
6
6
  title :text
7
7
  position :integer
8
8
 
9
+ answer :boolean
10
+
9
11
  timestamps
10
12
  end
11
13
 
@@ -12,8 +12,11 @@ module Effective
12
12
  effective_resource do
13
13
  # The response
14
14
  date :date
15
+ decimal :decimal
15
16
  email :string
16
17
  number :integer
18
+ percentage :integer
19
+ price :integer
17
20
  long_answer :text
18
21
  short_answer :text
19
22
 
@@ -27,6 +30,9 @@ module Effective
27
30
  end
28
31
 
29
32
  validates :date, presence: true, if: -> { question&.required? && question.date? }
33
+ validates :decimal, presence: true, if: -> { question&.required? && question.decimal? }
34
+ validates :percentage, presence: true, if: -> { question&.required? && question.percentage? }
35
+ validates :price, presence: true, if: -> { question&.required? && question.price? }
30
36
  validates :email, presence: true, email: true, if: -> { question&.required? && question.email? }
31
37
  validates :number, presence: true, if: -> { question&.required? && question.number? }
32
38
  validates :long_answer, presence: true, if: -> { question&.required? && question.long_answer? }
@@ -53,8 +59,24 @@ module Effective
53
59
  length: { maximum: 5, message: 'please select 5 options or fewer' }
54
60
 
55
61
  def to_s
56
- #model_name.human
57
- response.to_s
62
+ return '' unless question.present?
63
+ return '' unless response.present?
64
+
65
+ case question.category
66
+ when 'Date'
67
+ response.strftime('%Y-%m-%d')
68
+ when 'Price'
69
+ "$#{'%0.2f' % (response / 100.0)}"
70
+ when 'Percentage'
71
+ precision = (response % 1000).zero? ? 0 : 3
72
+ "#{format("%.#{precision}f", response.to_f / 1000)}%"
73
+ when 'Upload File'
74
+ response.filename.to_s
75
+ when 'Select all that apply', 'Select up to 2', 'Select up to 3', 'Select up to 4', 'Select up to 5'
76
+ Array(response).map(&:to_s).join(', ')
77
+ else
78
+ response.to_s
79
+ end
58
80
  end
59
81
 
60
82
  def response
@@ -63,13 +85,16 @@ module Effective
63
85
  return date if question.date?
64
86
  return email if question.email?
65
87
  return number if question.number?
88
+ return percentage if question.percentage?
89
+ return price if question.price?
90
+ return decimal if question.decimal?
66
91
  return long_answer if question.long_answer?
67
92
  return short_answer if question.short_answer?
68
93
  return upload_file if question.upload_file?
69
94
 
70
- return question_options.first if question.choose_one?
71
- return question_options.first if question.select_up_to_1?
72
- return question_options if question.question_option?
95
+ return response_options.first if question.choose_one?
96
+ return response_options.first if question.select_up_to_1?
97
+ return response_options if question.question_option?
73
98
 
74
99
  raise('unknown response for unexpected question category')
75
100
  end
@@ -79,8 +104,54 @@ module Effective
79
104
  end
80
105
 
81
106
  def completed?
82
- return false if responsable.blank?
83
- responsable.completed?
107
+ responsable.responsable_completed?
108
+ end
109
+
110
+ def correct?
111
+ return unless question.present? && question.scored?
112
+ return false unless response.present?
113
+
114
+ if question.question_option?
115
+ # For option-based questions, check if selected options match answer options
116
+ answers = question.answer.map(&:id)
117
+ selected = Array(response).map(&:question_option_id)
118
+
119
+ if question.select_all_that_apply?
120
+ return selected.sort == answers.sort
121
+ else
122
+ # For choose_one or select_up_to_X, all selected must be correct
123
+ return selected.present? && (selected - answers).blank?
124
+ end
125
+ end
126
+
127
+ # For non-option questions, check against question_answer
128
+ answer = question.question_answer
129
+ return false unless answer.present?
130
+
131
+ case answer.operation
132
+ when 'Equal to'
133
+ if response.is_a?(String) && answer.answer.is_a?(String)
134
+ response.downcase.strip == answer.answer.downcase.strip
135
+ else
136
+ response == answer.answer
137
+ end
138
+ when 'Within range'
139
+ answer.answer.cover?(response)
140
+ when 'Less than'
141
+ response < answer.answer
142
+ when 'Less than or equal to'
143
+ response <= answer.answer
144
+ when 'Greater than'
145
+ response > answer.answer
146
+ when 'Greater than or equal to'
147
+ response >= answer.answer
148
+ when 'Contains'
149
+ response.to_s.downcase.include?(answer.answer.to_s.downcase)
150
+ when 'Does not contain'
151
+ !response.to_s.downcase.include?(answer.answer.to_s.downcase)
152
+ else
153
+ false
154
+ end
84
155
  end
85
156
 
86
157
  end
@@ -2,5 +2,10 @@ module Effective
2
2
  class ResponseOption < ActiveRecord::Base
3
3
  belongs_to :response
4
4
  belongs_to :question_option
5
+
6
+ def to_s
7
+ question_option&.to_s
8
+ end
9
+
5
10
  end
6
11
  end
@@ -0,0 +1,76 @@
1
+ = f.show_if :category, 'Choose one' do
2
+ = card('Choose one') do
3
+ %p Display radio buttons to select one option
4
+
5
+ = f.show_if :category, 'Select all that apply' do
6
+ = card('Select all that apply') do
7
+ %p Display checkboxes to select all options that apply
8
+
9
+ = f.show_if :category, 'Select up to 1' do
10
+ = card('Select up to 1 (one)') do
11
+ %p Display checkboxes to select up to 1 option
12
+
13
+ = f.show_if :category, 'Select up to 2' do
14
+ = card('Select up to 2 (two)') do
15
+ %p Display checkboxes to select up to 2 options
16
+
17
+ = f.show_if :category, 'Select up to 3' do
18
+ = card('Select up to 3 (three)') do
19
+ %p Display checkboxes to select up to 3 options
20
+
21
+ = f.show_if :category, 'Select up to 4' do
22
+ = card('Select up to 4 (four)') do
23
+ %p Display checkboxes to select up to 4 options
24
+
25
+ = f.show_if :category, 'Select up to 5' do
26
+ = card('Select up to 5 (five)') do
27
+ %p Display checkboxes to select up to 5 options
28
+
29
+ = f.show_if :category, 'Short Answer' do
30
+ = card('Short Answer') do
31
+ %p Display a text field to enter a short text answer
32
+
33
+ = f.show_if :category, 'Long Answer' do
34
+ = card('Long Answer') do
35
+ %p Display a textarea to enter a long text answer
36
+
37
+ = f.show_if :category, 'Date' do
38
+ = card('Date') do
39
+ %p Display a date field to enter a date
40
+
41
+ = f.show_if :category, 'Decimal' do
42
+ = card('Decimal') do
43
+ %p Display a decimal field to enter a decimal number
44
+
45
+ = f.show_if :category, 'Email' do
46
+ = card('Email') do
47
+ %p Display an email field to enter an email
48
+
49
+ = f.show_if :category, 'Number' do
50
+ = card('Number') do
51
+ %p Display a number field to enter an integer number
52
+
53
+ = f.show_if :category, 'Percentage' do
54
+ = card('Percentage') do
55
+ %p Display a percentage field to enter a percentage
56
+
57
+ = f.show_if :category, 'Price' do
58
+ = card('Price') do
59
+ %p Display a price field to enter a price
60
+
61
+ = f.show_if :category, 'Upload File' do
62
+ = card('Upload File') do
63
+ %p Display a file field to upload a file
64
+
65
+ = f.show_if_any :category, Effective::Question::WITH_OPTIONS_CATEGORIES do
66
+ .mt-3
67
+ = card do
68
+ %h5 Options
69
+ %p Display the following options:
70
+
71
+ = f.has_many :question_options, class: 'tight' do |fa|
72
+ .row
73
+ .col= fa.text_field :title, label: false
74
+ = f.show_if(:scored, true) do
75
+ .col.mr-3= fa.check_box :answer, label: "Correct answer"
76
+
@@ -0,0 +1,131 @@
1
+ = f.show_if(:scored, true) do
2
+ = f.fields_for :question_answers, f.object.question_answer do |fa|
3
+ = card('Answer') do
4
+ %p The answer to this question is:
5
+
6
+ = f.show_if :category, 'Choose one' do
7
+ %ul
8
+ %li One correct answer option as selected above.
9
+
10
+ = f.show_if :category, 'Select up to 1' do
11
+ %ul
12
+ %li One correct answer option as selected above.
13
+
14
+ = f.show_if :category, 'Select up to 2' do
15
+ %ul
16
+ %li Two correct answer options as selected above.
17
+
18
+ = f.show_if :category, 'Select up to 3' do
19
+ %ul
20
+ %li Three correct answer options as selected above.
21
+
22
+ = f.show_if :category, 'Select up to 4' do
23
+ %ul
24
+ %li Four correct answer options as selected above.
25
+
26
+ = f.show_if :category, 'Select up to 5' do
27
+ %ul
28
+ %li Five correct answer options as selected above.
29
+
30
+ = f.show_if :category, 'Select all that apply' do
31
+ %ul
32
+ %li All correct answer options as selected above.
33
+
34
+ = f.show_if :category, 'Short Answer' do
35
+ - operations = fa.object.operations('Short Answer')
36
+ = fa.radios :operation, operations, buttons: true, label: false
37
+
38
+ = fa.show_if(:operation, 'Equal to') do
39
+ = fa.text_field :short_answer
40
+
41
+ = fa.show_if_any(:operation, operations - ['Equal to']) do
42
+ = fa.text_field :short_answer, label: 'Matched text', hint: 'Does not support wildcards'
43
+
44
+ = f.show_if :category, 'Long Answer' do
45
+ - operations = fa.object.operations('Long Answer')
46
+ = fa.radios :operation, operations, buttons: true, label: false
47
+
48
+ = fa.show_if(:operation, 'Equal to') do
49
+ = fa.text_field :long_answer
50
+
51
+ = fa.show_if_any(:operation, operations - ['Equal to']) do
52
+ = fa.text_field :long_answer, label: 'Matched text', hint: 'Does not support wildcards'
53
+
54
+ = f.show_if :category, 'Date' do
55
+ - operations = fa.object.operations('Date')
56
+
57
+ = fa.radios :operation, operations, buttons: true, label: false
58
+
59
+ = fa.show_if_any(:operation, operations - ['Within range']) do
60
+ .d-flex= fa.date_field :date
61
+
62
+ = fa.show_if(:operation, 'Within range') do
63
+ .d-flex.align-items-center
64
+ = fa.date_field :date_begin
65
+ %span.mx-4 to
66
+ = fa.date_field :date_end
67
+
68
+ = f.show_if :category, 'Decimal' do
69
+ - operations = fa.object.operations('Decimal')
70
+ = fa.radios :operation, operations, buttons: true, label: false
71
+
72
+ = fa.show_if_any(:operation, operations - ['Within range']) do
73
+ .d-flex= fa.float_field :decimal
74
+
75
+ = fa.show_if(:operation, 'Within range') do
76
+ .d-flex.align-items-center
77
+ = fa.float_field :decimal_begin
78
+ %span.mx-4 to
79
+ = fa.float_field :decimal_end
80
+
81
+ = f.show_if :category, 'Email' do
82
+ - operations = fa.object.operations('Email')
83
+ = fa.radios :operation, operations, buttons: true, label: false
84
+
85
+ = fa.show_if(:operation, 'Equal to') do
86
+ .d-flex= fa.email_field :email
87
+
88
+ = fa.show_if_any(:operation, operations - ['Equal to']) do
89
+ .d-flex= fa.text_field :email, label: 'Text to match'
90
+
91
+ = f.show_if :category, 'Number' do
92
+ - operations = fa.object.operations('Number')
93
+ = fa.radios :operation, operations, buttons: true, label: false
94
+
95
+ = fa.show_if_any(:operation, operations - ['Within range']) do
96
+ .d-flex= fa.number_field :number
97
+
98
+ = fa.show_if(:operation, 'Within range') do
99
+ .d-flex.align-items-center
100
+ = fa.number_field :number_begin
101
+ %span.mx-4 to
102
+ = fa.number_field :number_end
103
+
104
+ = f.show_if :category, 'Percentage' do
105
+ - operations = fa.object.operations('Percentage')
106
+ = fa.radios :operation, operations, buttons: true, label: false
107
+
108
+ = fa.show_if_any(:operation, operations - ['Within range']) do
109
+ .d-flex= fa.percent_field :percentage
110
+
111
+ = fa.show_if(:operation, 'Within range') do
112
+ .d-flex.align-items-center
113
+ = fa.percent_field :percentage_begin
114
+ %span.mx-4 to
115
+ = fa.percent_field :percentage_end
116
+
117
+ = f.show_if :category, 'Price' do
118
+ - operations = fa.object.operations('Price')
119
+ = fa.radios :operation, operations, buttons: true, label: false
120
+
121
+ = fa.show_if_any(:operation, operations - ['Within range']) do
122
+ .d-flex= fa.price_field :price
123
+
124
+ = fa.show_if(:operation, 'Within range') do
125
+ .d-flex.align-items-center
126
+ = fa.price_field :price_begin
127
+ %span.mx-4 to
128
+ = fa.price_field :price_end
129
+
130
+ = f.show_if :category, 'Upload File' do
131
+ %p Automatic scoring is not supported for this question type.
@@ -21,6 +21,9 @@
21
21
  = f.text_field :follow_up_value, label: 'Answer required to display this question', required: true, hint: 'Enter the exact answer value that should trigger this question to appear'
22
22
 
23
23
  = f.text_field :title, label: 'Question Title'
24
+ .row
25
+ .col= f.text_field :task, hint: 'An optional task name or number used in exports'
26
+ .col= f.number_field :points, hint: 'An optional number of points awarded for this question'
24
27
 
25
28
  - if defined?(EffectiveArticleEditor)
26
29
  = f.article_editor :body, label: 'Body (optional)'
@@ -28,94 +31,12 @@
28
31
  = f.rich_text_area :body, label: 'Body (optional)'
29
32
 
30
33
  = f.check_box :required, hint: 'A response to this question will be required'
31
- = f.select :category, Effective::Question::CATEGORIES
32
-
33
- = f.show_if :category, 'Choose one' do
34
- .mt-3.card
35
- .card-body
36
- %h5 Choose one
37
- %p Display radio buttons to select one option
38
-
39
- = f.show_if :category, 'Select all that apply' do
40
- .card
41
- .card-body
42
- %h5 Select all that apply
43
- %p Display checkboxes to select all options that apply
44
-
45
- = f.show_if :category, 'Select up to 1' do
46
- .card
47
- .card-body
48
- %h5 Select up to 1 (one)
49
- %p Display checkboxes to select up to 1 option
50
-
51
- = f.show_if :category, 'Select up to 2' do
52
- .card
53
- .card-body
54
- %h5 Select up to 2 (two)
55
- %p Display checkboxes to select up to 2 options
56
-
57
- = f.show_if :category, 'Select up to 3' do
58
- .card
59
- .card-body
60
- %h5 Select up to 3 (three)
61
- %p Display checkboxes to select up to 3 options
62
-
63
- = f.show_if :category, 'Select up to 4' do
64
- .card
65
- .card-body
66
- %h5 Select up to 4 (four)
67
- %p Display checkboxes to select up to 4 options
68
-
69
- = f.show_if :category, 'Select up to 5' do
70
- .card
71
- .card-body
72
- %h5 Select up to 5 (five)
73
- %p Display checkboxes to select up to 5 options
74
34
 
75
- = f.show_if :category, 'Short Answer' do
76
- .card
77
- .card-body
78
- %h5 Short Answer
79
- %p Display a text field to enter a short text answer
80
-
81
- = f.show_if :category, 'Long Answer' do
82
- .card
83
- .card-body
84
- %h5 Long Answer
85
- %p Display a textarea to enter a long text answer
86
-
87
- = f.show_if :category, 'Date' do
88
- .card
89
- .card-body
90
- %h5 Date
91
- %p Display a date field to enter a date
92
-
93
- = f.show_if :category, 'Email' do
94
- .card
95
- .card-body
96
- %h5 Email
97
- %p Display an email field to enter an email
98
-
99
- = f.show_if :category, 'Number' do
100
- .card
101
- .card-body
102
- %h5 Number
103
- %p Display a number field to enter an integer number
104
-
105
- = f.show_if :category, 'Upload File' do
106
- .card
107
- .card-body
108
- %h5 Upload File
109
- %p Display a file field to upload a file
110
-
111
- = f.show_if_any :category, Effective::Question::WITH_OPTIONS_CATEGORIES do
112
- .mt-3.card
113
- .card-body
114
- %h5 Options
115
- %p Display the following options:
35
+ = f.select :category, Effective::Question::CATEGORIES
36
+ .mt-3= render('admin/questions/fields_category', f: f)
116
37
 
117
- = f.has_many :question_options, class: 'tight' do |fa|
118
- = fa.text_field :title, label: false
38
+ = f.check_box :scored, hint: 'Any responses to this question will be automatically scored'
39
+ .mt-3= render('admin/questions/fields_scored', f: f)
119
40
 
120
41
  = effective_submit(f)
121
42
 
@@ -1,6 +1,6 @@
1
1
  - (question.question_options + [nil]).each do |question_option|
2
2
  - selected = responses.select { |br| br.response.blank? } if question_option.blank?
3
- - selected ||= responses.select { |br| Array(br.response).include?(question_option) }
3
+ - selected ||= responses.select { |br| Array(br.response).any? { |ro| ro.question_option == question_option } }
4
4
 
5
5
  - count = selected.length
6
6
  - total = responses.length
@@ -1,10 +1,14 @@
1
- - # The individiaual responses for one resonsable resource
1
+ - # The individiaual responses for one responsable resource
2
+ - include_scores = (local_assigns[:include_scores] == true)
3
+
2
4
  .responsable-results
3
5
  %table.table.table-hover
4
6
  %thead
5
7
  %tr
6
8
  %th Question
7
9
  %th Response
10
+ - if include_scores
11
+ %th Scored Result
8
12
 
9
13
  %tbody
10
14
  - questionable.questions.top_level.deep.each do |question|
@@ -14,6 +18,17 @@
14
18
  %td= question
15
19
  %td= render('effective/responses/response', response: response)
16
20
 
21
+ - if include_scores
22
+ %td
23
+ - if question.scored?
24
+ %span{title: "Correct answer was #{question.answer_to_s}"}
25
+ - if response.correct?
26
+ = badge('Correct', 'badge-success')
27
+ - else
28
+ = badge('Incorrect', 'badge-danger')
29
+ - else
30
+ = '-'
31
+
17
32
  - question.follow_up_questions.deep.each do |follow_up_question|
18
33
  - response = responsable.response(question: follow_up_question)
19
34
 
@@ -21,3 +36,14 @@
21
36
  %tr
22
37
  %td= follow_up_question
23
38
  %td= render('effective/responses/response', response: response)
39
+
40
+ - if include_scores
41
+ %td
42
+ - if follow_up_question.scored?
43
+ %span{title: "Correct answer was #{follow_up_question.answer_to_s}"}
44
+ - if response.correct?
45
+ = badge('Correct', 'badge-success')
46
+ - else
47
+ = badge('Incorrect', 'badge-danger')
48
+ - else
49
+ = '-'
@@ -0,0 +1,2 @@
1
+ .row
2
+ .col-sm-6= f.float_field :decimal, label: false, required: question.required?
@@ -0,0 +1,2 @@
1
+ .row
2
+ .col-sm-6= f.percent_field :percentage, label: false, required: question.required?
@@ -0,0 +1,2 @@
1
+ .row
2
+ .col-sm-6= f.price_field :price, label: false, required: question.required?
@@ -1 +1 @@
1
- = response.response.presence || '-'
1
+ = response.response&.question_option || '-'
@@ -0,0 +1 @@
1
+ = response.response.presence || '-'
@@ -0,0 +1,5 @@
1
+ - if response.response.present?
2
+ - precision = (response.response % 1000).zero? ? 0 : 3
3
+ = number_to_percentage(response.response / 1000.0, precision: precision)
4
+ - else
5
+ = '-'
@@ -0,0 +1 @@
1
+ = price_to_currency(response.response) || '-'
@@ -1,5 +1,5 @@
1
- - response.response.each do |question_option|
2
- %div= question_option.title
1
+ - response.response.each do |response_option|
2
+ %div= response_option
3
3
 
4
4
  - if response.response.blank?
5
5
  = '-'
@@ -1 +1 @@
1
- = response.response&.title || '-'
1
+ = response.response || '-'
@@ -1,5 +1,5 @@
1
- - response.response.each do |question_option|
2
- %div= question_option.title
1
+ - response.response.each do |response_option|
2
+ %div= response_option
3
3
 
4
4
  - if response.response.blank?
5
5
  = '-'
@@ -1,5 +1,5 @@
1
- - response.response.each do |question_option|
2
- %div= question_option.title
1
+ - response.response.each do |response_option|
2
+ %div= response_option
3
3
 
4
4
  - if response.response.blank?
5
5
  = '-'
@@ -1,5 +1,5 @@
1
- - response.response.each do |question_option|
2
- %div= question_option.title
1
+ - response.response.each do |response_option|
2
+ %div= response_option
3
3
 
4
4
  - if response.response.blank?
5
5
  = '-'
@@ -1,5 +1,5 @@
1
- - response.response.each do |question_option|
2
- %div= question_option.title
1
+ - response.response.each do |response_option|
2
+ %div= response_option
3
3
 
4
4
  - if response.response.blank?
5
5
  = '-'
data/config/routes.rb CHANGED
@@ -8,6 +8,7 @@ EffectiveQuestions::Engine.routes.draw do
8
8
 
9
9
  namespace :admin do
10
10
  resources :questions, except: [:show]
11
+ resources :responses, only: [:index]
11
12
  end
12
13
 
13
14
  end
@@ -16,6 +16,11 @@ class CreateEffectiveQuestions < ActiveRecord::Migration[6.0]
16
16
  t.integer :question_id
17
17
  t.integer :question_option_id
18
18
 
19
+ t.boolean :scored, default: false
20
+
21
+ t.string :task
22
+ t.decimal :points
23
+
19
24
  t.datetime :updated_at
20
25
  t.datetime :created_at
21
26
  end
@@ -26,6 +31,8 @@ class CreateEffectiveQuestions < ActiveRecord::Migration[6.0]
26
31
  t.string :title
27
32
  t.integer :position
28
33
 
34
+ t.boolean :answer, default: false
35
+
29
36
  t.datetime :updated_at
30
37
  t.datetime :created_at
31
38
  end
@@ -44,6 +51,9 @@ class CreateEffectiveQuestions < ActiveRecord::Migration[6.0]
44
51
  t.integer :number
45
52
  t.text :long_answer
46
53
  t.text :short_answer
54
+ t.decimal :decimal
55
+ t.integer :percentage
56
+ t.integer :price
47
57
 
48
58
  t.datetime :updated_at
49
59
  t.datetime :created_at
@@ -56,5 +66,39 @@ class CreateEffectiveQuestions < ActiveRecord::Migration[6.0]
56
66
  t.datetime :updated_at
57
67
  t.datetime :created_at
58
68
  end
69
+
70
+ create_table :question_answers do |t|
71
+ t.integer :question_id
72
+
73
+ t.string :operation
74
+
75
+ t.date :date
76
+ t.date :date_begin
77
+ t.date :date_end
78
+
79
+ t.decimal :decimal
80
+ t.decimal :decimal_begin
81
+ t.decimal :decimal_end
82
+
83
+ t.string :email
84
+
85
+ t.integer :number
86
+ t.integer :number_begin
87
+ t.integer :number_end
88
+
89
+ t.integer :percentage
90
+ t.integer :percentage_begin
91
+ t.integer :percentage_end
92
+
93
+ t.integer :price
94
+ t.integer :price_begin
95
+ t.integer :price_end
96
+
97
+ t.text :long_answer
98
+ t.text :short_answer
99
+
100
+ t.timestamps
101
+ end
102
+
59
103
  end
60
104
  end
@@ -1,3 +1,3 @@
1
1
  module EffectiveQuestions
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.4'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_questions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-29 00:00:00.000000000 Z
11
+ date: 2026-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -177,15 +177,20 @@ files:
177
177
  - app/assets/config/effective_questions_manifest.js
178
178
  - app/assets/stylesheets/effective_questions.scss
179
179
  - app/controllers/admin/questions_controller.rb
180
+ - app/controllers/admin/responses_controller.rb
180
181
  - app/datatables/admin/effective_questionable_results_datatable.rb
181
182
  - app/datatables/admin/effective_questions_datatable.rb
183
+ - app/datatables/admin/effective_responses_datatable.rb
182
184
  - app/helpers/effective_questions_helper.rb
183
185
  - app/models/concerns/acts_as_questionable.rb
184
186
  - app/models/concerns/acts_as_responsable.rb
185
187
  - app/models/effective/question.rb
188
+ - app/models/effective/question_answer.rb
186
189
  - app/models/effective/question_option.rb
187
190
  - app/models/effective/response.rb
188
191
  - app/models/effective/response_option.rb
192
+ - app/views/admin/questions/_fields_category.html.haml
193
+ - app/views/admin/questions/_fields_scored.html.haml
189
194
  - app/views/admin/questions/_form.html.haml
190
195
  - app/views/effective/questionable/_result.html.haml
191
196
  - app/views/effective/questionable/_results.html.haml
@@ -210,9 +215,12 @@ files:
210
215
  - app/views/effective/responses/_response.html.haml
211
216
  - app/views/effective/responses/fields/_choose_one.html.haml
212
217
  - app/views/effective/responses/fields/_date.html.haml
218
+ - app/views/effective/responses/fields/_decimal.html.haml
213
219
  - app/views/effective/responses/fields/_email.html.haml
214
220
  - app/views/effective/responses/fields/_long_answer.html.haml
215
221
  - app/views/effective/responses/fields/_number.html.haml
222
+ - app/views/effective/responses/fields/_percentage.html.haml
223
+ - app/views/effective/responses/fields/_price.html.haml
216
224
  - app/views/effective/responses/fields/_select_all_that_apply.html.haml
217
225
  - app/views/effective/responses/fields/_select_up_to_1.html.haml
218
226
  - app/views/effective/responses/fields/_select_up_to_2.html.haml
@@ -223,9 +231,12 @@ files:
223
231
  - app/views/effective/responses/fields/_upload_file.html.haml
224
232
  - app/views/effective/responses/responses/_choose_one.html.haml
225
233
  - app/views/effective/responses/responses/_date.html.haml
234
+ - app/views/effective/responses/responses/_decimal.html.haml
226
235
  - app/views/effective/responses/responses/_email.html.haml
227
236
  - app/views/effective/responses/responses/_long_answer.html.haml
228
237
  - app/views/effective/responses/responses/_number.html.haml
238
+ - app/views/effective/responses/responses/_percentage.html.haml
239
+ - app/views/effective/responses/responses/_price.html.haml
229
240
  - app/views/effective/responses/responses/_select_all_that_apply.html.haml
230
241
  - app/views/effective/responses/responses/_select_up_to_1.html.haml
231
242
  - app/views/effective/responses/responses/_select_up_to_2.html.haml