quby-compiler 0.4.3 → 0.4.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51cced2ed60f5bf58986b0bb1359a06e20349ee41f8313a86fd8abe7743b942f
4
- data.tar.gz: 63dfc7514011d1a0518a5a5fd5dec191dd326104dab042eba491bb3b71380ff8
3
+ metadata.gz: 7f3028be16a77f8fbf5f30b7abfb04839a2a501c4e829bda046a415723beafd4
4
+ data.tar.gz: d682aa3b84a73c5b2f3e41f90c82c0e20fbcb431149be21b6136416ddfa827d8
5
5
  SHA512:
6
- metadata.gz: 0cef3595b41bdeb1eb0cd4ced9b903fc63441359a98a12b2999bdfda66daa2b705c07316cf6eaf16912e64a20098296348b838565eb89c6f15b42bab659fbdee
7
- data.tar.gz: a5ad54369e5bf53e516d2f03665309d0e3e530b3acd488be9cad1eb4d1bcab0cebb6e3d23274c5d3bbfba225475e8970aa24fc776eee1b5206049428dd8c1d0d
6
+ metadata.gz: 91673fe9c11612f9c9dba10ab129217942d388830423550376f36972250643660eca53af578183c77d924aee9db44587b4ee4ec19e3e66d445a91367a6df7cff
7
+ data.tar.gz: 19d1703844eb8a809f68b8a4634af28d97f627a42b575cdb468737dc43b87fda0351ef875edb0c21e70483aaec84d2362256b38c531840f64c9820d7ba69efa6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ # 0.4.7
2
+
3
+ * seeds.yml: Fix missing title questions (broken by 0.4.6)
4
+
5
+ # 0.4.6
6
+
7
+ * Added type validations for boolean attributes
8
+ * roqua.json
9
+ * add unit to questions
10
+ * add title_question_key to questions
11
+ * add parent_question_key and parent_option_key to questions
12
+ * add child_question_keys to question_options
13
+ * models
14
+ * Made title_question attribute on question instead of adding options.last.questions.
15
+ * Made v1 serializer create backward compatible json (for now)
16
+ * Made v1 serializer add the full title_question json to question.title_question.
17
+
18
+ # 0.4.5
19
+
20
+ * roqua.json: Don't add inner titles and placeholder options
21
+
22
+ # 0.4.4
23
+
24
+ * quby-compile will now validate, not compile invalid questionnaires and return 1 when any invalid.
25
+ * roqua.json
26
+ * add questions hash
27
+ * quby2.json
28
+ * add `as` to all questions, defaulting to type.
29
+ * add `minimum` and `maximum` to integer/float questions.
30
+ * add `step`, `defaultPosition`, `startThumbHidden`, `valueTooltip` and `labels` to slider questions.
31
+
1
32
  # 0.4.3
2
33
 
3
34
  * roqua.json: add scores hash.
data/exe/quby-compile CHANGED
@@ -30,11 +30,21 @@ end
30
30
 
31
31
  lookup_tables = Quby::Compiler::Entities::LookupTables.new(lookup_tables_path)
32
32
 
33
+ validation_errors_found = false
33
34
  paths.each do |path|
34
35
  puts "Compiling #{path}"
35
36
 
36
37
  key = File.basename(File.dirname(path))
37
38
  sourcecode = File.read(path)
39
+
40
+ definition = Quby::Compiler.validate(key, sourcecode, lookup_tables: lookup_tables)
41
+ if definition.errors.any?
42
+ error = definition.errors[:sourcecode].first
43
+ STDERR.puts error[:message], error[:backtrace], ''
44
+ validation_errors_found = true
45
+ next # let's not create new output files for questionnaires with errors
46
+ end
47
+
38
48
  compiled = Quby::Compiler.compile(key, sourcecode, path: path, lookup_tables: lookup_tables)
39
49
 
40
50
  FileUtils.mkdir_p(File.join(output_path, key))
@@ -45,3 +55,7 @@ paths.each do |path|
45
55
  end
46
56
  end
47
57
  end
58
+
59
+ if validation_errors_found
60
+ exit(1)
61
+ end
@@ -114,16 +114,6 @@ module Quby
114
114
  def initialize(key, **options, &block)
