ruqlcqb 0.0.0
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/bin/ruqlcqb +95 -0
- data/lib/ruql.rb +23 -0
- data/lib/ruql/answer.rb +28 -0
- data/lib/ruql/fill_in.rb +15 -0
- data/lib/ruql/multiple_choice.rb +11 -0
- data/lib/ruql/question.rb +73 -0
- data/lib/ruql/quiz.rb +105 -0
- data/lib/ruql/renderer.rb +20 -0
- data/lib/ruql/renderers/auto_qcm_renderer.rb +85 -0
- data/lib/ruql/renderers/edxml_renderer.rb +74 -0
- data/lib/ruql/renderers/html5_renderer.rb +166 -0
- data/lib/ruql/renderers/html_form_renderer.rb +198 -0
- data/lib/ruql/renderers/json_renderer.rb +15 -0
- data/lib/ruql/renderers/qualtrics_renderer.rb +63 -0
- data/lib/ruql/renderers/xml_renderer.rb +148 -0
- data/lib/ruql/select_multiple.rb +7 -0
- data/lib/ruql/tex_output.rb +27 -0
- data/lib/ruql/true_false.rb +17 -0
- data/templates/autoqcm.tex.erb +1 -0
- data/templates/html5.html.erb +42 -0
- data/templates/htmlform.html.erb +44 -0
- metadata +94 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
class EdXmlRenderer
|
2
|
+
require 'builder'
|
3
|
+
|
4
|
+
attr_reader :output
|
5
|
+
def initialize(quiz,options={})
|
6
|
+
@only_question = options.delete('n') || options.delete('name')
|
7
|
+
@output = ''
|
8
|
+
@b = Builder::XmlMarkup.new(:target => @output, :indent => 2)
|
9
|
+
@quiz = quiz
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(thing)
|
13
|
+
case thing
|
14
|
+
when MultipleChoice,SelectMultiple,TrueFalse then render_multiple_choice(thing)
|
15
|
+
when FillIn then render_fill_in(thing)
|
16
|
+
else
|
17
|
+
raise "Unknown question type: #{thing}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_quiz
|
22
|
+
# entire quiz can be in one question group, as long as we specify
|
23
|
+
# that ALL question from the group must be used to make the quiz.
|
24
|
+
question_list = if @only_question
|
25
|
+
then @quiz.questions.select { |q| q.name == @only_question }
|
26
|
+
else @quiz.questions
|
27
|
+
end
|
28
|
+
question_list.each { |question| render(question) }
|
29
|
+
@output
|
30
|
+
end
|
31
|
+
|
32
|
+
def render_fill_in(question)
|
33
|
+
raise "Not yet implemented for edXML"
|
34
|
+
end
|
35
|
+
|
36
|
+
def render_multiple_choice(question)
|
37
|
+
@b.problem do
|
38
|
+
# if question text has explicit newlines, use them to separate <p>'s
|
39
|
+
if question.raw?
|
40
|
+
@b.p { |p| p << question.question_text }
|
41
|
+
else
|
42
|
+
question.question_text.lines.map(&:chomp).each do |line|
|
43
|
+
if question.raw? then @b.p { |p| p << line } else @b.p(line) end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
@b.multiplechoiceresponse do
|
47
|
+
@b.choicegroup :type => 'MultipleChoice' do
|
48
|
+
question.answers.each do |answer|
|
49
|
+
if question.raw?
|
50
|
+
@b.choice(:correct => answer.correct?) do |choice|
|
51
|
+
choice << answer.answer_text
|
52
|
+
choice << "\n"
|
53
|
+
end
|
54
|
+
else
|
55
|
+
@b.choice answer.answer_text, :correct => answer.correct?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
@b.solution do
|
61
|
+
@b.div :class => 'detailed_solution' do
|
62
|
+
@b.p 'Explanation'
|
63
|
+
if question.raw?
|
64
|
+
@b.p { |p| p << question.correct_answer.explanation }
|
65
|
+
else
|
66
|
+
@b.p question.correct_answer.explanation
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,166 @@
|
|
1
|
+
class Html5Renderer
|
2
|
+
require 'builder'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
attr_reader :output
|
6
|
+
|
7
|
+
def initialize(quiz,options={})
|
8
|
+
@css = options.delete('c') || options.delete('css')
|
9
|
+
@show_solutions = options.delete('s') || options.delete('solutions')
|
10
|
+
@template = options.delete('t') ||
|
11
|
+
options.delete('template') ||
|
12
|
+
File.join(Gem.loaded_specs['ruql'].full_gem_path, 'templates/html5.html.erb')
|
13
|
+
@output = ''
|
14
|
+
@quiz = quiz
|
15
|
+
@h = Builder::XmlMarkup.new(:target => @output, :indent => 2)
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_quiz
|
19
|
+
if @template
|
20
|
+
render_with_template do
|
21
|
+
render_questions
|
22
|
+
@output
|
23
|
+
end
|
24
|
+
else
|
25
|
+
@h.html do
|
26
|
+
@h.head do
|
27
|
+
@h.title @quiz.title
|
28
|
+
@h.link(:rel => 'stylesheet', :type =>'text/css', :href=>@css) if @css
|
29
|
+
end
|
30
|
+
@h.body do
|
31
|
+
render_questions
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def render_with_template
|
39
|
+
# local variables that should be in scope in the template
|
40
|
+
quiz = @quiz
|
41
|
+
# the ERB template includes 'yield' where questions should go:
|
42
|
+
output = ERB.new(IO.read(File.expand_path @template)).result(binding)
|
43
|
+
@output = output
|
44
|
+
end
|
45
|
+
|
46
|
+
def render_questions
|
47
|
+
render_random_seed
|
48
|
+
@h.ol :class => 'questions' do
|
49
|
+
@quiz.questions.each_with_index do |q,i|
|
50
|
+
case q
|
51
|
+
when MultipleChoice, SelectMultiple, TrueFalse then render_multiple_choice(q,i)
|
52
|
+
when FillIn then render_fill_in(q, i)
|
53
|
+
else
|
54
|
+
raise "Unknown question type: #{q}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def render_multiple_choice(q,index)
|
62
|
+
render_question_text(q, index) do
|
63
|
+
answers =
|
64
|
+
if q.class == TrueFalse then q.answers.sort.reverse # True always first
|
65
|
+
elsif q.randomize then q.answers.sort_by { rand }
|
66
|
+
else q.answers
|
67
|
+
end
|
68
|
+
@h.ol :class => 'answers' do
|
69
|
+
answers.each do |answer|
|
70
|
+
if @show_solutions
|
71
|
+
render_answer_for_solutions(answer, q.raw?, q.class == TrueFalse)
|
72
|
+
else
|
73
|
+
if q.raw? then @h.li { |l| l << answer.answer_text } else @h.li answer.answer_text end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def render_fill_in(q, idx)
|
82
|
+
render_question_text(q, idx) do
|
83
|
+
if @show_solutions
|
84
|
+
answer = q.answers[0]
|
85
|
+
if answer.has_explanation?
|
86
|
+
if q.raw? then @h.p(:class => 'explanation') { |p| p << answer.explanation }
|
87
|
+
else @h.p(answer.explanation, :class => 'explanation') end
|
88
|
+
end
|
89
|
+
answers = (answer.answer_text.kind_of?(Array) ? answer.answer_text : [answer.answer_text])
|
90
|
+
@h.ol :class => 'answers' do
|
91
|
+
answers.each do |answer|
|
92
|
+
if answer.kind_of?(Regexp)
|
93
|
+
answer = answer.inspect
|
94
|
+
if !q.case_sensitive
|
95
|
+
answer += 'i'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
@h.li do
|
99
|
+
if q.raw? then @h.p { |p| p << answer } else @h.p answer end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def render_answer_for_solutions(answer,raw,is_true_false = nil)
|
108
|
+
args = {:class => (answer.correct? ? 'correct' : 'incorrect')}
|
109
|
+
if is_true_false
|
110
|
+
answer.answer_text.prepend(
|
111
|
+
answer.correct? ? "CORRECT: " : "INCORRECT: ")
|
112
|
+
end
|
113
|
+
@h.li(args) do
|
114
|
+
if raw then @h.p { |p| p << answer.answer_text } else @h.p answer.answer_text end
|
115
|
+
if answer.has_explanation?
|
116
|
+
if raw then @h.p(:class => 'explanation') { |p| p << answer.explanation }
|
117
|
+
else @h.p(answer.explanation, :class => 'explanation') end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_question_text(question,index)
|
123
|
+
html_args = {
|
124
|
+
:id => "question-#{index}",
|
125
|
+
:class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')]
|
126
|
+
.join(' ')
|
127
|
+
}
|
128
|
+
@h.li html_args do
|
129
|
+
@h.div :class => 'text' do
|
130
|
+
qtext = "[#{question.points} point#{'s' if question.points>1}] " <<
|
131
|
+
('Select ALL that apply: ' if question.multiple).to_s <<
|
132
|
+
if question.class == FillIn then question.question_text.gsub(/\-+/, '_____________________________')
|
133
|
+
else question.question_text
|
134
|
+
end
|
135
|
+
if question.raw?
|
136
|
+
@h.p { |p| p << qtext }
|
137
|
+
else
|
138
|
+
qtext.each_line do |p|
|
139
|
+
@h.p do |par|
|
140
|
+
par << p # preserves HTML markup
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
yield # render answers
|
146
|
+
end
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
def quiz_header
|
151
|
+
@h.div(:id => 'student-name') do
|
152
|
+
@h.p 'Name:'
|
153
|
+
@h.p 'Student ID:'
|
154
|
+
end
|
155
|
+
if @quiz.options[:instructions]
|
156
|
+
@h.div :id => 'instructions' do
|
157
|
+
@quiz.options[:instructions].each_line { |p| @h.p p }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
def render_random_seed
|
164
|
+
@h.comment! "Seed: #{@quiz.seed}"
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
class HtmlFormRenderer
|
2
|
+
require 'builder'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
attr_reader :output
|
6
|
+
|
7
|
+
def initialize(quiz,options={})
|
8
|
+
@css = options.delete('c') || options.delete('css')
|
9
|
+
@js = options.delete('j') || options.delete('js')
|
10
|
+
@show_solutions = options.delete('s') || options.delete('solutions')
|
11
|
+
@template = options.delete('t') ||
|
12
|
+
options.delete('template') #||
|
13
|
+
#File.join(Gem.loaded_specs['ruql'].full_gem_path, 'templates/htmlform.html.erb')
|
14
|
+
@output = ''
|
15
|
+
@quiz = quiz
|
16
|
+
@h = Builder::XmlMarkup.new(:target => @output, :indent => 2)
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_quiz
|
20
|
+
if @template
|
21
|
+
render_with_template do
|
22
|
+
render_questions
|
23
|
+
@output
|
24
|
+
end
|
25
|
+
else
|
26
|
+
@h.html do
|
27
|
+
@h.head do
|
28
|
+
@h.title @quiz.title
|
29
|
+
@h.link(:rel => 'stylesheet', :type =>'text/css', :href =>@css) if @css
|
30
|
+
@h.script(:type => 'text/javascript', :src => @js) do
|
31
|
+
end if @js
|
32
|
+
end
|
33
|
+
@h.body do
|
34
|
+
render_questions
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_with_template
|
42
|
+
# local variables that should be in scope in the template
|
43
|
+
quiz = @quiz
|
44
|
+
title = "Quiz" unless @title
|
45
|
+
# the ERB template includes 'yield' where questions should go:
|
46
|
+
output = ERB.new(IO.read(File.expand_path @template)).result(binding)
|
47
|
+
@output = output
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_questions
|
51
|
+
render_random_seed
|
52
|
+
@h.form do
|
53
|
+
@h.ol :class => 'questions' do
|
54
|
+
@quiz.questions.each_with_index do |q,i|
|
55
|
+
case q
|
56
|
+
when SelectMultiple then render_select_multiple(q,i)
|
57
|
+
when MultipleChoice, TrueFalse then render_multiple_choice(q,i)
|
58
|
+
when FillIn then render_fill_in(q, i)
|
59
|
+
else
|
60
|
+
raise "Unknown question type: #{q}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@h.input(:type => 'submit', :value => 'Enviar')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def render_multiple_choice(q,index)
|
70
|
+
render_question_text(q, index) do
|
71
|
+
answers =
|
72
|
+
if q.class == TrueFalse then q.answers.sort.reverse # True always first
|
73
|
+
elsif q.randomize then q.answers.sort_by { rand }
|
74
|
+
else q.answers
|
75
|
+
end
|
76
|
+
@h.ol :class => 'answers' do
|
77
|
+
answers.each do |answer|
|
78
|
+
if @show_solutions
|
79
|
+
render_answer_for_solutions(answer, q.raw?)
|
80
|
+
else
|
81
|
+
#if q.raw? then @h.li { |l| l << answer.answer_text } else @h.li answer.answer_text end
|
82
|
+
@h.input(:type => 'radio', :name => 'a', :class => 'select') { |p|
|
83
|
+
p << answer.answer_text
|
84
|
+
p << '</br>'
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
def render_select_multiple(q,index)
|
94
|
+
render_question_text(q, index) do
|
95
|
+
answers =
|
96
|
+
if q.randomize then q.answers.sort_by { rand }
|
97
|
+
else q.answers
|
98
|
+
end
|
99
|
+
@h.ol :class => 'answers' do
|
100
|
+
answers.each do |answer|
|
101
|
+
if @show_solutions
|
102
|
+
render_answer_for_solutions(answer, q.raw?)
|
103
|
+
else
|
104
|
+
#if q.raw? then @h.li { |l| l << answer.answer_text } else @h.li answer.answer_text end
|
105
|
+
@h.input(:type => 'checkbox', :name => 'b', :class => 'check') { |p|
|
106
|
+
p << answer.answer_text
|
107
|
+
p << '</br>'
|
108
|
+
}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def render_fill_in(q, idx)
|
117
|
+
render_question_text(q, idx) do
|
118
|
+
if @show_solutions
|
119
|
+
answer = q.answers[0]
|
120
|
+
if answer.has_explanation?
|
121
|
+
if q.raw? then @h.p(:class => 'explanation') { |p| p << answer.explanation }
|
122
|
+
else @h.p(answer.explanation, :class => 'explanation') end
|
123
|
+
end
|
124
|
+
answers = (answer.answer_text.kind_of?(Array) ? answer.answer_text : [answer.answer_text])
|
125
|
+
@h.ol :class => 'answers' do
|
126
|
+
answers.each do |answer|
|
127
|
+
if answer.kind_of?(Regexp)
|
128
|
+
answer = answer.inspect
|
129
|
+
if !q.case_sensitive
|
130
|
+
answer += 'i'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
@h.li do
|
134
|
+
if q.raw? then @h.p { |p| p << answer } else @h.p answer end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def render_answer_for_solutions(answer,raw)
|
143
|
+
args = {:class => (answer.correct? ? 'correct' : 'incorrect')}
|
144
|
+
@h.li(args) do
|
145
|
+
if raw then @h.p { |p| p << answer.answer_text } else @h.p answer.answer_text end
|
146
|
+
if answer.has_explanation?
|
147
|
+
if raw then @h.p(:class => 'explanation') { |p| p << answer.explanation }
|
148
|
+
else @h.p(answer.explanation, :class => 'explanation') end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def render_question_text(question,index)
|
154
|
+
html_args = {
|
155
|
+
:id => "question-#{index}",
|
156
|
+
:class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')]
|
157
|
+
.join(' ')
|
158
|
+
}
|
159
|
+
@h.li html_args do
|
160
|
+
@h.div :class => 'text' do
|
161
|
+
qtext = "[#{question.points} point#{'s' if question.points>1}] " <<
|
162
|
+
('Select ALL that apply: ' if question.multiple).to_s <<
|
163
|
+
if question.class == FillIn then question.question_text.gsub(/\-+/, '')
|
164
|
+
else question.question_text
|
165
|
+
end
|
166
|
+
#if question.raw?
|
167
|
+
# @h.p { |p| p << qtext }
|
168
|
+
#else
|
169
|
+
qtext.each_line do |p|
|
170
|
+
@h.p do |par|
|
171
|
+
par << p # preserves HTML markup
|
172
|
+
@h.input(:type => 'text', :class => 'fillin') if (question.class == FillIn)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
#end
|
176
|
+
end
|
177
|
+
yield # render answers
|
178
|
+
end
|
179
|
+
self
|
180
|
+
end
|
181
|
+
|
182
|
+
def quiz_header
|
183
|
+
@h.div(:id => 'student-name') do
|
184
|
+
@h.p 'Name:'
|
185
|
+
@h.p 'Student ID:'
|
186
|
+
end
|
187
|
+
if @quiz.options[:instructions]
|
188
|
+
@h.div :id => 'instructions' do
|
189
|
+
@quiz.options[:instructions].each_line { |p| @h.p p }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
def render_random_seed
|
196
|
+
@h.comment! "Seed: #{@quiz.seed}"
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class JSONRenderer
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
attr_reader :output
|
5
|
+
|
6
|
+
def initialize(quiz,options={})
|
7
|
+
@output = ''
|
8
|
+
@json_array = []
|
9
|
+
@quiz = quiz
|
10
|
+
end
|
11
|
+
|
12
|
+
def render_quiz
|
13
|
+
@output = @quiz.questions.map {|question| JSON.pretty_generate(question.to_JSON)}
|
14
|
+
end
|
15
|
+
end
|