quby 5.0.0 → 5.0.5

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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.markdown +11 -2
  3. data/app/assets/javascripts/quby/answers/validation.js +3 -1
  4. data/lib/quby/answers/services/answer_validator.rb +44 -0
  5. data/lib/quby/questionnaires/deserializer.rb +10 -5
  6. data/lib/quby/questionnaires/entities/charting/bar_chart.rb +1 -5
  7. data/lib/quby/questionnaires/entities/charting/chart.rb +5 -1
  8. data/lib/quby/questionnaires/entities/charting/radar_chart.rb +1 -5
  9. data/lib/quby/questionnaires/specs/api_specs.rb +7 -5
  10. data/lib/quby/version.rb +1 -1
  11. data/spec/features/charts_spec.rb +9 -2
  12. data/spec/features/editing_completed_answer_spec.rb +2 -2
  13. data/spec/features/question_dependencies/group_maximum_answered_spec.rb +65 -2
  14. data/spec/features/question_dependencies/group_minimum_answered_spec.rb +42 -2
  15. data/spec/features/question_dependencies/group_minmax_answered_spec.rb +44 -7
  16. data/spec/fixtures/questionnaire_with_chart.rb +1 -0
  17. data/spec/fixtures/scores_from_schema.rb +25 -0
  18. data/spec/quby/answers/entities/score_spec.rb +51 -44
  19. data/spec/quby/answers/services/answer_validator_spec.rb +50 -9
  20. data/spec/quby/questionnaires/entities/charting/bar_chart_spec.rb +2 -8
  21. data/spec/quby/questionnaires/entities/charting/radar_chart_spec.rb +0 -5
  22. data/spec/quby/questionnaires/entities/fields_spec.rb +2 -2
  23. data/spec/quby/questionnaires/entities/questionnaire_spec.rb +4 -2
  24. data/spec/quby/questionnaires/repos/disk_repo_spec.rb +3 -1
  25. data/spec/quby/questionnaires/repos/memory_repo_spec.rb +2 -1
  26. data/spec/spec_helper.rb +13 -2
  27. data/spec/support/examples_for_chart.rb +10 -0
  28. data/spec/support/validation_helpers.rb +9 -0
  29. data/spec/teaspoon_env.rb +32 -2
  30. metadata +9 -7
@@ -15,6 +15,7 @@ shared_examples 'group_minimum_answered_tests' do
15
15
  fill_in_question('v_date_year', '2013')
16
16
  fill_in_question('v_date_month', '12')
17
17
  fill_in_question('v_date_day', '10')
18
+ set_slider_value('v_slider', '50')
18
19
  run_validations
19
20
  expect_no_errors
20
21
  expect_saved_value 'v_radio', 'a1'
@@ -27,6 +28,7 @@ shared_examples 'group_minimum_answered_tests' do
27
28
  expect_saved_value 'v_date_year', "2013"
28
29
  expect_saved_value 'v_date_month', "12"
29
30
  expect_saved_value 'v_date_day', "10"
31
+ # expect_saved_value 'v_slider', '50' # occasional client side rounding issue :S
30
32
  end
31
33
 
32
34
  it 'is not valid when radio question is not filled' do
@@ -41,6 +43,7 @@ shared_examples 'group_minimum_answered_tests' do
41
43
  fill_in_question('v_date_year', '2013')
42
44
  fill_in_question('v_date_month', '12')
43
45
  fill_in_question('v_date_day', '10')
46
+ set_slider_value('v_slider', '50')
44
47
  run_validations
45
48
  expect_errors_on_group
46
49
  end
@@ -57,6 +60,7 @@ shared_examples 'group_minimum_answered_tests' do
57
60
  fill_in_question('v_date_year', '2013')
58
61
  fill_in_question('v_date_month', '12')
59
62
  fill_in_question('v_date_day', '10')
63
+ set_slider_value('v_slider', '50')
60
64
  run_validations
61
65
  expect_errors_on_group
62
66
  end
@@ -73,6 +77,7 @@ shared_examples 'group_minimum_answered_tests' do
73
77
  fill_in_question('v_date_year', '2013')
74
78
  fill_in_question('v_date_month', '12')
75
79
  fill_in_question('v_date_day', '10')
