ruql 0.0.9 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4efcebefa4b89ca759cce170acace5355feaeefd
4
- data.tar.gz: d7ab50a7a43b935ec9fbba0919f039b00c843b3d
3
+ metadata.gz: 8a6637987324f5db32e7387b7183ea4591957817
4
+ data.tar.gz: 05dc9bbffafb43de71688bbbd1a0b70302d51665
5
5
  SHA512:
6
- metadata.gz: 149a8f650812e0d3e6be26670448d4d56388c0847cbfc3a901504ee91af388dc349e47fb7aef116da0634a64d11ca2803674f400814f223c8ed07d5f85cdd164
7
- data.tar.gz: efb47e061b140a3bca3f1cc8001631e2bf08b5c342c5225a663455b62922e6e05e41f74ca2e388fcc6f37ea25de3561c383851206647ea9d68958c0db7dfa40c
6
+ metadata.gz: bd17352a49b61a9b0844cb0d043093c3ba8fe33ba302ac83aac60cb602a4f534074f7c02c4dd0f0772d301232dd360c57d997ddd6fd67ac171172983b0e8c93b
7
+ data.tar.gz: 2f34ee1cd6b63f9f81ccd6bafecc7864143d4512d127d1a5740e035f73d9cf5c83abd68bd0eda6b232837bb1a68e60223977ca38920c9a8926b1d167779fd25d
data/bin/ruql CHANGED
@@ -15,7 +15,6 @@ renderer choices are:
15
15
  HtmlForm - HTML forms
16
16
  EdXml - XML for OpenEdX platform in-course questions
17
17
  AutoQCM - LaTeX for use with AutoQCM (http://home.gna.org/auto-qcm)
18
- Coursera - [obsolete] Coursera HTML/XML format for online auto-grading
19
18
  JSON - [partially implemented] JSON format
20
19
  Qualtrics -[partially implemented] Qualtrics survey (txt) format
21
20
 
@@ -39,7 +38,7 @@ The EdXML renderer supports these options:
39
38
  some systems.
40
39
 
41
40
  -y <file.yml>, --yaml=<file.yml>
42
- Render the questions with a specific yaml file.
41
+ Render open-assessment questions using info in given Yaml file.
43
42
 
44
43
  The HTML5 and HTML Forms renderers supports these options:
45
44
  -o <type>, --list-type=<type>
@@ -47,14 +46,13 @@ The HTML5 and HTML Forms renderers supports these options:
47
46
  Sets the values of the 'start' and 'type' attributes for the HTML <ol>
48
47
  element. Type should be one of 1,a,A,i,I. Start should always be
49
48
  a number indicating the ordinal value (starting from 1) of first item.
50
- -c <href>, --css=<href>
51
- embed <href> for stylesheet into generated HTML5
52
49
  -j <src>, --js=<src>
53
50
  embed <src> for JavaScript
54
51
  -t <file.html.erb>, --template=<file.html.erb>
55
52
  Use file.html.erb as HTML template rather than generating our own file.
56
53
  file.html.erb should have <%= yield %> where questions should go.
57
- An example is in the templates/ directory of RuQL.
54
+ The default template in the templates/ directory of RuQL will be used
55
+ if this option is not given.
58
56
  The following local variables will be replaced with their values in
59
57
  the template:
60
58
  <%= quiz.title %> - the quiz title
@@ -99,11 +97,9 @@ def main
99
97
  ['-l', '--log', Getopt::REQUIRED],
100
98
  ['-y', '--yaml', Getopt::REQUIRED]
101
99
  )
102
- Quiz.set_yaml_file opts.delete("y") || opts.delete("yaml")
103
100
  Quiz.instance_eval "#{IO.read(filename)}"
104
101
  Quiz.quizzes.each { |quiz| puts quiz.render_with(renderer, opts) }
105
102
  end
106
103
 
107
104
  usage if ARGV.length < 2
108
105
  main
109
-
data/lib/ruql/answer.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  class Answer
2
2
  include Comparable
3
- attr_accessor :answer_text, :explanation
3
+ attr_accessor :answer_text, :explanation, :correct
4
4
  attr_reader :correct
5
5
  attr_reader :builder
6
6
  attr_reader :question
@@ -8,9 +8,21 @@ class Answer
8
8
  def <=>(other) ; self.answer_text <=> other.answer_text ; end
9
9
  def correct? ; !!correct ; end
10
10
  def has_explanation? ; !!explanation ; end
11
- def initialize(answer_text, correct, explanation=nil)
11
+ def initialize(answer_text = '', correct = false, explanation=nil)
12
12
  @answer_text = answer_text
