ruql 0.0.2

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.
data/bin/ruql ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ruql'
4
+ require 'getopt/long'
5
+
6
+ def usage
7
+ name = $0.gsub( Regexp.new '^[^/]+/', '')
8
+ STDERR.puts <<eos
9
+ Usage: #{name} filename.rb renderer [options]
10
+ filename.rb contains questions expressed in RuQL
11
+
12
+ renderer choices are:
13
+ Html5 - HTML 5 suitable for Web display/printing
14
+ EdXml - XML for OpenEdX platform in-course questions
15
+ AutoQCM - LaTeX for use with AutoQCM (http://home.gna.org/auto-qcm)
16
+ Coursera- [obsolete] Coursera HTML/XML format for online auto-grading
17
+ JSON - [partially implemented] JSON format
18
+
19
+ Global options:
20
+ -l <loglevel>, --log=<loglevel>
21
+ In increasing verbosity, they are 'error' (nonfatal), 'warn', 'info',
22
+ 'debug'; default is 'warn'
23
+ -p <penalty>, --penalty <penalty>
24
+ The penalty for WRONG answers, as a fraction of the penalty for
25
+ RIGHT answers. For a 2-point question, penalty=0.25 means 1/2 point
26
+ (0.25 * 2) deducted for WRONG answer. Default is 0. Not all
27
+ output formats are able to do something useful with this.
28
+
29
+ The EdXML renderer supports these options:
30
+ -n <name>, --name=<name>
31
+ Only render the question(s) that have :name => 'name'.
32
+ NOTE: Some markup that is legal in RuQL questions will break the EdX parser.
33
+ Manually check your questions as you enter them into EdX. Code seems to
34
+ be particularly troublesome.
35
+ NOTE: The 'points' and 'randomize' attributes of questions are not honored by
36
+ some systems.
37
+
38
+ The HTML5 renderer supports these options:
39
+ -c <href>, --css=<href>
40
+ embed <href> for stylesheet into generated HTML5
41
+ -t <file.html.erb>, --template=<file.html.erb>
42
+ Use file.html.erb as HTML template rather than generating our own file.
43
+ file.html.erb should have <%= yield %> where questions should go.
44
+ An example is in the templates/ directory of RuQL.
45
+ The following local variables will be replaced with their values in
46
+ the template:
47
+ <%= quiz.title %> - the quiz title
48
+ <%= quiz.num_questions %> - total number of questions
49
+ <%= quiz.points %> - total number of points for whole quiz
50
+ -s, --solutions
51
+ generate solutions (showing correct answers and explanations)
52
+ NOTE: If there is more than one quiz (collection of questions) in the file,
53
+ a complete <html>...</html> block is produced in the output for EACH quiz.
54
+
55
+ The AutoQCM renderer supports these options:
56
+ -t <file.tex.erb>, --template=<file.tex.erb>
57
+ MANDATORY: Use file.tex.erb as LaTeX/AutoQCM template.
58
+ The file should have <%= yield %> where questions should go.
59
+ See the description of template under HTML5 renderer for variable
60
+ substitutions that can occur in the quiz body.
61
+
62
+ The JSON renderer currently supports no options
63
+
64
+ eos
65
+ exit
66
+ end
67
+
68
+ def main
69
+ filename = ARGV.shift
70
+ raise "Cannot read #{filename}" unless File.readable? filename
71
+ renderer = ARGV.shift
72
+ raise "Unknown renderer '#{renderer}'" unless Quiz.get_renderer(renderer)
73
+ opts = Getopt::Long.getopts(
74
+ ['-c', '--css', Getopt::REQUIRED],
75
+ ['-t', '--template', Getopt::REQUIRED],
76
+ ['-s', '--solutions', Getopt::BOOLEAN],
77
+ ['-n', '--name', Getopt::REQUIRED],
78
+ ['-l', '--log', Getopt::REQUIRED],
79
+ )
80
+ Quiz.instance_eval "#{IO.read(filename)}"
81
+ Quiz.quizzes.each { |quiz| puts quiz.render_with(renderer, opts) }
82
+ end
83
+
84
+ usage if ARGV.length < 2
85
+ main
86
+
@@ -0,0 +1,16 @@
1
+ class Answer
2
+ include Comparable
3
+ attr_accessor :answer_text, :explanation
4
+ attr_reader :correct
5
+ attr_reader :builder
6
+ attr_reader :question
7
+
8
+ def <=>(other) ; self.answer_text <=> other.answer_text ; end
9
+ def correct? ; !!correct ; end
10
+ def has_explanation? ; !!explanation ; end
11
+ def initialize(answer_text, correct, explanation=nil)
12
+ @answer_text = answer_text
13
+ @correct = !!correct # ensure boolean
14
+ @explanation = explanation
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ class FillIn < Question
2
+
3
+ attr_accessor :order
4
+ attr_accessor :case_sensitive
5
+
6
+ def initialize(text='', opts={})
7
+ super
8
+ self.question_text = text
9
+ self.order = !!opts[:order]
10
+ self.case_sensitive = !!opts[:case_sensitive]
11
+ end
12
+
13
+ def multiple ; false ; end
14
+
15
+ end
@@ -0,0 +1,12 @@
1
+ class MultipleChoice < Question
2
+
3
+ attr_accessor :multiple
4
+
5
+ def initialize(text='', opts={})
6
+ super
7
+ self.question_text = text
8
+ self.multiple = !!opts[:multiple]
9
+ self.randomize = !!opts[:randomize]
10
+ end
11
+
12
+ end
@@ -0,0 +1,47 @@
1
+ class Question
2
+ attr_accessor :question_text, :answers, :randomize, :points, :name, :question_tags, :question_comment
3
+
4
+ def initialize(*args)
5
+ options = if args[-1].kind_of?(Hash) then args[-1] else {} end
6
+ @answers = options[:answers] || []
7
+ @points = [options[:points].to_i, 1].max
8
+ @raw = options[:raw]
9
+ @name = options[:name]
10
+ @question_tags = []
11
+ @question_comment = ''
12
+ end
13
+
14
+ def raw? ; !!@raw ; end
15
+
16
+ def text(s) ; @question_text = s ; end
17
+
18
+ def explanation(text)
19
+ @answers.each { |answer| answer.explanation ||= text }
20
+ end
21
+
22
+ def answer(text, opts={})
23
+ @answers << Answer.new(text, correct=true, opts[:explanation])
24
+ end
25
+
26
+ def distractor(text, opts={})
27
+ @answers << Answer.new(text, correct=false, opts[:explanation])
28
+ end
29
+
30
+ # these are ignored but legal for now:
31
+ def tags(*args) # string or array of strings
32
+ if args.length > 1
33
+ @question_tags += args.map(&:to_s)
34
+ else
35
+ @question_tags << args.first.to_s
36
+ end
37
+ end
38
+
39
+ def comment(str = '')
40
+ @question_comment = str.to_s
41
+ end
42
+
43
+ def correct_answer ; @answers.detect(&:correct?) ; end
44
+
45
+ def correct_answers ; @answers.collect(&:correct?) ; end
46
+
47
+ end
data/lib/ruql/quiz.rb ADDED
@@ -0,0 +1,105 @@
1
+
2
+ class Quiz
3
+ @@quizzes = []
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
+ }
22
+
23
+ attr_reader :renderer
24
+ attr_reader :questions
25
+ attr_reader :options
26
+ attr_reader :output
27
+ attr_reader :seed
28
+ attr_reader :logger
29
+ attr_accessor :title
30
+
31
+ def initialize(title, options={})
32
+ @output = ''
33
+ @questions = options[:questions] || []
34
+ @title = title
35
+ @options = @@default_options.merge(options)
36
+ @seed = srand
37
+ @logger = Logger.new(STDERR)
38
+ @logger.level = Logger.const_get (options.delete('l') ||
39
+ options.delete('log') || 'warn').upcase
40
+ end
41
+
42
+ def self.get_renderer(renderer)
43
+ Object.const_get(renderer.to_s + 'Renderer') rescue nil
44
+ end
45
+
46
+ def render_with(renderer,options={})
47
+ srand @seed
48
+ @renderer = Quiz.get_renderer(renderer).send(:new,self,options)
49
+ @renderer.render_quiz
50
+ @output = @renderer.output
51
+ end
52
+
53
+ def points ; questions.map(&:points).inject { |sum,points| sum + points } ; end
54
+
55
+ def num_questions ; questions.length ; end
56
+
57
+ def random_seed(num)
58
+ @seed = num.to_i
59
+ end
60
+
61
+ # this should really be done using mixins.
62
+ def choice_answer(*args, &block)
63
+ if args.first.is_a?(Hash) # no question text
64
+ q = MultipleChoice.new('',*args)
65
+ else
66
+ text = args.shift
67
+ q = MultipleChoice.new(text, *args)
68
+ end
69
+ q.instance_eval(&block)
70
+ @questions << q
71
+ end
72
+
73
+ def select_multiple(*args, &block)
74
+ if args.first.is_a?(Hash) # no question text
75
+ q = SelectMultiple.new('',*args)
76
+ else
77
+ text = args.shift
78
+ q = SelectMultiple.new(text, *args)
79
+ end
80
+ q.instance_eval(&block)
81
+ @questions << q
82
+ end
83
+
84
+ def truefalse(*args)
85
+ q = TrueFalse.new(*args)
86
+ @questions << q
87
+ end
88
+
89
+ def fill_in(*args, &block)
90
+ if args.first.is_a?(Hash) # no question text
91
+ q = FillIn.new('', *args)
92
+ else
93
+ text = args.shift
94
+ q = FillIn.new(text, *args)
95
+ end
96
+ q.instance_eval(&block)
97
+ @questions << q
98
+ end
99
+
100
+ def self.quiz(*args,&block)
101
+ quiz = Quiz.new(*args)
102
+ quiz.instance_eval(&block)
103
+ @@quizzes << quiz
104
+ end
105
+ end
@@ -0,0 +1,20 @@
1
+ class Renderer
2
+
3
+ attr_reader :quiz
4
+ def initialize(quiz)
5
+ @quiz = quiz
6
+ end
7
+
8
+ @@render_handlers = {}
9
+ def self.render(thing, &block)
10
+ @@render_handlers[thing.to_sym] = block
11
+ end
12
+ def self.renderer_for(thing)
13
+ @@render_handlers[thing.to_sym]
14
+ end
15
+
16
+ def render_quiz!
17
+ self.instance_eval@@render_handlers[:quiz].call(self.quiz)
18
+ end
19
+
20
+ end
@@ -0,0 +1,70 @@
1
+ require 'erb'
2
+ require 'ruql/tex_output'
3
+
4
+ class AutoQCMRenderer
5
+ include TexOutput
6
+ attr_reader :output
7
+
8
+ def initialize(quiz, options={})
9
+ @output = ''
10
+ @quiz = quiz
11
+ @template = options.delete('t') ||
12
+ options.delete('template') ||
13
+ File.join(Gem.loaded_specs['ruql'].full_gem_path, 'templates/autoqcm.tex.erb')
14
+ @penalty = (options.delete('p') || options.delete('penalty') || '0').to_f
15
+ end
16
+
17
+ def render_quiz
18
+ quiz = @quiz # make quiz object available in template's scope
19
+ with_erb_template(IO.read(File.expand_path @template)) do
20
+ output = ''
21
+ render_random_seed
22
+ @quiz.questions.each_with_index do |q,i|
23
+ next_question = render_question q,i
24
+ output << next_question
25
+ end
26
+ output
27
+ end
28
+ end
29
+
30
+ def with_erb_template(template)
31
+ # template will 'yield' back to render_quiz to render the questions
32
+ @output = ERB.new(template).result(binding)
33
+ end
34
+
35
+ def render_question(q,index)
36
+ case q
37
+ when MultipleChoice then render(q, index)
38
+ when SelectMultiple,TrueFalse then render(q, index, 'mult')
39
+ else
40
+ @quiz.logger.error "Question #{index} (#{q.question_text[0,15]}...): AutoQCM can only handle multiple_choice, truefalse, or select_multiple questions"
41
+ ''
42
+ end
43
+ end
44
+
45
+ def render_random_seed
46
+ seed = @quiz.seed
47
+ @output << "\n%% Random seed: #{seed}\n"
48
+ @output << "\\AMCrandomseed{#{seed}}\n\n"
49
+ end
50
+
51
+ def render(question, index, type='')
52
+ output = ''
53
+ output << "\\begin{question#{type}}{q#{index}}\n"
54
+ output << " \\scoring{b=#{question.points},m=#{@penalty*question.points}}\n"
55
+ output << " " << to_tex(question.question_text) << "\n"
56
+
57
+ # answers - ignore randomization
58
+
59
+ output << " \\begin{choices}\n"
60
+ question.answers.each do |answer|
61
+ answer_text = to_tex(answer.answer_text)
62
+ answer_type = if answer.correct? then 'correct' else 'wrong' end
63
+ output << " \\#{answer_type}choice{#{answer_text}}\n"
64
+ end
65
+ output << " \\end{choices}\n"
66
+ output << "\\end{question}\n\n"
67
+ output
68
+ end
69
+
70
+ end
@@ -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,162 @@
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?)
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)
108
+ args = {:class => (answer.correct? ? 'correct' : 'incorrect')}
109
+ @h.li(args) do
110
+ if raw then @h.p { |p| p << answer.answer_text } else @h.p answer.answer_text end
111
+ if answer.has_explanation?
112
+ if raw then @h.p(:class => 'explanation') { |p| p << answer.explanation }
113
+ else @h.p(answer.explanation, :class => 'explanation') end
114
+ end
115
+ end
116
+ end
117
+
118
+ def render_question_text(question,index)
119
+ html_args = {
120
+ :id => "question-#{index}",
121
+ :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')]
122
+ .join(' ')
123
+ }
124
+ @h.li html_args do
125
+ @h.div :class => 'text' do
126
+ qtext = "[#{question.points} point#{'s' if question.points>1}] " <<
127
+ ('Select ALL that apply: ' if question.multiple).to_s <<
128
+ if question.class == FillIn then question.question_text.gsub(/\-+/, '_____________________________')
129
+ else question.question_text
130
+ end
131
+ if question.raw?
132
+ @h.p { |p| p << qtext }
133
+ else
134
+ qtext.each_line do |p|
135
+ @h.p do |par|
136
+ par << p # preserves HTML markup
137
+ end
138
+ end
139
+ end
140
+ end
141
+ yield # render answers
142
+ end
143
+ self
144
+ end
145
+
146
+ def quiz_header
147
+ @h.div(:id => 'student-name') do
148
+ @h.p 'Name:'
149
+ @h.p 'Student ID:'
150
+ end
151
+ if @quiz.options[:instructions]
152
+ @h.div :id => 'instructions' do
153
+ @quiz.options[:instructions].each_line { |p| @h.p p }
154
+ end
155
+ end
156
+ self
157
+ end
158
+
159
+ def render_random_seed
160
+ @h.comment! "Seed: #{@quiz.seed}"
161
+ end
162
+ end
@@ -0,0 +1,49 @@
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
+ @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)
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
+
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
+
@@ -0,0 +1,8 @@
1
+ class SelectMultiple < MultipleChoice
2
+
3
+ def initialize(text='', opts={})
4
+ super
5
+ self.multiple = true
6
+ end
7
+
8
+ end
@@ -0,0 +1,18 @@
1
+ module TexOutput
2
+
3
+ @@tex_replace = {
4
+ /_/ => '\textunderscore{}',
5
+ Regexp.new('<tt>([^<]+)</tt>', Regexp::IGNORECASE) => "\\texttt{\\1}",
6
+ Regexp.new('<pre>(.*?)</pre>', Regexp::MULTILINE | Regexp::IGNORECASE) => "\\begin{Verbatim}\\1\\end{Verbatim}",
7
+ }
8
+
9
+ @@tex_escape = Regexp.new '\$|&|%|#|\{|\}'
10
+
11
+ def to_tex(str)
12
+ str = str.gsub(@@tex_escape) { |match| "\\#{match}" }
13
+ @@tex_replace.each_pair do |match, replace|
14
+ str = str.gsub(match, replace)
15
+ end
16
+ str
17
+ end
18
+ 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
data/lib/ruql.rb ADDED
@@ -0,0 +1,21 @@
1
+ # basic gems/libs we rely on
2
+ require 'builder'
3
+ require 'logger'
4
+
5
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__)))
6
+
7
+ # renderers
8
+ require 'ruql/renderers/xml_renderer'
9
+ require 'ruql/renderers/html5_renderer'
10
+ require 'ruql/renderers/edxml_renderer'
11
+ require 'ruql/renderers/auto_qcm_renderer'
12
+ require 'ruql/renderers/json_renderer'
13
+
14
+ # question types
15
+ require 'ruql/quiz'
16
+ require 'ruql/question'
17
+ require 'ruql/answer'
18
+ require 'ruql/multiple_choice'
19
+ require 'ruql/select_multiple'
20
+ require 'ruql/true_false'
21
+ require 'ruql/fill_in'
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -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,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Armando Fox
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: builder
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: getopt
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Ruby-embedded DSL for creating short-answer quiz questions
47
+ email: fox@cs.berkeley.edu
48
+ executables:
49
+ - ruql
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - lib/ruql.rb
54
+ - lib/ruql/quiz.rb
55
+ - lib/ruql/answer.rb
56
+ - lib/ruql/question.rb
57
+ - lib/ruql/renderer.rb
58
+ - lib/ruql/select_multiple.rb
59
+ - lib/ruql/fill_in.rb
60
+ - lib/ruql/multiple_choice.rb
61
+ - lib/ruql/true_false.rb
62
+ - lib/ruql/tex_output.rb
63
+ - lib/ruql/renderers/auto_qcm_renderer.rb
64
+ - lib/ruql/renderers/edxml_renderer.rb
65
+ - lib/ruql/renderers/html5_renderer.rb
66
+ - lib/ruql/renderers/json_renderer.rb
67
+ - lib/ruql/renderers/xml_renderer.rb
68
+ - templates/autoqcm.tex.erb
69
+ - templates/html5.html.erb
70
+ - bin/ruql
71
+ homepage: http://github.com/saasbook/ruql
72
+ licenses:
73
+ - CC By-SA
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 1.8.25
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Ruby question language
96
+ test_files: []