80
+ set_slider_value('v_slider', '50')
76
81
  run_validations
77
82
  expect_errors_on_group
78
83
  end
@@ -89,6 +94,7 @@ shared_examples 'group_minimum_answered_tests' do
89
94
  fill_in_question('v_date_year', '2013')
90
95
  fill_in_question('v_date_month', '12')
91
96
  fill_in_question('v_date_day', '10')
97
+ set_slider_value('v_slider', '50')
92
98
  run_validations
93
99
  expect_errors_on_group
94
100
  end
@@ -105,6 +111,7 @@ shared_examples 'group_minimum_answered_tests' do
105
111
  fill_in_question('v_date_year', '2013')
106
112
  fill_in_question('v_date_month', '12')
107
113
  fill_in_question('v_date_day', '10')
114
+ set_slider_value('v_slider', '50')
108
115
  run_validations
109
116
  expect_errors_on_group
110
117
  end
@@ -121,6 +128,7 @@ shared_examples 'group_minimum_answered_tests' do
121
128
  fill_in_question('v_date_year', '2013')
122
129
  fill_in_question('v_date_month', '12')
123
130
  fill_in_question('v_date_day', '10')
131
+ set_slider_value('v_slider', '50')
124
132
  run_validations
125
133
  expect_errors_on_group
126
134
  end
@@ -137,6 +145,7 @@ shared_examples 'group_minimum_answered_tests' do
137
145
  fill_in_question('v_date_year', '2013')
138
146
  fill_in_question('v_date_month', '12')
139
147
  fill_in_question('v_date_day', '10')
148
+ set_slider_value('v_slider', '50')
140
149
  run_validations
141
150
  expect_errors_on_group
142
151
  end
@@ -153,6 +162,24 @@ shared_examples 'group_minimum_answered_tests' do
153
162
  # fill_in_question('v_date_year', '2013')
154
163
  # fill_in_question('v_date_month', '12')
155
164
  # fill_in_question('v_date_day', '10')
165
+ set_slider_value('v_slider', '50')
166
+ run_validations
167
+ expect_errors_on_group
168
+ end
169
+
170
+ it 'is not valid when slider question is not filled' do
171
+ select_radio_option 'v_radio', 'a1'
172
+ select_radio_option 'v_scale', 'a1'
173
+ select_select_option 'v_select', 'a1'
174
+ check_option 'v_ck_a1'
175
+ uncheck_option 'v_ck_a2'
176
+ fill_in_question 'v_string', 'kittens!'
177
+ fill_in_question 'v_integer', '37'
178
+ fill_in_question 'v_float', '4.2'
179
+ fill_in_question('v_date_year', '2013')
180
+ fill_in_question('v_date_month', '12')
181
+ fill_in_question('v_date_day', '10')
182
+ # set_slider_value('v_slider', '50')
156
183
  run_validations
157
184
  expect_errors_on_group
158
185
  end
@@ -166,7 +193,7 @@ shared_examples 'group_minimum_answered' do
166
193
  context 'minimum number of given answers in a group' do
167
194
  let(:questionnaire) do
168
195
  inject_questionnaire 'test', <<-END
169
- n = 8
196
+ n = 9
170
197
 
171
198
  panel do
172
199
  question :v_radio, type: :radio, question_group: :group1, group_minimum_answered: n do
@@ -210,6 +237,12 @@ shared_examples 'group_minimum_answered' do
210
237
  year_key: :v_date_year, month_key: :v_date_month, day_key: :v_date_day do
211
238
  title "Enter a date"
212
239
  end
240
+
241
+ question :v_slider, type: :float, as: :slider, size: 20, default_position: :hidden,
242
+ question_group: :group1, group_minimum_answered: n do
243
+ title "Slider"
244
+ validates_in_range 0..100
245
+ end
213
246
  end; end_panel
214
247
  END
215
248
  end
@@ -220,7 +253,7 @@ shared_examples 'group_minimum_answered' do
220
253
  context 'table-based minimum number of given answers in a group' do
221
254
  let(:questionnaire) do
222
255
  inject_questionnaire 'test', <<-END
223
- n = 8
256
+ n = 9
224
257
 
225
258
  panel do
226
259
  table columns: 20 do
@@ -266,6 +299,12 @@ shared_examples 'group_minimum_answered' do
266
299
  title "Enter a date"