115
115
  super
116
116
  @default_question_options = options[:default_question_options] || {}
117
- @title_question = nil
118
- end
119
-
120
- def build
121
- if @title_question
122
- @question.options.last.questions << @title_question
123
- @title_question = nil
124
- end
125
-
126
- super
127
117
  end
128
118
 
129
119
  def title_question(key, **options, &block)
@@ -138,7 +128,7 @@ module Quby
138
128
  question = QuestionBuilder.build(key, **options, &block)
139
129
 
140
130
  @questionnaire.register_question(question)
141
- @title_question = question
131
+ @question.title_question = question
142
132
  end
143
133
 
144
134
  def question(key, **options, &block)
@@ -29,9 +29,11 @@ module Quby
29
29
 
30
30
  # To hide old questions
31
31
  attr_accessor :hidden
32
+ validates :hidden, inclusion: {in: [true, false, nil], message: "must be boolean"}
32
33
 
33
34
  # Whether to skip the uniqueness validation on radio and select option values
34
35
  attr_reader :allow_duplicate_option_values
36
+ validates :allow_duplicate_option_values, inclusion: {in: [true, false, nil], message: "must be boolean"}
35
37
 
36
38
  # In what modes do we display this question
37
39
  # NOTE We always display questions in print-view (if they have an answer)
@@ -40,9 +42,13 @@ module Quby
40
42
  # Multiple-choice questions have options to choose from
41
43
  attr_accessor :options
42
44
 
45
+ # string question that is displayed after the title of this question.
46
+ attr_accessor :title_question
47
+
43
48
  # Question validation fails when there are no title and no context_free_title.
44
49
  # When :allow_blank_titles => true passed, validation does not fail. Any other value will raise the failure.
45
50
  attr_accessor :allow_blank_titles
51
+ validates :allow_blank_titles, inclusion: {in: [true, false, nil], message: "must be boolean"}
46
52
 
47
53
  # Minimum and maximum values for float and integer types
48
54
  attr_accessor :minimum
@@ -72,6 +78,7 @@ module Quby
72
78
 
73
79
  # Whether this radio question is deselectable
74
80
  attr_accessor :deselectable
81
+ validates :deselectable, inclusion: {in: [true, false, nil], message: "must be boolean"}
75
82
 
76
83
  # Some questions are a tree.
77
84
  attr_accessor :parent
@@ -79,6 +86,7 @@ module Quby
79
86
 
80
87
  # Whether we can collapse this in bulk view
81
88
  attr_accessor :disallow_bulk
89
+ validates :disallow_bulk, inclusion: {in: [true, false, nil], message: "must be boolean"}
82
90
 
83
91
  # This question should not validate itself unless the depends_on question is answered.
84
92
  # May also be an array of "#{question_key}_#{option_key}" strings that specify options
@@ -115,6 +123,7 @@ module Quby
115
123
  attr_accessor :row_span
116
124
 
117
125
  attr_accessor :default_invisible
126
+ validates :default_invisible, inclusion: {in: [true, false], message: "must be boolean"}
118
127
 
119
128
  # Slider only: where to place the sliding thing by default
120
129
  # Can have value :hidden for a hidden handle.
@@ -240,14 +249,17 @@ module Quby
240
249
  parentOptionKey: parent_option_key,
241
250
  deselectable: deselectable,
242
251
  presentation: presentation,
243
- as: as,
252
+ as: as || type, # default to type so typescript can narrow on it.
244
253
  questionGroup: question_group
245
254
  ).tap do |json|
246
255
  json[:unit] = unit if %i[integer float string].include?(type) && as != :slider
247
- json[:labels] = labels if as == :slider
248
256
  end
249
257
  end
250
258
 
259
+ def title_question?
260
+ presentation == :next_to_title
261
+ end
262
+
251
263
  # Returns all keys belonging to html inputs generated by this question.
252
264
  def input_keys
253
265
  if options.blank?
@@ -4,6 +4,8 @@ module Quby
4
4
  module Compiler
5
5
  module Entities
6
6
  class QuestionOption
7
+ include ActiveModel::Validations
8
+
7
9
  MARKDOWN_ATTRIBUTES = %w(description).freeze
8
10
 
