ruql 0.0.4 → 0.0.8

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: 997beabb8740224d399921151286761f842799d9
4
+ data.tar.gz: ead3b2482c09f2f2b494fff9b5fb19f41e841931
5
+ SHA512:
6
+ metadata.gz: c681825fc87751977bb73d8fc403860f8591588a11f809b3dc5ddbf8b1cc9699441b1ec3f67e8ca07d54dd5fd0fb493dcbbb794e517206d99c4ec6aba6e35e63
7
+ data.tar.gz: 08054ec2bb224f2e86be37d8e5e69ce51872150eaa47827f52fa238c8c465415dcccba861339fdd587e36b1357c06dca4f44f79ce830262a7865380c23a9631b
data/bin/ruql CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'ruql'
3
+ # require 'ruql'
4
+ require_relative '../lib/ruql'
4
5
  require 'getopt/long'
5
6
 
6
7
  def usage
@@ -16,6 +17,7 @@ renderer choices are:
16
17
  AutoQCM - LaTeX for use with AutoQCM (http://home.gna.org/auto-qcm)
17
18
  Coursera - [obsolete] Coursera HTML/XML format for online auto-grading
18
19
  JSON - [partially implemented] JSON format
20
+ Qualtrics -[partially implemented] Qualtrics survey (txt) format
19
21
 
20
22
  Global options:
21
23
  -l <loglevel>, --log=<loglevel>
@@ -36,6 +38,9 @@ The EdXML renderer supports these options:
36
38
  NOTE: The 'points' and 'randomize' attributes of questions are not honored by
37
39
  some systems.
38
40
 
41
+ -y <file.yml>, --yaml=<file.yml>
42
+ Render the questions with a specific yaml file.
43
+
39
44
  The HTML5 and HTML Forms renderers supports these options:
40
45
  -c <href>, --css=<href>
41
46
  embed <href> for stylesheet into generated HTML5
@@ -46,7 +51,7 @@ The HTML5 and HTML Forms renderers supports these options:
46
51
  file.html.erb should have <%= yield %> where questions should go.
47
52
  An example is in the templates/ directory of RuQL.
48
53
  The following local variables will be replaced with their values in
49
- the template:
54
+ the template:
50
55
  <%= quiz.title %> - the quiz title
51
56
  <%= quiz.num_questions %> - total number of questions
52
57
  <%= quiz.points %> - total number of points for whole quiz
@@ -64,6 +69,10 @@ The AutoQCM renderer supports these options:
64
69
 
65
70
  The JSON renderer currently supports no options
66
71
 
72
+ The Qualtrics renderer supports these options:
73
+ -t <file.txt.erb>, --template=<file.txt.erb>
74
+ 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.
75
+
67
76
  eos
68
77
  exit
69
78
  end
@@ -73,6 +82,7 @@ def main
73
82
  raise "Cannot read #{filename}" unless File.readable? filename
74
83
  renderer = ARGV.shift
75
84
  raise "Unknown renderer '#{renderer}'" unless Quiz.get_renderer(renderer)
85
+
76
86
  opts = Getopt::Long.getopts(
77
87
  ['-c', '--css', Getopt::REQUIRED],
78
88
  ['-j', '--js', Getopt::REQUIRED],
@@ -80,7 +90,9 @@ def main
80
90
  ['-s', '--solutions', Getopt::BOOLEAN],
81
91
  ['-n', '--name', Getopt::REQUIRED],
82
92
  ['-l', '--log', Getopt::REQUIRED],
83
- )
93
+ ['-y', '--yaml', Getopt::REQUIRED]
94
+ )
95
+ Quiz.set_yaml_file opts.delete("y") || opts.delete("yaml")
84
96
  Quiz.instance_eval "#{IO.read(filename)}"
85
97
  Quiz.quizzes.each { |quiz| puts quiz.render_with(renderer, opts) }
86
98
  end
