ruql 0.1.3 → 1.0.3

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.
@@ -1,10 +1,10 @@
1
1
  class MultipleChoice < Question
2
+
2
3
  attr_accessor :multiple
3
4
 
4
5
  def initialize(text='', opts={})
5
6
  super
6
7
  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?
@@ -1,40 +1,27 @@
1
1
  class Question
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
-
2
+ attr_accessor :question_text, :answers, :randomize, :points, :name, :question_tags, :question_group, :question_comment, :uid
3
+
13
4
  def initialize(*args)
14
5
  options = if args[-1].kind_of?(Hash) then args[-1] else {} end
15
6
  @answers = options[:answers] || []
16
7
  @points = [options[:points].to_i, 1].max
17
8
  @raw = options[:raw]
18
9
  @name = options[:name]
19
- @question_image = options[:image]
20
10
  @question_tags = []
21
- @question_uid = (options.delete(:uid) || SecureRandom.uuid).to_s
11
+ @question_group = ''
22
12
  @question_comment = ''
23
13
  end
14
+
15
+ def uid(str); @uid = str; end
16
+
24
17
  def raw? ; !!@raw ; end
25
-
26
- def uid(u) ; @question_uid = u ; end
27
-
18
+
28
19
  def text(s) ; @question_text = s ; end
29
20
 
30
21
  def explanation(text)
31
22
  @answers.each { |answer| answer.explanation ||= text }
32
23
  end
33
24
 
34
- def image(url)
35
- @question_image = url
36
- end
37
-
38
25
  def answer(text, opts={})
39
26
  @answers << Answer.new(text, correct=true, opts[:explanation])
40
27
  end
@@ -43,6 +30,10 @@ class Question
43
30
  @answers << Answer.new(text, correct=false, opts[:explanation])
44
31
  end
45
32
 
33
+ def group(str)
34
+ @question_group = str
35
+ end
36
+
46
37
  # these are ignored but legal for now:
47
38
  def tags(*args) # string or array of strings
48
39
  if args.length > 1
@@ -60,30 +51,4 @@ class Question
60
51
 
61
52
  def correct_answers ; @answers.collect(&:correct?) ; end
62
53
 
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
-
89
54
  end
@@ -1,24 +1,6 @@
1
1
  class Quiz
2
2
  @@quizzes = []
3
- @quiz_yaml = {}
4
- def self.quizzes ; @@quizzes ; end
5
- @@default_options =
6
- {
7
- :open_time => Time.now,
8
- :soft_close_time => Time.now + 24*60*60,
9
- :hard_close_time => Time.now + 24*60*60,
10
- :maximum_submissions => 1,
11
- :duration => 3600,
12
- :retry_delay => 600,
13
- :parameters => {
14
- :show_explanations => {
15
- :question => 'before_soft_close_time',
16
- :option => 'before_soft_close_time',
17
- :score => 'before_soft_close_time',
18
- }
19
- },
20
- :maximum_score => 1,
21
- }
3
+ @@options = {}
22
4
 
23
5
  attr_reader :renderer
24
6
  attr_reader :questions
@@ -26,36 +8,65 @@ class Quiz
26
8
  attr_reader :output
27
9
  attr_reader :seed
28
10
  attr_reader :logger
29
- attr_accessor :title, :quizzes
11
+ attr_accessor :title
12
+
13
+ def self.reset
14
+ @@quizzes = []
15
+ @@options = {}
16
+ end
17
+ def self.quizzes ; @@quizzes ; end
18
+ def self.options ; @@options ; end
30
19
 
31
20
  def initialize(title, options={})
32
21
  @output = ''
33
- @questions = options.delete(:questions) || []
22
+ @questions = options[:questions] || []
34
23
  @title = title
35
- @options = @@default_options.merge(options)
24
+ @options = @@options.merge(options)
36
25
  @seed = srand
37
26
  @logger = Logger.new(STDERR)