13
13
  @correct = !!correct # ensure boolean
14
14
  @explanation = explanation
15
15
  end
16
+
17
+ def to_JSON
18
+ Hash[instance_variables.collect { |var| [var.to_s.delete('@'), instance_variable_get(var)] }]
19
+ end
20
+
21
+ def self.from_JSON(hash)
22
+ answer = Answer.new
23
+ hash.each do |key, value|
24
+ answer.send((key + '=').to_sym, value)
25
+ end
26
+ answer
27
+ end
16
28
  end
@@ -1,10 +1,10 @@
1
1
  class MultipleChoice < Question
2
-
3
2
  attr_accessor :multiple
4
3
 
5
4
  def initialize(text='', opts={})
6
5
  super
7
6
  self.question_text = text
7
+
8
8
  self.multiple = !!opts[:multiple]
9
9
  self.randomize = !!opts[:randomize]
10
10
  end
@@ -115,7 +115,7 @@ class OpenAssessment
115
115
  # Adds fields for a simple_open_assessment question
116
116
  def add_simple_question
117
117
  criterion = Criterion.new
118
- criterion.name("How'd you do?")
118
+ criterion.name("How'd you do?") # "
119
119
  criterion.label("Scoring Rubric")
120
120
 
121
121
  raise "Must have answer for question" if @question_answer.nil?
data/lib/ruql/question.rb CHANGED
@@ -1,24 +1,40 @@
1
1
  class Question
2
- attr_accessor :question_text, :answers, :randomize, :points, :name, :question_tags, :question_comment
3
-
2
+ attr_accessor :question_text,
3
+ :answers,
4
+ :question_image,
5
+ :randomize,
6
+ :points,
7
+ :name,
8
+ :question_tags,
9
+ :question_uid,
10
+ :question_comment,
11
+ :raw
12
+
4
13
  def initialize(*args)
5
14
  options = if args[-1].kind_of?(Hash) then args[-1] else {} end
6
15
  @answers = options[:answers] || []
7
16
  @points = [options[:points].to_i, 1].max
8
17
  @raw = options[:raw]
9
18
  @name = options[:name]
19
+ @question_image = options[:image]
10
20
  @question_tags = []
21
+ @question_uid = (options.delete(:uid) || SecureRandom.uuid).to_s
11
22
  @question_comment = ''
12
23
  end
13
-
14
24
  def raw? ; !!@raw ; end
15
-
25
+
26
+ def uid(u) ; @question_uid = u ; end
27
+
16
28
  def text(s) ; @question_text = s ; end
17
29
 
18
30
  def explanation(text)
19
31
  @answers.each { |answer| answer.explanation ||= text }
20
32
  end
21
33
 
34
+ def image(url)
35
+ @question_image = url
36
+ end
37
+
22
38
  def answer(text, opts={})
23
39
  @answers << Answer.new(text, correct=true, opts[:explanation])
24
40
  end
@@ -44,4 +60,30 @@ class Question
44
60
 
45
61
  def correct_answers ; @answers.collect(&:correct?) ; end
46
62
 
63
+ def answer_helper(obj)
64
+ if obj.is_a? Array and obj.size and obj[0].is_a? Answer
65
+ return obj.map {|answer| answer.to_JSON}
66
+ end
67
+ obj
68
+ end
69
+
70
+ #creates a JSON hash of the object with its object name. we should convert this to a mixin for answer and question. aaron
71
+ def to_JSON
72
+ h = Hash[instance_variables.collect { |var| [var.to_s.delete('@'), answer_helper(instance_variable_get(var))] }]
73
+ h['question_type'] = self.class.to_s
74
+ return h
75
+ end
76
+
77
+ #factory method to return correct type of question
78
+ def self.from_JSON(hash_str)
79
+ hash = JSON.parse(hash_str)
80
+ #create the appropriate class of the object from the hash's class name
81
+ question = Object.const_get(hash.fetch('question_type')).new()
82
+ hash.reject{|key| key == 'answers' or key == 'question_type'}.each do |key, value|
83
+ question.send((key + '=').to_sym, value)
84
+ end
85
+ question.answers = hash['answers'].map{|answer_hash| Answer.from_JSON(answer_hash)}
86
+ question
87
+ end
88
+
47
89
  end
data/lib/ruql/quiz.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  class Quiz
2
2
  @@quizzes = []
3
- @@yaml_file = nil
4
3
  @quiz_yaml = {}
5
4
  def self.quizzes ; @@quizzes ; end
6
5
  @@default_options =
@@ -27,18 +26,20 @@ class Quiz
27
26
  attr_reader :output
28
27
  attr_reader :seed