267
300
  end
268
301
  end
302
+
303
+ question :v_slider, type: :float, as: :slider, size: 20, default_position: :hidden,
304
+ question_group: :group1, group_minimum_answered: n do
305
+ title "Slider"
306
+ validates_in_range 0..100
307
+ end
269
308
  end
270
309
  end_panel
271
310
  END
@@ -352,6 +391,7 @@ shared_examples 'group_minimum_answered' do
352
391
  expect_error_on 'v_integer', 'answer_group_minimum'
353
392
  expect_error_on 'v_float', 'answer_group_minimum'
354
393
  expect_error_on 'v_date', 'answer_group_minimum'
394
+ expect_error_on 'v_slider', 'answer_group_minimum'
355
395
  end
356
396
  end
357
397
 
@@ -3,35 +3,58 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  shared_examples 'group_minmax_answered_tests' do
6
- it 'is valid when first question is filled in' do
6
+ it 'is valid when radio question is not filled in' do
7
+ # select_radio_option 'v_radio', 'a1'
8
+ select_radio_option 'v_scale', 'a1'
9
+ set_slider_value 'v_slider', '50'
10
+ run_validations
11
+ expect_no_errors
12
+ # expect_saved_value 'v_radio', 'a1'
13
+ expect_saved_value 'v_scale', 'a1'
14
+ end
15
+
16
+ it 'is valid when scale question is not filled in' do
7
17
  select_radio_option 'v_radio', 'a1'
8
18
  # select_radio_option 'v_scale', 'a1'
19
+ set_slider_value 'v_slider', '50'
9
20
  run_validations
10
21
  expect_no_errors
11
22
  expect_saved_value 'v_radio', 'a1'
12
23
  # expect_saved_value 'v_scale', 'a1'
13
24
  end
14
25
 
15
- it 'is valid when second question is filled in' do
16
- # select_radio_option 'v_radio', 'a1'
26
+ it 'is valid when slider question is not filled in' do
27
+ select_radio_option 'v_radio', 'a1'
17
28
  select_radio_option 'v_scale', 'a1'
29
+ # set_slider_value 'v_slider', '50'
18
30
  run_validations
19
31
  expect_no_errors
20
- # expect_saved_value 'v_radio', 'a1'
32
+ expect_saved_value 'v_radio', 'a1'
21
33
  expect_saved_value 'v_scale', 'a1'
22
34
  end
23
35
 
24
36
  it 'is not valid when no questions are filled' do
25
37
  # select_radio_option 'v_radio', 'a1'
26
38
  # select_radio_option 'v_scale', 'a1'
39
+ # set_slider_value 'v_slider', '50'
27
40
  run_validations
28
41
  expect_error_on 'v_radio', 'answer_group_minimum'
29
42
  expect_error_on 'v_scale', 'answer_group_minimum'
30
43
  end
31
44
 
32
- it 'is not valid when both questions are filled' do
45
+ it 'is not valid when only one question is filled' do
46
+ select_radio_option 'v_radio', 'a1'
47
+ # select_radio_option 'v_scale', 'a1'
48
+ # set_slider_value 'v_slider', '50'
49
+ run_validations
50
+ expect_error_on 'v_radio', 'answer_group_minimum'
51
+ expect_error_on 'v_scale', 'answer_group_minimum'
52
+ end
53
+
54
+ it 'is not valid when all questions are filled' do
33
55
  select_radio_option 'v_radio', 'a1'
34
56
  select_radio_option 'v_scale', 'a1'
57
+ set_slider_value 'v_slider', '50'
35
58
  run_validations
36
59
  expect_error_on 'v_radio', 'answer_group_maximum'
37
60
  expect_error_on 'v_scale', 'answer_group_maximum'
@@ -45,7 +68,7 @@ shared_examples 'group_minmax_answered' do
45
68
  context 'normal' do
46
69
  let(:questionnaire) do
47
70
  inject_questionnaire 'test', <<-END
48
- n = 1
71
+ n = 2
49
72
 
50
73
  panel do
51
74
  question :v_radio, type: :radio, question_group: :group1, group_minimum_answered: n,
@@ -61,6 +84,13 @@ shared_examples 'group_minmax_answered' do
61
84
  option :a1, value: 1, description: "Scale Option 1"
