ruql 0.1.5 → 1.0.5

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.
@@ -0,0 +1,12 @@
1
+ module Ruql
2
+ class Json
3
+ attr_reader :output
4
+ def initialize(quiz, options={})
5
+ @quiz = quiz
6
+ @output = ''
7
+ end
8
+ def render_quiz
9
+ @output = JSON.pretty_generate(@quiz.as_json)
10
+ end
11
+ end
12
+ end
@@ -1,10 +1,10 @@
1
1
  class MultipleChoice < Question
2
+
2
3
  attr_accessor :multiple
3
4
 
4
5
  def initialize(text='', opts={})
5
6
  super
6
7
  self.question_text = text
7
-
8
8
  self.multiple = !!opts[:multiple]
9
9
  self.randomize = !!opts[:randomize]
10
10
  end
@@ -115,7 +115,7 @@ class OpenAssessment
115
115
  # Adds fields for a simple_open_assessment question
116
116
  def add_simple_question
117
117
  criterion = Criterion.new
118
- criterion.name("How'd you do?") # "
118
+ criterion.name("How'd you do?")
119
119
  criterion.label("Scoring Rubric")
120
120
 
121
121
  raise "Must have answer for question" if @question_answer.nil?
@@ -1,55 +1,52 @@
1
1
  class Question
2
- attr_accessor :question_text,
3
- :answers,
4
- :question_image,
5
- :randomize,
6
- :points,
7
- :name,
8
- :question_tags,
9
- :question_uid,
10
- :question_comment,
11
- :raw
12
-
2
+ attr_accessor :question_text, :answers, :randomize, :points, :name, :question_tags, :question_group, :question_comment, :uid
3
+
13
4
  def initialize(*args)
14
5
  options = if args[-1].kind_of?(Hash) then args[-1] else {} end
15
6
  @answers = options[:answers] || []
16
7
  @points = [options[:points].to_i, 1].max
17
8
  @raw = options[:raw]
18
9
  @name = options[:name]
19
- @question_image = options[:image]
20
10
  @question_tags = []
21
- @question_uid = (options.delete(:uid) || SecureRandom.uuid).to_s
22
- @explanation = nil
11
+ @question_group = ''
23
12
  @question_comment = ''
24
13
  end
25
- def raw? ; !!@raw ; end
26
-
27
- def has_explanation? ; @explanation.to_s != '' ; end
28
14
 
29
- def explanation(text=nil)
30
- if text
31
- @explanation = text
32
- else
33
- @explanation
34
- end
15
+ def as_json
16
+ Hash(
17
+ :question_text => @question_text,
18
+ :question_tags => @question_tags.compact,
19
+ :question_group => @question_group,
20
+ :answers => @answers.map(&:as_json),
21
+ :question_type => self.class.to_s,
22
+ :raw => !!@raw,
23
+ :name => @name,
24
+ :points => @points
25
+ ).compact
35
26
  end
36
27
 
37
- def uid(u) ; @question_uid = u ; end
38
-
28
+ def uid(str); @uid = str; end
29
+
30
+ def raw? ; !!@raw ; end
31
+
39
32
  def text(s) ; @question_text = s ; end
40
33
 
41
- def image(url)
42
- @question_image = url
34
+ def explanation(text)
35
+ @answers.each { |answer| answer.explanation ||= text }
43
36
  end
44
-
37
+
45
38
  def answer(text, opts={})
46
- @answers << Answer.new(text, correct=true, opts[:explanation], self)
39
+ @answers << Answer.new(text, correct=true, opts[:explanation])
47
40
  end
48
41
 
49
42
  def distractor(text, opts={})
50
- @answers << Answer.new(text, correct=false, opts[:explanation], self)
43
+ @answers << Answer.new(text, correct=false, opts[:explanation])
51
44
  end
52
45
 
46
+ def group(str)
47
+ @question_group = str
48
+ end
49
+
53
50
  # these are ignored but legal for now:
54
51
  def tags(*args) # string or array of strings
55
52
  if args.length > 1
@@ -67,30 +64,4 @@ class Question
67
64
 
68
65
  def correct_answers ; @answers.collect(&:correct?) ; end
69
66
 