29
28
  attr_reader :logger
30
- attr_accessor :title
29
+ attr_accessor :title, :quizzes
31
30
 
32
- def initialize(title, yaml, options={})
31
+ def initialize(title, options={})
33
32
  @output = ''
34
- @questions = options[:questions] || []
33
+ @questions = options.delete(:questions) || []
35
34
  @title = title
36
35
  @options = @@default_options.merge(options)
37
36
  @seed = srand
38
37
  @logger = Logger.new(STDERR)
39
38
  @logger.level = Logger.const_get (options.delete('l') ||
40
39
  options.delete('log') || 'warn').upcase
41
- @quiz_yaml = yaml
40
+ if (yaml = options.delete(:yaml))
41
+ @quiz_yaml = YAML::load(IO.read yaml)
42
+ end
42
43
  end
43
44
 
44
45
  def self.get_renderer(renderer)
@@ -52,10 +53,6 @@ class Quiz
52
53
  @output = @renderer.output
53
54
  end
54
55
 
55
- def self.set_yaml_file(file)
56
- @@yaml_file = file && YAML::load_file(file)
57
- end
58
-
59
56
  def points ; questions.map(&:points).inject { |sum,points| sum + points } ; end
60
57
 
61
58
  def num_questions ; questions.length ; end
@@ -130,7 +127,7 @@ class Quiz
130
127
  end
131
128
 
132
129
  def self.quiz(*args, &block)
133
- quiz = Quiz.new(*args, (@@yaml_file.shift if @@yaml_file))
130
+ quiz = Quiz.new(*args)
134
131
  quiz.instance_eval(&block)
135
132
  @@quizzes << quiz
136
133
  end
@@ -4,7 +4,7 @@ require 'yaml'
4
4
  class EdXmlRenderer
5
5
 
6
6
  attr_reader :output
7
- attr_accessor :file, :yaml_file
7
+ attr_accessor :file
8
8
  def initialize(quiz,options={})
9
9
  @only_question = options.delete('n') || options.delete('name')
10
10
  @output = ''
@@ -5,11 +5,10 @@ class Html5Renderer
5
5
  attr_reader :output
6
6
 
7
7
  def initialize(quiz,options={})
8
- @css = options.delete('c') || options.delete('css')
9
8
  @show_solutions = options.delete('s') || options.delete('solutions')
10
9
  @template = options.delete('t') ||
11
10
  options.delete('template') ||
12
- File.join(Gem.loaded_specs['ruql'].full_gem_path, 'templates/html5.html.erb')
11
+ File.join(File.dirname(__FILE__), '../../../templates/html5.html.erb')
13
12
  @output = ''
14
13
  @list_type = options.delete('o') || options.delete('list-type') || '1'
15
14
  @list_start = options.delete('a') || options.delete('list-start') || '1'
@@ -18,21 +17,9 @@ class Html5Renderer
18
17
  end
19
18
 
20
19
  def render_quiz
21
- if @template
22
- render_with_template do
23
- render_questions
24
- @output
25
- end
26
- else
27
- @h.html do
28
- @h.head do
29
- @h.title @quiz.title
30
- @h.link(:rel => 'stylesheet', :type =>'text/css', :href=>@css) if @css
31
- end
32
- @h.body do
33
- render_questions
34
- end
35
- end
20
+ render_with_template do
21
+ render_questions
22
+ @output
36
23
  end
37
24
  self
38
25
  end
@@ -124,10 +111,17 @@ class Html5Renderer
124
111
  def render_question_text(question,index)
125
112
  html_args = {
126
113
  :id => "question-#{index}",
127
- :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')]
128
- .join(' ')
114
+ :'data-uid' => question.question_uid,
115
+ :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')].join(' ')
129
116
  }
117
+ if question.question_image # add CSS class to both <li> and <img>
118
+ html_args[:class] << 'question-with-image'
119
+ end
130
120
  @h.li html_args do
121
+ # if there's an image, render it first
122
+ if question.question_image
123
+ @h.img :src => question.question_image, :class => 'question-image'
124
+ end
131
125
  @h.div :class => 'text' do
132
126
  qtext = "[#{question.points} point#{'s' if question.points>1}] " <<
133
127
  ('Select ALL that apply: ' if question.multiple).to_s <<
@@ -10,40 +10,6 @@ class JSONRenderer
10
10
  end
11
11
 
12
12
  def render_quiz