@@ -0,0 +1,19 @@
1
+ class Dropdown < Question
2
+
3
+ DropdownChoice = Struct.new(:correct, :list)
4
+ DropdownText = Struct.new(:text)
5
+
6
+ def choice(correct, list)
7
+ @choices << DropdownChoice.new(correct, list)
8
+ end
9
+ def label(str)
10
+ @choices << DropdownChoice.new(0, [str])
11
+ end
12
+
13
+ attr_accessor :choices
14
+
15
+ def initialize(opts={})
16
+ super
17
+ self.choices = []
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ class Criterion
2
+
3
+ attr_accessor :options, :feedback, :criterion_name,
4
+ :criterion_label, :criterion_prompt
5
+
6
+ ##
7
+ # Initializes a criterion
8
+ def initialize(options={})
9
+ @options = []
10
+ @feedback = options[:feedback] || "required"
11
+ end
12
+
13
+ ##
14
+ # Sets the criterion name
15
+ def name(name) ; @criterion_name = name ; end
16
+
17
+ ##
18
+ # Sets the criterion label
19
+ def label(label) ; @criterion_label = label ; end
20
+
21
+ ##
22
+ # Sets the criterion prompt
23
+ def prompt(prompt) ; @criterion_prompt = prompt ; end
24
+
25
+ ##
26
+ # Adds an option to the block and evaluates the proc bloc
27
+ def option(*args, &block)
28
+ option = Option.new(*args)
29
+ option.instance_eval(&block)
30
+ raise "Missing option parameters" if option.missing_parameters?
31
+ options << option
32
+ end
33
+
34
+ ##
35
+ # Adds an already initialized option
36
+ def add_option(option)
37
+ options << option
38
+ end
39
+
40
+ ##
41
+ # Validation to make sure that all the required fields are in
42
+ def missing_parameters?
43
+ @criterion_name.nil? || @criterion_label.nil? || @criterion_prompt.nil?
44
+ end
45
+ end
@@ -0,0 +1,141 @@
1
+ require 'securerandom'
2
+ class OpenAssessment
3
+
4
+ attr_accessor :question_title, :prompts, :criterions, :name,
5
+ :url_name, :self_assessment, :peer_review,
6
+ :must_grade, :graded_by
7
+ attr_accessor :allow_file_upload, :allow_latex
8
+ attr_accessor :submission_start, :submission_due,
9
+ :submission_start_time, :submission_due_time,
10
+ :self_assessment_start, :self_assessment_due,
11
+ :self_assessment_start_time, :self_assessment_due_time,
12
+ :peer_review_start, :peer_review_due,
13
+ :peer_review_start_time, :peer_review_due_time
14
+ attr_accessor :question_feedback_prompt, :question_feedback_default_text
15
+ attr_accessor :yaml
16
+ attr_accessor :trainings
17
+ attr_accessor :answer, :question_scoring_guideline
18
+
19
+ @@single_question_scores =
20
+ [[5, "Excellent", "You got all of the question correct"],
21
+ [4, "Great", "You got most of the question correct"],
22
+ [3, "Good", "You got half of the question correct"],
23
+ [2, "Fair", "You got parts of the question correct"],
24
+ [1, "OK", "You got bits of the question correct"],
25
+ [0, "Poor", "You got none of the question correct"]]
26
+
27
+ @@DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/
28
+
29
+ ##
30
+ # Initializes the open assessment question
31
+ def initialize(options={}, yaml={})
32
+ @peer_review = options[:peer_review] || false
33
+ @self_assessment = options[:self_assessment]
34
+ @self_assessment = true if @self_assessment.nil?
35
+
36
+ @prompts = []
37
+ @criterions = []
38
+ @trainings = []
39
+
40
+ @url_name = options[:url_name] || SecureRandom.hex
41
+ @yaml = yaml
42
+
43
+ @must_grade = @yaml["must_grade"] || 5
44
+ @graded_by = @yaml["graded_by"] || 3
45
+
46
+ @allow_file_upload = options[:allow_file_upload] || false
47
+ @allow_latex = options[:allow_latex] || false
48
+
49
+ # Parsing start/due dates
50
+ start_date = @yaml["submission_start"] || Time.now.to_s
51
+ end_date = @yaml["submission_due"] || (Time.now + 14).to_s
52
+
53
+ peer_review_start = @yaml["peer_review_start"] || start_date
54
+ peer_review_due = @yaml["peer_review_due"] || end_date
55
+
56
+ self_assessment_start = @yaml["self_assessment_start"] || start_date
57
+ self_assessment_due = @yaml["self_assessment_due"] || end_date
58
+
59
+ @submission_start = Date.parse(start_date)
60
+ @submission_due = Date.parse(end_date)
61
+ @submission_start_time = @yaml["submission_start_time"] || "00:00"
62
+ @submission_due_time = @yaml["submission_due_time"] || "00:00"
63
+
64
+ @peer_review_start = Date.parse(peer_review_start)
65
+ @peer_review_due = Date.parse(peer_review_due)
66
+ @peer_review_start_time = @yaml["peer_review_start_time"] || "00:00"
67
+ @peer_review_due_time = @yaml["peer_review_due_time"] || "00:00"
68
+
69
+ @self_assessment_start = Date.parse(self_assessment_start)
70
+ @self_assessment_due = Date.parse(self_assessment_due)
71
+ @self_assessment_start_time = @yaml["self_assessment_start_time"] || "00:00"
72
+ @self_assessment_due_time = @yaml["self_assessment_due_time"] || "00:00"
73
+
74
+ # Default feedback settings
75
+ @question_feedback_prompt = "Leave feedback"
76
+ @question_feedback_default_text = "Let them know how they did"
77
+ end
78
+
79
+ ##
80
+ # Sets the title of the question
81
+ def title(title) ; @question_title = title ; end
82
+
83
+ ##
84
+ # Adds a prompt to the question - you must have at least one
85
+ def prompt(prompt) ; @prompts << prompt ; end
86
+
87
+ ##
88
+ # Sets the answers for a simple_open_assessment question
89
+ def answer(answer) ; @question_answer = answer ; end
90
+
91
+ ##
92
+ # Sets the scoring guidelines for a simple_open_assesment question
93
+ def scoring_guideline(score_array) ; @question_scoring_guideline = score_array ; end
94
+
95
+ ##
96
+ # Sets the feedback prompt if you want students to leave feedback
97
+ def feedback_prompt(fb_prompt) ; @question_feedback_prompt = fb_prompt ; end
98
+
99
+ ##
100
+ # Sets the default text for the feedback textarea
101
+ def feedback_default_text(fb_text) ; @question_feedback_default_text = fb_text ; end
102
+
103
+ ##
104
+ # Adds a criterion and evaluates its proc block.
105
+ def criterion(*args, &block)
106
+ criterion = Criterion.new(*args)
107
+ criterion.instance_eval(&block)
108
+
109
+ raise "Missing criterion parameters" if criterion.missing_parameters?
110
+
111
+ @criterions << criterion
112
+ end
113
+
114
+ ##
115
+ # Adds fields for a simple_open_assessment question
116
+ def add_simple_question
117
+ criterion = Criterion.new
118
+ criterion.name("How'd you do?")
119
+ criterion.label("Scoring Rubric")
120
+
121
+ raise "Must have answer for question" if @question_answer.nil?
122
+ criterion.prompt(@question_answer)
123
+
124
+ criterions << criterion
125
+ scoring_guidelines = @question_scoring_guideline || @@single_question_scores
126
+ scoring_guidelines.each do |score_array|
127
+ option = Option.new({ points: score_array[0] })
128
+ option.add_params(score_array)
129
+ criterion.add_option(option)
130
+ end
131
+ end
132
+
133
+ ##
134
+ # Adds a student training question - only used with peer review enabled questions
135
+ def student_training(*args, &block)
136
+ return unless @peer_review
137
+ training = Training.new(*args)
138
+ training.instance_eval(&block)
139
+ @trainings << training
140
+ end
141
+ end
@@ -0,0 +1,34 @@
1
+ class Option
2
+ attr_accessor :points, :opt_name, :opt_label, :opt_explanation
3
+
4
+ ##
5
+ # Initializes an option
6
+ def initialize(options={})
7
+ @points = options[:points] || 0
8
+ end
9
+
10
+ ##
11
+ # Adds name to option
12
+ def name(name) ; @opt_name = name ; end
13
+
14
+ ##
15
+ # Adds label to option
16
+ def label(label) ; @opt_label = label ; end
17
+
18
+ ##
19
+ # Adds explanation to option
20
+ def explanation(explanation) ; @opt_explanation = explanation ; end
21
+
22
+ ##
23
+ # Sets preset option paramters
24
+ def add_params(score_array)
25
+ _, @opt_label, @opt_explanation = score_array
26
+ @opt_name = @opt_label
27
+ end
28
+
29
+ ##
30
+ # Validation to make sure that all the required fields are in
31
+ def missing_parameters?
32
+ @opt_name.nil? || @opt_label.nil? || @opt_explanation.nil?
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ class Training
2
+
3
+ attr_accessor :training_criterions, :training_answer
4
+
5
+ ##
6
+ # Initializes a training response
7
+ def initialize(options={})
8
+ @training_criterions = []
9
+ end
10
+
11
+ ##
12
+ # Sets the answer for the training response
13
+ def answer(s) ; @training_answer = s ; end
14
+
15
+ ##
16
+ # Adds a training criterion and evaluates to proc block
17
+ def training_criterion(*args)
18
+ training = TrainingCriterion.new(*args)
19
+ @training_criterions << training
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ class TrainingCriterion
2
+ attr_accessor :criterion, :option
3
+
4
+ ##
5
+ # Initializes a training criterion for a training response
6
+ def initialize(options={})
7
+ @criterion = options[:criterion]
8
+ @option = options[:option]
9
+
10
+ # Validation
11
+ if @criterion.nil? || @option.nil?
12
+ raise "Must include criterion and option."
13
+ end
14
+ end
15
+ end
data/lib/ruql/question.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  class Question
2
2
  attr_accessor :question_text, :answers, :randomize, :points, :name, :question_tags, :question_comment
