quby 4.0.2 → 5.0.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.markdown +0 -1
  3. data/lib/quby.rb +2 -10
  4. data/lib/quby/answers/entities/answer.rb +4 -2
  5. data/lib/quby/answers/services/outcome_calculation.rb +1 -2
  6. data/lib/quby/answers/services/score_calculator.rb +1 -3
  7. data/lib/quby/engine.rb +0 -1
  8. data/lib/quby/questionnaires.rb +1 -0
  9. data/lib/quby/questionnaires/api.rb +5 -1
  10. data/lib/quby/questionnaires/deserializer.rb +439 -0
  11. data/lib/quby/questionnaires/dsl.rb +12 -15
  12. data/lib/quby/questionnaires/entities.rb +1 -0
  13. data/lib/quby/questionnaires/entities/charting/line_chart.rb +23 -0
  14. data/lib/quby/questionnaires/entities/charting/overview_chart.rb +3 -1
  15. data/lib/quby/questionnaires/entities/definition.rb +3 -5
  16. data/lib/quby/questionnaires/entities/fields.rb +0 -15
  17. data/lib/quby/questionnaires/entities/question.rb +9 -32
  18. data/lib/quby/questionnaires/entities/questionnaire.rb +4 -15
  19. data/lib/quby/questionnaires/entities/questions/checkbox_question.rb +0 -24
  20. data/lib/quby/questionnaires/entities/questions/date_question.rb +0 -8
  21. data/lib/quby/questionnaires/entities/score_calculation.rb +36 -3
  22. data/lib/quby/questionnaires/repos.rb +1 -0
  23. data/lib/quby/questionnaires/repos/bundle_disk_repo.rb +51 -0
  24. data/lib/quby/table_backend/range_tree.rb +0 -51
  25. data/lib/quby/version.rb +1 -1
  26. data/spec/features/tables_spec.rb +40 -0
  27. data/spec/internal/log/test-events.log +531 -0
  28. data/spec/internal/log/test.log +21098 -0
  29. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-22-54.041.html +207 -0
  30. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-22-54.041.png +0 -0
  31. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-23.175.html +323 -0
  32. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-23.175.png +0 -0
  33. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-32.352.html +323 -0
  34. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-32.352.png +0 -0
  35. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-35.247.html +323 -0
  36. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-35.247.png +0 -0
  37. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-38.152.html +323 -0
  38. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-38.152.png +0 -0
  39. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-41.012.html +323 -0
  40. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-41.012.png +0 -0
  41. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-43.918.html +323 -0
  42. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-43.918.png +0 -0
  43. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-46.782.html +323 -0
  44. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-46.782.png +0 -0
  45. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-49.678.html +323 -0
  46. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-49.678.png +0 -0
  47. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-52.495.html +323 -0
  48. data/spec/internal/tmp/capybara/screenshot_2020-10-22-10-23-52.495.png +0 -0
  49. data/spec/internal/tmp/capybara/screenshot_2020-10-27-14-25-21.063.html +207 -0
  50. data/spec/internal/tmp/capybara/screenshot_2020-10-27-14-25-21.063.png +0 -0
  51. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-21-57.510.html +1 -0
  52. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-21-57.510.png +0 -0
  53. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-23-56.006.html +1 -0
  54. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-23-56.006.png +0 -0
  55. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-24-43.842.html +12 -0
  56. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-24-43.842.png +0 -0
  57. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-25-04.631.html +12 -0
  58. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-25-04.631.png +0 -0
  59. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-25-11.690.html +12 -0
  60. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-25-11.690.png +0 -0
  61. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-26-25.111.html +12 -0
  62. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-26-25.111.png +0 -0
  63. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-26-57.026.html +12 -0
  64. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-26-57.026.png +0 -0
  65. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-27-13.545.html +12 -0
  66. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-27-13.545.png +0 -0
  67. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-27-45.475.html +12 -0
  68. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-27-45.475.png +0 -0
  69. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-32-13.907.html +1 -0
  70. data/spec/internal/tmp/capybara/screenshot_2020-10-27-18-32-13.907.png +0 -0
  71. data/spec/quby/answers/services/answer_validations_spec.rb +8 -8
  72. data/spec/quby/answers/services/score_calculator_spec.rb +4 -14
  73. data/spec/quby/questionnaires/deserializer/questionnaire_spec.rb +237 -0
  74. data/spec/quby/questionnaires/dsl_spec.rb +0 -9
  75. data/spec/quby/questionnaires/entities/fields_spec.rb +3 -3
  76. data/spec/quby/questionnaires/entities/question_spec.rb +0 -8
  77. data/spec/quby/questionnaires/entities/questionnaire_spec.rb +2 -26
  78. data/spec/quby/table_backend/range_tree_spec.rb +46 -13
  79. data/spec/spec_helper.rb +1 -1
  80. metadata +108 -55
  81. data/lib/quby/lookup_table.rb +0 -29
  82. data/lib/quby/lookup_table_repo.rb +0 -24
  83. data/lib/quby/questionnaires/dsl/base.rb +0 -20
  84. data/lib/quby/questionnaires/dsl/calls_custom_methods.rb +0 -29
  85. data/lib/quby/questionnaires/dsl/charting/bar_chart_builder.rb +0 -18
  86. data/lib/quby/questionnaires/dsl/charting/chart_builder.rb +0 -91
  87. data/lib/quby/questionnaires/dsl/charting/line_chart_builder.rb +0 -47
  88. data/lib/quby/questionnaires/dsl/charting/overview_chart_builder.rb +0 -31
  89. data/lib/quby/questionnaires/dsl/charting/radar_chart_builder.rb +0 -18
  90. data/lib/quby/questionnaires/dsl/helpers.rb +0 -51
  91. data/lib/quby/questionnaires/dsl/panel_builder.rb +0 -80
  92. data/lib/quby/questionnaires/dsl/question_builder.rb +0 -40
  93. data/lib/quby/questionnaires/dsl/questionnaire_builder.rb +0 -252
  94. data/lib/quby/questionnaires/dsl/questions/base.rb +0 -179
  95. data/lib/quby/questionnaires/dsl/questions/checkbox_question_builder.rb +0 -20
  96. data/lib/quby/questionnaires/dsl/questions/date_question_builder.rb +0 -18
  97. data/lib/quby/questionnaires/dsl/questions/deprecated_question_builder.rb +0 -18
  98. data/lib/quby/questionnaires/dsl/questions/float_question_builder.rb +0 -21
  99. data/lib/quby/questionnaires/dsl/questions/integer_question_builder.rb +0 -21
  100. data/lib/quby/questionnaires/dsl/questions/radio_question_builder.rb +0 -20
  101. data/lib/quby/questionnaires/dsl/questions/select_question_builder.rb +0 -18
  102. data/lib/quby/questionnaires/dsl/questions/string_question_builder.rb +0 -20
  103. data/lib/quby/questionnaires/dsl/questions/text_question_builder.rb +0 -22
  104. data/lib/quby/questionnaires/dsl/score_builder.rb +0 -22
  105. data/lib/quby/questionnaires/dsl/standardized_panel_generators.rb +0 -33
  106. data/lib/quby/questionnaires/dsl/table_builder.rb +0 -48
  107. data/lib/quby/questionnaires/services/definition_validator.rb +0 -298
  108. data/spec/benchmarks/load_normscore_csv.rb +0 -18
  109. data/spec/quby/lookup_table_repo_spec.rb +0 -20
  110. data/spec/quby/lookup_table_spec.rb +0 -38
  111. data/spec/quby/questionnaires/dsl/calls_custom_methods_spec.rb +0 -38
  112. data/spec/quby/questionnaires/dsl/charting/bar_chart_builder_spec.rb +0 -41
  113. data/spec/quby/questionnaires/dsl/charting/chart_builder_spec.rb +0 -127
  114. data/spec/quby/questionnaires/dsl/charting/line_chart_builder_spec.rb +0 -58
  115. data/spec/quby/questionnaires/dsl/charting/radar_chart_builder_spec.rb +0 -41
  116. data/spec/quby/questionnaires/dsl/helpers_spec.rb +0 -80
  117. data/spec/quby/questionnaires/dsl/questionnaire_builder_spec.rb +0 -480
  118. data/spec/quby/questionnaires/services/definition_validator_spec.rb +0 -793
  119. data/spec/support/examples_for_chart_builders.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca4ec590512a375ce3cef25aa1630ad6ab4a4bd05174e66bf919c06e8babdf79