38
- @logger.level = Logger.const_get (options.delete('l') ||
39
- options.delete('log') || 'warn').upcase
40
- if (yaml = options.delete(:yaml))
41
- @quiz_yaml = YAML::load(IO.read yaml)
42
- end
43
- end
44
-
45
- def self.get_renderer(renderer)
46
- Object.const_get(renderer.to_s + 'Renderer') rescue nil
27
+ @logger.level = (@options['-V'] || @options['--verbose']) ? Logger::INFO : Logger::WARN
28
+ #@quiz_yaml = yaml
47
29
  end
48
30
 
49
31
  def render_with(renderer,options={})
50
32
  srand @seed
51
- @renderer = Quiz.get_renderer(renderer).send(:new,self,options)
33
+ @renderer = renderer.send(:new,self,options)
52
34
  @renderer.render_quiz
53
35
  @output = @renderer.output
54
36
  end
55
37
 
56
- def points ; questions.map(&:points).inject { |sum,points| sum + points } ; end
38
+ def self.set_options(options)
39
+ @@options = options
40
+ end
41
+
42
+ def ungrouped_questions
43
+ questions.filter { |q| q.question_group.to_s == '' }
44
+ end
45
+
46
+ def grouped_questions
47
+ questions.filter { |q| q.question_group.to_s != '' }.sort_by(&:question_group)
48
+ end
57
49
 
58
- def num_questions ; questions.length ; end
50
+ def groups ; questions.map(&:question_group).uniq.reject { |g| g.to_s == '' } ; end
51
+
52
+ def ungrouped_points
53
+ ungrouped_questions.map(&:points).sum
54
+ end
55
+
56
+ def grouped_points
57
+ gq = grouped_questions
58
+ groups.sum do |g|
59
+ gq.detect { |q| q.question_group == g }.points
60
+ end
61
+ end
62
+
63
+ def points
64
+ ungrouped_points + grouped_points
65
+ end
66
+
67
+ def num_questions
68
+ groups.length + ungrouped_questions.length
69
+ end
59
70
 
60
71
  def random_seed(num)
61
72
  @seed = num.to_i
@@ -126,8 +137,8 @@ class Quiz
126
137
  @questions << q
127
138
  end
128
139
 
129
- def self.quiz(*args, &block)
130
- quiz = Quiz.new(*args)
140
+ def self.quiz(title, args={}, &block)
141
+ quiz = Quiz.new(title, args)
131
142
  quiz.instance_eval(&block)
132
143
  @@quizzes << quiz
133
144
  end
@@ -4,7 +4,7 @@ require 'yaml'
4
4
  class EdXmlRenderer
5
5
 
6
6
  attr_reader :output
7
- attr_accessor :file
7
+ attr_accessor :file, :yaml_file
8
8
  def initialize(quiz,options={})
9
9
  @only_question = options.delete('n') || options.delete('name')
10
10
  @output = ''
@@ -5,21 +5,65 @@ class Html5Renderer
5
5
  attr_reader :output
6
6
 
7
7
  def initialize(quiz,options={})
8
+ @css = options.delete('c') || options.delete('css')
8
9
  @show_solutions = options.delete('s') || options.delete('solutions')
10
+ @show_tags = options.delete('T') || options.delete('show-tags')
9
11
  @template = options.delete('t') ||
10
12
  options.delete('template') ||
11
- File.join(File.dirname(__FILE__), '../../../templates/html5.html.erb')
13
+ File.join((Gem.loaded_specs['ruql'].full_gem_path rescue '.'), 'templates/html5.html.erb')
12
14
  @output = ''
13
- @list_type = options.delete('o') || options.delete('list-type') || '1'
14
- @list_start = options.delete('a') || options.delete('list-start') || '1'
15
15
  @quiz = quiz
16
16
  @h = Builder::XmlMarkup.new(:target => @output, :indent => 2)
17
17
  end
18
18
 