3
-
3
+
4
4
  def initialize(*args)
5
5
  options = if args[-1].kind_of?(Hash) then args[-1] else {} end
6
6
  @answers = options[:answers] || []
@@ -12,9 +12,9 @@ class Question
12
12
  end
13
13
 
14
14
  def raw? ; !!@raw ; end
15
-
15
+
16
16
  def text(s) ; @question_text = s ; end
17
-
17
+
18
18
  def explanation(text)
19
19
  @answers.each { |answer| answer.explanation ||= text }
20
20
  end
@@ -28,7 +28,7 @@ class Question
28
28
  end
29
29
 
30
30
  # these are ignored but legal for now:
31
- def tags(*args) # string or array of strings
31
+ def tags(*args) # string or array of strings
32
32
  if args.length > 1
33
33
  @question_tags += args.map(&:to_s)
34
34
  else
data/lib/ruql/quiz.rb CHANGED
@@ -1,8 +1,9 @@
1
-
2
1
  class Quiz
3
2
  @@quizzes = []
3
+ @@yaml_file = nil
4
+ @quiz_yaml = {}
4
5
  def self.quizzes ; @@quizzes ; end
5
- @@default_options =
6
+ @@default_options =
6
7
  {
7
8
  :open_time => Time.now,
8
9
  :soft_close_time => Time.now + 24*60*60,
@@ -28,7 +29,7 @@ class Quiz
28
29
  attr_reader :logger
29
30
  attr_accessor :title
30
31
 
31
- def initialize(title, options={})
32
+ def initialize(title, yaml, options={})
32
33
  @output = ''
33
34
  @questions = options[:questions] || []
34
35
  @title = title
@@ -36,12 +37,13 @@ class Quiz
36
37
  @seed = srand
37
38
  @logger = Logger.new(STDERR)
38
39
  @logger.level = Logger.const_get (options.delete('l') ||
39
- options.delete('log') || 'warn').upcase
40
+ options.delete('log') || 'warn').upcase
41
+ @quiz_yaml = yaml
40
42
  end