9
11
  attr_reader :key
@@ -11,16 +13,17 @@ module Quby
11
13
  attr_reader :description
12
14
  attr_reader :questions
13
15
  attr_reader :inner_title
16
+ validates :inner_title, inclusion: {in: [true, false, nil], message: "must be boolean"}
14
17
  attr_reader :hides_questions
15
18
  attr_reader :shows_questions
16
19
  attr_reader :hidden
20
+ validates :hidden, inclusion: {in: [true, false], message: "must be boolean"}
17
21
  attr_reader :placeholder
22
+ validates :placeholder, inclusion: {in: [true, false], message: "must be boolean"}
18
23
  attr_reader :question
19
24
  attr_reader :view_id
20
25
  attr_reader :input_key
21
26
 
22
- attr_reader :start_chosen
23
-
24
27
  def initialize(key, question, options = {})
25
28
  @key = key
26
29
  @question = question
@@ -0,0 +1,23 @@
1
+ module Quby::Compiler::Entities::Questions::Concerns
2
+ module Slider
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ validates :minimum, :maximum, presence: true, if: -> { as == :slider }
7
+ end
8
+
9
+ def as_json(options = {})
10
+ if as == :slider
11
+ super.merge(
12
+ step: step,
13
+ defaultPosition: default_position.is_a?(Numeric) ? default_position : minimum,
14
+ startThumbHidden: default_position == :hidden,
15
+ valueTooltip: input_data[:value_tooltip] || false,
16
+ labels: labels
17
+ )
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,22 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/slider'
4
+
3
5
  module Quby
4
6
  module Compiler
5
7
  module Entities
6
8
  module Questions
7
9
  class FloatQuestion < Question
10
+ include Concerns::Slider
11
+
8
12
  def as_json(options = {})
9
13
  super.merge(
10
14
  minimum: minimum,
11
- maximum: maximum,
12
- step: 0.01, # fixed in v1.
13
- # defaultPosition: default_position # Needs discussion, can be number or string "hidden"
15
+ maximum: maximum
14
16
  )
15
17
  end
16
18
 
17
19
  def size
18
20
  @size || 30
19
21
  end
22
+
23
+ def step
24
+ 0.01
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -1,22 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/slider'
4
+
3
5
  module Quby
4
6
  module Compiler
5
7
  module Entities
6
8
  module Questions
7
9
  class IntegerQuestion < Question
10
+ include Concerns::Slider
11
+
8
12
  def as_json(options = {})
9
13
  super.merge(
10
14
  minimum: minimum,
11
- maximum: maximum,
12
- step: 1, # fixed in v1.
13
- # defaultPosition: default_position # Needs discussion, can be number or string "hidden"
15
+ maximum: maximum
14
16
  )
15
17
  end
16
18
 
17
19
  def size
18
20
  @size || 30
19
21
  end
22
+
23
+ def step
24
+ 1
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -61,6 +61,7 @@ module Quby
61
61
  question_type: question.type,
62
62
  key: question.key,
63
63
  title: question.title,
64
+ title_question: (question_as_json(question.title_question) if question.title_question),
64
65
  context_free_title: question.context_free_title,
65
66
  description: question.description,
66
67
  presentation: question.presentation,
@@ -96,7 +97,7 @@ module Quby
96
97
  case question
97
98
  when Quby::Compiler::Entities::Questions::CheckboxQuestion
98
99
  base_options.merge(
99
- options: question.options.map { |option| option_as_json(option) },
100
+ options: options_as_json(question),
100
101
  check_all_option: question.check_all_option,
101
102
  uncheck_all_option: question.uncheck_all_option,
102
103
  maximum_checked_allowed: question.maximum_checked_allowed,
@@ -114,7 +115,7 @@ module Quby
114
115
  )
115
116
  when Quby::Compiler::Entities::Questions::DeprecatedQuestion
116
117
  base_options.merge(
117
- options: question.options.map { |option| option_as_json(option) }
118
+ options: options_as_json(question)
118
119
  )
119
120
  when Quby::Compiler::Entities::Questions::FloatQuestion
120
121
  base_options.merge(
@@ -130,11 +131,11 @@ module Quby
130
131
  )
