ruql 0.0.4 → 0.0.8

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: 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: []