13
- @quiz.questions.each do |question|
14
- case question
15
- when MultipleChoice, SelectMultiple, TrueFalse then render_multiple_choice(question)
16
- when FillIn then render_fill_in(question) # not currently supported
17
- else
18
- raise "Unknown question type: #{question}"
19
- end
20
- end
21
- @output = JSON.pretty_generate(@json_array)
13
+ @output = @quiz.questions.map {|question| JSON.pretty_generate(question.to_JSON)}
22
14
  end
23
-
24
- def render_multiple_choice(question)
25
- question_hash = {
26
- "text" => question.question_text,
27
- "answers" => answers_to_json_array(question.answers)
28
- }
29
- @json_array.push(question_hash)
30
- end
31
-
32
- def answers_to_json_array(answers)
33
- answers_array = []
34
- answers.each do |answer|
35
- answer_json = {
36
- "text" => answer.answer_text,
37
- "explanation" => answer.explanation,
38
- "correct" => answer.correct
39
- }
40
- answers_array.push(answer_json)
41
- end
42
- answers_array
43
- end
44
-
45
- def render_fill_in(q)
46
- # fill-in-the-blank questions not currently supported
47
- end
48
-
49
15
  end
@@ -1,5 +1,4 @@
1
1
  class SelectMultiple < MultipleChoice
2
-
3
2
  def initialize(text='', opts={})
4
3
  super
5
4
  self.multiple = true
@@ -1,6 +1,6 @@
1
1
  class TrueFalse < Question
2
2
 
3
- def initialize(text, correct_answer, opts=nil)
3
+ def initialize(text='', correct_answer=false, opts=nil)
4
4
  super
5
5
  opts ||= {}
6
6
  opts[:explanation] ||= ''
data/lib/ruql.rb CHANGED
@@ -6,13 +6,7 @@ require 'date'
6
6
  $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__)))
7
7
 
8
8
  # renderers
9
- require 'ruql/renderers/xml_renderer'
10
- require 'ruql/renderers/html5_renderer'
11
- require 'ruql/renderers/html_form_renderer'
12
- require 'ruql/renderers/edxml_renderer'
13
- require 'ruql/renderers/auto_qcm_renderer'
14
- require 'ruql/renderers/json_renderer'
15
- require 'ruql/renderers/qualtrics_renderer'
9
+ Dir[File.join(File.dirname(__FILE__),'ruql/renderers/*.rb')].each { |file| require file }
16
10
 
17
11
  # question types
18
12
  require 'ruql/quiz'
metadata CHANGED
@@ -1,43 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Armando Fox
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-30 00:00:00.000000000 Z
11
+ date: 2016-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ~>
16
+ - - '>='
17
17
  - !ruby/object:Gem::Version
18
18
  version: '3.0'
19
19
  prerelease: false
20
20
  name: builder
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - ~>
23
+ - - '>='
24
24
  - !ruby/object:Gem::Version
25
25
  version: '3.0'
26
26
  type: :runtime
27
27
  - !ruby/object:Gem::Dependency
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - '='
30
+ - - '>='
31
31
  - !ruby/object:Gem::Version
32
- version: 1.4.2
32
+ version: '1.0'
33
33
  prerelease: false
34
34
  name: getopt
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - '='
37
+ - - '>='
38
38
  - !ruby/object:Gem::Version
39
- version: 1.4.2
39
+ version: '1.0'
40
40
  type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ prerelease: false
48
+ name: rspec
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ type: :development
55
+ - !ruby/object:Gem::Dependency
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '4.0'
61
+ prerelease: false
62
+ name: activesupport
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: '4.0'
68
+ type: :development
69
+ - !ruby/object:Gem::Dependency
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ prerelease: false
76
+ name: byebug
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
41
83
  description: Ruby-embedded DSL for creating short-answer quiz questions
42
84
  email: fox@cs.berkeley.edu
43
85
  executables:
@@ -65,7 +107,6 @@ files:
65
107
  - lib/ruql/renderers/html_form_renderer.rb
66
108
  - lib/ruql/renderers/json_renderer.rb
67
109
  - lib/ruql/renderers/qualtrics_renderer.rb
68
- - lib/ruql/renderers/xml_renderer.rb
69
110
  - lib/ruql/select_multiple.rb
70
111
  - lib/ruql/tex_output.rb
71
112
  - lib/ruql/true_false.rb
