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,108 @@
|
|
|
1
|
+
module Quby
|
|
2
|
+
module Compiler
|
|
3
|
+
module Outputs
|
|
4
|
+
class RoquaSerializer
|
|
5
|
+
attr_reader :questionnaire
|
|
6
|
+
|
|
7
|
+
def initialize(questionnaire)
|
|
8
|
+
@questionnaire = questionnaire
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def as_json(options = {})
|
|
12
|
+
{
|
|
13
|
+
key: questionnaire.key,
|
|
14
|
+
versions: versions,
|
|
15
|
+
keys: questionnaire.roqua_keys,
|
|
16
|
+
roqua_keys: questionnaire.roqua_keys,
|
|
17
|
+
sbg_key: questionnaire.sbg_key,
|
|
18
|
+
sbg_domains: questionnaire.sbg_domains,
|
|
19
|
+
outcome_regeneration_requested_at: questionnaire.outcome_regeneration_requested_at,
|
|
20
|
+
deactivate_answers_requested_at: questionnaire.deactivate_answers_requested_at,
|
|
21
|
+
respondent_types: questionnaire.respondent_types,
|
|
22
|
+
tags: questionnaire.tags.to_h.keys,
|
|
23
|
+
charts: charts,
|
|
24
|
+
outcome_tables_schema: outcome_tables_schema,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def versions
|
|
29
|
+
questionnaire.versions.map do |version|
|
|
30
|
+
{
|
|
31
|
+
number: version.number,
|
|
32
|
+
release_notes: version.release_notes,
|
|
33
|
+
regenerate_outcome: version.regenerate_outcome,
|
|
34
|
+
deactivate_answers: version.deactivate_answers
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def charts
|
|
40
|
+
{
|
|
41
|
+
overview: questionnaire.charts.overview && {
|
|
42
|
+
subscore: questionnaire.charts.overview.subscore,
|
|
43
|
+
y_max: questionnaire.charts.overview.y_max,
|
|
44
|
+
},
|
|
45
|
+
others: questionnaire.charts.map do |chart|
|
|
46
|
+
case chart
|
|
47
|
+
when Quby::Compiler::Entities::Charting::LineChart
|
|
48
|
+
{
|
|
49
|
+
y_label: chart.y_label,
|
|
50
|
+
tonality: chart.tonality,
|
|
51
|
+
baseline: YAML.dump(chart.baseline),
|
|
52
|
+
clinically_relevant_change: chart.clinically_relevant_change,
|
|
53
|
+
}
|
|
54
|
+
when Quby::Compiler::Entities::Charting::OverviewChart
|
|
55
|
+
{
|
|
56
|
+
subscore: chart.subscore,
|
|
57
|
+
y_max: chart.y_max,
|
|
58
|
+
}
|
|
59
|
+
else
|
|
60
|
+
{}
|
|
61
|
+
end.merge(
|
|
62
|
+
key: chart.key,
|
|
63
|
+
type: chart.type,
|
|
64
|
+
title: chart.title,
|
|
65
|
+
plottables: chart.plottables,
|
|
66
|
+
y_categories: chart.y_categories,
|
|
67
|
+
y_range_categories: chart.y_range_categories,
|
|
68
|
+
chart_type: chart.chart_type,
|
|
69
|
+
y_range: chart.y_range,
|
|
70
|
+
tick_interval: chart.tick_interval,
|
|
71
|
+
plotbands: chart.plotbands,
|
|
72
|
+
plotlines: chart.plotlines
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# configuration for outcome tables.
|
|
79
|
+
# tables:
|
|
80
|
+
# <outcome_table_name:Symbol>: # each entry is a table.
|
|
81
|
+
# score_keys: Set[<schema.key:Symbol>] # rows in the table
|
|
82
|
+
# subscore_keys: Set[<subschema.key:Symbol>] # columns in the table
|
|
83
|
+
# headers:
|
|
84
|
+
# <subscore_key:Symbol>: <subscore.label:String> # headers for each subscore key for all tables.
|
|
85
|
+
|
|
86
|
+
def outcome_tables_schema
|
|
87
|
+
# hash of tables, with the score keys (rows) and subscore keys (columns) used for each
|
|
88
|
+
tables = Hash.new{ |hash, key| hash[key] = {score_keys: Set.new, subscore_keys: Set.new } }
|
|
89
|
+
# hash of `subscore_key: subscore_label` pairs used in tables
|
|
90
|
+
headers = {}
|
|
91
|
+
|
|
92
|
+
questionnaire.score_schemas.values.each do |schema|
|
|
93
|
+
schema.subscore_schemas.each do |subschema|
|
|
94
|
+
tables[subschema.outcome_table][:subscore_keys] << subschema.key
|
|
95
|
+
tables[subschema.outcome_table][:score_keys] << schema.key
|
|
96
|
+
headers[subschema.key] = subschema.label
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
headers: headers,
|
|
102
|
+
tables: tables,
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'quby/compiler/services/quby_proxy'
|
|
2
|
+
|
|
3
|
+
module Quby
|
|
4
|
+
module Compiler
|
|
5
|
+
module Outputs
|
|
6
|
+
class SeedSerializer
|
|
7
|
+
attr_reader :questionnaire
|
|
8
|
+
attr_reader :seeds
|
|
9
|
+
|
|
10
|
+
def initialize(questionnaire, seeds)
|
|
11
|
+
@questionnaire = questionnaire
|
|
12
|
+
@seeds = seeds || []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate
|
|
16
|
+
roqua_keys = seeds.present? ? seeds.map { |seed| seed["key"] } : questionnaire.roqua_keys
|
|
17
|
+
|
|
18
|
+
roqua_keys.map do |roqua_key|
|
|
19
|
+
seed = seeds.find { |seed| seed["key"] == roqua_key } || {}
|
|
20
|
+
|
|
21
|
+
new_seed = Services::QubyProxy.new(
|
|
22
|
+
questionnaire,
|
|
23
|
+
quby_key: questionnaire.key,
|
|
24
|
+
roqua_key: roqua_key,
|
|
25
|
+
skip_score_keys_consistency_check: true
|
|
26
|
+
).generate(seed)
|
|
27
|
+
|
|
28
|
+
Services::SeedDiff.new.apply_patch(new_seed, questionnaire.seeds_patch[roqua_key])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'nokogumbo'
|
|
3
|
+
require 'active_model'
|
|
4
|
+
require 'quby/compiler/entities/questionnaire'
|
|
5
|
+
|
|
6
|
+
module Quby
|
|
7
|
+
module Compiler
|
|
8
|
+
module Services
|
|
9
|
+
class DefinitionValidator < ActiveModel::Validator
|
|
10
|
+
MAX_KEY_LENGTH = 19
|
|
11
|
+
KEY_PREFIX = 'v_'
|
|
12
|
+
|
|
13
|
+
attr_reader :definition
|
|
14
|
+
attr_reader :questionnaire
|
|
15
|
+
|
|
16
|
+
def validate(definition)
|
|
17
|
+
questionnaire = DSL.build_from_definition(definition)
|
|
18
|
+
validate_fields(questionnaire)
|
|
19
|
+
validate_title(questionnaire)
|
|
20
|
+
validate_questions(questionnaire)
|
|
21
|
+
validate_scores(questionnaire)
|
|
22
|
+
validate_table_edgecases(questionnaire)
|
|
23
|
+
validate_flags(questionnaire)
|
|
24
|
+
validate_respondent_types(questionnaire)
|
|
25
|
+
validate_outcome_tables(questionnaire)
|
|
26
|
+
validate_markdown_fields(questionnaire) if questionnaire.validate_html
|
|
27
|
+
validate_raw_content_items(questionnaire) if questionnaire.validate_html
|
|
28
|
+
validate_scores(questionnaire)
|
|
29
|
+
# Some compilation errors are Exceptions (pure syntax errors) and some StandardErrors (NameErrors)
|
|
30
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
|
31
|
+
definition.errors.add(:sourcecode, {message: "Questionnaire error: #{definition.key}\n" \
|
|
32
|
+
"#{exception.message}",
|
|
33
|
+
backtrace: exception.backtrace[0..5].join("<br/>")})
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_fields(questionnaire)
|
|
37
|
+
questionnaire.fields.input_keys
|
|
38
|
+
.find { |k| !k.is_a?(Symbol) }
|
|
39
|
+
&.tap { |k| fail "Input key #{k} is not a symbol" }
|
|
40
|
+
questionnaire.fields.answer_keys
|
|
41
|
+
.find { |k| !k.is_a?(Symbol) }
|
|
42
|
+
&.tap { |k| fail "Answer key #{k} is not a symbol" }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_title(questionnaire)
|
|
46
|
+
if questionnaire.title.blank?
|
|
47
|
+
fail "Questionnaire title is missing."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_questions(questionnaire)
|
|
52
|
+
questionnaire.answer_keys.each do |key|
|
|
53
|
+
validate_key_format(key)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
questionnaire.question_hash.each_value do |question|
|
|
57
|
+
validate_question(question)
|
|
58
|
+
subquestions_cant_have_default_invisible question
|
|
59
|
+
validate_subquestion_absence_in_select question
|
|
60
|
+
validate_placeholder_options_nil_values question
|
|
61
|
+
validate_values_unique question
|
|
62
|
+
|
|
63
|
+
validate_question_options(questionnaire, question)
|
|
64
|
+
validate_presence_of_titles question
|
|
65
|
+
validate_no_spaces_before_question_nr_in_title question
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_question(question)
|
|
70
|
+
unless question.valid?
|
|
71
|
+
fail "Question #{question.key} is invalid: #{question.errors.full_messages.join(', ')}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_scores(questionnaire)
|
|
76
|
+
questionnaire.scores.each do |score|
|
|
77
|
+
validate_score_key_length(score)
|
|
78
|
+
validate_score_label_present(score)
|
|
79
|
+
|
|
80
|
+
score_schema = questionnaire.score_schemas[score.key]
|
|
81
|
+
fail "Score #{score.key} does not have a score schema" unless score_schema
|
|
82
|
+
fail "Score label langer dan 100 tekens (geeft problemen oru accare)\n #{score_schema.label}" if score_schema.label&.length > 100
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validate_question_options(questionnaire, question)
|
|
87
|
+
question.options.each do |option|
|
|
88
|
+
msg_base = "Question #{option.question.key} option #{option.key}"
|
|
89
|
+
to_be_hidden_questions_exist_and_not_subquestion?(questionnaire, option, msg_base: msg_base)
|
|
90
|
+
to_be_shown_questions_exist_and_not_subquestion?(questionnaire, option, msg_base: msg_base)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_presence_of_titles(question)
|
|
95
|
+
return if question.allow_blank_titles
|
|
96
|
+
if !question.subquestion? && question.title.blank? && question.context_free_title.blank?
|
|
97
|
+
fail "Question #{question.key} must define either `:title` or `:context_free_title`."
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validate_table_edgecases(questionnaire)
|
|
102
|
+
questionnaire.panels.each do |panel|
|
|
103
|
+
tables = panel.items.select { |item| item.is_a?(Entities::Table) }
|
|
104
|
+
tables.each do |table|
|
|
105
|
+
questions = table.items.select { |item| item.is_a?(Entities::Question) }
|
|
106
|
+
questions.each { |question| validate_table_question(question) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_flags(questionnaire)
|
|
112
|
+
questionnaire.flags.each_value do |flag|
|
|
113
|
+
validate_flag_shows(questionnaire, flag)
|
|
114
|
+
validate_flag_hides(questionnaire, flag)
|
|
115
|
+
validate_flag_depends_on(questionnaire, flag)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def validate_flag_shows(questionnaire, flag)
|
|
120
|
+
unknown_questions = flag.shows_questions.select { |key| !questionnaire.key_in_use?(key) }
|
|
121
|
+
return if unknown_questions.blank?
|
|
122
|
+
|
|
123
|
+
fail ArgumentError, "Flag '#{key}' has unknown shows_questions keys #{unknown_questions}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validate_flag_hides(questionnaire, flag)
|
|
127
|
+
unknown_questions = flag.hides_questions.select { |key| !questionnaire.key_in_use?(key) }
|
|
128
|
+
return if unknown_questions.blank?
|
|
129
|
+
|
|
130
|
+
fail ArgumentError, "Flag '#{key}' has unknown hides_questions keys #{unknown_questions}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate_flag_depends_on(questionnaire, flag)
|
|
134
|
+
return if flag.depends_on.blank? || questionnaire.flags.key?(flag.depends_on)
|
|
135
|
+
|
|
136
|
+
fail ArgumentError, "Flag #{flag.key} depends_on nonexistent flag '#{flag.depends_on}'"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_respondent_types(questionnaire)
|
|
140
|
+
valid_respondent_types = Entities::Questionnaire::RESPONDENT_TYPES
|
|
141
|
+
|
|
142
|
+
invalid_types = questionnaire.respondent_types - valid_respondent_types
|
|
143
|
+
|
|
144
|
+
if invalid_types.present?
|
|
145
|
+
fail "Invalid respondent types: :#{invalid_types.join(', :')}\n"\
|
|
146
|
+
"Choose one or more from: :#{valid_respondent_types.join(', :')}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_outcome_tables(questionnaire)
|
|
151
|
+
questionnaire.outcome_tables.each do |table|
|
|
152
|
+
next if table.valid?
|
|
153
|
+
fail "Outcome table #{table.errors.full_messages}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def to_be_hidden_questions_exist_and_not_subquestion?(questionnaire, option, msg_base:)
|
|
158
|
+
return if option.hides_questions.blank?
|
|
159
|
+
msg_base += " hides_questions"
|
|
160
|
+
option.hides_questions.each do |key|
|
|
161
|
+
validate_question_key_exists?(questionnaire, key, msg_base: msg_base)
|
|
162
|
+
validate_not_subquestion(questionnaire, key, msg_base: msg_base)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def to_be_shown_questions_exist_and_not_subquestion?(questionnaire, option, msg_base:)
|
|
167
|
+
return if option.shows_questions.blank?
|
|
168
|
+
msg_base += " shows_questions"
|
|
169
|
+
option.shows_questions.each do |key|
|
|
170
|
+
validate_question_key_exists?(questionnaire, key, msg_base: msg_base)
|
|
171
|
+
validate_not_subquestion(questionnaire, key, msg_base: msg_base)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def subquestions_cant_have_default_invisible(question)
|
|
176
|
+
if question.subquestion? && question.default_invisible
|
|
177
|
+
fail "Question #{question.key} is a subquestion with default_invisible."
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.check_score_keys_consistency(seed)
|
|
182
|
+
score_keys = seed["properties"][:score_keys]
|
|
183
|
+
most_keys = score_keys.map { |score| keys_for_score(score).join }.max_by(&:length)
|
|
184
|
+
|
|
185
|
+
faulty_scores = score_keys.reject do |score|
|
|
186
|
+
most_keys.starts_with? keys_for_score(score).join
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if faulty_scores.present?
|
|
190
|
+
raise "scores mismatch other scores, check if this was intentional: #{faulty_scores}
|
|
191
|
+
|
|
192
|
+
If this was intentional, rerun quby proxy with the flag '--skip_score_keys_consistency_check' *and* manually add \
|
|
193
|
+
scores_schema tables to the resulting seed."
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def self.check_duplicate_headers(seed)
|
|
198
|
+
return
|
|
199
|
+
# TODO
|
|
200
|
+
# column_headers = DataExport::QuestionnaireHeaders.new(questionnaire).headers
|
|
201
|
+
# duplicate_header_names = column_headers.find_all { |e| column_headers.rindex(e) != column_headers.index(e) }.uniq
|
|
202
|
+
# raise "key clashes for: #{duplicate_header_names}" if duplicate_header_names.present?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
# Don't write question numbers as " 1. Title", but as "1\\. Title".
|
|
209
|
+
def validate_no_spaces_before_question_nr_in_title(question)
|
|
210
|
+
if question.title && question.title.match(/^\s{2,}\d+\\\./)
|
|
211
|
+
fail "Question with number does not need leading spaces."
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def validate_question_key_exists?(questionnaire, key, msg_base:)
|
|
216
|
+
unless questionnaire.question_hash[key]
|
|
217
|
+
fail msg_base + " references nonexistent question #{key}"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def validate_not_subquestion(questionnaire, key, msg_base:)
|
|
222
|
+
if questionnaire.question_hash[key].subquestion?
|
|
223
|
+
fail msg_base + " references subquestion #{key}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def validate_key_format(key)
|
|
228
|
+
if key.to_s.length > MAX_KEY_LENGTH
|
|
229
|
+
fail "Key '#{key}' should contain at most #{MAX_KEY_LENGTH} characters."
|
|
230
|
+
end
|
|
231
|
+
unless key.to_s.start_with?(KEY_PREFIX)
|
|
232
|
+
fail "Key '#{key}' should start with '#{KEY_PREFIX}'."
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def validate_score_key_length(score)
|
|
237
|
+
if score.key.to_s.length > MAX_KEY_LENGTH
|
|
238
|
+
fail "Score key `#{score.key}` should contain at most #{MAX_KEY_LENGTH} characters."
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def validate_score_label_present(score)
|
|
243
|
+
fail "Score #{score.key} label must be passed in as an option." unless score.label.present?
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def validate_subquestion_absence_in_select(question)
|
|
249
|
+
return unless question.type == :select
|
|
250
|
+
question.options.each do |option|
|
|
251
|
+
unless option.questions.empty?
|
|
252
|
+
fail "Question '#{question.key}' of type ':select' may not include other questions."
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def validate_placeholder_options_nil_values(question)
|
|
258
|
+
question.options.each do |question_option|
|
|
259
|
+
if question_option.placeholder && question_option.value.present?
|
|
260
|
+
fail "#{question.key}:#{question_option.key}: Placeholder options should not have values defined."
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def validate_values_unique(question)
|
|
266
|
+
return if question.type == :check_box || question.allow_duplicate_option_values
|
|
267
|
+
|
|
268
|
+
question.options.each_with_object([]) do |question_option, seen_values|
|
|
269
|
+
next if question_option.placeholder || question_option.inner_title
|
|
270
|
+
|
|
271
|
+
fail "#{question.key}:#{question_option.key}: Has no option value defined." if question_option.value.blank?
|
|
272
|
+
if seen_values.include?(question_option.value)
|
|
273
|
+
fail "#{question.key}:#{question_option.key}: " \
|
|
274
|
+
"Another option with value #{question_option.value} is already defined."
|
|
275
|
+
end
|
|
276
|
+
seen_values << question_option.value
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def validate_table_question(question)
|
|
281
|
+
question.subquestions.each do |subquestion|
|
|
282
|
+
if subquestion.presentation != :next_to_title
|
|
283
|
+
fail "Question #{question.key} is inside a table, but has a subquestion #{subquestion.key}, " \
|
|
284
|
+
"which is not allowed."
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def validate_markdown_fields(questionnaire)
|
|
290
|
+
questionnaire.panels.each do |panel|
|
|
291
|
+
panel.items.select { |item| item.is_a?(Entities::Text) }.each do |text_item|
|
|
292
|
+
validate_markdown(text_item.str, text_item.str)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
questionnaire.questions.each do |question|
|
|
296
|
+
Entities::Question::MARKDOWN_ATTRIBUTES.each do |attr|
|
|
297
|
+
validate_markdown(question.send(attr), "#{question.key}.#{attr}")
|
|
298
|
+
question.options.each do |option|
|
|
299
|
+
Entities::QuestionOption::MARKDOWN_ATTRIBUTES.each do |option_attr|
|
|
300
|
+
validate_markdown(option.send(option_attr), "#{question.key}:#{option.key}.#{option_attr}")
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def validate_raw_content_items(questionnaire)
|
|
308
|
+
questionnaire.panels.each do |panel|
|
|
309
|
+
panel.items.each do |item|
|
|
310
|
+
next if item.raw_content.blank?
|
|
311
|
+
|
|
312
|
+
validate_html(item.raw_content)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def validate_markdown(markdown, key)
|
|
318
|
+
validate_html(MarkdownParser.new(markdown).to_html, key)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def validate_html(html, key = nil)
|
|
322
|
+
fragment = Nokogiri::HTML5.fragment(html, max_errors: 3)
|
|
323
|
+
return unless fragment.errors.present?
|
|
324
|
+
|
|
325
|
+
fail "#{key || html} contains invalid html: #{fragment.errors.map(&:to_s).join(', ')}."
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|