4
- data.tar.gz: 421fd333ee6608ff4dd230dbd66f55bea9ca411e95c3d8d182487b2e493b9cf8
3
+ metadata.gz: ddc10a44111cef2b40f5bdacb0e71087d785834eb1870318eb7c98f29cbbfb3e
4
+ data.tar.gz: 974ad9eca496310b69b7b6c1b99ddd4c37207b83f4bf4e34ab5b21b474792f61
5
5
  SHA512:
6
- metadata.gz: bd8be25a600524b60166b943bee7e186742d53cb2a59dea6002be2586d44647c65d9c4d409935f7c8c9c1c58e9280050d410d2b273bb1d4cf7d38606e3cc2d3b
7
- data.tar.gz: 4d217cd346e901b5d27b95d048507cc4851f99126fafaaa964fe4ec42f0506a7ee9c4a262977a4a30bc8133ba48c124d98d7371282dcc5c94572d463cedb38f0
6
+ metadata.gz: 7e5cde14431489cb85d05cba333d6f4f3daca33d5859ebf7342a127f9a7807822d0376fb24f396799b0049c90d6a965da2827fb9b576af46854836032fbf1c3b
7
+ data.tar.gz: ed1786bc33ffb3c17ccc49cde2ae7afc6610ee52a57f709e669ebabec761af91c3633ffe75dd46951bb3b0fc447efe4211f5447033208afb32181dbaab33ef28
@@ -27,7 +27,6 @@ and where it can store its answers.
27
27
  ```ruby
28
28
  Quby.questionnaire_repo = Quby::Questionnaires::Repos::DiskRepo.new(Rails.root.join("db/questionnaires/definitions"))
29
29
  Quby.answer_repo = Quby::Answers::Repos::MongoidRepo.new
30
- Quby.lookup_table_repo = Quby::LookupTableRepo::Disk.new(Rails.root.join("db/questionnaires/lookup_tables"))
31
30
  Quby::Settings.shared_secret = ENV["QUBY_SHARED_SECRET"]
32
31
  ```