70
- def answer_helper(obj)
71
- if obj.is_a? Array and obj.size and obj[0].is_a? Answer
72
- return obj.map {|answer| answer.to_JSON}
73
- end
74
- obj
75
- end
76
-
77
- #creates a JSON hash of the object with its object name. we should convert this to a mixin for answer and question. aaron
78
- def to_JSON
79
- h = Hash[instance_variables.collect { |var| [var.to_s.delete('@'), answer_helper(instance_variable_get(var))] }]
80
- h['question_type'] = self.class.to_s
81
- return h
82
- end
83
-
84
- #factory method to return correct type of question
85
- def self.from_JSON(hash_str)
86
- hash = JSON.parse(hash_str)
87
- #create the appropriate class of the object from the hash's class name
88
- question = Object.const_get(hash.fetch('question_type')).new()
89
- hash.reject{|key| key == 'answers' or key == 'question_type'}.each do |key, value|
90
- question.send((key + '=').to_sym, value)
91
- end
92
- question.answers = hash['answers'].map{|answer_hash| Answer.from_JSON(answer_hash)}
93
- question
94
- end
95
-
96
67
  end
@@ -1,98 +1,78 @@
1
1
  class Quiz
2
2
  @@quizzes = []
3
- @quiz_yaml = {}
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
- }
3
+ @@options = {}
22
4
 
23
5
  attr_reader :renderer
24
6
  attr_reader :questions
25
- attr_reader :first_question_number
26
7
  attr_reader :options
27
8
  attr_reader :output
28
- attr_reader :suppress_random
29
9
  attr_reader :seed
30
10
  attr_reader :logger
31
- attr_reader :points_threshold
32
- attr_reader :points_string
33
- attr_accessor :title, :quizzes
11
+ attr_accessor :title
34
12
 
13
+ def self.reset
14
+ @@quizzes = []
15
+ @@options = {}
16
+ end
17
+ def self.quizzes ; @@quizzes ; end
18
+ def self.options ; @@options ; end
35
19
 
36
20
  def initialize(title, options={})
37
21
  @output = ''
38
- @questions = options.delete(:questions) || []
22
+ @questions = options[:questions] || []
39
23
  @title = title
40
- @options = @@default_options.merge(options)
24
+ @options = @@options.merge(options)
41
25
  @seed = srand
42
26
  @logger = Logger.new(STDERR)
43
- @logger.level = Logger.const_get (options.delete('l') ||
44
- options.delete('log') || 'warn').upcase
45
- if (yaml = options.delete(:yaml))
46
- @quiz_yaml = YAML::load(IO.read yaml)
47
- end
48
- end
49
-
50
- def get_first_question_number(spec)
51
- return 1 if spec.nil?
52
- return $1.to_i if spec =~ /^(\d+)$/
53
- # file?
54
- begin
55
- File.readlines(spec).each do |f|
56
- return 1 + $1.to_i if f =~ /^last\s+(\d+)/
57
- end
58
- return 1
59
- rescue StandardError => e
60
- warn "Warning: starting question numbering at 1, cannot read #{spec}: #{e.message}"
61
- return 1
62
- end
27
+ @logger.level = (@options['-V'] || @options['--verbose']) ? Logger::INFO : Logger::WARN
28
+ #@quiz_yaml = yaml
63
29
  end
64
30
 
65
- def self.get_renderer(renderer)
66
- Object.const_get(renderer.to_s + 'Renderer') rescue nil
31
+ def as_json
32
+ Hash(:title => @title,
33
+ :questions => @questions.map(&:as_json),
34
+ :seed => @seed
35
+ )
67
36
  end
68
37
 
69
38
  def render_with(renderer,options={})
70
39
  srand @seed
71
- @first_question_number = get_first_question_number(options.delete('a'))
72
- @points_threshold = (options.delete('p') || 0).to_i
73
- @points_string = options.delete('P') || "[%d point%s]"
74
- @suppress_random = !!options['R']
75
- @renderer = Quiz.get_renderer(renderer).send(:new,self,options)
40
+ @renderer = renderer.send(:new,self,options)
76
41
  @renderer.render_quiz
77
- if (report = options.delete('r'))
78
- File.open(report, "w") do |f|
79
- f.puts "questions #{num_questions}"
80
- f.puts "first #{first_question_number}"
81
- f.puts "last #{first_question_number + num_questions - 1}"
82
- f.puts "points #{self.points}"
83
- end
84
- end
85
42
  @output = @renderer.output
86
43
  end
87
44
 
88
- def points ; questions.map(&:points).inject { |sum,points| sum + points } ; end
45
+ def self.set_options(options)
46
+ @@options = options
47
+ end
89
48
 