@@ -1,148 +0,0 @@
1
- class XMLRenderer
2
- require 'builder'
3
-
4
- attr_reader :output
5
- def initialize(quiz,options={})
6
- @output = ''
7
- @b = Builder::XmlMarkup.new(:target => @output, :indent => 2)
8
- @quiz = quiz
9
- end
10
-
11
- def render(thing)
12
- case thing
13
- when MultipleChoice,SelectMultiple,TrueFalse then render_multiple_choice(thing)
14
- when FillIn then render_fill_in(thing)
15
- else
16
- raise "Unknown question type: #{thing}"
17
- end
18
- end
19
-
20
- def render_quiz
21
- # entire quiz can be in one question group, as long as we specify
22
- # that ALL question from the group must be used to make the quiz.
23
- xml_quiz do
24
- # after preamble...
25
- @b.question_groups do
26
- @b.question_group(:select => @quiz.questions.length) do
27
- @quiz.questions.each do |question|
28
- self.render(question)
29
- end
30
- end
31
- end
32
- end
33
- @output
34
- end
35
-
36
- def render_fill_in(question)
37
- @b.question :type => 'GS_Short_Answer_Question_Simple', :id => question.object_id.to_s(16) do
38
- @b.metadata {
39
- @b.parameters {
40
- @b.rescale_score question.points
41
- @b.type 'regexp'
42
- }
43
- }
44
- # since we want all the options to appear, we create N option
45
- # groups each containig 1 option, and specify that option to
46
- # always be selected for inclusion in the quiz. If the original
47
- # question specified 'random', use the 'randomize' attribute on
48
- # option_groups to scramble the order in which displayed;
49
- # otherwise, display in same order as answers appear in source.
50
- @b.data {
51
- @b.text { @b.cdata!(question.question_text) }
52
- @b.option_groups(:randomize => !!question.randomize) {
53
- @b.option_group(:select => 'all') {
54
- question.answers.each do |answer|
55
- option_args = {}
56
- option_args['selected_score'] = answer.correct? ? 1 : 0
57
- option_args['unselected_score'] =
58
- question.multiple ? 1 - option_args['selected_score'] : 0
59
- option_args['id'] = answer.object_id.to_s(16)
60
- @b.option(option_args) do
61
- answer_text = answer.answer_text
62
- if answer_text.kind_of?(Regexp)
63
- answer_text = answer_text.inspect
64
- if !question.case_sensitive
65
- answer_text += 'i'
66
- end
67
- end
68
- @b.text { @b.cdata!(answer_text) }
69
- @b.explanation { @b.cdata!(answer.explanation) } if answer.has_explanation?
70
- end
71
- end
72
- }
73
- }
74
- }
75
- end
76
- end
77
-
78
- def render_multiple_choice(question)
79
- @b.question :type => 'GS_Choice_Answer_Question', :id => question.object_id.to_s(16) do
80
- @b.metadata {
81
- @b.parameters {
82
- @b.rescale_score question.points
83
- @b.choice_type (question.multiple ? 'checkbox' : 'radio')
84
- }
85
- }
86
- # since we want all the options to appear, we create N option
87
- # groups each containig 1 option, and specify that option to
88
- # always be selected for inclusion in the quiz. If the original
89
- # question specified 'random', use the 'randomize' attribute on
90
- # option_groups to scramble the order in which displayed;
91
- # otherwise, display in same order as answers appear in source.
92
- @b.data {
93
- @b.text { @b.cdata!(question.question_text) }
94
- @b.option_groups(:randomize => !!question.randomize) {
95
- question.answers.each do |a|
96
- @b.option_group(:select => 'all') {
97
- self.render_multiple_choice_answer a, question.multiple
98
- }
99
- end
100
- }
101
- }
102
- end
103
- end
104
- alias :render_true_false :render_multiple_choice
105
-
106
- def render_multiple_choice_answer(answer, multiple_allowed)
107
- option_args = {}
108
- option_args['selected_score'] = answer.correct? ? 1 : 0
109
- option_args['unselected_score'] =
110
- multiple_allowed ? 1 - option_args['selected_score'] : 0
111
- option_args['id'] = answer.object_id.to_s(16)
112
- @b.option(option_args) do
113
- @b.text { @b.cdata!(answer.answer_text) }
114
- @b.explanation { @b.cdata!(answer.explanation) } if answer.has_explanation?
115
- end
116
- end
117
-
118
- private
119
-
120
- def options_to_xml(h)
121
- h.each_pair do |k,v|
122
- if v.is_a?(Hash)
123
- @b.__send__(k.to_sym) do
124
- options_to_xml v
125
- end
126
- else
127
- @b.__send__(k.to_sym, v)
128
- end
129
- end
130
- end
131
-
132
-
133
- def xml_quiz
134
- @b.quiz do
135
- @b.metadata do
136
- @b.type 'quiz'
137
- @b.title @quiz.title
138
- options_to_xml @quiz.options
139
- end
140
- @b.preamble
141
- @b.data do
142
- yield
143
- end
144
- end
145
- end
146
-
147
- end
148
-