62
85
  option :a2, value: 2, description: "Scale Option 2"
63
86
  end
87
+
88
+ question :v_slider, type: :float, as: :slider, size: 20, default_position: :hidden,
89
+ question_group: :group1, group_minimum_answered: n,
90
+ group_maximum_answered: n do
91
+ title "Slider"
92
+ validates_in_range 0..100
93
+ end
64
94
  end; end_panel
65
95
  END
66
96
  end
@@ -71,7 +101,7 @@ shared_examples 'group_minmax_answered' do
71
101
  context 'table-based' do
72
102
  let(:questionnaire) do
73
103
  inject_questionnaire 'test', <<-END
74
- n = 1
104
+ n = 2
75
105
 
76
106
  panel do
77
107
  table columns: 20 do
@@ -89,6 +119,13 @@ shared_examples 'group_minmax_answered' do
89
119
  option :a2, value: 2, description: "Scale Option 2"
90
120
  end
91
121
  end
122
+
123
+ question :v_slider, type: :float, as: :slider, size: 20, default_position: :hidden,
124
+ question_group: :group1, group_minimum_answered: n,
125
+ group_maximum_answered: n do
126
+ title "Slider"
127
+ validates_in_range 0..100
128
+ end
92
129
  end
93
130
  end_panel
94
131
  END
@@ -33,4 +33,5 @@ line_chart :tot do
33
33
  plot :v_3
34
34
  plotband 30, 50, :yellow
35
35
  plotband 50, 80, :red
36
+ plotline 20, :green
36
37
  end
@@ -0,0 +1,25 @@
1
+ title 'Score test'
2
+
3
+ question :v_1, type: :integer
4
+
5
+ score_schema :test, 'Testscore' do
6
+ subscore :value, 'Waarde', export_key: :tes do
7
+ value(:v_1)
8
+ end
9
+ subscore :interpretation, 'Waarde', export_key: :tes_i do
10
+ 'Matig'
11
+ end
12
+ end
13
+
14
+ score_schema :test2, 'Testscore 2' do
15
+ subscore :value, 'Waarde', export_key: :tes2 do
16
+ values_with_nils(:v_1).first + 5
17
+ end
18
+ subscore :interpretation, 'Waarde', export_key: :tes2_i do
19
+ 'Miniem'
20
+ end
21
+ end
22
+
23
+ completion do
24
+ values_with_nils(:v_1).compact.size / 1.0
25
+ end
@@ -4,15 +4,10 @@ require 'spec_helper'
4
4
 
5
5
  module Quby::Answers::Entities
6
6
  describe Score do
7
- let(:questionnaire) do
8
- Quby.questionnaires.find('score_test')
9
- end
10
-
11
7
  let(:v_1_value) { 10 }
12
-
13
8
  let(:answer) do
14
9
  # complicated way to get an answer
15
- answer = Quby.answers.create!('score_test')
10
+ answer = Quby.answers.create!(questionnaire_key)
16
11
  answer.value = {'v_1' => v_1_value}
17
12
  Quby.answers.update!(answer)
18
13
  Quby.answers.regenerate_outcome!(answer)
@@ -22,56 +17,68 @@ module Quby::Answers::Entities
22
17
  # it works with indifferent access
23
18
  subject { answer.score_objects['test'] }
24
19
 
25
- it 'has working completion' do
26
- expect(answer.outcome.completion).to eq("value" => 1.0)
27
- end
28
-
29
- it 'exposes score schema fields' do
30
- expect(subject.key).to eq(:test)
31
- expect(subject.label).to eq('Testscore')
32
- end
33
-
34
- it 'exposes #referenced_values' do
35
- expect(subject.referenced_values).to eq(["v_1"])
36
- end
20
+ shared_examples 'score test' do
21
+ it 'has working completion' do
22
+ expect(answer.outcome.completion).to eq("value" => 1.0)
23
+ end
37
24
 
38
- describe '#[] to access subscores' do
39
- let(:subscore) { subject[:value] }
40
- it 'exposes subscore schema fields' do
41
- expect(subscore.key).to eq(:value)
42
- expect(subscore.export_key).to eq(:tes)
43
- expect(subscore.only_for_export).to eq(nil)
44
- expect(subscore.label).to eq('Waarde')
25
+ it 'exposes score schema fields' do
26
+ expect(subject.key).to eq(:test)
27
+ expect(subject.label).to eq('Testscore')
45
28
  end