90
- def num_questions ; questions.length ; end
49
+ def ungrouped_questions
50
+ questions.filter { |q| q.question_group.to_s == '' }
51
+ end
91
52
 
92
- def point_string(points)
93
- points >= points_threshold ?
94
- sprintf(points_string.to_s, points, (points > 1 ? 's' : '')) :
95
- ''
53
+ def grouped_questions
54
+ questions.filter { |q| q.question_group.to_s != '' }.sort_by(&:question_group)
55
+ end
56
+
57
+ def groups ; questions.map(&:question_group).uniq.reject { |g| g.to_s == '' } ; end
58
+
59
+ def ungrouped_points
60
+ ungrouped_questions.map(&:points).sum
61
+ end
62
+
63
+ def grouped_points
64
+ gq = grouped_questions
65
+ groups.sum do |g|
66
+ gq.detect { |q| q.question_group == g }.points
67
+ end
68
+ end
69
+
70
+ def points
71
+ ungrouped_points + grouped_points
72
+ end
73
+
74
+ def num_questions
75
+ groups.length + ungrouped_questions.length
96
76
  end
97
77
 
98
78
  def random_seed(num)
@@ -164,9 +144,10 @@ class Quiz
164
144
  @questions << q
165
145
  end
166
146
 
167
- def self.quiz(*args, &block)
168
- quiz = Quiz.new(*args)
147
+ def self.quiz(title, args={}, &block)
148
+ quiz = Quiz.new(title, args)
169
149
  quiz.instance_eval(&block)
170
150
  @@quizzes << quiz
171
151
  end
152
+
172
153
  end
@@ -4,7 +4,7 @@ require 'yaml'
4
4
  class EdXmlRenderer
5
5
 
6
6
  attr_reader :output
7
- attr_accessor :file
7
+ attr_accessor :file, :yaml_file
8
8
  def initialize(quiz,options={})
9
9
  @only_question = options.delete('n') || options.delete('name')
10
10
  @output = ''
@@ -5,21 +5,65 @@ class Html5Renderer
5
5
  attr_reader :output
6
6
 
7
7
  def initialize(quiz,options={})
8
+ @css = options.delete('c') || options.delete('css')
8
9
  @show_solutions = options.delete('s') || options.delete('solutions')
10
+ @show_tags = options.delete('T') || options.delete('show-tags')
9
11
  @template = options.delete('t') ||
10
12
  options.delete('template') ||
11
- File.join(File.dirname(__FILE__), '../../../templates/html5.html.erb')
13
+ File.join((Gem.loaded_specs['ruql'].full_gem_path rescue '.'), 'templates/html5.html.erb')
12
14
  @output = ''
13
- @list_type = (options.delete('o') || options.delete('list-type') || 'o')[0] + "l"
14
- @list_start = quiz.first_question_number
15
15
  @quiz = quiz
16
16
  @h = Builder::XmlMarkup.new(:target => @output, :indent => 2)
17
17
  end
18
18
 
