effective_questions 0.0.2
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +133 -0
- data/Rakefile +18 -0
- data/app/assets/config/effective_questions_manifest.js +1 -0
- data/app/assets/stylesheets/effective_questions.scss +0 -0
- data/app/controllers/admin/questions_controller.rb +13 -0
- data/app/datatables/admin/effective_questionable_results_datatable.rb +44 -0
- data/app/datatables/admin/effective_questions_datatable.rb +45 -0
- data/app/helpers/effective_questions_helper.rb +2 -0
- data/app/models/concerns/acts_as_questionable.rb +30 -0
- data/app/models/concerns/acts_as_responsable.rb +30 -0
- data/app/models/effective/question.rb +124 -0
- data/app/models/effective/question_option.rb +26 -0
- data/app/models/effective/response.rb +87 -0
- data/app/models/effective/response_option.rb +6 -0
- data/app/views/admin/questions/_form.html.haml +133 -0
- data/app/views/effective/questionable/_result.html.haml +2 -0
- data/app/views/effective/questionable/_results.html.haml +31 -0
- data/app/views/effective/questionable/results/_choose_one.html.haml +2 -0
- data/app/views/effective/questionable/results/_date.html.haml +14 -0
- data/app/views/effective/questionable/results/_email.html.haml +14 -0
- data/app/views/effective/questionable/results/_long_answer.html.haml +2 -0
- data/app/views/effective/questionable/results/_number.html.haml +14 -0
- data/app/views/effective/questionable/results/_question_option.html.haml +17 -0
- data/app/views/effective/questionable/results/_select_all_that_apply.html.haml +2 -0
- data/app/views/effective/questionable/results/_select_up_to_1.html.haml +2 -0
- data/app/views/effective/questionable/results/_select_up_to_2.html.haml +2 -0
- data/app/views/effective/questionable/results/_select_up_to_3.html.haml +2 -0
- data/app/views/effective/questionable/results/_select_up_to_4.html.haml +2 -0
- data/app/views/effective/questionable/results/_select_up_to_5.html.haml +2 -0
- data/app/views/effective/questionable/results/_short_answer.html.haml +2 -0
- data/app/views/effective/questionable/results/_upload_file.html.haml +3 -0
- data/app/views/effective/questions/_question.html.haml +6 -0
- data/app/views/effective/responsable/_fields.html.haml +19 -0
- data/app/views/effective/responsable/_results.html.haml +23 -0
- data/app/views/effective/responses/_fields.html.haml +15 -0
- data/app/views/effective/responses/_response.html.haml +2 -0
- data/app/views/effective/responses/fields/_choose_one.html.haml +1 -0
- data/app/views/effective/responses/fields/_date.html.haml +2 -0
- data/app/views/effective/responses/fields/_email.html.haml +2 -0
- data/app/views/effective/responses/fields/_long_answer.html.haml +1 -0
- data/app/views/effective/responses/fields/_number.html.haml +2 -0
- data/app/views/effective/responses/fields/_select_all_that_apply.html.haml +1 -0
- data/app/views/effective/responses/fields/_select_up_to_1.html.haml +1 -0
- data/app/views/effective/responses/fields/_select_up_to_2.html.haml +1 -0
- data/app/views/effective/responses/fields/_select_up_to_3.html.haml +1 -0
- data/app/views/effective/responses/fields/_select_up_to_4.html.haml +1 -0
- data/app/views/effective/responses/fields/_select_up_to_5.html.haml +1 -0
- data/app/views/effective/responses/fields/_short_answer.html.haml +1 -0
- data/app/views/effective/responses/fields/_upload_file.html.haml +2 -0
- data/app/views/effective/responses/responses/_choose_one.html.haml +1 -0
- data/app/views/effective/responses/responses/_date.html.haml +1 -0
- data/app/views/effective/responses/responses/_email.html.haml +1 -0
- data/app/views/effective/responses/responses/_long_answer.html.haml +1 -0
- data/app/views/effective/responses/responses/_number.html.haml +1 -0
- data/app/views/effective/responses/responses/_select_all_that_apply.html.haml +5 -0
- data/app/views/effective/responses/responses/_select_up_to_1.html.haml +1 -0
- data/app/views/effective/responses/responses/_select_up_to_2.html.haml +5 -0
- data/app/views/effective/responses/responses/_select_up_to_3.html.haml +5 -0
- data/app/views/effective/responses/responses/_select_up_to_4.html.haml +5 -0
- data/app/views/effective/responses/responses/_select_up_to_5.html.haml +5 -0
- data/app/views/effective/responses/responses/_short_answer.html.haml +1 -0
- data/app/views/effective/responses/responses/_upload_file.html.haml +4 -0
- data/config/effective_questions.rb +5 -0
- data/config/locales/effective_questions.yml +11 -0
- data/config/routes.rb +13 -0
- data/db/migrate/101_create_effective_questions.rb +60 -0
- data/db/seeds.rb +15 -0
- data/lib/effective_questions/engine.rb +19 -0
- data/lib/effective_questions/version.rb +3 -0
- data/lib/effective_questions.rb +13 -0
- data/lib/generators/effective_questions/install_generator.rb +28 -0
- data/lib/tasks/effective_questions_tasks.rake +8 -0
- metadata +270 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d948f3c70d98667858558ab90fc1da83bb053636c058c3684dad93f7ccdab507
|
|
4
|
+
data.tar.gz: 479a4faf525f5d1f31a5fcc4d2039b0d23783eb736d1cc0338a711316e7cf3bb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: af99111296b23031caa4325bc8fe7a85460d54defc6344efb7627b65d5f9d55e4f60909d9d10cdece24b041c1aad36155e03759f2f77bc6949c5a8624eb6936d
|
|
7
|
+
data.tar.gz: 65db1d73024c9a9457da21febc75789578e7ca28a4efd607aabd443692a017536e512e7455fc7ffabf46221d1bff6671c9a76712df78a97800d8ceb97810919f
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2026 Code and Effect Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Effective Questions
|
|
2
|
+
|
|
3
|
+
Underlying questions and responses module for use with polls, cpd audits and more.
|
|
4
|
+
|
|
5
|
+
An admin creates an acts_as_questionable resource with one or more questions.
|
|
6
|
+
|
|
7
|
+
The responses are collected by an acts_as_responsable resource with one or more responses
|
|
8
|
+
|
|
9
|
+
Works with action_text for content bodies, and active_storage for file uploads.
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
This requires Rails 6 and Twitter Bootstrap 4 and just works with Devise.
|
|
14
|
+
|
|
15
|
+
Please first install the [effective_datatables](https://github.com/code-and-effect/effective_datatables) gem.
|
|
16
|
+
|
|
17
|
+
Please download and install the [Twitter Bootstrap4](http://getbootstrap.com)
|
|
18
|
+
|
|
19
|
+
Add to your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'haml'
|
|
23
|
+
gem 'effective_questions'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run the bundle command to install it:
|
|
27
|
+
|
|
28
|
+
```console
|
|
29
|
+
bundle install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then run the generator:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
rails generate effective_questions:install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The generator will install an initializer which describes all configuration options and creates a database migration.
|
|
39
|
+
|
|
40
|
+
If you want to tweak the table names, manually adjust both the configuration file and the migration now.
|
|
41
|
+
|
|
42
|
+
Then migrate the database:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
rake db:migrate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Set up your permissions:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# Regular signed up user. Guest users not supported.
|
|
52
|
+
if user.persisted?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if user.admin?
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
You can render the results with ``.
|
|
62
|
+
|
|
63
|
+
## Authorization
|
|
64
|
+
|
|
65
|
+
All authorization checks are handled via the config.authorization_method found in the `app/config/initializers/effective_questions.rb` file.
|
|
66
|
+
|
|
67
|
+
It is intended for flow through to CanCan or Pundit, but neither of those gems are required.
|
|
68
|
+
|
|
69
|
+
This method is called by all controller actions with the appropriate action and resource
|
|
70
|
+
|
|
71
|
+
Action will be one of [:index, :show, :new, :create, :edit, :update, :destroy]
|
|
72
|
+
|
|
73
|
+
Resource will the appropriate object or class
|
|
74
|
+
|
|
75
|
+
The authorization method is defined in the initializer file:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# As a Proc (with CanCan)
|
|
79
|
+
config.authorization_method = Proc.new { |controller, action, resource| authorize!(action, resource) }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# As a Custom Method
|
|
84
|
+
config.authorization_method = :my_authorization_method
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
and then in your application_controller.rb:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
def my_authorization_method(action, resource)
|
|
91
|
+
current_user.is?(:admin) || EffectivePunditPolicy.new(current_user, resource).send('#{action}?')
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
or disabled entirely:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
config.authorization_method = false
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
If the method or proc returns false (user is not authorized) an Effective::AccessDenied exception will be raised
|
|
102
|
+
|
|
103
|
+
You can rescue from this exception by adding the following to your application_controller.rb:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
rescue_from Effective::AccessDenied do |exception|
|
|
107
|
+
respond_to do |format|
|
|
108
|
+
format.html { render 'static_pages/access_denied', status: 403 }
|
|
109
|
+
format.any { render text: 'Access Denied', status: 403 }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
|
|
117
|
+
|
|
118
|
+
## Testing
|
|
119
|
+
|
|
120
|
+
Run tests by:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
rails test
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
1. Fork it
|
|
129
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
130
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
131
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
132
|
+
5. Bonus points for test coverage
|
|
133
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
|
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
|
+
load "rails/tasks/engine.rake"
|
|
5
|
+
|
|
6
|
+
load "rails/tasks/statistics.rake"
|
|
7
|
+
|
|
8
|
+
require "bundler/gem_tasks"
|
|
9
|
+
|
|
10
|
+
require "rake/testtask"
|
|
11
|
+
|
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
|
13
|
+
t.libs << 'test'
|
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
|
15
|
+
t.verbose = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task default: :test
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_directory ../stylesheets .css
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
class QuestionsController < 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_question).permit!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class Admin::EffectiveQuestionableResultsDatatable < Effective::Datatable
|
|
2
|
+
datatable do
|
|
3
|
+
col :responsable
|
|
4
|
+
|
|
5
|
+
col :position, visible: false
|
|
6
|
+
col :category, search: Effective::Question::CATEGORIES, visible: false
|
|
7
|
+
|
|
8
|
+
col :question, search: questionable.questions.pluck(:title).sort
|
|
9
|
+
col :responses
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
collection do
|
|
13
|
+
questionable.completed_responses.flat_map do |response|
|
|
14
|
+
rows = if response.question.question_option?
|
|
15
|
+
response.question_options.map do |question_option|
|
|
16
|
+
[
|
|
17
|
+
response.responsable.try(:token) || response.responsable_id,
|
|
18
|
+
response.question.position,
|
|
19
|
+
response.question.category,
|
|
20
|
+
response.question.to_s,
|
|
21
|
+
question_option.to_s
|
|
22
|
+
]
|
|
23
|
+
end
|
|
24
|
+
elsif response.response.present?
|
|
25
|
+
[
|
|
26
|
+
[
|
|
27
|
+
response.responsable.try(:token) || response.responsable_id,
|
|
28
|
+
response.question.position,
|
|
29
|
+
response.question.category,
|
|
30
|
+
response.question.to_s,
|
|
31
|
+
response.response.to_s
|
|
32
|
+
]
|
|
33
|
+
]
|
|
34
|
+
else
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def questionable
|
|
41
|
+
@questionable ||= (attributes[:questionable_type].constantize).where(id: attributes[:questionable_id]).first!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Admin::EffectiveQuestionsDatatable < Effective::Datatable
|
|
2
|
+
datatable do
|
|
3
|
+
reorder :position
|
|
4
|
+
|
|
5
|
+
col :updated_at, visible: false
|
|
6
|
+
col :created_at, visible: false
|
|
7
|
+
col :id, visible: false
|
|
8
|
+
|
|
9
|
+
if attributes[:follow_up]
|
|
10
|
+
col :show_if_value_to_s, label: 'When answered with'
|
|
11
|
+
else
|
|
12
|
+
col :questionable
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
col :position, visible: false do |question|
|
|
16
|
+
question.position.to_i + 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
col :title
|
|
20
|
+
col :body, as: :text, visible: !attributes[:follow_up]
|
|
21
|
+
col :required
|
|
22
|
+
|
|
23
|
+
col :category, label: 'Type'
|
|
24
|
+
col :question_options, label: 'Options'
|
|
25
|
+
|
|
26
|
+
unless attributes[:follow_up]
|
|
27
|
+
col :follow_up_questions, action: false, label: 'Follow up questions'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
actions_col
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
collection do
|
|
34
|
+
scope = Effective::Question.all.deep
|
|
35
|
+
|
|
36
|
+
if attributes[:follow_up]
|
|
37
|
+
scope = scope.where(follow_up: true, question_id: attributes[:question_id])
|
|
38
|
+
else
|
|
39
|
+
scope = scope.where(question_id: nil)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
scope
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#
|
|
2
|
+
# ActsAsQuestionable
|
|
3
|
+
#
|
|
4
|
+
module ActsAsQuestionable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module Base
|
|
8
|
+
def acts_as_questionable(options = nil)
|
|
9
|
+
include ::ActsAsQuestionable
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
def acts_as_questionable?; true; end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
included do
|
|
18
|
+
has_many :questions, -> { order(:position) }, class_name: 'Effective::Question', as: :questionable, dependent: :destroy
|
|
19
|
+
accepts_nested_attributes_for :questions, allow_destroy: true
|
|
20
|
+
|
|
21
|
+
has_many :responses, -> { order(:id) }, class_name: 'Effective::Response', as: :questionable, dependent: :destroy
|
|
22
|
+
accepts_nested_attributes_for :responses, allow_destroy: true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def completed_responses(question: nil)
|
|
26
|
+
return responses.select(&:completed?) if question.nil?
|
|
27
|
+
responses.select { |response| response.completed? && response.question_id == question.id }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#
|
|
2
|
+
# ActsAsResponsable
|
|
3
|
+
#
|
|
4
|
+
module ActsAsResponsable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module Base
|
|
8
|
+
def acts_as_responsable(options = nil)
|
|
9
|
+
include ::ActsAsResponsable
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
def acts_as_responsable?; true; end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
included do
|
|
18
|
+
has_many :responses, class_name: 'Effective::Response', as: :responsable, dependent: :destroy
|
|
19
|
+
accepts_nested_attributes_for :responses, allow_destroy: true
|
|
20
|
+
|
|
21
|
+
validates :responses, associated: true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Find or build
|
|
25
|
+
def response(question:)
|
|
26
|
+
response = responses.find { |r| r.question_id == question.id }
|
|
27
|
+
response ||= responses.build(questionable: question.questionable, question: question)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Question < ActiveRecord::Base
|
|
3
|
+
belongs_to :questionable, polymorphic: true
|
|
4
|
+
|
|
5
|
+
belongs_to :question, optional: true # Present when I'm a follow up question
|
|
6
|
+
belongs_to :question_option, optional: true # Might be present when I'm a follow up question
|
|
7
|
+
|
|
8
|
+
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
|
+
|
|
11
|
+
has_many :follow_up_questions, -> { order(:position) }, class_name: 'Effective::Question', foreign_key: :question_id, dependent: :destroy
|
|
12
|
+
|
|
13
|
+
has_rich_text :body
|
|
14
|
+
log_changes(to: :questionable) if respond_to?(:log_changes)
|
|
15
|
+
|
|
16
|
+
has_many :responses
|
|
17
|
+
|
|
18
|
+
CATEGORIES = [
|
|
19
|
+
'Choose one', # Radios
|
|
20
|
+
'Select all that apply', # Checks
|
|
21
|
+
'Select up to 1',
|
|
22
|
+
'Select up to 2',
|
|
23
|
+
'Select up to 3',
|
|
24
|
+
'Select up to 4',
|
|
25
|
+
'Select up to 5',
|
|
26
|
+
'Date', # Date Field
|
|
27
|
+
'Email', # Email Field
|
|
28
|
+
'Number', # Numeric Field
|
|
29
|
+
'Long Answer', # Text Area
|
|
30
|
+
'Short Answer', # Text Field
|
|
31
|
+
'Upload File' # File field
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
WITH_OPTIONS_CATEGORIES = [
|
|
35
|
+
'Choose one', # Radios
|
|
36
|
+
'Select all that apply', # Checks
|
|
37
|
+
'Select up to 1',
|
|
38
|
+
'Select up to 2',
|
|
39
|
+
'Select up to 3',
|
|
40
|
+
'Select up to 4',
|
|
41
|
+
'Select up to 5'
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
UNSUPPORTED_FOLLOW_UP_QUESTION_CATEGORIES = ['Upload File']
|
|
45
|
+
|
|
46
|
+
effective_resource do
|
|
47
|
+
title :string
|
|
48
|
+
category :string
|
|
49
|
+
required :boolean
|
|
50
|
+
|
|
51
|
+
position :integer
|
|
52
|
+
|
|
53
|
+
follow_up :boolean
|
|
54
|
+
follow_up_value :string
|
|
55
|
+
|
|
56
|
+
timestamps
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
before_validation(if: -> { question.present? }) do
|
|
60
|
+
assign_attributes(questionable: question.questionable)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Set position
|
|
64
|
+
before_validation do
|
|
65
|
+
source = question_option.try(:follow_up_questions) || questionable.try(:questions) || []
|
|
66
|
+
self.position ||= (source.map { |obj| obj.position }.compact.max || -1) + 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
scope :deep, -> { with_rich_text_body_and_embeds.includes(:question_options) }
|
|
70
|
+
scope :sorted, -> { order(:position) }
|
|
71
|
+
|
|
72
|
+
scope :top_level, -> { where(follow_up: false) }
|
|
73
|
+
scope :follow_up, -> { where(follow_up: true) }
|
|
74
|
+
|
|
75
|
+
validates :title, presence: true
|
|
76
|
+
validates :category, presence: true, inclusion: { in: CATEGORIES }
|
|
77
|
+
validates :position, presence: true
|
|
78
|
+
validates :question_options, presence: true, if: -> { question_option? }
|
|
79
|
+
|
|
80
|
+
validates :question, presence: true, if: -> { follow_up? }
|
|
81
|
+
validates :question_option, presence: true, if: -> { follow_up? && question.try(:question_option?) }
|
|
82
|
+
validates :follow_up_value, presence: true, if: -> { follow_up? && !question.try(:question_option?) }
|
|
83
|
+
|
|
84
|
+
# Create choose_one? and select_all_that_apply? methods for each category
|
|
85
|
+
CATEGORIES.each do |category|
|
|
86
|
+
define_method(category.parameterize.underscore + '?') { self.category == category }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_s
|
|
90
|
+
title.presence || model_name.human
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def show_if_attribute
|
|
94
|
+
return :question_option_ids if question_option?
|
|
95
|
+
|
|
96
|
+
case category
|
|
97
|
+
when 'Date' then :date
|
|
98
|
+
when 'Email' then :email
|
|
99
|
+
when 'Number' then :number
|
|
100
|
+
when 'Long Answer' then :long_answer
|
|
101
|
+
when 'Short Answer' then :short_answer
|
|
102
|
+
when 'Upload File' then :upload_file
|
|
103
|
+
else :unknown
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def show_if_value
|
|
108
|
+
question.try(:question_option?) ? question_option_id : follow_up_value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def show_if_value_to_s
|
|
112
|
+
(question.try(:question_option?) ? question_option : follow_up_value).to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def question_option?
|
|
116
|
+
WITH_OPTIONS_CATEGORIES.include?(category)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def category_partial
|
|
120
|
+
category.to_s.parameterize.underscore
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class QuestionOption < ActiveRecord::Base
|
|
3
|
+
belongs_to :question
|
|
4
|
+
|
|
5
|
+
effective_resource do
|
|
6
|
+
title :text
|
|
7
|
+
position :integer
|
|
8
|
+
|
|
9
|
+
timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before_validation(if: -> { question.present? }) do
|
|
13
|
+
self.position ||= (question.question_options.map { |obj| obj.position }.compact.max || -1) + 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
scope :sorted, -> { order(:position) }
|
|
17
|
+
|
|
18
|
+
validates :title, presence: true
|
|
19
|
+
validates :position, presence: true
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
title.presence || model_name.human
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Response < ActiveRecord::Base
|
|
3
|
+
belongs_to :responsable, polymorphic: true
|
|
4
|
+
belongs_to :questionable, polymorphic: true
|
|
5
|
+
belongs_to :question
|
|
6
|
+
|
|
7
|
+
has_many :response_options, dependent: :delete_all
|
|
8
|
+
has_many :question_options, through: :response_options
|
|
9
|
+
|
|
10
|
+
has_one_attached :upload_file
|
|
11
|
+
|
|
12
|
+
effective_resource do
|
|
13
|
+
# The response
|
|
14
|
+
date :date
|
|
15
|
+
email :string
|
|
16
|
+
number :integer
|
|
17
|
+
long_answer :text
|
|
18
|
+
short_answer :text
|
|
19
|
+
|
|
20
|
+
timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
scope :deep, -> { includes(:question, :question_options) }
|
|
24
|
+
|
|
25
|
+
validate(if: -> { questionable.present? && question.present? }) do
|
|
26
|
+
errors.add(:question, 'must match questionable') unless question.questionable == questionable
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
validates :date, presence: true, if: -> { question&.required? && question.date? }
|
|
30
|
+
validates :email, presence: true, email: true, if: -> { question&.required? && question.email? }
|
|
31
|
+
validates :number, presence: true, if: -> { question&.required? && question.number? }
|
|
32
|
+
validates :long_answer, presence: true, if: -> { question&.required? && question.long_answer? }
|
|
33
|
+
validates :short_answer, presence: true, if: -> { question&.required? && question.short_answer? }
|
|
34
|
+
validates :upload_file, presence: true, if: -> { question&.required? && question.upload_file? }
|
|
35
|
+
validates :question_option_ids, presence: true, if: -> { question&.required? && question.question_option? }
|
|
36
|
+
|
|
37
|
+
validates :question_option_ids, if: -> { question&.choose_one? },
|
|
38
|
+
length: { maximum: 1, message: 'please choose 1 option only' }
|
|
39
|
+
|
|
40
|
+
validates :question_option_ids, if: -> { question&.select_up_to_1? },
|
|
41
|
+
length: { maximum: 1, message: 'please select 1 option or fewer' }
|
|
42
|
+
|
|
43
|
+
validates :question_option_ids, if: -> { question&.select_up_to_2? },
|
|
44
|
+
length: { maximum: 2, message: 'please select 2 options or fewer' }
|
|
45
|
+
|
|
46
|
+
validates :question_option_ids, if: -> { question&.select_up_to_3? },
|
|
47
|
+
length: { maximum: 3, message: 'please select 3 options or fewer' }
|
|
48
|
+
|
|
49
|
+
validates :question_option_ids, if: -> { question&.select_up_to_4? },
|
|
50
|
+
length: { maximum: 4, message: 'please select 4 options or fewer' }
|
|
51
|
+
|
|
52
|
+
validates :question_option_ids, if: -> { question&.select_up_to_5? },
|
|
53
|
+
length: { maximum: 5, message: 'please select 5 options or fewer' }
|
|
54
|
+
|
|
55
|
+
def to_s
|
|
56
|
+
#model_name.human
|
|
57
|
+
response.to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def response
|
|
61
|
+
return nil unless question.present?
|
|
62
|
+
|
|
63
|
+
return date if question.date?
|
|
64
|
+
return email if question.email?
|
|
65
|
+
return number if question.number?
|
|
66
|
+
return long_answer if question.long_answer?
|
|
67
|
+
return short_answer if question.short_answer?
|
|
68
|
+
return upload_file if question.upload_file?
|
|
69
|
+
|
|
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?
|
|
73
|
+
|
|
74
|
+
raise('unknown response for unexpected question category')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def category_partial
|
|
78
|
+
question&.category_partial
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def completed?
|
|
82
|
+
return false if responsable.blank?
|
|
83
|
+
responsable.completed?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
end
|