46
29
 
47
- it 'exposes subscore values, with indifferent access' do
48
- expect(subscore.value).to eq(10)
49
- expect(subject['interpretation'].value).to eq('Matig')
30
+ it 'exposes #referenced_values' do
31
+ expect(subject.referenced_values).to eq(["v_1"])
50
32
  end
51
- end
52
33
 
53
- describe 'when the score has missing values' do
54
- let(:v_1_value) { nil }
55
- it 'exposes nil as the value for each subscore, and leaves schema information alone' do
56
- expect(subject[:interpretation].value).to eq(nil)
57
- expect(subject[:value].export_key).to eq(:tes)
58
- expect(subject.key).to eq(:test)
34
+ describe '#[] to access subscores' do
35
+ let(:subscore) { subject[:value] }
36
+ it 'exposes subscore schema fields' do
37
+ expect(subscore.key).to eq(:value)
38
+ expect(subscore.export_key).to eq(:tes)
39
+ expect(subscore.only_for_export).to eq(nil)
40
+ expect(subscore.label).to eq('Waarde')
41
+ end
42
+
43
+ it 'exposes subscore values, with indifferent access' do
44
+ expect(subscore.value).to eq(10)
45
+ expect(subject['interpretation'].value).to eq('Matig')
46
+ end
59
47
  end
60
48
 
61
- describe 'when the score has an exception' do
62
- # second score will error on nil
63
- subject { answer.score_objects[:test2] }
49
+ describe 'when the score has missing values' do
50
+ let(:v_1_value) { nil }
64
51
  it 'exposes nil as the value for each subscore, and leaves schema information alone' do
65
- expect(subject[:value].value).to eq(nil)
66
- expect(subject[:value].export_key).to eq(:tes2)
67
- expect(subject.key).to eq(:test2)
52
+ expect(subject[:interpretation].value).to eq(nil)
53
+ expect(subject[:value].export_key).to eq(:tes)
54
+ expect(subject.key).to eq(:test)
68
55
  end
69
56
 
70
- it 'exposes the exception and backtrace under #error' do
71
- expect(subject.error).to match({exception: "undefined method `+' for nil:NilClass",
72
- backtrace: an_instance_of(Array)})
57
+ describe 'when the score has an exception' do
58
+ # second score will error on nil
59
+ subject { answer.score_objects[:test2] }
60
+ it 'exposes nil as the value for each subscore, and leaves schema information alone' do
61
+ expect(subject[:value].value).to eq(nil)
62
+ expect(subject[:value].export_key).to eq(:tes2)
63
+ expect(subject.key).to eq(:test2)
64
+ end
65
+
66
+ it 'exposes the exception and backtrace under #error' do
67
+ expect(subject.error).to match({exception: "undefined method `+' for nil:NilClass",
68
+ backtrace: an_instance_of(Array)})
69
+ end
73
70
  end
74
71
  end
75
72
  end
73
+
74
+ describe 'with explicit score calculations' do
75
+ let(:questionnaire_key) {'score_test'}
76
+ it_behaves_like 'score test'
77
+ end
78
+
79
+ describe 'with score schema based score calculations' do
80
+ let(:questionnaire_key) {'scores_from_schema'}
81
+ it_behaves_like 'score test'
82
+ end
76
83
  end
77
84
  end
@@ -4,7 +4,7 @@ require 'spec_helper'
4
4
 
5
5
  module Quby::Answers::Services
6
6
  describe AnswerValidator do
7
- let(:questionnaire) do
7
+ let(:default_questionnaire) do
8
8
  inject_questionnaire "test", <<-END
9
9
  panel do
10
10
  question :v_check_box, type: :check_box do
@@ -24,7 +24,7 @@ module Quby::Answers::Services
24
24
  END
25
25
  end
26
26
 
27
- let(:answer_value) do
27
+ let(:default_answer_value) do
28
28
  { v_check_box: { v_ck_a1: 1, v_ck_a2: 0 },
29
29
  v_ck_a1: 1,
30
30
  v_ck_a2: 0,
@@ -34,12 +34,16 @@ module Quby::Answers::Services
34
34
  }
