quby-compiler 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|