131
132
  when Quby::Compiler::Entities::Questions::RadioQuestion
132
133
  base_options.merge(
133
- options: question.options.map { |option| option_as_json(option) }
134
+ options: options_as_json(question)
134
135
  )
135
136
  when Quby::Compiler::Entities::Questions::SelectQuestion
136
137
  base_options.merge(
137
- options: question.options.map { |option| option_as_json(option) }
138
+ options: options_as_json(question)
138
139
  )
139
140
  when Quby::Compiler::Entities::Questions::StringQuestion
140
141
  base_options.merge(
@@ -177,6 +178,16 @@ module Quby
177
178
  end
178
179
  end
179
180
 
181
+ # Also adds the title question under the last option.
182
+ # TODO old dsl put it there, remove after quby gem uses title_question
183
+ def options_as_json(question)
184
+ question.options.map { |option| option_as_json(option) }.tap do |options_json|
185
+ if question.title_question
186
+ options_json.last[:questions] << question_as_json(question.title_question)
187
+ end
188
+ end
189
+ end
190
+
180
191
  def option_as_json(option)
181
192
  {
182
193
  key: option.key,
@@ -2,6 +2,18 @@ module Quby
2
2
  module Compiler
3
3
  module Outputs
4
4
  class RoquaSerializer
5
+ QUBY_TYPE_TO_ROQUA_TYPE = {
6
+ check_box: 'multi_select',
7
+ date: 'date_parts',
8
+ float: 'float',
9
+ integer: 'integer',
10
+ radio: 'single_select',
11
+ scale: 'single_select',
12
+ select: 'single_select',
13
+ string: 'text',
14
+ textarea: 'text'
15
+ }
16
+
5
17
  attr_reader :questionnaire
6
18
 
7
19
  def initialize(questionnaire)
@@ -22,6 +34,7 @@ module Quby
22
34
  tags: questionnaire.tags.to_h.keys,
23
35
  charts: charts,
24
36
  outcome_tables_schema: outcome_tables_schema,
37
+ questions: questions,
25
38
  scores: scores,
26
39
  }
27
40
  end
@@ -141,6 +154,81 @@ module Quby
141
154
  }
142
155
  end
143
156
 
157
+ # {v_1: {.., options: {..}, date_parts: {..}}, ..}
158
+ # nils are removed from hash.
159
+ # key [string]
160
+ # sbg_key [nil/string] _not_ defaulted to key
161
+ # type [string]
162
+ # title [nil/string] no html
163
+ # context_free_title [nil/string] no html, defaulted to title
164
+ # unit [nil/string] no html (not in quby either)
165
+ # deprecated [nil/true] only show if filled for old answers
166
+ # default_invisible [nil/true] only show if filled or when all questions are shown
167
+ # parent_question_key [nil/string] title_questions or question defined under option
168
+ # parent_option_key [nil/string] question defined under option
169
+ # title_question_key [nil/string]
170
+ # date_parts [nil/{..}]
171
+ # options: [nil/{..}]
172
+ def questions
173
+ questionnaire.questions.reject{_1.type == :hidden}.to_h { |question|
174
+ [
175
+ question.key,
176
+ {
177
+ key: question.key,
178
+ sbg_key: question.sbg_key,
179
+ type: QUBY_TYPE_TO_ROQUA_TYPE.fetch(question.type),
180
+ title: strip_tags_without_html_encode(question.title),
181
+ context_free_title: strip_tags_without_html_encode(question.context_free_title),
182
+ unit: question.unit,
183
+ deprecated: question.hidden.presence,
184
+ default_invisible: question.default_invisible.presence,
185
+ parent_question_key: question.parent&.key,
186
+ parent_option_key: question.parent_option_key,
187
+ title_question_key: question.title_question&.key,
188
+ date_parts: date_parts_for(question),
189
+ options: options_for(question)
190
+ }.compact
191
+ ]
192
+ }
193
+ end
194
+
195
+ # {year: {key: "v_date_yyyy"}, ..}
196
+ # key [string]
197
+ def date_parts_for(question)
198
+ return nil unless question.type == :date
199
+
200
+ question.components.to_h { |component|
201
+ [
202
+ component,
203
+ {
204
+ key: question.send("#{component}_key")
205
+ }
206
+ ]
207
+ }
208
+ end
209
+
210
+ # {a1: {..}}
211
+ # key [string]
212
+ # value [nil/string] nil for check_box, string otherwise
213
+ # description [nil/string] context_free_description, no html
214
+ # child_question_keys [nil/[string]]
215
+ def options_for(question)
216
+ return nil if question.options.empty?
217
+
218
+ question.options.reject { |option| option.inner_title || option.placeholder }
219
+ .to_h { |option|
220
+ [
221
+ option.key,
222
+ {
223
+ key: option.key,
224
+ value: (option.value.to_s unless question.type == :check_box),
225
+ description: strip_tags_without_html_encode(option.context_free_description),
226
+ child_question_keys: option.questions.map(&:key).presence
227
+ }.compact
228
+ ]
229
+ }
230
+ end
231
+
144
232
  # [{ key: { .., subscores: { subkey: {..} } } }]
