ruql 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: []