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.
- 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
|