ruqlcqb 0.0.0

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