41
43
 
42
44
  def self.get_renderer(renderer)
43
45
  Object.const_get(renderer.to_s + 'Renderer') rescue nil
44
- end
46
+ end
45
47
 
46
48
  def render_with(renderer,options={})
47
49
  srand @seed
@@ -49,7 +51,11 @@ class Quiz
49
51
  @renderer.render_quiz
50
52
  @output = @renderer.output
51
53
  end
52
-
54
+
55
+ def self.set_yaml_file(file)
56
+ @@yaml_file = file && YAML::load_file(file)
57
+ end
58
+
53
59
  def points ; questions.map(&:points).inject { |sum,points| sum + points } ; end
54
60
 
55
61
  def num_questions ; questions.length ; end
@@ -57,7 +63,7 @@ class Quiz
57
63
  def random_seed(num)
58
64
  @seed = num.to_i
59
65
  end
60
-
66
+
61
67
  # this should really be done using mixins.
62
68
  def choice_answer(*args, &block)
63
69
  if args.first.is_a?(Hash) # no question text
@@ -72,7 +78,7 @@ class Quiz
72
78
 
73
79
  def select_multiple(*args, &block)
74
80
  if args.first.is_a?(Hash) # no question text
75
- q = SelectMultiple.new('',*args)
81
+ q = SelectMultiple.new('', *args)
76
82
  else
77
83
  text = args.shift
78
84
  q = SelectMultiple.new(text, *args)
@@ -86,6 +92,32 @@ class Quiz
86
92
  @questions << q
87
93
  end
88
94
 