35
35
  end
36
36
 
37
- let!(:answer) do
38
- answer = Quby.answers.create!(questionnaire.key, value: answer_value, flags: {}, textvars: {})
37
+ def build_answer(questionnaire: default_questionnaire, value: default_answer_value)
38
+ answer = Quby.answers.create!(questionnaire.key, value: value, flags: {}, textvars: {})
39
39
  answer.extend AnswerValidations
40
+ answer
40
41
  end
41
42
 
42
- let(:validator) { described_class.new(questionnaire, answer) }
43
+
44
+ def validator(questionnaire: default_questionnaire, answer: build_answer)
45
+ described_class.new(questionnaire, answer)
46
+ end
43
47
 
44
48
  describe '#depends_on_key_answered' do
45
49
  it 'returns true if a check box option is answered' do
@@ -64,17 +68,54 @@ module Quby::Answers::Services
64
68
 
65
69
  describe '#validate' do
66
70
  it 'sets validation errors on the answer' do
67
- validator.validate
71
+ answer = build_answer
72
+ validator(answer: answer).validate
68
73
  expect(answer.errors).to_not be_empty
69
74
  end
70
75
 
71
76
  context 'when answer is aborted' do
72
- before do
77
+ it 'skips the validate_required validation' do
78
+ answer = build_answer
73
79
  allow(answer).to receive(:aborted).and_return(true)
80
+ validator(answer: answer).validate
81
+ expect(answer.errors).to be_empty
74
82
  end
75
83
 
76
- it 'skips the validate_required validation' do
77
- validator.validate
84
+ it 'skips the validate_minimum_checked_required validation' do
85
+ questionnaire = inject_questionnaire "test", <<-END
86
+ question :v_ck, type: :check_box, required: true, minimum_checked_required: 2 do
87
+ option :v_ck_a1, value: 1
88
+ option :v_ck_a2, value: 2
89
+ option :v_ck_a3, value: 3
90
+ end
91
+ END
92
+
93
+ answer = build_answer(questionnaire: questionnaire, value: {v_ck: {v_ck_a1: 0, v_ck_a2: 0, v_ck_a3: 1}})
94
+ validator(questionnaire: questionnaire, answer: answer).validate
95
+ expect(answer.errors[:v_ck]).to include(message: "Not enough options checked.", valtype: :minimum_checked_required)
96
+
97
+ answer = build_answer(questionnaire: questionnaire, value: {v_ck: {v_ck_a1: 0, v_ck_a2: 0, v_ck_a3: 1}})
98
+ allow(answer).to receive(:aborted).and_return(true)
99
+ validator(questionnaire: questionnaire, answer: answer).validate
100
+ expect(answer.errors).to be_empty
101
+ end
102
+
103
+ it 'skips the validate_answer_group_minimum validation' do
104
+ questionnaire = inject_questionnaire "test", <<-END
105
+ question :v_1, type: :string, question_group: :grp, group_minimum_answered: 2
106
+ question :v_2, type: :string, question_group: :grp, group_minimum_answered: 2
107
+ question :v_3, type: :string, question_group: :grp, group_minimum_answered: 2
108
+ END
109
+
110
+ answer = build_answer(questionnaire: questionnaire, value: {v_1: nil, v_2: nil, v_3: 'bla'})
111
+ validator(questionnaire: questionnaire, answer: answer).validate
112
+ expect(answer.errors[:v_1]).to include(message: "Needs at least 2 question(s) answered.", valtype: :answer_group_minimum)
113
+ expect(answer.errors[:v_2]).to include(message: "Needs at least 2 question(s) answered.", valtype: :answer_group_minimum)
114
+ expect(answer.errors[:v_3]).to include(message: "Needs at least 2 question(s) answered.", valtype: :answer_group_minimum)
115
+
116
+ answer = build_answer(questionnaire: questionnaire, value: {v_1: nil, v_2: nil, v_3: 'bla'})
117
+ allow(answer).to receive(:aborted).and_return(true)
118
+ validator(questionnaire: questionnaire, answer: answer).validate
78
119
  expect(answer.errors).to be_empty
79
120
  end
80
121
  end