19
+ def allowed_options
20
+ opts = [
21
+ ['-c', '--css', Getopt::REQUIRED],
22
+ ['-t', '--template', Getopt::REQUIRED],
23
+ ['-s', '--solutions', Getopt::BOOLEAN],
24
+ ['-T', '--show-tags', Getopt::BOOLEAN]
25
+ ]
26
+ help = <<eos
27
+ The HTML5 and HTML Forms renderers supports these options:
28
+ -c <href>, --css=<href>
29
+ embed <href> for stylesheet into generated HTML5
30
+ -j <src>, --js=<src>
31
+ embed <src> for JavaScrips
32
+ -t <file.html.erb>, --template=<file.html.erb>
33
+ Use file.html.erb as HTML template rather than generating our own file.
34
+ file.html.erb should have <%= yield %> where questions should go.
35
+ An example is in the templates/ directory of RuQL.
36
+ The following local variables will be replaced with their values in
37
+ the template:
38
+ <%= quiz.title %> - the quiz title
39
+ <%= quiz.num_questions %> - total number of questions
40
+ <%= quiz.points %> - total number of points for whole quiz
41
+ -T, --show-tags
42
+ Show the tag(s) associated with a question within an element <div class="tags">.
43
+ -s, --solutions
44
+ generate solutions (showing correct answers and explanations)
45
+ NOTE: If there is more than one quiz (collection of questions) in the file,
46
+ a complete <html>...</html> block is produced in the output for EACH quiz.
47
+ eos
48
+ return(
49
+
50
+
19
51
  def render_quiz
20
- render_with_template do
21
- render_questions
22
- @output
52
+ if @template
53
+ render_with_template do
54
+ render_questions
55
+ @output
56
+ end
57
+ else
58
+ @h.html do
59
+ @h.head do
60
+ @h.title @quiz.title
61
+ @h.link(:rel => 'stylesheet', :type =>'text/css', :href=>@css) if @css
62
+ end
63
+ @h.body do
64
+ render_questions
65
+ end
66
+ end
23
67
  end
24
68
  self
25
69
  end
@@ -34,7 +78,7 @@ class Html5Renderer
34
78
 
35
79
  def render_questions
36
80
  render_random_seed
37
- @h.ol :class => 'questions', :start => @list_start do
81
+ @h.ol :class => 'questions' do
38
82
  @quiz.questions.each_with_index do |q,i|
39
83
  case q
40
84
  when MultipleChoice, SelectMultiple, TrueFalse then render_multiple_choice(q,i)
@@ -51,10 +95,10 @@ class Html5Renderer
51
95
  render_question_text(q, index) do
52
96
  answers =
53
97
  if q.class == TrueFalse then q.answers.sort.reverse # True always first
54
- elsif q.randomize && !@quiz.suppress_random then q.answers.sort_by { rand }
98
+ elsif q.randomize then q.answers.sort_by { rand }
55
99
  else q.answers
56
100
  end
57
- @h.__send__(@list_type, :class => 'answers') do
101
+ @h.ol :class => 'answers' do
58
102
  answers.each do |answer|
59
103
  if @show_solutions
60
104
  render_answer_for_solutions(answer, q.raw?, q.class == TrueFalse)
@@ -62,10 +106,6 @@ class Html5Renderer
62
106
  if q.raw? then @h.li { |l| l << answer.answer_text } else @h.li answer.answer_text end
63
107
  end
64
108
  end
65
- if @show_solutions && ((exp = q.explanation.to_s) != '')
66
- @h.br
67
- if q.raw? then @h.span(:class => 'explanation') { |p| p << exp } else @h.span(exp, :class => 'explanation') end
68
- end
69
109
  end
70
110
  end
71
111
  self
@@ -104,10 +144,10 @@ class Html5Renderer
104
144
  answer.correct? ? "CORRECT: " : "INCORRECT: ")
105
145
  end
106
146
  @h.li(args) do
107
- if raw then @h.span { |p| p << answer.answer_text } else @h.span answer.answer_text end
147
+ if raw then @h.p { |p| p << answer.answer_text } else @h.p answer.answer_text end
108
148
  if answer.has_explanation?
109
- @h.br
110
- if raw then @h.span(:class => 'explanation') { |p| p << answer.explanation } else @h.span(answer.explanation, :class => 'explanation') end
149
+ if raw then @h.p(:class => 'explanation') { |p| p << answer.explanation }
150
+ else @h.p(answer.explanation, :class => 'explanation') end
111
151
  end
112
152
  end
113
153
  end
@@ -115,23 +155,21 @@ class Html5Renderer
115
155
  def render_question_text(question,index)
116
156
  html_args = {
117
157
  :id => "question-#{index}",
118
- :'data-uid' => question.question_uid,
119
- :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')].join(' ')
158
+ :class => ['question', question.class.to_s.downcase, (question.multiple ? 'multiple' : '')]
159
+ .join(' ')
120
160
  }
121
- if question.question_image # add CSS class to both <li> and <img>
122
- html_args[:class] << 'question-with-image'
123
- end
124
161
  @h.li html_args do
125
- # if there's an image, render it first
126
- if question.question_image
127
- @h.img :src => question.question_image, :class => 'question-image'
128
- end
129
162
  @h.div :class => 'text' do
130
- qtext = @quiz.point_string(question.points) << ' ' <<
131
- ('Select <b>all</b> that apply: ' if question.multiple).to_s <<
163
+ qtext = "[#{question.points} point#{'s' if question.points>1}] " <<
164
+ ('Select ALL that apply: ' if question.multiple).to_s <<
132
165
  if question.class == FillIn then question.question_text.gsub(/\-+/, '_____________________________')
133
166
  else question.question_text
134
167
  end
168
+ if @show_tags
169
+ @h.div(:class => 'text') do
170
+ question.tags.join(',')
171
+ end
172
+ end
135
173
  if question.raw?
136
174
  @h.p { |p| p << qtext }
137
175
  else