95
+ def dropdown(*args, &block)
96
+ q = Dropdown.new(*args)
97
+ q.instance_eval(&block)
98
+ @questions << q
99
+ end
100
+
101
+ def open_assessment(*args, &block)
102
+ q = get_open_assessment(*args, &block)
103
+ @questions << q
104
+ end
105
+
106
+ def simple_open_assessment(*args, &block)
107
+ q = get_open_assessment(*args, &block)
108
+ q.add_simple_question
109
+ @questions << q
110
+ end
111
+
112
+ def get_open_assessment(*args, &block)
113
+ y = @quiz_yaml.shift
114
+ raise "Cannot continue - You must have a yaml block for each peer evaluation question" if y.nil?
115
+ yaml = y[1][0]
116
+ q = OpenAssessment.new(*args, yaml)
117
+ q.instance_eval(&block)
118
+ q
119
+ end
120
+
89
121
  def fill_in(*args, &block)
90
122
  if args.first.is_a?(Hash) # no question text
91
123
  q = FillIn.new('', *args)
@@ -97,8 +129,8 @@ class Quiz
97
129
  @questions << q
98
130
  end
99
131
 
100
- def self.quiz(*args,&block)
101
- quiz = Quiz.new(*args)
132
+ def self.quiz(*args, &block)
133
+ quiz = Quiz.new(*args, (@@yaml_file.shift if @@yaml_file))
102
134
  quiz.instance_eval(&block)
103
135
  @@quizzes << quiz
104
136
  end
data/lib/ruql/renderer.rb CHANGED
@@ -17,4 +17,12 @@ class Renderer
17
17
  self.instance_eval@@render_handlers[:quiz].call(self.quiz)
18
18
  end
19
19
 
20
+ # Override these methods in each renderer:
21
+
22
+ %w(fill_in multiple_choice select_multiple dropdown open_assessment true_false).each do |q|
23
+ define_method "render_#{q}" do |question|
24
+ STDERR.puts "Question type '#{q}' not yet implemented for this renderer"
25
+ end
26
+ end
27
+
20
28
  end
@@ -12,6 +12,8 @@ class AutoQCMRenderer
12
12
  options.delete('template') ||
13
13
  File.join(Gem.loaded_specs['ruql'].full_gem_path, 'templates/autoqcm.tex.erb')
14
14
  @penalty = (options.delete('p') || options.delete('penalty') || '0').to_f
15
+ @show_solutions = options.delete('s') || options.delete('solutions')
16
+
15
17
  end
16
18
 
17
19
  def render_quiz
@@ -34,8 +36,8 @@ class AutoQCMRenderer
34
36
 
35
37
  def render_question(q,index)
36
38
  case q
39
+ when SelectMultiple,TrueFalse then render(q, index, 'mult') # These are subclasses of MultipleChoice, should go first
37
40
  when MultipleChoice then render(q, index)
38
- when SelectMultiple,TrueFalse then render(q, index, 'mult')
39
41
  else
40
42
  @quiz.logger.error "Question #{index} (#{q.question_text[0,15]}...): AutoQCM can only handle multiple_choice, truefalse, or select_multiple questions"
41
43
  ''
@@ -48,10 +50,15 @@ class AutoQCMRenderer
48
50
  @output << "\\AMCrandomseed{#{seed}}\n\n"
49
51
  end
50
52
 
51
- def render(question, index, type='')
53
+ def render(question, index, type='')
52
54
  output = ''
53
55
  output << "\\begin{question#{type}}{q#{index}}\n"
54
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
55
62
  output << " " << to_tex(question.question_text) << "\n"
56
63
 
57
64
  # answers - ignore randomization
@@ -61,9 +68,17 @@ class AutoQCMRenderer
61
68
  answer_text = to_tex(answer.answer_text)
62
69
  answer_type = if answer.correct? then 'correct' else 'wrong' end
63
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
64
79
  end
65
80
  output << " \\end{choices}\n"
66
- output << "\\end{question}\n\n"
81
+ output << "\\end{question#{type}}\n\n"
67
82
  output
68
83
  end
69
84
 
@@ -1,7 +1,10 @@
1
+ require 'builder'
2
+ require 'yaml'
3
+
1
4
  class EdXmlRenderer
2
- require 'builder'
3
5
 
4
6
  attr_reader :output
7
+ attr_accessor :file, :yaml_file
5
8
  def initialize(quiz,options={})
6
9
  @only_question = options.delete('n') || options.delete('name')
7
10
  @output = ''
@@ -13,11 +16,13 @@ class EdXmlRenderer
13
16
  case thing
14
17
  when MultipleChoice,SelectMultiple,TrueFalse then render_multiple_choice(thing)
