ruql 0.1.5 → 1.0.5

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