quby-compiler 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.gitlab-ci.yml +5 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +11 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/config/locales/de.yml +58 -0
- data/config/locales/en.yml +57 -0
- data/config/locales/nl.yml +57 -0
- data/config/locales/rails-i18n/README.md +4 -0
- data/config/locales/rails-i18n/de.yml +223 -0
- data/config/locales/rails-i18n/en.yml +216 -0
- data/config/locales/rails-i18n/nl.yml +214 -0
- data/exe/quby-compile +56 -0
- data/lib/quby/array_attribute_valid_validator.rb +15 -0
- data/lib/quby/attribute_valid_validator.rb +14 -0
- data/lib/quby/compiler.rb +50 -0
- data/lib/quby/compiler/dsl.rb +29 -0
- data/lib/quby/compiler/dsl/base.rb +20 -0
- data/lib/quby/compiler/dsl/calls_custom_methods.rb +29 -0
- data/lib/quby/compiler/dsl/charting/bar_chart_builder.rb +14 -0
- data/lib/quby/compiler/dsl/charting/chart_builder.rb +95 -0
- data/lib/quby/compiler/dsl/charting/line_chart_builder.rb +34 -0
- data/lib/quby/compiler/dsl/charting/overview_chart_builder.rb +31 -0
- data/lib/quby/compiler/dsl/charting/radar_chart_builder.rb +14 -0
- data/lib/quby/compiler/dsl/helpers.rb +53 -0
- data/lib/quby/compiler/dsl/panel_builder.rb +80 -0
- data/lib/quby/compiler/dsl/question_builder.rb +40 -0
- data/lib/quby/compiler/dsl/questionnaire_builder.rb +279 -0
- data/lib/quby/compiler/dsl/questions/base.rb +180 -0
- data/lib/quby/compiler/dsl/questions/checkbox_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/date_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/deprecated_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/float_question_builder.rb +21 -0
- data/lib/quby/compiler/dsl/questions/integer_question_builder.rb +21 -0
- data/lib/quby/compiler/dsl/questions/radio_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/select_question_builder.rb +18 -0
- data/lib/quby/compiler/dsl/questions/string_question_builder.rb +20 -0
- data/lib/quby/compiler/dsl/questions/text_question_builder.rb +22 -0
- data/lib/quby/compiler/dsl/score_builder.rb +22 -0
- data/lib/quby/compiler/dsl/score_schema_builder.rb +53 -0
- data/lib/quby/compiler/dsl/standardized_panel_generators.rb +33 -0
- data/lib/quby/compiler/dsl/table_builder.rb +48 -0
- data/lib/quby/compiler/entities.rb +38 -0
- data/lib/quby/compiler/entities/charting/bar_chart.rb +17 -0
- data/lib/quby/compiler/entities/charting/chart.rb +101 -0
- data/lib/quby/compiler/entities/charting/charts.rb +42 -0
- data/lib/quby/compiler/entities/charting/line_chart.rb +38 -0
- data/lib/quby/compiler/entities/charting/overview_chart.rb +20 -0
- data/lib/quby/compiler/entities/charting/plottable.rb +20 -0
- data/lib/quby/compiler/entities/charting/radar_chart.rb +17 -0
- data/lib/quby/compiler/entities/definition.rb +26 -0
- data/lib/quby/compiler/entities/fields.rb +119 -0
- data/lib/quby/compiler/entities/flag.rb +55 -0
- data/lib/quby/compiler/entities/item.rb +40 -0
- data/lib/quby/compiler/entities/lookup_tables.rb +71 -0
- data/lib/quby/compiler/entities/outcome_table.rb +31 -0
- data/lib/quby/compiler/entities/panel.rb +82 -0
- data/lib/quby/compiler/entities/question.rb +365 -0
- data/lib/quby/compiler/entities/question_option.rb +96 -0
- data/lib/quby/compiler/entities/questionnaire.rb +440 -0
- data/lib/quby/compiler/entities/questions/checkbox_question.rb +82 -0
- data/lib/quby/compiler/entities/questions/date_question.rb +84 -0
- data/lib/quby/compiler/entities/questions/deprecated_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/float_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/integer_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/radio_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/select_question.rb +19 -0
- data/lib/quby/compiler/entities/questions/string_question.rb +15 -0
- data/lib/quby/compiler/entities/questions/text_question.rb +15 -0
- data/lib/quby/compiler/entities/score_calculation.rb +35 -0
- data/lib/quby/compiler/entities/score_schema.rb +25 -0
- data/lib/quby/compiler/entities/subscore_schema.rb +23 -0
- data/lib/quby/compiler/entities/table.rb +143 -0
- data/lib/quby/compiler/entities/text.rb +71 -0
- data/lib/quby/compiler/entities/textvar.rb +23 -0
- data/lib/quby/compiler/entities/validation.rb +17 -0
- data/lib/quby/compiler/entities/version.rb +23 -0
- data/lib/quby/compiler/entities/visibility_rule.rb +71 -0
- data/lib/quby/compiler/instance.rb +72 -0
- data/lib/quby/compiler/output.rb +13 -0
- data/lib/quby/compiler/outputs.rb +4 -0
- data/lib/quby/compiler/outputs/quby_frontend_v1_serializer.rb +362 -0
- data/lib/quby/compiler/outputs/quby_frontend_v2_serializer.rb +15 -0
- data/lib/quby/compiler/outputs/roqua_serializer.rb +108 -0
- data/lib/quby/compiler/outputs/seed_serializer.rb +34 -0
- data/lib/quby/compiler/services/definition_validator.rb +330 -0
- data/lib/quby/compiler/services/quby_proxy.rb +405 -0
- data/lib/quby/compiler/services/seed_diff.rb +116 -0
- data/lib/quby/compiler/services/text_transformation.rb +30 -0
- data/lib/quby/compiler/version.rb +5 -0
- data/lib/quby/markdown_parser.rb +38 -0
- data/lib/quby/range_categories.rb +38 -0
- data/lib/quby/settings.rb +86 -0
- data/lib/quby/text_transformation.rb +26 -0
- data/lib/quby/type_validator.rb +12 -0
- data/quby-compiler.gemspec +39 -0
- metadata +277 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quby
|
|
4
|
+
module Compiler
|
|
5
|
+
module Entities
|
|
6
|
+
class Flag < Struct.new(:key, :description_true, :description_false, :description, :internal, :trigger_on,
|
|
7
|
+
:shows_questions, :hides_questions, :depends_on, :default_in_interface)
|
|
8
|
+
# rubocop:disable ParameterLists
|
|
9
|
+
def initialize(key:,
|
|
10
|
+
description_true: nil,
|
|
11
|
+
description_false: nil,
|
|
12
|
+
description: nil,
|
|
13
|
+
internal: false,
|
|
14
|
+
trigger_on: true,
|
|
15
|
+
shows_questions: [],
|
|
16
|
+
hides_questions: [],
|
|
17
|
+
depends_on: nil, # used in interface to hide this flag unless the depended on flag is set to true
|
|
18
|
+
default_in_interface: nil) # used in interface to set a default for the flag state,
|
|
19
|
+
# does not have an effect outside of the interface
|
|
20
|
+
super(key, description_true, description_false, description, internal, trigger_on, shows_questions,
|
|
21
|
+
hides_questions, depends_on, default_in_interface)
|
|
22
|
+
ensure_valid_descriptions
|
|
23
|
+
end
|
|
24
|
+
# rubocop:enable ParameterLists
|
|
25
|
+
|
|
26
|
+
def if_triggered_by(answer_flags)
|
|
27
|
+
yield if answer_flags[key] == trigger_on
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def variable_description
|
|
31
|
+
"#{description} (true - '#{description_true}', false - '#{description_false}')"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_codebook(_options = {})
|
|
35
|
+
output = []
|
|
36
|
+
output << "#{key} flag"
|
|
37
|
+
output << "'#{description}'" if description.present?
|
|
38
|
+
output << " 'true' - #{description_true}"
|
|
39
|
+
output << " 'false' - #{description_false}"
|
|
40
|
+
output << " '' (leeg) - Vlag niet ingesteld, informatie onbekend"
|
|
41
|
+
output << ""
|
|
42
|
+
output.join("\n")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def ensure_valid_descriptions
|
|
48
|
+
unless (description_false.present? && description_true.present?) || description.present?
|
|
49
|
+
raise "Flag '#{key}' Requires at least either both description_true and description_false or a description"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_model'
|
|
4
|
+
|
|
5
|
+
module Quby
|
|
6
|
+
module Compiler
|
|
7
|
+
module Entities
|
|
8
|
+
class Item
|
|
9
|
+
include ActiveModel::Validations
|
|
10
|
+
include ActiveSupport::Callbacks
|
|
11
|
+
define_callbacks :after_dsl_enhance
|
|
12
|
+
|
|
13
|
+
attr_accessor :presentation
|
|
14
|
+
attr_accessor :switch_cycle
|
|
15
|
+
|
|
16
|
+
# Raw content may contain a raw HTML replacement for this item
|
|
17
|
+
attr_accessor :raw_content
|
|
18
|
+
|
|
19
|
+
def initialize(options = {})
|
|
20
|
+
@raw_content = options[:raw_content]
|
|
21
|
+
@switch_cycle = options[:switch_cycle] || false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def presentation
|
|
25
|
+
@presentation || "vertical"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def as_json(options = {})
|
|
29
|
+
{
|
|
30
|
+
class: self.class.to_s
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_codebook(questionnaire, options = {})
|
|
35
|
+
""
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
|
|
3
|
+
module Quby
|
|
4
|
+
module Compiler
|
|
5
|
+
module Entities
|
|
6
|
+
class LookupTables
|
|
7
|
+
def initialize(path)
|
|
8
|
+
@path = path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def fetch(key)
|
|
12
|
+
csv_path = File.join(@path, "#{key}.csv")
|
|
13
|
+
data = CSV.read(csv_path, col_sep: ';', skip_blanks: true)
|
|
14
|
+
headers = data.shift
|
|
15
|
+
compare = data.shift
|
|
16
|
+
self.class.from_csv(levels: headers, compare: compare, data: data)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# load csv data into a tree.
|
|
20
|
+
# each row is a path through the tree.
|
|
21
|
+
# String and float types are used to make an exact match.
|
|
22
|
+
# A range is always a range between two floats where the range is between
|
|
23
|
+
# the low value (inclusive) and the high value (exclusive),
|
|
24
|
+
# written as 4:5 (low:high). These boundaries can be given as floats or
|
|
25
|
+
# integers, but internally they are always treated as a floats.
|
|
26
|
+
# The low and high values of a range cannot be equal.
|
|
27
|
+
# Use minfinity or infinity to create infinite ranges.
|
|
28
|
+
#
|
|
29
|
+
# @params levels [Array<String>] An array of column names
|
|
30
|
+
# @param compare [Array<String>]An array of lookup types (string, float or range) for each column
|
|
31
|
+
# @param data [Array<Array<>>] The rows describing a path through the tree.
|
|
32
|
+
def self.from_csv(levels:, compare:, data:)
|
|
33
|
+
tree = data.each_with_object({}) do |row, tree|
|
|
34
|
+
add_to_tree(tree, row, levels, compare)
|
|
35
|
+
end
|
|
36
|
+
{levels: levels, tree: tree}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def self.add_to_tree(tree, (value, *path), (level, *levels), (compare, *compares))
|
|
42
|
+
key = case compare
|
|
43
|
+
when 'string' then value.to_s
|
|
44
|
+
when 'float' then parse_float(value)
|
|
45
|
+
when 'range' then create_range(value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if levels.empty?
|
|
49
|
+
return key
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
tree.merge! key => add_to_tree(tree[key] || {}, path, levels, compares)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.create_range(value)
|
|
56
|
+
min, max = value.split(':').map { |val| parse_float(val) }
|
|
57
|
+
fail 'Cannot create range between two equal values' if min == max
|
|
58
|
+
(min...max)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.parse_float(value)
|
|
62
|
+
case value
|
|
63
|
+
when 'infinity' then Float::INFINITY
|
|
64
|
+
when 'minfinity' then -Float::INFINITY
|
|
65
|
+
else Float(value)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Quby
|
|
2
|
+
module Compiler
|
|
3
|
+
module Entities
|
|
4
|
+
# OutcomeTable describes how scores are formatted in a table in outcome views
|
|
5
|
+
# @param key [Symbol] key to reference this outcome table by
|
|
6
|
+
# @param score_keys [Array<Symbol>] which scores are selected for the rows of the table
|
|
7
|
+
# @param subscore_keys [Array<Symbol>] which subscores (:value, :interpretation etc.) make up the table columns
|
|
8
|
+
# @param name [String] a title that will be shown above the table
|
|
9
|
+
# @param default_collapsed [Boolean] if true, collapses the table to only show the name by default
|
|
10
|
+
# @param questionnaire [Questionnaire] for validating score keys and subscore keys according to its score_schema
|
|
11
|
+
class OutcomeTable
|
|
12
|
+
include ActiveModel::Model
|
|
13
|
+
attr_accessor :score_keys, :subscore_keys, :name, :default_collapsed, :questionnaire, :key
|
|
14
|
+
|
|
15
|
+
validates :score_keys, :subscore_keys, :questionnaire, :key, presence: true
|
|
16
|
+
validates :name, presence: true, if: proc { |table| table.default_collapsed }
|
|
17
|
+
validate :references_existing_score_keys
|
|
18
|
+
|
|
19
|
+
def references_existing_score_keys
|
|
20
|
+
(score_keys - questionnaire.score_schemas.values.map(&:key)).each do |missing_key|
|
|
21
|
+
errors.add :score_keys, "#{missing_key.inspect} not found in score schemas"
|
|
22
|
+
end
|
|
23
|
+
existing_subscore_keys = questionnaire.score_schemas.values.flat_map(&:subscore_schemas).map(&:key)
|
|
24
|
+
(subscore_keys - existing_subscore_keys).each do |missing_key|
|
|
25
|
+
errors.add :subscore_keys, "#{missing_key.inspect} not found in subscore schemas"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'quby/compiler/entities'
|
|
4
|
+
|
|
5
|
+
module Quby
|
|
6
|
+
module Compiler
|
|
7
|
+
module Entities
|
|
8
|
+
class Panel < Item
|
|
9
|
+
attr_accessor :title
|
|
10
|
+
attr_accessor :items
|
|
11
|
+
attr_accessor :key
|
|
12
|
+
attr_reader :questionnaire
|
|
13
|
+
|
|
14
|
+
def initialize(options = {})
|
|
15
|
+
@questionnaire = options[:questionnaire]
|
|
16
|
+
@title = options[:title]
|
|
17
|
+
@key = options[:key]
|
|
18
|
+
@items = options[:items] || []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def as_json(options = {})
|
|
22
|
+
super.merge(title: title, index: index, items: json_items)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def index
|
|
26
|
+
@questionnaire.panels.index(self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def next
|
|
30
|
+
this_panel_index = index
|
|
31
|
+
|
|
32
|
+
if this_panel_index < @questionnaire.panels.size
|
|
33
|
+
return @questionnaire.panels[this_panel_index + 1]
|
|
34
|
+
else
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def prev
|
|
40
|
+
this_panel_index = index
|
|
41
|
+
|
|
42
|
+
if this_panel_index > 0
|
|
43
|
+
return @questionnaire.panels[this_panel_index - 1]
|
|
44
|
+
else
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def json_items
|
|
50
|
+
items.map do |item|
|
|
51
|
+
case item
|
|
52
|
+
when Text
|
|
53
|
+
{ type: 'html', html: item.html }
|
|
54
|
+
when Question
|
|
55
|
+
next if item.table # things inside a table are added to the table, AND ALSO to the panel. skip them.
|
|
56
|
+
{ type: 'question', key: item.key }
|
|
57
|
+
when Table
|
|
58
|
+
{ type: "table" }
|
|
59
|
+
end
|
|
60
|
+
end.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validations
|
|
64
|
+
vals = {}
|
|
65
|
+
items.each do |item|
|
|
66
|
+
if item.is_a? Question
|
|
67
|
+
item.options.each do |opt|
|
|
68
|
+
if opt.questions
|
|
69
|
+
opt.questions.each do |q|
|
|
70
|
+
vals[q.key] = q.validations
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
vals[item.key] = item.validations
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
vals
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'quby/compiler/entities/item'
|
|
4
|
+
|
|
5
|
+
module Quby
|
|
6
|
+
module Compiler
|
|
7
|
+
module Entities
|
|
8
|
+
class Question < Item
|
|
9
|
+
MARKDOWN_ATTRIBUTES = %w(description title).freeze
|
|
10
|
+
|
|
11
|
+
set_callback :after_dsl_enhance, :expand_depends_on_input_keys
|
|
12
|
+
|
|
13
|
+
# Standard attributes
|
|
14
|
+
attr_accessor :key
|
|
15
|
+
validates :key, presence: true, 'quby/type': {is_a: Symbol}
|
|
16
|
+
attr_accessor :sbg_key
|
|
17
|
+
attr_accessor :questionnaire
|
|
18
|
+
attr_accessor :title
|
|
19
|
+
attr_accessor :context_free_title
|
|
20
|
+
attr_accessor :description
|
|
21
|
+
|
|
22
|
+
attr_accessor :labels
|
|
23
|
+
|
|
24
|
+
# What kind of question is this?
|
|
25
|
+
attr_accessor :type
|
|
26
|
+
|
|
27
|
+
# How should we display this question
|
|
28
|
+
attr_accessor :as
|
|
29
|
+
|
|
30
|
+
# To hide old questions
|
|
31
|
+
attr_accessor :hidden
|
|
32
|
+
|
|
33
|
+
# Whether to skip the uniqueness validation on radio and select option values
|
|
34
|
+
attr_reader :allow_duplicate_option_values
|
|
35
|
+
|
|
36
|
+
# In what modes do we display this question
|
|
37
|
+
# NOTE We always display questions in print-view (if they have an answer)
|
|
38
|
+
attr_accessor :display_modes
|
|
39
|
+
|
|
40
|
+
# Multiple-choice questions have options to choose from
|
|
41
|
+
attr_accessor :options
|
|
42
|
+
|
|
43
|
+
# Question validation fails when there are no title and no context_free_title.
|
|
44
|
+
# When :allow_blank_titles => true passed, validation does not fail. Any other value will raise the failure.
|
|
45
|
+
attr_accessor :allow_blank_titles
|
|
46
|
+
|
|
47
|
+
# Minimum and maximum values for float and integer types
|
|
48
|
+
attr_accessor :minimum
|
|
49
|
+
attr_accessor :maximum
|
|
50
|
+
|
|
51
|
+
# Whether the browser should autocomplete this question (off by default)
|
|
52
|
+
attr_accessor :autocomplete
|
|
53
|
+
|
|
54
|
+
# Whether we show the value for each option
|
|
55
|
+
# :all => in all questionnaire display modes
|
|
56
|
+
# :none => in none of display modes
|
|
57
|
+
# :paged => for only in :paged display mode
|
|
58
|
+
# :bulk => for only in :bulk display mode
|
|
59
|
+
attr_accessor :show_values
|
|
60
|
+
validates :show_values, inclusion: {
|
|
61
|
+
in: [:all, :none, :paged, :bulk, :print],
|
|
62
|
+
message: "option invalid: %{value}. Valid options: :all, :none, :paged, :bulk)" }
|
|
63
|
+
|
|
64
|
+
# Structuring
|
|
65
|
+
attr_accessor :validations
|
|
66
|
+
attr_accessor :dependencies
|
|
67
|
+
|
|
68
|
+
# To display unit for number items
|
|
69
|
+
attr_accessor :unit
|
|
70
|
+
# To specify size of string/number input boxes
|
|
71
|
+
attr_accessor :size
|
|
72
|
+
|
|
73
|
+
# Whether this radio question is deselectable
|
|
74
|
+
attr_accessor :deselectable
|
|
75
|
+
|
|
76
|
+
# Some questions are a tree.
|
|
77
|
+
attr_accessor :parent
|
|
78
|
+
attr_accessor :parent_option_key
|
|
79
|
+
|
|
80
|
+
# Whether we can collapse this in bulk view
|
|
81
|
+
attr_accessor :disallow_bulk
|
|
82
|
+
|
|
83
|
+
# This question should not validate itself unless the depends_on question is answered.
|
|
84
|
+
# May also be an array of "#{question_key}_#{option_key}" strings that specify options
|
|
85
|
+
# this question depends on.
|
|
86
|
+
attr_accessor :depends_on
|
|
87
|
+
|
|
88
|
+
# Extra data hash to store on the question item's html element
|
|
89
|
+
attr_accessor :extra_data
|
|
90
|
+
|
|
91
|
+
# data-attributes for the input tag.
|
|
92
|
+
attr_accessor :input_data
|
|
93
|
+
|
|
94
|
+
# Whether we use the :description, the :value or :none for the score header above this question
|
|
95
|
+
attr_accessor :score_header
|
|
96
|
+
|
|
97
|
+
# options for grouping questions and setting a minimum or maximum number of answered questions in the group
|
|
98
|
+
attr_accessor :question_group
|
|
99
|
+
attr_accessor :group_minimum_answered
|
|
100
|
+
attr_accessor :group_maximum_answered
|
|
101
|
+
|
|
102
|
+
# Text variable name that will be replaced with the answer to this question
|
|
103
|
+
# In all following text elements that support markdown
|
|
104
|
+
attr_accessor :sets_textvar
|
|
105
|
+
|
|
106
|
+
# Amount of rows and cols a textarea has
|
|
107
|
+
attr_accessor :lines
|
|
108
|
+
attr_accessor :cols
|
|
109
|
+
|
|
110
|
+
# Table this question might belong to
|
|
111
|
+
attr_accessor :table
|
|
112
|
+
|
|
113
|
+
# In case of being displayed inside a table, amount of columns/rows to span
|
|
114
|
+
attr_accessor :col_span
|
|
115
|
+
attr_accessor :row_span
|
|
116
|
+
|
|
117
|
+
attr_accessor :default_invisible
|
|
118
|
+
|
|
119
|
+
# Slider only: where to place the sliding thing by default
|
|
120
|
+
# Can have value :hidden for a hidden handle.
|
|
121
|
+
attr_accessor :default_position
|
|
122
|
+
|
|
123
|
+
##########################################################
|
|
124
|
+
|
|
125
|
+
# rubocop:disable CyclomaticComplexity, Metrics/MethodLength
|
|
126
|
+
def initialize(key, options = {})
|
|
127
|
+
super(options)
|
|
128
|
+
|
|
129
|
+
@extra_data ||= {}
|
|
130
|
+
@options = []
|
|
131
|
+
@allow_duplicate_option_values = options[:allow_duplicate_option_values]
|
|
132
|
+
@questionnaire = options[:questionnaire]
|
|
133
|
+
@key = key
|
|
134
|
+
@sbg_key = options[:sbg_key]
|
|
135
|
+
@type = options[:type]
|
|
136
|
+
@as = options[:as]
|
|
137
|
+
@title = options[:title]
|
|
138
|
+
@context_free_title = options[:context_free_title]
|
|
139
|
+
@allow_blank_titles = options[:allow_blank_titles]
|
|
140
|
+
@description = options[:description]
|
|
141
|
+
@display_modes = options[:display_modes]
|
|
142
|
+
@presentation = options[:presentation]
|
|
143
|
+
@validations = []
|
|
144
|
+
@parent = options[:parent]
|
|
145
|
+
@hidden = options[:hidden]
|
|
146
|
+
@table = options[:table]
|
|
147
|
+
@parent_option_key = options[:parent_option_key]
|
|
148
|
+
@autocomplete = options[:autocomplete] || "off"
|
|
149
|
+
@show_values = options[:show_values] || :bulk
|
|
150
|
+
@deselectable = (options[:deselectable].nil? || options[:deselectable])
|
|
151
|
+
@disallow_bulk = options[:disallow_bulk]
|
|
152
|
+
@score_header = options[:score_header] || :none
|
|
153
|
+
@sets_textvar = "#{questionnaire.key}_#{options[:sets_textvar]}" if options[:sets_textvar]
|
|
154
|
+
@unit = options[:unit]
|
|
155
|
+
@lines = options[:lines] || 6
|
|
156
|
+
@cols = options[:cols] || 40
|
|
157
|
+
@default_invisible = options[:default_invisible] || false
|
|
158
|
+
@labels ||= []
|
|
159
|
+
|
|
160
|
+
@col_span = options[:col_span] || 1
|
|
161
|
+
@row_span = options[:row_span] || 1
|
|
162
|
+
|
|
163
|
+
set_depends_on(options[:depends_on])
|
|
164
|
+
|
|
165
|
+
@question_group = options[:question_group]
|
|
166
|
+
@group_minimum_answered = options[:group_minimum_answered]
|
|
167
|
+
@group_maximum_answered = options[:group_maximum_answered]
|
|
168
|
+
|
|
169
|
+
@input_data = {}
|
|
170
|
+
@input_data[:value_tooltip] = true if options[:value_tooltip]
|
|
171
|
+
|
|
172
|
+
# Require subquestions of required questions by default
|
|
173
|
+
options[:required] = true if @parent&.validations&.first&.fetch(:type, nil) == :requires_answer
|
|
174
|
+
@validations << {type: :requires_answer, explanation: options[:error_explanation]} if options[:required]
|
|
175
|
+
|
|
176
|
+
if @type == :float
|
|
177
|
+
@validations << {type: :valid_float, explanation: options[:error_explanation]}
|
|
178
|
+
elsif @type == :integer
|
|
179
|
+
@validations << {type: :valid_integer, explanation: options[:error_explanation]}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if options[:minimum] and (@type == :integer || @type == :float)
|
|
183
|
+
fail "deprecated" # pretty sure this is not used anywhere
|
|
184
|
+
end
|
|
185
|
+
if options[:maximum] and (@type == :integer || @type == :float)
|
|
186
|
+
fail "deprecated" # pretty sure this is not used anywhere
|
|
187
|
+
end
|
|
188
|
+
@default_position = options[:default_position]
|
|
189
|
+
|
|
190
|
+
if @question_group
|
|
191
|
+
if @group_minimum_answered
|
|
192
|
+
@validations << {type: :answer_group_minimum, group: @question_group, value: @group_minimum_answered,
|
|
193
|
+
explanation: options[:error_explanation]}
|
|
194
|
+
end
|
|
195
|
+
if @group_maximum_answered
|
|
196
|
+
@validations << {type: :answer_group_maximum, group: @question_group, value: @group_maximum_answered,
|
|
197
|
+
explanation: options[:error_explanation]}
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
# rubocop:enable CyclomaticComplexity, Metrics/MethodLength
|
|
202
|
+
|
|
203
|
+
# rubocop:disable AccessorMethodName
|
|
204
|
+
def set_depends_on(keys)
|
|
205
|
+
return if keys.blank?
|
|
206
|
+
keys = [keys] unless keys.is_a?(Array)
|
|
207
|
+
@depends_on = keys
|
|
208
|
+
end
|
|
209
|
+
# rubocop:enable AccessorMethodName
|
|
210
|
+
|
|
211
|
+
def context_free_title
|
|
212
|
+
@context_free_title || @title
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def expand_depends_on_input_keys
|
|
216
|
+
return unless @depends_on
|
|
217
|
+
@depends_on = questionnaire.expand_input_keys(@depends_on)
|
|
218
|
+
@extra_data[:"depends-on"] = @depends_on.to_json
|
|
219
|
+
rescue => e
|
|
220
|
+
raise e.class, "Question #{key} depends_on contains an error: #{e.message}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def col_span
|
|
224
|
+
options.length > 0 && type != :select ? options.length : @col_span
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def as_json(options = {})
|
|
228
|
+
# rubocop:disable SymbolName
|
|
229
|
+
super.merge(
|
|
230
|
+
key: key,
|
|
231
|
+
title: Quby::MarkdownParser.new(title).to_html,
|
|
232
|
+
description: Quby::MarkdownParser.new(description).to_html,
|
|
233
|
+
type: type,
|
|
234
|
+
unit: unit,
|
|
235
|
+
size: size,
|
|
236
|
+
hidden: hidden?,
|
|
237
|
+
displayModes: display_modes,
|
|
238
|
+
defaultInvisible: default_invisible,
|
|
239
|
+
viewSelector: view_selector,
|
|
240
|
+
parentKey: parent&.key,
|
|
241
|
+
parentOptionKey: parent_option_key,
|
|
242
|
+
deselectable: deselectable,
|
|
243
|
+
presentation: presentation,
|
|
244
|
+
as: as,
|
|
245
|
+
questionGroup: question_group
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Returns all keys belonging to html inputs generated by this question.
|
|
250
|
+
def input_keys
|
|
251
|
+
if options.blank?
|
|
252
|
+
answer_keys
|
|
253
|
+
else
|
|
254
|
+
# Some options don't have a key (inner_title), they are stripped
|
|
255
|
+
options.map { |opt| opt.input_key }.compact
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def key_in_use?(k)
|
|
260
|
+
claimed_keys.include?(k) ||
|
|
261
|
+
options.any? { |option| option.key_in_use?(k) }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The keys this question claims as his own. Not including options and subquestions.
|
|
265
|
+
# Includes keys for the question, inputs and answers.
|
|
266
|
+
def claimed_keys
|
|
267
|
+
[key]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Returns all possible answer keys of this question (excluding subquestions, including options).
|
|
271
|
+
# radio/select/scale-options do not create answer_keys, but answer values.
|
|
272
|
+
def answer_keys
|
|
273
|
+
[key]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def default_position
|
|
277
|
+
return unless as == :slider
|
|
278
|
+
half = (type == :float) ? 2.0 : 2
|
|
279
|
+
@default_position || ((minimum + maximum) / half if minimum && maximum)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def minimum
|
|
283
|
+
validations.find { |i| i[:type] == :minimum }.try(:fetch, :value)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def maximum
|
|
287
|
+
validations.find { |i| i[:type] == :maximum }.try(:fetch, :value)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def html_id
|
|
291
|
+
"answer_#{key}_input"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def view_selector
|
|
295
|
+
table.blank? ? "#item_#{key}" : "[data-for=#{key}], #answer_#{key}_input"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def hidden?
|
|
299
|
+
hidden
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def show_values_in_mode?(mode)
|
|
303
|
+
case show_values
|
|
304
|
+
when :none then false
|
|
305
|
+
when :all then true
|
|
306
|
+
else show_values == mode
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def subquestions
|
|
311
|
+
options.map { |opt| opt.questions }.flatten
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def subquestion?
|
|
315
|
+
!parent_option_key.nil?
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def to_codebook(questionnaire, opts = {})
|
|
319
|
+
output = []
|
|
320
|
+
question_key = codebook_key(key, questionnaire, opts)
|
|
321
|
+
output << "#{question_key} #{codebook_output_type} #{codebook_output_range}#{' deprecated' if hidden}"
|
|
322
|
+
output << "\"#{context_free_title}\"" unless context_free_title.blank?
|
|
323
|
+
options_string = options.map do |option|
|
|
324
|
+
option.to_codebook(questionnaire, opts)
|
|
325
|
+
end.compact.join("\n")
|
|
326
|
+
output << options_string unless options.blank?
|
|
327
|
+
output.join("\n")
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def codebook_key(key, questionnaire, opts = {})
|
|
331
|
+
key.to_s.gsub(/^v_/, "#{opts[:roqua_key] || questionnaire.key.to_s}_")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def codebook_output_type
|
|
335
|
+
type
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def codebook_output_range
|
|
339
|
+
range_min = validations.find { |i| i[:type] == :minimum }&.fetch(:value, nil)
|
|
340
|
+
range_max = validations.find { |i| i[:type] == :maximum }&.fetch(:value, nil)
|
|
341
|
+
|
|
342
|
+
if range_min || range_max
|
|
343
|
+
"(#{[range_min, "value", range_max].compact.join(" <= ")})"
|
|
344
|
+
else
|
|
345
|
+
""
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def variable_descriptions
|
|
350
|
+
{key => context_free_title}.with_indifferent_access
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
require 'quby/compiler/entities/questions/checkbox_question'
|
|
358
|
+
require 'quby/compiler/entities/questions/date_question'
|
|
359
|
+
require 'quby/compiler/entities/questions/deprecated_question'
|
|
360
|
+
require 'quby/compiler/entities/questions/float_question'
|
|
361
|
+
require 'quby/compiler/entities/questions/integer_question'
|
|
362
|
+
require 'quby/compiler/entities/questions/radio_question'
|
|
363
|
+
require 'quby/compiler/entities/questions/select_question'
|
|
364
|
+
require 'quby/compiler/entities/questions/string_question'
|
|
365
|
+
require 'quby/compiler/entities/questions/text_question'
|