ruql 0.1.5 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +14 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/LICENSE +6 -0
- data/README.md +318 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/ruql +90 -114
- data/bin/setup +8 -0
- data/examples/estilo.css +1 -0
- data/examples/example.rb +37 -0
- data/examples/file.html +94 -0
- data/examples/help.txt +55 -0
- data/examples/preguntas-TFG-20140224-0050.txt +32 -0
- data/examples/preguntas-TFG-20140224-0050.xml +168 -0
- data/examples/prueba.js +3 -0
- data/lib/ruql.rb +10 -3
- data/lib/ruql/answer.rb +6 -14
- data/lib/ruql/json.rb +12 -0
- data/lib/ruql/multiple_choice.rb +1 -1
- data/lib/ruql/open_assessment/open_assessment.rb +1 -1
- data/lib/ruql/question.rb +27 -56
- data/lib/ruql/quiz.rb +50 -69
- data/lib/ruql/renderers/edxml_renderer.rb +1 -1
- data/lib/ruql/renderers/html5_renderer.rb +65 -27
- data/lib/ruql/renderers/json_renderer.rb +35 -1
- data/lib/ruql/renderers/xml_renderer.rb +148 -0
- data/lib/ruql/select_multiple.rb +5 -1
- data/lib/ruql/stats.rb +18 -0
- data/lib/ruql/true_false.rb +1 -1
- data/lib/ruql/version.rb +3 -0
- data/ruql.gemspec +43 -0
- metadata +71 -38
- data/templates/autoqcm.tex.erb +0 -1
- data/templates/html5.html.erb +0 -42
- data/templates/htmlform.html.erb +0 -44
data/lib/ruql/json.rb
ADDED
data/lib/ruql/multiple_choice.rb
CHANGED
@@ -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?
|
data/lib/ruql/question.rb
CHANGED
@@ -1,55 +1,52 @@
|
|
1
1
|
class Question
|
2
|
-
attr_accessor :question_text,
|
3
|
-
|
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
|
-
@
|
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
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@
|
34
|
-
|
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(
|
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
|
42
|
-
@
|
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]
|
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]
|
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
|
data/lib/ruql/quiz.rb
CHANGED
@@ -1,98 +1,78 @@
|
|
1
1
|
class Quiz
|
2
2
|
@@quizzes = []
|
3
|
-
|
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
|
-
|
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
|
22
|
+
@questions = options[:questions] || []
|
39
23
|
@title = title
|
40
|
-
@options = @@
|
24
|
+
@options = @@options.merge(options)
|
41
25
|
@seed = srand
|
42
26
|
@logger = Logger.new(STDERR)
|
43
|
-
@logger.level =
|
44
|
-
|
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
|
66
|
-
|
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
|
-
@
|
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
|
45
|
+
def self.set_options(options)
|
46
|
+
@@options = options
|
47
|
+
end
|
89
48
|
|
90
|
-
def
|
49
|
+
def ungrouped_questions
|
50
|
+
questions.filter { |q| q.question_group.to_s == '' }
|
51
|
+
end
|
91
52
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
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(
|
168
|
-
quiz = Quiz.new(
|
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
|
@@ -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(
|
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
|
-
|
21
|
-
|
22
|
-
|
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'
|
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
|
98
|
+
elsif q.randomize then q.answers.sort_by { rand }
|
55
99
|
else q.answers
|
56
100
|
end
|
57
|
-
@h.
|
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.
|
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.
|
110
|
-
|
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
|
-
:'
|
119
|
-
|
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 =
|
131
|
-
('Select
|
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
|