33
32
 
@@ -38,14 +38,6 @@ module Quby
38
38
  @questionnaires_api = nil
39
39
  end
40
40
 
41
- def lookup_table_repo=(repo)
42
- @lookup_table_repo = repo
43
- end
44
-
45
- def lookup_table_repo
46
- @lookup_table_repo || fail("Quby does not have its lookup table repo (Quby.lookup_table_repo) configured.")
47
- end
48
-
49
41
  def fixtures_path
50
42
  File.expand_path File.join('..', '..', 'spec', 'fixtures'), __FILE__
51
43
  end
@@ -79,7 +71,7 @@ module Quby
79
71
  end
80
72
 
81
73
  require 'quby/settings'
74
+ require 'quby/table_backend/range_tree'
82
75
  require 'quby/questionnaires'
83
76
  require 'quby/answers'
84
- require 'quby/engine'
85
- require 'quby/lookup_table_repo'
77
+ require 'quby/engine'
@@ -82,7 +82,7 @@ module Quby
82
82
  attr_accessor :extra_question_values
83
83
  attr_accessor :extra_failed_validations
84
84
 
85
- def initialize(_id: nil, questionnaire_id: nil, questionnaire_key: nil,
85
+ def initialize(_id: nil, questionnaire_id: nil, questionnaire_key: nil, questionnaire: nil,
86
86
  raw_params: nil, value: nil, patient_id: nil, patient: nil,
87
87
  token: nil, active: true, test: false, created_at: nil, updated_at: nil,
88
88
  started_at: nil, completed_at: nil, outcome: nil, outcome_generated_at: nil,
@@ -111,6 +111,8 @@ module Quby
111
111
  self.import_notes = import_notes || {}
112
112
  self.flags = flags
113
113
  self.textvars = textvars
114
+
115
+ @questionnaire = questionnaire
114
116
  end
115
117
 
116
118
  def id
@@ -162,7 +164,7 @@ module Quby
162
164
 
163
165
  # Faux belongs_to :questionnaire
164
166
  def questionnaire
165
- Quby.questionnaires.find(questionnaire_key)
167
+ @questionnaire ||= Quby.questionnaires.find(questionnaire_key)
166
168
  end
167
169
 
168
170
  def mark_completed(start_time)
@@ -74,8 +74,7 @@ module Quby
74
74
  end
75
75
 
76
76
  def value_by_regular_values
77
- regular_values = answer.value_by_regular_values
78
- @value_by_regular_values ||= regular_values.sort_by do |key, value|
77
+ @value_by_regular_values ||= answer.value_by_regular_values.sort_by do |key, value|
79
78
  questionnaire.fields.question_hash.keys.index(key) || Float::INFINITY
80
79
  end.to_h
81
80
  end
@@ -231,9 +231,7 @@ module Quby
231
231
  end
232
232
 
233
233
  def table_lookup(table_key, parameters)
234
- @questionnaire.lookup_tables[table_key] ||= Quby::LookupTable.new table_key
235
- @questionnaire.lookup_tables[table_key] \
236
- .lookup(parameters)
234
+ @questionnaire.lookup_tables.fetch(table_key).lookup(parameters)
237
235
  end
238
236
 
239
237
  # Public: Ensure given question_keys have answers. Strings with nothing but whitespace are
@@ -15,7 +15,6 @@ require 'jquery-ui-rails'
15
15
  require 'compass-blueprint'
16
16
 
17
17
  require 'susy'
18
- require 'quby/lookup_table'
19
18
  require 'quby/range_categories'
20
19
  require 'quby/pdf_renderer'
21
20
 
@@ -3,3 +3,4 @@
3
3
  require 'quby/questionnaires/dsl'
4
4
  require 'quby/questionnaires/repos'
5
5
  require 'quby/questionnaires/api'
6
+ require 'quby/questionnaires/deserializer'
@@ -39,7 +39,11 @@ module Quby
39
39
 
40
40
  def build_from_definition(definition)
41
41
  ActiveSupport::Notifications.instrument('quby.questionaire.build') do
42
- DSL.build_from_definition(definition)
42
+ if definition.json
43
+ DSL.from_json(definition.json)
44
+ else
45
+ DSL.build_from_definition(definition)
46
+ end
43
47
  end
44
48
  end
45
49
 
@@ -0,0 +1,439 @@
1
+ module Quby
2
+ module Questionnaires
3
+ module Deserializer
4
+ # This symbolizes various things. Do not run on arbitrary JSON.
5
+ def self.from_json(json)
6
+ # TODO: last_update
7
+ Entities::Questionnaire.new(json.fetch("key"), json).tap do |questionnaire|
8
+ questionnaire.title = json.fetch("title")
9
+ questionnaire.description = json.fetch("description")
10
+ questionnaire.outcome_description = json.fetch("outcome_description")
11
+ questionnaire.short_description = json.fetch("short_description")
12
+ questionnaire.abortable = json.fetch("abortable")
13
+ questionnaire.enable_previous_questionnaire_button = json.fetch("enable_previous_questionnaire_button")
14
+ questionnaire.default_answer_value = json.fetch("default_answer_value")
15
+ questionnaire.leave_page_alert = json.fetch("leave_page_alert")
16
+ questionnaire.allow_hotkeys = json.fetch("allow_hotkeys")
17
+ questionnaire.license = json.fetch("license").try(:to_sym)
18
+ questionnaire.licensor = json.fetch("licensor")
19
+ questionnaire.language = json.fetch("language").try(:to_sym)
20
+ questionnaire.renderer_version = json.fetch("renderer_version")
21
+ questionnaire.last_update = Time.zone.parse(json.fetch("last_update"))
22
+ questionnaire.last_author = json.fetch("last_author")
23
+ questionnaire.extra_css = json.fetch("extra_css")
24
+ questionnaire.allow_switch_to_bulk = json.fetch("allow_switch_to_bulk")
25
+
26
+ questionnaire.flags = json.fetch("flags").with_indifferent_access.transform_values do |attrs|
27
+ build_flag(attrs)
28
+ end
29
+
30
+ questionnaire.textvars = json.fetch("textvars").with_indifferent_access.transform_values do |attrs|
31
+ build_textvar(attrs)
32
+ end
33
+
34
+ questionnaire.lookup_tables = YAML.load(json.fetch("lookup_tables")).transform_values do |attrs|
35
+ Quby::TableBackend::RangeTree.new(levels: attrs[:levels], tree: attrs[:tree])
36
+ end
37
+
38
+ questionnaire.score_calculations = json.fetch("score_calculations").with_indifferent_access.transform_values do |attrs|
39
+ build_score_calculation(attrs)
40
+ end
41
+
42
+ questionnaire.score_schemas = json.fetch("score_schemas").with_indifferent_access.transform_values do |schema|
43
+ build_score_schema(schema)
44
+ end
45
+
46
+ json.fetch("panels").each do |panel_json|
47
+ load_panel(questionnaire, panel_json)
48
+ end
49
+
50
+ # roqua domain
51
+ questionnaire.roqua_keys = json.fetch("roqua_keys")
52
+ questionnaire.sbg_key = json.fetch("sbg_key")
53
+ questionnaire.sbg_domains = json.fetch("sbg_domains").map(&:symbolize_keys)
54
+ questionnaire.outcome_regeneration_requested_at = json.fetch("outcome_regeneration_requested_at").try { |str| Time.zone.parse(str) }
55
+ questionnaire.deactivate_answers_requested_at = json.fetch("deactivate_answers_requested_at").try { |str| Time.zone.parse(str) }
56
+ questionnaire.respondent_types = json.fetch("respondent_types").map(&:to_sym)
57
+ questionnaire.tags = json.fetch("tags")
58
+
59
+ if overview_json = json.fetch("charts").fetch("overview")
60
+ questionnaire.charts.overview = Quby::Questionnaires::Entities::Charting::OverviewChart.new(
61
+ subscore: overview_json.fetch("subscore").to_sym,
62
+ y_max: overview_json.fetch("y_max"),
63
+ )
64
+ end
65
+
66
+ json.fetch("charts").fetch("others").each do |chart_json|
67
+ questionnaire.add_chart(build_chart(questionnaire, chart_json))
68
+ end
69
+
70
+ questionnaire.outcome_tables = json.fetch("outcome_tables").map do |attributes|
71
+ build_outcome_table(questionnaire, attributes)
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.load_panel(questionnaire, panel_json)
77
+ panel = Entities::Panel.new(
78
+ questionnaire: questionnaire,
79
+ key: panel_json.fetch("key"),
80
+ title: panel_json.fetch("title"),
81
+ items: []
82
+ )
83
+
84
+ panel_json.fetch("items").each do |item_json|
85
+ load_item(questionnaire, item_json, panel: panel)
86
+ end
87
+
88
+ questionnaire.add_panel(panel)
89
+ end
90
+
91
+ def self.load_item(questionnaire, item_json, panel: nil)
92
+ case item_json.fetch("type")
93
+ when "text"
94
+ panel.items << build_text(item_json)
95
+ when "question"
96
+ question = build_question(questionnaire, item_json)
97
+ questionnaire.register_question(question)
98
+ panel.items << question
99
+ when "table"
100
+ table = Entities::Table.new(
101
+ title: item_json.fetch("title"),
102
+ description: item_json.fetch("description"),
103
+ columns: item_json.fetch("columns"),
104
+ show_option_desc: item_json.fetch("show_option_desc"),
105
+ )
106
+ panel.items << table
107
+
108
+ item_json.fetch("items").each do |table_item_json|
109
+ case table_item_json.fetch("type")
110
+ when "text"
111
+ table.items << build_text(table_item_json)
112
+ when "question"
113
+ question = build_question(questionnaire, table_item_json, table: table)
114
+ questionnaire.register_question(question)
115
+ table.items << question
116
+ panel.items << question
117
+ else
118
+ raise "Unknown table item: #{table_item_json}"
119
+ end
120
+ end
121
+ else
122
+ raise "Unknown item: #{item_json}"
123
+ end
124
+ end
125
+
126
+ def self.build_text(item_json)
127
+ Entities::Text.new(item_json.fetch("str"), {
128
+ html_content: item_json.fetch("html_content"),
129
+ display_in: item_json.fetch("display_in").map(&:to_sym),
130
+ col_span: item_json.fetch("col_span"),
131
+ row_span: item_json.fetch("row_span"),
132
+ raw_content: item_json.fetch("raw_content"),
133
+ switch_cycle: item_json.fetch("switch_cycle")
134
+ })
135
+ end
136
+
137
+ def self.build_question(questionnaire, item_json, parent: nil, table: nil)
138
+ key = item_json.fetch("key").to_sym
139
+ attributes = {
140
+ questionnaire: questionnaire,
141
+ parent: parent,
142
+ type: item_json.fetch("question_type").to_sym,
143
+ title: item_json.fetch("title"),
144
+ context_free_title: item_json.fetch("context_free_title"),
145
+ description: item_json.fetch("description"),
146
+ presentation: item_json.fetch("presentation").to_sym,
147
+ hidden: item_json.fetch("hidden"),
148
+ depends_on: item_json.fetch("depends_on")&.map(&:to_sym),
149
+ default_position: item_json.fetch("default_position"),
150
+ validations: item_json.fetch("validations").map {|attrs| build_question_validation(attrs)},
151
+ table: table,
152
+ col_span: item_json.fetch("col_span"),
153
+ row_span: item_json.fetch("row_span"),
154
+
155
+ # only selectable via options passed in DSL, not via DSL methods
156
+ # many apply only to certain types of questions
157
+ sbg_key: item_json.fetch("sbg_key"),
158
+ allow_duplicate_option_values: item_json.fetch("allow_duplicate_option_values"),
159
+ allow_blank_titles: item_json.fetch("allow_blank_titles"),
160
+ as: item_json.fetch("as")&.to_sym,
161
+ display_modes: item_json.fetch("display_modes")&.map(&:to_sym),
162
+ autocomplete: item_json.fetch("autocomplete"),
163
+ show_values: item_json.fetch("show_values").to_sym,
164
+ deselectable: item_json.fetch("deselectable"),
165
+ disallow_bulk: item_json.fetch("disallow_bulk"),
166
+ score_header: item_json.fetch("score_header").to_sym,
167
+ sets_textvar: item_json.fetch("sets_textvar"),
168
+ default_invisible: item_json.fetch("default_invisible"),
169
+ question_group: item_json.fetch("question_group"), # sometimes string, sometimes a symbol in the DSL. Just have to hope this works
170
+ group_minimum_answered: item_json.fetch("group_minimum_answered"),
171
+ group_maximum_answered: item_json.fetch("group_maximum_answered"),
172
+ value_tooltip: item_json.fetch("value_tooltip"),
173
+
174
+ # might be able to deduce from tree structure
175
+ parent_option_key: item_json.fetch("parent_option_key")&.to_sym
176
+ }
177
+
178
+ case item_json.fetch("question_type")
179
+ when "check_box"
180
+ Entities::Questions::CheckboxQuestion.new(key, attributes.merge(
181
+ check_all_option: item_json.fetch("check_all_option")&.to_sym,
182
+ uncheck_all_option: item_json.fetch("uncheck_all_option")&.to_sym,
183
+ maximum_checked_allowed: item_json.fetch("maximum_checked_allowed"),
184
+ minimum_checked_required: item_json.fetch("minimum_checked_required"),
185
+ )).tap do |question|
186
+ item_json.fetch("options").each do |option_json|
187
+ question.options << build_option(questionnaire, question, option_json)
188
+ end
189
+ end
190
+ when "date"
191
+ Entities::Questions::DateQuestion.new(key, attributes.merge(
192
+ components: item_json.fetch("components").map(&:to_sym),
193
+ required_components: item_json.fetch("required_components").map(&:to_sym),
194
+ year_key: item_json.fetch("year_key")&.to_sym,
195
+ month_key: item_json.fetch("month_key")&.to_sym,
196
+ day_key: item_json.fetch("day_key")&.to_sym,
197
+ hour_key: item_json.fetch("hour_key")&.to_sym,
198
+ minute_key: item_json.fetch("minute_key")&.to_sym,
199
+ ))
200
+ when "deprecated", "hidden"
201
+ Entities::Questions::DeprecatedQuestion.new(key, attributes).tap do |question|
202
+ item_json.fetch("options").each do |option_json|
203
+ question.options << build_option(questionnaire, question, option_json)
204
+ end
205
+ end
206
+ when "float"
207
+ Entities::Questions::FloatQuestion.new(key, attributes.merge(
208
+ labels: item_json.fetch("labels"),
209
+ unit: item_json.fetch("unit"),
210
+ size: item_json.fetch("size"),
211
+ ))
212
+ when "integer"
213
+ Entities::Questions::IntegerQuestion.new(key, attributes.merge(
214
+ labels: item_json.fetch("labels"),
215
+ unit: item_json.fetch("unit"),
216
+ size: item_json.fetch("size"),
217
+ ))
218
+ when "radio", "scale"
219
+ Entities::Questions::RadioQuestion.new(key, attributes).tap do |question|
220
+ item_json.fetch("options").each do |option_json|
221
+ question.options << build_option(questionnaire, question, option_json)
222
+ end
223
+ end
224
+ when "select"
225
+ Entities::Questions::SelectQuestion.new(key, attributes).tap do |question|
226
+ item_json.fetch("options").each do |option_json|
227
+ question.options << build_option(questionnaire, question, option_json)
228
+ end
229
+ end
230
+ when "string"
231
+ Entities::Questions::StringQuestion.new(key, attributes.merge(
232
+ unit: item_json.fetch("unit"),
233
+ size: item_json.fetch("size"),
234
+ ))
235
+ when "textarea"
236
+ Entities::Questions::TextQuestion.new(key, attributes.merge(
237
+ lines: item_json.fetch("lines"),
238
+ ))
239
+ else
240
+ raise "Unknown question type: #{item_json}"
241
+ end
242
+ end
243
+
244
+ def self.build_option(questionnaire, question, option_json)
245
+ option = Entities::QuestionOption.new(option_json.fetch("key")&.to_sym, question,
246
+ value: option_json.fetch("value"),
247
+ description: option_json.fetch("description"),
248
+ context_free_description: option_json.fetch("context_free_description"),
249
+ inner_title: option_json.fetch("inner_title"),
250
+ hides_questions: option_json.fetch("hides_questions").map(&:to_sym),
251
+ shows_questions: option_json.fetch("shows_questions").map(&:to_sym),
252
+ hidden: option_json.fetch("hidden"),
253
+ placeholder: option_json.fetch("placeholder"),
254
+ )
255
+
256
+ option_json.fetch("questions").each do |question_json|
257
+ subquestion = build_question(questionnaire, question_json, parent: question)
258
+ questionnaire.register_question(subquestion)
259
+ option.questions << subquestion
260
+ end
261
+
262
+ option
263
+ end
264
+
265
+ def self.build_question_validation(attrs)
266
+ base_validation = {
267
+ type: attrs.fetch("type").to_sym,
268
+ explanation: attrs["explanation"] # not always specified for min/max validation
269
+ }
270
+
271
+ case attrs.fetch("type")
272
+ when "requires_answer"
273
+ base_validation
274
+ when "answer_group_minimum", "answer_group_maximum"
275
+ base_validation.merge(
276
+ group: attrs.fetch("group"), # TODO: sometimes a symbol, sometimes a string in the original, but I hope it doesn't matter
277
+ value: attrs.fetch("value")
278
+ )
279
+ when "valid_integer", "valid_float"
280
+ base_validation
281
+ when "valid_date"
282
+ base_validation.merge(
283
+ subtype: attrs.fetch("subtype").to_sym
284
+ )
285
+ when "minimum", "maximum"
286
+ value = case attrs.fetch("value_type")
287
+ when "Date"
288
+ Date.parse(attrs.fetch("value"))
289
+ when "DateTime"
290
+ DateTime.parse(attrs.fetch("value"))
291
+ when "Time", "ActiveSuport::TimeWithZone"
292
+ Time.zone.parse(attrs.fetch("value"))
293
+ else
294
+ attrs.fetch("value")
295
+ end
296
+
297
+ base_validation.merge(
298
+ value: value,
299
+ subtype: attrs.fetch("subtype").to_sym,
300
+ )
301
+ when "too_many_checked"
302
+ base_validation.merge(
303
+ uncheck_all_key: attrs.fetch("uncheck_all_key").to_sym
304
+ )
305
+ when "minimum_checked_required"
306
+ base_validation.merge(
307
+ minimum_checked_value: attrs.fetch("minimum_checked_value")
308
+ )
309
+ when "maximum_checked_allowed"
310
+ base_validation.merge(
311
+ maximum_checked_value: attrs.fetch("maximum_checked_value")
312
+ )
313
+ when "regexp"
314
+ base_validation.merge(
315
+ matcher: Regexp.new(attrs.fetch("matcher"))
316
+ )
317
+ when "not_all_checked"
318
+ base_validation.merge(
319
+ check_all_key: attrs.fetch("check_all_key").to_sym
320
+ )
321
+ else
322
+ raise "Unknown validation type: #{attrs.inspect}"
323
+ end
324
+ end
325
+
326
+ def self.build_score_calculation(attrs)
327
+ Entities::ScoreCalculation.new(attrs.fetch("key").to_sym,
328
+ label: attrs.fetch("label"),
329
+ sbg_key: attrs.fetch("sbg_key"),
330
+ options: attrs.fetch("options").symbolize_keys,
331
+ sourcecode: attrs.fetch("sourcecode"),
332
+ )
333
+ end
334
+
335
+ def self.build_flag(attrs)
336
+ Entities::Flag.new(
337
+ key: attrs.fetch("key").to_sym,
338
+ description_true: attrs.fetch("description_true"),
339
+ description_false: attrs.fetch("description_false"),
340
+ description: attrs.fetch("description"),
341
+ internal: attrs.fetch("internal"),
342
+ trigger_on: attrs.fetch("trigger_on"),
343
+ shows_questions: attrs.fetch("shows_questions").map(&:to_sym),
344
+ hides_questions: attrs.fetch("hides_questions").map(&:to_sym),
345
+ depends_on: attrs.fetch("depends_on"), # TODO: emperically determined to be a string in DSL, is that right?
346
+ default_in_interface: attrs.fetch("default_in_interface"),
347
+ )
348
+ end
349
+
350
+ def self.build_textvar(attrs)
351
+ Entities::Textvar.new(
352
+ key: attrs.fetch("key").to_sym,
353
+ description: attrs.fetch("description"),
354
+ default: attrs.fetch("default"),
355
+ depends_on_flag: attrs.fetch("depends_on_flag")&.to_sym
356
+ )
357
+ end
358
+
359
+ def self.build_chart(questionnaire, chart_json)
360
+ base_args = {
361
+ title: chart_json.fetch("title"),
362
+ plottables: chart_json.fetch("plottables").map do |plottable_json|
363
+ Quby::Questionnaires::Entities::Charting::Plottable.new(
364
+ plottable_json.fetch("key").to_sym,
365
+ label: plottable_json.fetch("label"),
366
+ plotted_key: plottable_json.fetch("plotted_key").to_sym,
367
+ global: plottable_json.fetch("global"),
368
+ questionnaire_key: plottable_json.fetch("questionnaire_key")
369
+ )
370
+ end,
371
+ y_categories: chart_json.fetch("y_categories"),
372
+ y_range_categories: chart_json.fetch("y_range_categories"),
373
+ chart_type: chart_json.fetch("chart_type"),
374
+ y_range: deserialize_range(chart_json.fetch("y_range")),
375
+ tick_interval: chart_json.fetch("tick_interval"),
376
+ plotbands: chart_json.fetch("plotbands").map do |plotband_json|
377
+ {
378
+ color: plotband_json.fetch("color").to_sym,
379
+ from: plotband_json.fetch("from"),
380
+ to: plotband_json.fetch("to")
381
+ }
382
+ end,
383
+ }
384
+
385
+ case chart_json.fetch("type")
386
+ when "bar_chart"
387
+ Quby::Questionnaires::Entities::Charting::BarChart.new(chart_json.fetch("key").to_sym,
388
+ plotlines: chart_json.fetch("plotlines"),
389
+ **base_args
390
+ )
391
+ when "line_chart"
392
+ Quby::Questionnaires::Entities::Charting::LineChart.new(chart_json.fetch("key").to_sym,
393
+ y_label: chart_json.fetch("y_label"),
394
+ tonality: chart_json.fetch("tonality").to_sym,
395
+ baseline: YAML.load(chart_json.fetch("baseline")),
396
+ clinically_relevant_change: chart_json.fetch("clinically_relevant_change"),
397
+ **base_args
398
+ )
399
+ when "radar_chart"
400
+ Quby::Questionnaires::Entities::Charting::BarChart.new(chart_json.fetch("key").to_sym,
401
+ plotlines: chart_json.fetch("plotlines"),
402
+ **base_args
403
+ )
404
+ end
405
+ end
406
+
407
+ def self.build_score_schema(attributes)
408
+ Entities::ScoreSchema.new(
409
+ key: attributes.fetch("key").to_sym,
410
+ label: attributes.fetch("label"),
411
+ subscore_schemas: attributes.fetch("subscore_schemas").map do |subschema|
412
+ {
413
+ key: subschema.fetch("key").to_sym,
414
+ label: subschema.fetch("label"),
415
+ export_key: subschema.fetch("export_key").to_sym,
416
+ only_for_export: subschema.fetch("only_for_export")
417
+ }
418
+ end
419
+ )
420
+ end
421
+
422
+ def self.build_outcome_table(questionnaire, attributes)
423
+ Entities::OutcomeTable.new(
424
+ questionnaire: questionnaire,
425
+ key: attributes.fetch("key").to_sym,
426
+ score_keys: attributes.fetch("score_keys").map(&:to_sym),
427
+ subscore_keys: attributes.fetch("subscore_keys").map(&:to_sym),
428
+ name: attributes.fetch("name"),
429
+ default_collapsed: attributes.fetch("default_collapsed"),
430
+ )
431
+ end
432
+
433
+ def self.deserialize_range(range_attributes)
434
+ return unless range_attributes
435
+ Range.new(range_attributes.fetch("begin"), range_attributes.fetch("end"), range_attributes.fetch("exclude_end"))
436
+ end
437
+ end
438
+ end
439
+ end