ruqlcqb 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d81b97a766f2c8045e559d68b15f609fa8ba1c1
4
+ data.tar.gz: 243f8d96f129e17672957bdda0895a83f641d046
5
+ SHA512:
6
+ metadata.gz: 9f62bd2e0b3c1f1cff9e89850ada70ef0e5dff7397ee0c5db2d6f995e5da6b205018807834f51a0aa20e288f14389d1f4877895d960acda559d4a6e99483ebc7
7
+ data.tar.gz: 0c88b4a37c4aaa43c7f3a0eb7961e1e757f06dd7a83a8958eff8504cef2246f61b268d8ae3119a9301f9476706be28266837ba7647e03209013d5ecc94ed9e60
data/bin/ruqlcqb ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ruql'
4
+ require 'getopt/long'
5
+
6
+ def usage
7
+ name = File.basename $0
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
+ HtmlForm - HTML forms
15
+ EdXml - XML for OpenEdX platform in-course questions
16
+ AutoQCM - LaTeX for use with AutoQCM (http://home.gna.org/auto-qcm)
17
+ Coursera - [obsolete] Coursera HTML/XML format for online auto-grading
18
+ JSON - [partially implemented] JSON format
19
+ Qualtrics -[partially implemented] Qualtrics survey (txt) format
20
+
21
+ Global options:
22
+ -l <loglevel>, --log=<loglevel>
23
+ In increasing verbosity, they are 'error' (nonfatal), 'warn', 'info',
24
+ 'debug'; default is 'warn'
25
+ -p <penalty>, --penalty <penalty>
26
+ The penalty for WRONG answers, as a fraction of the penalty for
27
+ RIGHT answers. For a 2-point question, penalty=0.25 means 1/2 point
28
+ (0.25 * 2) deducted for WRONG answer. Default is 0. Not all
29
+ output formats are able to do something useful with this.
30
+
31
+ The EdXML renderer supports these options:
32
+ -n <name>, --name=<name>
33
+ Only render the question(s) that have :name => 'name'.
34
+ NOTE: Some markup that is legal in RuQL questions will break the EdX parser.
35
+ Manually check your questions as you enter them into EdX. Code seems to
36
+ be particularly troublesome.
37
+ NOTE: The 'points' and 'randomize' attributes of questions are not honored by
38
+ some systems.
39
+
40
+ The HTML5 and HTML Forms renderers supports these options:
41
+ -c <href>, --css=<href>
42
+ embed <href> for stylesheet into generated HTML5
43
+ -j <src>, --js=<src>
44
+ embed <src> for JavaScrips
45
+ -t <file.html.erb>, --template=<file.html.erb>
46
+ Use file.html.erb as HTML template rather than generating our own file.
47
+ file.html.erb should have <%= yield %> where questions should go.
48
+ An example is in the templates/ directory of RuQL.
49
+ The following local variables will be replaced with their values in
50
+ the template:
51
+ <%= quiz.title %> - the quiz title
52
+ <%= quiz.num_questions %> - total number of questions
53
+ <%= quiz.points %> - total number of points for whole quiz
54
+ -s, --solutions
55
+ generate solutions (showing correct answers and explanations)
56
+ NOTE: If there is more than one quiz (collection of questions) in the file,
57
+ a complete <html>...</html> block is produced in the output for EACH quiz.
58
+
59
+ The AutoQCM renderer supports these options:
60
+ -t <file.tex.erb>, --template=<file.tex.erb>
61
+ MANDATORY: Use file.tex.erb as LaTeX/AutoQCM template.
62
+ The file should have <%= yield %> where questions should go.
63
+ See the description of template under HTML5 renderer for variable
64
+ substitutions that can occur in the quiz body.
65
+
66
+ The JSON renderer currently supports no options
67
+
68
+ The Qualtrics renderer supports these options:
69
+ -t <file.txt.erb>, --template=<file.txt.erb>
70
+ The file should have <%= yield %> where questions should go. Since this just creates survey questions, grading information is ignored.Currently supports the same type of questions as the AutoQCM renderer.
71
+
72
+ eos
73
+ exit
74
+ end
75
+
76
+ def main
77
+ filename = ARGV.shift
78
+ raise "Cannot read #{filename}" unless File.readable? filename
79
+ renderer = ARGV.shift
80
+ raise "Unknown renderer '#{renderer}'" unless Quiz.get_renderer(renderer)
81
+ opts = Getopt::Long.getopts(
82
+ ['-c', '--css', Getopt::REQUIRED],
83
+ ['-j', '--js', Getopt::REQUIRED],
84
+ ['-t', '--template', Getopt::REQUIRED],
85
+ ['-s', '--solutions', Getopt::BOOLEAN],
86
+ ['-n', '--name', Getopt::REQUIRED],
87
+ ['-l', '--log', Getopt::REQUIRED],
88
+ )
89
+ Quiz.instance_eval "#{IO.read(filename)}"
90
+ Quiz.quizzes.each { |quiz| puts quiz.render_with(renderer, opts) }
91
+ end
92
+
93
+ usage if ARGV.length < 2
94
+ main
95
+
data/lib/ruql.rb ADDED
@@ -0,0 +1,23 @@
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/html_form_renderer'
11
+ require 'ruql/renderers/edxml_renderer'
12
+ require 'ruql/renderers/auto_qcm_renderer'
13
+ require 'ruql/renderers/json_renderer'
14
+ require 'ruql/renderers/qualtrics_renderer'
15
+
16
+ # question types
17
+ require 'ruql/quiz'
18
+ require 'ruql/question'
19
+ require 'ruql/answer'
20
+ require 'ruql/multiple_choice'
21
+ require 'ruql/select_multiple'
22
+ require 'ruql/true_false'
23
+ require 'ruql/fill_in'
@@ -0,0 +1,28 @@
1
+ class Answer
2
+ include Comparable
3
+ attr_accessor :answer_text, :explanation, :correct
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 = false, explanation=nil)
12
+ @answer_text = answer_text
13
+ @correct = !!correct # ensure boolean
14
+ @explanation = explanation
15
+ end
16
+
17
+ def to_JSON
18
+ Hash[instance_variables.collect { |var| [var.to_s.delete('@'), instance_variable_get(var)] }]
19
+ end
20
+
21
+ def self.from_JSON(hash)
22
+ answer = Answer.new
23
+ hash.each do |key, value|
24
+ answer.send((key + '=').to_sym, value)
25
+ end
26
+ answer
27
+ end
28
+ 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,11 @@
1
+ class MultipleChoice < Question
2
+ attr_accessor :multiple
3
+
4
+ def initialize(text='', opts={})
5
+ super
6
+ self.question_text = text
7
+ self.multiple = !!opts[:multiple]
8
+ self.randomize = !!opts[:randomize]
9
+ end
10
+
11
+ end
@@ -0,0 +1,73 @@
1
+ class Question
2
+ attr_accessor :question_text, :answers, :randomize, :points, :name, :question_tags, :question_comment, :raw
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
+ def answer_helper(obj)
48
+ if obj.is_a? Array and obj.size and obj[0].is_a? Answer
49
+ return obj.map {|answer| answer.to_JSON}
50
+ end
51
+ obj
52
+ end
53
+
54
+ #creates a JSON hash of the object with its object name. we should convert this to a mixin for answer and question. aaron
55
+ def to_JSON
56
+ h = Hash[instance_variables.collect { |var| [var.to_s.delete('@'), answer_helper(instance_variable_get(var))] }]
57
+ h['question_type'] = self.class.to_s
58
+ return h
59
+ end
60
+
61
+ #factory method to return correct type of question
62
+ def self.from_JSON(hash_str)
63
+ hash = JSON.parse(hash_str)
64
+ #create the appropriate class of the object from the hash's class name
65
+ question = Object.const_get(hash.fetch('question_type')).new()
66
+ hash.reject{|key| key == 'answers' or key == 'question_type'}.each do |key, value|
67
+ question.send((key + '=').to_sym, value)
68
+ end
69
+ question.answers = hash['answers'].map{|answer_hash| Answer.from_JSON(answer_hash)}
70
+ question
71
+ end
72
+
73
+ 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,85 @@
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
+ @show_solutions = options.delete('s') || options.delete('solutions')
16
+
17
+ end
18
+
19
+ def render_quiz
20
+ quiz = @quiz # make quiz object available in template's scope
21
+ with_erb_template(IO.read(File.expand_path @template)) do
22
+ output = ''
23
+ render_random_seed
24
+ @quiz.questions.each_with_index do |q,i|
25
+ next_question = render_question q,i
26
+ output << next_question
27
+ end
28
+ output
29
+ end
30
+ end
31
+
32
+ def with_erb_template(template)
33
+ # template will 'yield' back to render_quiz to render the questions
34
+ @output = ERB.new(template).result(binding)
35
+ end
36
+
37
+ def render_question(q,index)
38
+ case q
39
+ when SelectMultiple,TrueFalse then render(q, index, 'mult') # These are subclasses of MultipleChoice, should go first
40
+ when MultipleChoice then render(q, index)
41
+ else
42
+ @quiz.logger.error "Question #{index} (#{q.question_text[0,15]}...): AutoQCM can only handle multiple_choice, truefalse, or select_multiple questions"
43
+ ''
44
+ end
45
+ end
46
+
47
+ def render_random_seed
48
+ seed = @quiz.seed
49
+ @output << "\n%% Random seed: #{seed}\n"
50
+ @output << "\\AMCrandomseed{#{seed}}\n\n"
51
+ end
52
+
53
+ def render(question, index, type='')
54
+ output = ''
55
+ output << "\\begin{question#{type}}{q#{index}}\n"
56
+ output << " \\scoring{b=#{question.points},m=#{@penalty*question.points}}\n"
57
+ if type == 'mult'
58
+ question.question_text = "Select ALL that apply. " + question.question_text
59
+ elsif type == ''
60
+ question.question_text = "Choose ONE answer. " + question.question_text
61
+ end
62
+ output << " " << to_tex(question.question_text) << "\n"
63
+
64
+ # answers - ignore randomization
65
+
66
+ output << " \\begin{choices}\n"
67
+ question.answers.each do |answer|
68
+ answer_text = to_tex(answer.answer_text)
69
+ answer_type = if answer.correct? then 'correct' else 'wrong' end
70
+ output << " \\#{answer_type}choice{#{answer_text}}\n"
71
+ if @show_solutions and answer.explanation
72
+ explanation = to_tex(answer.explanation)
73
+ if answer_type == 'wrong'
74
+ output << "{\\color{red}\\tab #{explanation}}"
75
+ else
76
+ output << "{\\color[rgb]{0,.5,0}\\tab #{explanation}}"
77
+ end
78
+ end
79
+ end
80
+ output << " \\end{choices}\n"
81
+ output << "\\end{question#{type}}\n\n"
82
+ output
83
+ end
84
+
85
+ end