15
18
  when FillIn then render_fill_in(thing)
19
+ when OpenAssessment then render_open_assessment(thing)
20
+ when Dropdown then render_dropdown(thing)
16
21
  else
17
22
  raise "Unknown question type: #{thing}"
18
23
  end
19
24
  end
20
-
25
+
21
26
  def render_quiz
22
27
  # entire quiz can be in one question group, as long as we specify
23
28
  # that ALL question from the group must be used to make the quiz.
@@ -29,11 +34,16 @@ class EdXmlRenderer
29
34
  @output
30
35
  end
31
36
 
32
- def render_fill_in(question)
33
- raise "Not yet implemented for edXML"
34
- end
35
-
36
37
  def render_multiple_choice(question)
38
+ # the OLX for select-multiple and select-one are frustratingly different in arbitrary ways
39
+ # single choice has <multiplechoiceresponse> element containing a <choicegroup> with <choice>s
40
+ # select-mult has <choiceresponse> element containing a <checkboxgroup> with <choice>s
41
+
42
+ question_type, answer_type =
43
+ if question.kind_of?(SelectMultiple)
44
+ then ['choiceresponse', 'checkboxgroup']
45
+ else ['multiplechoiceresponse', 'choicegroup']
46
+ end
37
47
  @b.problem do
38
48
  # if question text has explicit newlines, use them to separate <p>'s
39
49
  if question.raw?
@@ -43,32 +53,147 @@ class EdXmlRenderer
43
53
  if question.raw? then @b.p { |p| p << line } else @b.p(line) end
44
54
  end
45
55
  end
46
- @b.multiplechoiceresponse do
47
- @b.choicegroup :type => 'MultipleChoice' do
56
+
57
+ @b.__send__(question_type) do
58
+ @b.__send__(answer_type, :type => 'MultipleChoice') do
48
59
  question.answers.each do |answer|
49
60
  if question.raw?
50
- @b.choice(:correct => answer.correct?) do |choice|
51
- choice << answer.answer_text
52
- choice << "\n"
53
- end
61
+ @b.choice(:correct => answer.correct?) { @b << answer.answer_text.chomp }
54
62
  else
55
63
  @b.choice answer.answer_text, :correct => answer.correct?
56
64
  end
57
65
  end
58
66
  end
59
67
  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
68
+ if (ans = question.correct_answer.explanation)
69
+ @b.solution do
70
+ @b.div :class => 'detailed-solution' do
71
+ @b.p 'Explanation'
72
+ if question.raw?
73
+ @b.p { |p| p << ans }
74
+ else
75
+ @b.p ans
76
+ end
67
77
  end
68
78
  end
69
79
  end
70
80
  end
71
81
  end
72
82
 
73
- end
83
+ def render_dropdown(question)
84
+ @b.problem do
85
+ if question.raw?
86
+ @b.p { |p| p << question.question_text }
87
+ else
88
+ question.question_text.lines.map(&:chomp).each do |line|
89
+ if question.raw? then @b.p { |p| p << line } else @b.p(line) end
90
+ end
91
+ end
92
+ question.choices.each do |choice|
93
+ idx = choice.correct
94
+ menu_opts = choice.list
95
+ if menu_opts.length == 1 # this is actually a label
96
+ @b.span menu_opts[0]
97
+ else
98
+ @b.optionresponse do
99
+ debugger if menu_opts[idx].nil?
100
+ @b.optioninput :options => options_list_for_attribute(menu_opts),
101
+ :correct => escape_doublequotes(menu_opts[idx])
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def render_open_assessment(question)
109
+ @b.openassessment url_name: question.url_name,
110
+ submission_start: "#{question.submission_start.to_s}T"\
111
+ "#{question.submission_start_time}:00+00:00",
112
+ submission_due: "#{question.submission_due.to_s}T"\
113
+ "#{question.submission_due_time}:00+00:00",
114
+ allow_file_upload: question.allow_file_upload.to_s.capitalize,
115
+ allow_latex: question.allow_latex.to_s.capitalize do
116
+
117
+ @b.title question.question_title
74
118
 
