ruqlcqb 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,63 @@
|
|
1
|
+
class QualtricsRenderer
|
2
|
+
|
3
|
+
attr_reader :output
|
4
|
+
|
5
|
+
def initialize(quiz, options={})
|
6
|
+
@output = ''
|
7
|
+
@quiz = quiz
|
8
|
+
@template = options.delete('t') || options.delete('template')
|
9
|
+
end
|
10
|
+
|
11
|
+
def render_quiz
|
12
|
+
quiz = @quiz # make quiz object available in template's scope
|
13
|
+
with_erb_template(IO.read(File.expand_path @template)) do
|
14
|
+
output = ''
|
15
|
+
@quiz.questions.each_with_index do |q,i|
|
16
|
+
next_question = render_question q,i
|
17
|
+
output << next_question
|
18
|
+
end
|
19
|
+
output
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_erb_template(template)
|
24
|
+
# template will 'yield' back to render_quiz to render the questions
|
25
|
+
@output = ERB.new(template).result(binding)
|
26
|
+
end
|
27
|
+
|
28
|
+
def render_question(q,index)
|
29
|
+
case q
|
30
|
+
when SelectMultiple,TrueFalse then render(q, index, 'Multiple') # These are subclasses of MultipleChoice, should go first
|
31
|
+
when MultipleChoice then render(q, index, 'Single')
|
32
|
+
else
|
33
|
+
@quiz.logger.error "Question #{index} (#{q.question_text[0,15]}...): Only written to handle multiple_choice, truefalse, or select_multiple questions at this time."
|
34
|
+
''
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def render(question, index, type='')
|
39
|
+
output = ''
|
40
|
+
output << "[[Question:MC:#{type}Answer]]\n"
|
41
|
+
output << "[[ID:#{index}]]\n"
|
42
|
+
if type == 'Multiple'
|
43
|
+
question.question_text = "Select ALL that apply. " + question.question_text
|
44
|
+
elsif type == 'Single'
|
45
|
+
question.question_text = "Choose ONE answer. " + question.question_text
|
46
|
+
end
|
47
|
+
output << question.question_text << "\n"
|
48
|
+
|
49
|
+
# answers - ignore randomization
|
50
|
+
|
51
|
+
output << "[[AdvancedChoices]]\n"
|
52
|
+
question.answers.each do |answer|
|
53
|
+
output << "[[Choice]]\n"
|
54
|
+
output << "#{answer.answer_text}\n"
|
55
|
+
end
|
56
|
+
if type == 'Multiple'
|
57
|
+
output << "[[Choice]]\n"
|
58
|
+
output << "<i>None of these answers are correct.</i>\n"
|
59
|
+
end
|
60
|
+
output
|
61
|
+
end
|
62
|
+
|
63
|
+
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
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module TexOutput
|
2
|
+
|
3
|
+
def add_newlines(str)
|
4
|
+
str.gsub(Regexp.new('<pre>(.*?)</pre>', Regexp::MULTILINE | Regexp::IGNORECASE) ) do |code_section|
|
5
|
+
code_section.gsub!("\n"){"\\\\"}
|
6
|
+
code_section.gsub!(" "){"\\hspace*{2em}"}
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
@@tex_replace = {
|
11
|
+
/_/ => '\textunderscore{}',
|
12
|
+
Regexp.new('<tt>([^<]+)</tt>', Regexp::IGNORECASE) => "\\texttt{\\1}",
|
13
|
+
Regexp.new('<pre>(.*?)</pre>', Regexp::MULTILINE | Regexp::IGNORECASE) => "\\texttt{\\1}",
|
14
|
+
}
|
15
|
+
|
16
|
+
@@tex_escape = Regexp.new '\$|&|%|#|\{|\}'
|
17
|
+
|
18
|
+
def to_tex(str)
|
19
|
+
str = str.gsub(@@tex_escape) { |match| "\\#{match}" }
|
20
|
+
str = add_newlines(str)
|
21
|
+
|
22
|
+
@@tex_replace.each_pair do |match, replace|
|
23
|
+
str = str.gsub(match, replace)
|
24
|
+
end
|
25
|
+
str
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class TrueFalse < Question
|
2
|
+
|
3
|
+
def initialize(text='', correct_answer=false, opts=nil)
|
4
|
+
super
|
5
|
+
opts ||= {}
|
6
|
+
opts[:explanation] ||= ''
|
7
|
+
correct_answer = !!correct_answer # ensure 'true' or 'false' only
|
8
|
+
self.question_text = "True or False: #{text}"
|
9
|
+
self.answer correct_answer.to_s.capitalize
|
10
|
+
self.distractor (!correct_answer).to_s.capitalize, :explanation => opts[:explanation]
|
11
|
+
end
|
12
|
+
|
13
|
+
def multiple ; false ; end
|
14
|
+
def incorrect_answer ; self.answers.reject(&:correct).first ; end
|
15
|
+
def explanation ; incorrect_answer.explanation ; end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= yield %>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title><%= quiz.title %></title>
|
4
|
+
<style type="text/css" media="all">
|
5
|
+
body { font-family: Times, serif; }
|
6
|
+
.header { text-align: right; font-weight: bold; padding-right: 30%; line-height: 300%; }
|
7
|
+
h1 { text-align: center; }
|
8
|
+
ol.questions { list-style-type: number; }
|
9
|
+
li.question { page-break-inside: avoid; border-bottom: 1px solid grey; }
|
10
|
+
li.multiplechoice ol.answers { list-style-type: lower-alpha; }
|
11
|
+
li.selectmultiple ol.answers { vertical-align: center; list-style-type: none; list-style-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAEklEQVQImWNgAIL6/w+ogoEAAKI4Kp2NVIeDAAAAAElFTkSuQmCC'); }
|
12
|
+
li.truefalse ol.answers { list-style-type: none; }
|
13
|
+
.correct { color: green; font-weight: bold; border: 1px solid black; padding: 0.2ex; }
|
14
|
+
.incorrect { color: red }
|
15
|
+
.explanation { font-style: italic }
|
16
|
+
.instructions { clear: both; border: 1px solid black; align: center; padding: 2ex; margin: 2ex; }
|
17
|
+
</style>
|
18
|
+
</head>
|
19
|
+
<body>
|
20
|
+
<div class="header">
|
21
|
+
<div id="name">Name:</div>
|
22
|
+
<div id="sid">SID:</div>
|
23
|
+
</div>
|
24
|
+
<h1><%= quiz.title %></h1>
|
25
|
+
<div class="instructions">
|
26
|
+
<ul>
|
27
|
+
<li>No books, notes, or electronic devices allowed. </li>
|
28
|
+
<li>Time limit is 30 minutes.</li>
|
29
|
+
<li><%= quiz.num_questions %> multiple-choice questions, points indicated per question,
|
30
|
+
<%= quiz.points %> points total. Points per question are intended
|
31
|
+
to reflect approximate times they should take, at about 1 point per minute.</li>
|
32
|
+
<li>For 'select all that apply' questions worth N points,
|
33
|
+
you get 1/N of the points for each RIGHT answer that you check, plus
|
34
|
+
1/N of the points for each WRONG answer that you correctly
|
35
|
+
leave unchecked. That is, equal weight is given to deciding
|
36
|
+
whether each choice is part of the right answer or not.</li>
|
37
|
+
</ul>
|
38
|
+
<b>Good skill!</b>
|
39
|
+
</div>
|
40
|
+
<%= yield %>
|
41
|
+
</body>
|
42
|
+
</html>
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title><%= quiz.title %></title>
|
4
|
+
<style type="text/css" media="all">
|
5
|
+
body { font-family: Times, serif; }
|
6
|
+
.header { text-align: right; font-weight: bold; padding-right: 30%; line-height: 300%; }
|
7
|
+
h1 { text-align: center; }
|
8
|
+
ol.questions { list-style-type: number; }
|
9
|
+
li.question { page-break-inside: avoid; border-bottom: 1px solid grey; padding-bottom: 2ex; }
|
10
|
+
li.multiplechoice ol.answers { list-style-type: lower-alpha; }
|
11
|
+
li.multiplechoice ol.answers li { padding-bottom: 0.5ex; }
|
12
|
+
li.selectmultiple ol.answers { vertical-align: center; list-style-type: none; list-style-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUAQMAAAC3R49OAAAABlBMVEUAAAD///+l2Z/dAAAAEklEQVQImWNgAIL6/w+ogoEAAKI4Kp2NVIeDAAAAAElFTkSuQmCC'); }
|
13
|
+
li.truefalse ol.answers { list-style-type: none; }
|
14
|
+
li.truefalse ol.answers li { width: 15%; display: inline-block; }
|
15
|
+
.correct { color: green }
|
16
|
+
.incorrect { color: red }
|
17
|
+
.explanation { font-style: italic }
|
18
|
+
.instructions { clear: both; border: 1px solid black; align: center; padding: 2ex; margin: 2ex; }
|
19
|
+
</style>
|
20
|
+
</head>
|
21
|
+
<body>
|
22
|
+
<div class="header">
|
23
|
+
<div id="name">Name:</div>
|
24
|
+
<div id="sid">SID:</div>
|
25
|
+
</div>
|
26
|
+
<h1><%= quiz.title %></h1>
|
27
|
+
<div class="instructions">
|
28
|
+
<ul>
|
29
|
+
<li>No books, notes, or electronic devices allowed. </li>
|
30
|
+
<li>Time limit is 30 minutes.</li>
|
31
|
+
<li><%= quiz.num_questions %> multiple-choice questions, points indicated per question,
|
32
|
+
<%= quiz.points %> points total. Points per question are intended
|
33
|
+
to reflect approximate times they should take, at about 1 point per minute.</li>
|
34
|
+
<li>For 'select all that apply' questions worth N points,
|
35
|
+
you get 1/N of the points for each RIGHT answer that you check, plus
|
36
|
+
1/N of the points for each WRONG answer that you correctly
|
37
|
+
leave unchecked. That is, equal weight is given to deciding
|
38
|
+
whether each choice is part of the right answer or not.</li>
|
39
|
+
</ul>
|
40
|
+
<b>Good skill!</b>
|
41
|
+
</div>
|
42
|
+
<%= yield %>
|
43
|
+
</body>
|
44
|
+
</html>
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruqlcqb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Armando Fox
|
8
|
+
- Aaron Zhang
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-04-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: builder
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: getopt
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
description: Ruby-embedded DSL for creating short-answer quiz questions
|
43
|
+
email: aaron.zhang@berkeley.edu
|
44
|
+
executables:
|
45
|
+
- ruqlcqb
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- bin/ruqlcqb
|
50
|
+
- lib/ruql.rb
|
51
|
+
- lib/ruql/answer.rb
|
52
|
+
- lib/ruql/fill_in.rb
|
53
|
+
- lib/ruql/multiple_choice.rb
|
54
|
+
- lib/ruql/question.rb
|
55
|
+
- lib/ruql/quiz.rb
|
56
|
+
- lib/ruql/renderer.rb
|
57
|
+
- lib/ruql/renderers/auto_qcm_renderer.rb
|
58
|
+
- lib/ruql/renderers/edxml_renderer.rb
|
59
|
+
- lib/ruql/renderers/html5_renderer.rb
|
60
|
+
- lib/ruql/renderers/html_form_renderer.rb
|
61
|
+
- lib/ruql/renderers/json_renderer.rb
|
62
|
+
- lib/ruql/renderers/qualtrics_renderer.rb
|
63
|
+
- lib/ruql/renderers/xml_renderer.rb
|
64
|
+
- lib/ruql/select_multiple.rb
|
65
|
+
- lib/ruql/tex_output.rb
|
66
|
+
- lib/ruql/true_false.rb
|
67
|
+
- templates/autoqcm.tex.erb
|
68
|
+
- templates/html5.html.erb
|
69
|
+
- templates/htmlform.html.erb
|
70
|
+
homepage: http://github.com/saasbook/ruql
|
71
|
+
licenses:
|
72
|
+
- CC By-SA
|
73
|
+
metadata: {}
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 2.4.5
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: Ruby question language
|
94
|
+
test_files: []
|