ruql_cqb 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/ruql +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, 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: ruql_cqb
|
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-13 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
|
+
- ruql
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- bin/ruql
|
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: []
|