145
233
  def scores
146
234
  questionnaire.score_schemas.transform_values { |score|
@@ -166,6 +254,10 @@ module Quby
166
254
  ]
167
255
  }
168
256
  end
257
+
258
+ def strip_tags_without_html_encode(html)
259
+ Nokogiri::HTML(html).text.presence
260
+ end
169
261
  end
170
262
  end
171
263
  end
@@ -100,6 +100,9 @@ module Quby
100
100
  def validate_question_options(questionnaire, question)
101
101
  question.options.each do |option|
102
102
  msg_base = "Question #{option.question.key} option #{option.key}"
103
+ unless option.valid?
104
+ fail "#{msg_base} #{option.errors.full_messages.to_sentence}"
105
+ end
103
106
  to_be_hidden_questions_exist_and_not_subquestion?(questionnaire, option, msg_base: msg_base)
104
107
  to_be_shown_questions_exist_and_not_subquestion?(questionnaire, option, msg_base: msg_base)
105
108
  end
@@ -59,6 +59,9 @@ module Quby
59
59
  values = [value, value.to_s].uniq
60
60
  handle_subquestions(question, question_titles, d_qtypes, vars, option, values, option.key.to_s)
61
61
  end
62
+ if question.title_question
63
+ subquestion(question, question_titles, d_qtypes, vars, question.title_question, nil, nil)
64
+ end
62
65
  update_hidden_questions_for(question, for_checkbox: true)
63
66
  when :textarea
64
67
  d_qtypes[question.key.to_s] = { type: :text_field }
@@ -280,6 +283,9 @@ module Quby
280
283
  key = question.key.to_s
281
284
  handle_subquestions(question, quests, d_qtypes, vars, option, [option.value.to_s], key)
282
285
  end
286
+ if question.title_question
287
+ subquestion(question, quests, d_qtypes, vars, question.title_question, nil, nil)
288
+ end
283
289
  end
284
290
 
285
291
  def handle_textfield(question, d_qtypes)
@@ -1,5 +1,5 @@
1
1
  module Quby
2
2
  module Compiler
3
- VERSION = "0.4.3"
3
+ VERSION = "0.4.7"
4
4
  end
5
5
  end
data/lib/quby/compiler.rb CHANGED
@@ -38,7 +38,7 @@ module Quby
38
38
  )
39
39
  end
40
40
 
41
- def self.validate(key, sourcecode)
41
+ def self.validate(key, sourcecode, lookup_tables:)
42
42
  Quby::Compiler::Instance.new(lookup_tables: lookup_tables).validate(
43
43
  key: key,
44
44
  sourcecode: sourcecode,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quby-compiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marten Veldthuis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-21 00:00:00.000000000 Z
11
+ date: 2022-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -204,6 +204,7 @@ files:
204
204
  - lib/quby/compiler/entities/question_option.rb
205
205
  - lib/quby/compiler/entities/questionnaire.rb
206
206
  - lib/quby/compiler/entities/questions/checkbox_question.rb
207
+ - lib/quby/compiler/entities/questions/concerns/slider.rb
207
208
  - lib/quby/compiler/entities/questions/date_question.rb
208
209
  - lib/quby/compiler/entities/questions/deprecated_question.rb
209
210
  - lib/quby/compiler/entities/questions/float_question.rb