19
+ def allowed_options
20
+ opts = [
21
+ ['-c', '--css', Getopt::REQUIRED],
22
+ ['-t', '--template', Getopt::REQUIRED],
23
+ ['-s', '--solutions', Getopt::BOOLEAN],
24
+ ['-T', '--show-tags', Getopt::BOOLEAN]
25
+ ]
26
+ help = <<eos
27
+ The HTML5 and HTML Forms renderers supports these options:
28
+ -c <href>, --css=<href>
29
+ embed <href> for stylesheet into generated HTML5
30
+ -j <src>, --js=<src>
31
+ embed <src> for JavaScrips
32
+ -t <file.html.erb>, --template=<file.html.erb>
33
+ Use file.html.erb as HTML template rather than generating our own file.
34
+ file.html.erb should have <%= yield %> where questions should go.
35
+ An example is in the templates/ directory of RuQL.
36
+ The following local variables will be replaced with their values in
37
+ the template:
38
+ <%= quiz.title %> - the quiz title
39
+ <%= quiz.num_questions %> - total number of questions
40
+ <%= quiz.points %> - total number of points for whole quiz
41
+ -T, --show-tags
42
+ Show the tag(s) associated with a question within an element <div class="tags">.
43
+ -s, --solutions
44
+ generate solutions (showing correct answers and explanations)
45
+ NOTE: If there is more than one quiz (collection of questions) in the file,
46
+ a complete <html>...</html> block is produced in the output for EACH quiz.
47
+ eos
48
+ return(
49
+
50
+
19
51
  def render_quiz
20
- render_with_template do
21
- render_questions
22
- @output
52
+ if @template
53
+ render_with_template do
54
+ render_questions
55
+ @output
56
+ end
57
+ else
58
+ @h.html do
59
+ @h.head do
60
+ @h.title @quiz.title
61
+ @h.link(:rel => 'stylesheet', :type =>'text/css', :href=>@css) if @css
62
+ end
63
+ @h.body do
64
+ render_questions
65
+ end
66
+ end
23
67
  end
24
68
  self
25
69
  end
@@ -34,7 +78,7 @@ class Html5Renderer
34
78
 
35
79
  def render_questions
36
80
  render_random_seed
37
- @h.ol :class => 'questions', :type => @list_type, :start => @list_start do
81
+ @h.ol :class => 'questions' do
38
82
  @quiz.questions.each_with_index do |q,i|
39
83
  case q
40
84
  when MultipleChoice, SelectMultiple, TrueFalse then render_multiple_choice(q,i)
@@ -111,23 +155,21 @@ class Html5Renderer
111
155
  def render_question_text(question,index)
112
156
  html_args = {
113
157
  :id => "question-#{index}",
114
- :'data-uid' => question.question_uid,
115
- :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')].join(' ')
158
+ :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')]
159
+ .join(' ')
116
160
  }
117
- if question.question_image # add CSS class to both <li> and <img>
118
- html_args[:class] << 'question-with-image'
119
- end
120
161
  @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
125
162
  @h.div :class => 'text' do
126
163
  qtext = "[#{question.points} point#{'s' if question.points>1}] " <<
127
164
  ('Select ALL that apply: ' if question.multiple).to_s <<
128
165
  if question.class == FillIn then question.question_text.gsub(/\-+/, '_____________________________')
129
166
  else question.question_text
130
167
  end
168
+ if @show_tags
169
+ @h.div(:class => 'text') do
170
+ question.tags.join(',')
171
+ end
172
+ end
131
173
  if question.raw?
132
174
  @h.p { |p| p << qtext }
133
175
  else
@@ -10,6 +10,40 @@ class JSONRenderer
10
10
  end
11
11
 
12
12
  def render_quiz
13
- @output = @quiz.questions.map {|question| JSON.pretty_generate(question.to_JSON)}
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)
14
22
  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
+
15
49
  end
@@ -0,0 +1,148 @@
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
+