119
+ # Oh good lord my eyes
120
+ @b.assessments do
121
+ if question.trainings.size > 0
122
+ @b.assessment name: "student-training" do
123
+ question.trainings.each do |training|
124
+ @b.example do
125
+ @b.answer do
126
+ @b.part training.training_answer
127
+ end
128
+ training.training_criterions.each do |criterion|
129
+ @b.select criterion: criterion.criterion,
130
+ option: criterion.option
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ if question.peer_review
138
+ @b.assessment name: "peer-assessment",
139
+ must_grade: question.must_grade,
140
+ must_be_graded_by: question.graded_by,
141
+ start: "#{question.peer_review_start.to_s}T" \
142
+ "#{question.peer_review_start_time}:00+00:00",
143
+ due: "#{question.peer_review_due.to_s}T"\
144
+ "#{question.peer_review_due_time}:00+00:00"
145
+
146
+ end
147
+
148
+ if question.self_assessment
149
+ @b.assessment name: "self-assessment",
150
+ start: "#{question.self_assessment_start.to_s}T"\
151
+ "#{question.self_assessment_start_time}:00+00:00",
152
+ due: "#{question.self_assessment_due.to_s}T"\
153
+ "#{question.self_assessment_due_time}:00+00:00"
154
+ end
155
+ end
156
+
157
+ @b.prompts do
158
+ question.prompts.each do |description|
159
+ @b.prompt do
160
+ @b.description description
161
+ end
162
+ end
163
+ end
164
+
165
+ @b.rubric do
166
+ question.criterions.each do |criterion|
167
+ @b.criterion feedback: criterion.feedback do
168
+ @b.name criterion.criterion_name
169
+ @b.label criterion.criterion_label
170
+ @b.prompt criterion.criterion_prompt
171
+
172
+ criterion.options.each do |option|
173
+ @b.option points: option.points do
174
+ @b.name option.opt_name
175
+ @b.label option.opt_label
176
+ @b.explanation option.opt_explanation
177
+ end
178
+ end
179
+ end
180
+ end
181
+ @b.feedbackprompt question.question_feedback_prompt
182
+ @b.feedback_default_text question.question_feedback_default_text
183
+ end
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ def escape_doublequotes(str)
190
+ str.gsub(/"/, '&quot;')
191
+ end
192
+
193
+ def options_list_for_attribute(list)
194
+ # takes a list of strings, places single quotes around each element
195
+ # and HTML-escapes doublequotes within any element. Because OLX.
196
+ attr = list.map { |e| "'" << escape_doublequotes(e) << "'" }.join(',')
197
+ "(#{attr})"
198
+ end
199
+ end
@@ -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
@@ -1,15 +1,24 @@
1
1
  module TexOutput
2
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
+
3
10
  @@tex_replace = {
4
11
  /_/ => '\textunderscore{}',
5
12
  Regexp.new('<tt>([^<]+)</tt>', Regexp::IGNORECASE) => "\\texttt{\\1}",
6
- Regexp.new('<pre>(.*?)</pre>', Regexp::MULTILINE | Regexp::IGNORECASE) => "\\begin{Verbatim}\\1\\end{Verbatim}",
13
+ Regexp.new('<pre>(.*?)</pre>', Regexp::MULTILINE | Regexp::IGNORECASE) => "\\texttt{\\1}",
7
14
  }
8
15
 
9
16
  @@tex_escape = Regexp.new '\$|&|%|#|\{|\}'
10
17
 
11
18
  def to_tex(str)
12
19
  str = str.gsub(@@tex_escape) { |match| "\\#{match}" }
20
+ str = add_newlines(str)
21
+
13
22
  @@tex_replace.each_pair do |match, replace|
14
23
  str = str.gsub(match, replace)
15
24
  end
data/lib/ruql.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # basic gems/libs we rely on
2
2
  require 'builder'
3
3
  require 'logger'
4
+ require 'date'
4
5
 
5
6
  $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__)))
6
7
 
@@ -11,6 +12,7 @@ require 'ruql/renderers/html_form_renderer'
11
12
  require 'ruql/renderers/edxml_renderer'
12
13
  require 'ruql/renderers/auto_qcm_renderer'
13
14
  require 'ruql/renderers/json_renderer'
15
+ require 'ruql/renderers/qualtrics_renderer'
14
16
 
15
17
  # question types
16
18
  require 'ruql/quiz'
@@ -20,3 +22,9 @@ require 'ruql/multiple_choice'
20
22
  require 'ruql/select_multiple'
21
23
  require 'ruql/true_false'
22
24
  require 'ruql/fill_in'
25
+ require 'ruql/dropdown'
26
+ require 'ruql/open_assessment/open_assessment'
27
+ require 'ruql/open_assessment/criterion'
28
+ require 'ruql/open_assessment/option'
29
+ require 'ruql/open_assessment/training'
30
+ require 'ruql/open_assessment/training_criterion'
metadata CHANGED
@@ -1,48 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
5
- prerelease:
4
+ version: 0.0.8
6
5
  platform: ruby
7
6
  authors:
8
7
  - Armando Fox
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-05-25 00:00:00.000000000 Z
11
+ date: 2016-02-13 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: builder
16
14
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
15
  requirements:
19
- - - ! '>='
16
+ - - ~>
20
17
  - !ruby/object:Gem::Version
21
- version: '0'
22
- type: :runtime
18
+ version: '3.0'
23
19
  prerelease: false
20
+ name: builder
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
22
  requirements:
27
- - - ! '>='
23
+ - - ~>
28
24
  - !ruby/object:Gem::Version
29
- version: '0'
25
+ version: '3.0'
26
+ type: :runtime
30
27
  - !ruby/object:Gem::Dependency
31
- name: getopt
32
28
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
29
  requirements:
35
- - - ! '>='
30
+ - - '='
36
31
  - !ruby/object:Gem::Version
37
- version: '0'
38
- type: :runtime
32
+ version: 1.4.2
39
33
  prerelease: false
34
+ name: getopt
40
35
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
36
  requirements:
43
- - - ! '>='
37
+ - - '='
44
38
  - !ruby/object:Gem::Version
45
- version: '0'
39
+ version: 1.4.2
40
+ type: :runtime
46
41
  description: Ruby-embedded DSL for creating short-answer quiz questions
47
42
  email: fox@cs.berkeley.edu
48
43
  executables:
@@ -50,49 +45,55 @@ executables:
50
45
  extensions: []
51
46
  extra_rdoc_files: []
52
47
  files:
48
+ - bin/ruql
53
49
  - lib/ruql.rb
54
- - lib/ruql/quiz.rb
55
50
  - lib/ruql/answer.rb
56
- - lib/ruql/question.rb
57
- - lib/ruql/renderer.rb
58
- - lib/ruql/select_multiple.rb
51
+ - lib/ruql/dropdown.rb
59
52
  - lib/ruql/fill_in.rb
60
53
  - lib/ruql/multiple_choice.rb
61
- - lib/ruql/true_false.rb
62
- - lib/ruql/tex_output.rb
54
+ - lib/ruql/open_assessment/criterion.rb
55
+ - lib/ruql/open_assessment/open_assessment.rb
56
+ - lib/ruql/open_assessment/option.rb
57
+ - lib/ruql/open_assessment/training.rb
58
+ - lib/ruql/open_assessment/training_criterion.rb
59
+ - lib/ruql/question.rb
60
+ - lib/ruql/quiz.rb
61
+ - lib/ruql/renderer.rb
63
62
  - lib/ruql/renderers/auto_qcm_renderer.rb
64
63
  - lib/ruql/renderers/edxml_renderer.rb
65
64
  - lib/ruql/renderers/html5_renderer.rb
66
65
  - lib/ruql/renderers/html_form_renderer.rb
67
66
  - lib/ruql/renderers/json_renderer.rb
67
+ - lib/ruql/renderers/qualtrics_renderer.rb
68
68
  - lib/ruql/renderers/xml_renderer.rb
69
+ - lib/ruql/select_multiple.rb
70
+ - lib/ruql/tex_output.rb
71
+ - lib/ruql/true_false.rb
69
72
  - templates/autoqcm.tex.erb
70
73
  - templates/html5.html.erb
71
74
  - templates/htmlform.html.erb
72
- - bin/ruql
73
75
  homepage: http://github.com/saasbook/ruql
74
76
  licenses:
75
77
  - CC By-SA
78
+ metadata: {}
76
79
  post_install_message:
77
80
  rdoc_options: []
78
81
  require_paths:
79
82
  - lib
80
83
  required_ruby_version: !ruby/object:Gem::Requirement
81
- none: false
82
84
  requirements:
83
- - - ! '>='
85
+ - - '>='
84
86
  - !ruby/object:Gem::Version
85
87
  version: '0'
86
88
  required_rubygems_version: !ruby/object:Gem::Requirement
87
- none: false
88
89
  requirements:
89
- - - ! '>='
90
+ - - '>='
90
91
  - !ruby/object:Gem::Version
91
92
  version: '0'
92
93
  requirements: []
93
94
  rubyforge_project:
94
- rubygems_version: 1.8.25
95
+ rubygems_version: 2.2.5
95
96
  signing_key:
96
- specification_version: 3
97
+ specification_version: 4
97
98
  summary: Ruby question language
98
99
  test_files: []