quiz_master 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +7 -0
- data/README.md +52 -20
- data/Rakefile +14 -14
- data/lib/quiz_master.rb +37 -10
- data/lib/quiz_master/answer.rb +13 -3
- data/lib/quiz_master/answers_renderer.rb +18 -14
- data/lib/quiz_master/question.rb +69 -0
- data/lib/quiz_master/quiz.rb +80 -1
- data/lib/quiz_master/quiz_variant.rb +36 -0
- data/lib/quiz_master/rendered_question.rb +2 -2
- data/lib/quiz_master/reorderer.rb +68 -0
- data/lib/quiz_master/reordering_vector.rb +95 -0
- data/lib/quiz_master/text_answer_sheet_formatter.rb +1 -1
- data/lib/quiz_master/text_assembler.rb +3 -3
- data/lib/quiz_master/text_question_formatter.rb +1 -1
- data/lib/quiz_master/text_question_sheet_formatter.rb +2 -1
- data/lib/quiz_master/version.rb +1 -1
- data/quiz_master.gemspec +17 -15
- data/test/question_spec.rb +55 -0
- data/test/quiz_spec.rb +55 -0
- data/test/quiz_variant_spec.rb +34 -0
- data/test/reorderer_spec.rb +33 -0
- data/test/reordering_vector_spec.rb +59 -0
- data/test/spec_helper.rb +58 -0
- data/test/text_answer_sheet_formatter_spec.rb +14 -0
- data/test/text_question_formatter_spec.rb +18 -0
- data/test/text_question_sheet_formatter_spec.rb +34 -0
- data/test/version_spec.rb +7 -0
- metadata +27 -10
- data/test/test_text_answer_sheet_formatter.rb +0 -39
- data/test/test_text_question_formatter.rb +0 -30
- data/test/test_text_question_sheet_formatter.rb +0 -56
@@ -0,0 +1,36 @@
|
|
1
|
+
module QuizMaster
|
2
|
+
# This class allows us to build a QuizVariant which is a Quiz whose Questions
|
3
|
+
# and thier Answers have been re-ordered in the way specified by the orderings
|
4
|
+
# supplied to new.
|
5
|
+
#
|
6
|
+
# Once a QuizVariant has been made it is not intended to be mutated, it is an
|
7
|
+
# "end result" which can be used to generate output.
|
8
|
+
class QuizVariant
|
9
|
+
# Get the questions for this QuizVariant.
|
10
|
+
attr_reader :questions
|
11
|
+
# Get the header for this QuizVariant.
|
12
|
+
attr_reader :header
|
13
|
+
# Get the footer for this QuizVariant.
|
14
|
+
attr_reader :footer
|
15
|
+
|
16
|
+
# Public: Initialize this QuizVariant. The Answers are reordered before the
|
17
|
+
# Questions.
|
18
|
+
#
|
19
|
+
# source - A Quiz to be "shuffled" according to the reorderings.
|
20
|
+
# answer_orderings - An Array of ReorderingVectors which specify how each
|
21
|
+
# Question's Answers are to be reordered.
|
22
|
+
# question_ordering - A ReorderingVector which specifies how the Questions
|
23
|
+
# are to be reordered.
|
24
|
+
#
|
25
|
+
# Returns a QuizVariant.
|
26
|
+
def initialize(source, answer_orderings, question_ordering)
|
27
|
+
@header = source.header.dup
|
28
|
+
@footer = source.footer.dup
|
29
|
+
@questions = source
|
30
|
+
.reorder_answers(answer_orderings)
|
31
|
+
.reorder_questions(question_ordering)
|
32
|
+
.questions
|
33
|
+
.map(&:render_answers)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module QuizMaster
|
2
|
+
# This is responsible for reordering an Array. It is a place to put any
|
3
|
+
# logic and error checking.
|
4
|
+
#
|
5
|
+
# Examples
|
6
|
+
#
|
7
|
+
# r = Reorderer.new(0, 2, 1, 3)
|
8
|
+
# r.reorder(%w(a b c d)) # => %w(a c b d)
|
9
|
+
class Reorderer
|
10
|
+
# Public: Initialize a Reorderer.
|
11
|
+
#
|
12
|
+
# args - Specify an Array of indices which must not be empty and must
|
13
|
+
# contain the values 0 ... size of array.
|
14
|
+
#
|
15
|
+
# Examples
|
16
|
+
#
|
17
|
+
# r = Reorderer.new(2, 0, 1)
|
18
|
+
#
|
19
|
+
# Raises ArgumentError if the args can't be validated.
|
20
|
+
def initialize(*args)
|
21
|
+
@indices = args.to_a
|
22
|
+
indices_invalid? &&
|
23
|
+
fail(ArgumentError, "#{args.inspect} is a bad set of indices")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Reorder the passed array based on the indices we were initialized
|
27
|
+
# with.
|
28
|
+
#
|
29
|
+
# array - An Array containing elements you're interested in reordering.
|
30
|
+
#
|
31
|
+
# Examples
|
32
|
+
#
|
33
|
+
# r.reorder([1, 3, 5])
|
34
|
+
#
|
35
|
+
# Raises an ArgumentError if the size of the Array isn't the same as the
|
36
|
+
# size of the indices specified in new.
|
37
|
+
#
|
38
|
+
# Returns a new Array containing the reordered elements of array.
|
39
|
+
def reorder(array)
|
40
|
+
fail(ArgumentError, "array should have #{@indices.size} elements") if
|
41
|
+
array.size != @indices.size
|
42
|
+
array.values_at(*@indices)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Determines which elements of a list will be moved when the list is
|
46
|
+
# reordered.
|
47
|
+
#
|
48
|
+
# Examples
|
49
|
+
#
|
50
|
+
# r = Reorderer.new(3,0,2,1)
|
51
|
+
# r.moving_indices # => [0, 1, 3]
|
52
|
+
# # ... so element 2 of an Array will be element 2 of the result
|
53
|
+
# # of the re-order, and elements 0, 1, and 3 will be moved in the
|
54
|
+
# # result:
|
55
|
+
# r.reorder(%w(zero one two three)) # => ["three", "zero", "two", "one"]
|
56
|
+
#
|
57
|
+
# Retruns an Array of source indices.
|
58
|
+
def moving_indices
|
59
|
+
@indices.each_with_index
|
60
|
+
.select { |destination, source| source != destination }
|
61
|
+
.map { |_, source| source }
|
62
|
+
end
|
63
|
+
|
64
|
+
private def indices_invalid?
|
65
|
+
@indices.empty? || @indices.sort != (0 ... @indices.size).to_a
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module QuizMaster
|
4
|
+
# A ReorderingVector is a way of specifying how an array should be reordered.
|
5
|
+
# It contains a mapping of source indices so you can say:
|
6
|
+
#
|
7
|
+
# reordered = original.values_at(*reordering_vector)
|
8
|
+
#
|
9
|
+
# The reordering is constructed "randomly" at initialization, and has the idea
|
10
|
+
# of "anchored" indices which are guaranteed to remain in their original
|
11
|
+
# place.
|
12
|
+
class ReorderingVector
|
13
|
+
extend Forwardable
|
14
|
+
include Comparable
|
15
|
+
|
16
|
+
# Public: Initialize a ReorderingVector.
|
17
|
+
#
|
18
|
+
# anchored - An Array of "truthy" values which indicate the size of the
|
19
|
+
# Array to reorder and which elements should be "anchored"
|
20
|
+
# at their current position in the reordering (indicated by
|
21
|
+
# a +true+ value).
|
22
|
+
# shuffle_block - A block used to shuffle the "un-anchored" indices. It is
|
23
|
+
# called with an Array as an argument and should return an
|
24
|
+
# Array containing the same elements. Optional
|
25
|
+
def initialize(anchored, &shuffle_block)
|
26
|
+
shuffle_block ||= ->(a) { a.shuffle }
|
27
|
+
@indices = create_indices(anchored, &shuffle_block)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# :method: []
|
32
|
+
# :args: index
|
33
|
+
#
|
34
|
+
# Public: Get the element at the specified index.
|
35
|
+
#
|
36
|
+
# Returns the element at the specified index.
|
37
|
+
def_delegator :@indices, :[]
|
38
|
+
|
39
|
+
##
|
40
|
+
# :method: size
|
41
|
+
#
|
42
|
+
# Public: Get the size of the ReorderingVector.
|
43
|
+
#
|
44
|
+
# Returns an Integer representing the size of the ReorderingVector.
|
45
|
+
def_delegator :@indices, :size
|
46
|
+
|
47
|
+
##
|
48
|
+
# :method: each
|
49
|
+
#
|
50
|
+
# Public: Allows us to enumerate the elements of the ReorderingVector.
|
51
|
+
#
|
52
|
+
# Yields each element of the Reordering Vector.
|
53
|
+
#
|
54
|
+
# Returns an Enumerator if no block given.
|
55
|
+
def_delegator :@indices, :each
|
56
|
+
|
57
|
+
# Public: Convert a ReorderingVector to an Array
|
58
|
+
#
|
59
|
+
# Examples
|
60
|
+
#
|
61
|
+
# r = ReorderingVector.new([true, false, false, true])
|
62
|
+
# r.to_a # => [0, 2, 1, 3]
|
63
|
+
#
|
64
|
+
# Returns an Array representation of this object.
|
65
|
+
def to_a
|
66
|
+
@indices.dup
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :to_ary, :to_a
|
70
|
+
|
71
|
+
# Public: Compare two ReorderingVectors. We include the Comparable module
|
72
|
+
# so this gives us all of the comparisons. We compare ReorderingVectors as
|
73
|
+
# Arrays.
|
74
|
+
#
|
75
|
+
# other - An other objecy which can be compared to an Array.
|
76
|
+
#
|
77
|
+
# Returns +1 / 0 / -1 for successful comparisons, and nil for incomparable
|
78
|
+
# values.
|
79
|
+
def <=>(other)
|
80
|
+
to_a <=> other.to_a
|
81
|
+
end
|
82
|
+
|
83
|
+
private def create_indices(anchored, &shuffle_block)
|
84
|
+
anchored_indices, floating_indices =
|
85
|
+
anchored.each_index.partition { |i| anchored[i] }
|
86
|
+
|
87
|
+
result = anchored_indices.each_with_object([]) { |i, a| a[i] = i }
|
88
|
+
|
89
|
+
shuffled = shuffle_block.call(floating_indices)
|
90
|
+
floating_indices.zip(shuffled).each_with_object(result) do |(to, from), a|
|
91
|
+
a[to] = from
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -13,7 +13,7 @@ module QuizMaster
|
|
13
13
|
# This generates an infinite range which we can use for numbering the
|
14
14
|
# questions.
|
15
15
|
numbers = (1 .. Float::INFINITY)
|
16
|
-
quiz.questions.zip(numbers).each_with_object(
|
16
|
+
quiz.questions.zip(numbers).each_with_object("") do |(q, number), result|
|
17
17
|
result << "#{number}) #{q.correct_answer.tag}\n"
|
18
18
|
end
|
19
19
|
end
|
@@ -21,7 +21,7 @@ module QuizMaster
|
|
21
21
|
# Public: Create a TextAssembler object
|
22
22
|
def initialize
|
23
23
|
@tags = []
|
24
|
-
@format_string =
|
24
|
+
@format_string = ""
|
25
25
|
end
|
26
26
|
|
27
27
|
# Public: Add a tag reference to the text we are assembling.
|
@@ -35,7 +35,7 @@ module QuizMaster
|
|
35
35
|
# Returns nothing.
|
36
36
|
def add_tag_reference(tag)
|
37
37
|
@tags << tag
|
38
|
-
@format_string <<
|
38
|
+
@format_string << "(%s)"
|
39
39
|
end
|
40
40
|
|
41
41
|
# Public: Add some literal text to the text we are assembling.
|
@@ -48,7 +48,7 @@ module QuizMaster
|
|
48
48
|
#
|
49
49
|
# Returns nothing.
|
50
50
|
def add_text(text)
|
51
|
-
@format_string << text.gsub(
|
51
|
+
@format_string << text.gsub("%", "%%")
|
52
52
|
end
|
53
53
|
|
54
54
|
# Public: Get the String representation of the TextAssembler.
|
@@ -36,7 +36,7 @@ module QuizMaster
|
|
36
36
|
#
|
37
37
|
# Returns a String containing the formatted Question.
|
38
38
|
def format(question, number)
|
39
|
-
answers = AnswersRenderer.new(answers: question.answers, first_tag:
|
39
|
+
answers = AnswersRenderer.new(answers: question.answers, first_tag: "a")
|
40
40
|
|
41
41
|
answers.each_with_object("#{number}) #{question.prompt}\n\n") do |a, text|
|
42
42
|
text << " #{a.tag}) #{a.text}\n"
|
@@ -1,8 +1,9 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative "text_question_formatter"
|
2
2
|
|
3
3
|
module QuizMaster
|
4
4
|
# This is responsible for generating a text version of Quizes.
|
5
5
|
class TextQuestionSheetFormatter
|
6
|
+
# Initialiize a TextQuestionSheetFormatter.
|
6
7
|
def initialize
|
7
8
|
@question_formatter = TextQuestionFormatter.new
|
8
9
|
end
|
data/lib/quiz_master/version.rb
CHANGED
data/quiz_master.gemspec
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
lib = File.expand_path(
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
4
|
+
require "quiz_master/version"
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = "quiz_master"
|
8
8
|
spec.version = QuizMaster::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
9
|
+
spec.authors = ["Mike Stok"]
|
10
|
+
spec.email = ["mike@stok.ca"]
|
11
|
+
spec.summary = "Simple manager for multiple choice quizzes"
|
12
12
|
# spec.description = %q{TODO: Write a longer description. Optional.}
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
13
|
+
spec.homepage = "https://github.com/mikestok/quiz_master"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.required_ruby_version = "~> 2.1"
|
15
17
|
|
16
18
|
# Because there are likely to be / characters in the regular expressions
|
17
19
|
# we relax the rubocop checks for a bit...
|
@@ -19,13 +21,13 @@ Gem::Specification.new do |spec|
|
|
19
21
|
spec.files = `git ls-files -z`.split("\x0")
|
20
22
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
23
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
-
spec.require_paths = [
|
24
|
+
spec.require_paths = ["lib"]
|
23
25
|
# rubocop:enable Style/RegexpLiteral
|
24
26
|
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
28
|
-
spec.add_development_dependency
|
29
|
-
spec.add_development_dependency
|
30
|
-
spec.add_development_dependency
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
spec.add_development_dependency "rubocop"
|
30
|
+
spec.add_development_dependency "rdoc"
|
31
|
+
spec.add_development_dependency "inch"
|
32
|
+
spec.add_development_dependency "sparkr" # inch should have included this...
|
31
33
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Question do
|
4
|
+
describe "reorder_answers" do
|
5
|
+
it "can reorder its answers" do
|
6
|
+
q = example_questions.first
|
7
|
+
reordered = q.reorder_answers(Reorderer.new(2, 0, 1, 3))
|
8
|
+
reordered.answers.map(&:text).must_equal(
|
9
|
+
[
|
10
|
+
"This is the third answer",
|
11
|
+
"This is the first answer",
|
12
|
+
"This is the second answer",
|
13
|
+
"{{3}} and {{5}}"
|
14
|
+
]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises an error if asked to move "anchored" answers' do
|
19
|
+
q = example_questions[2]
|
20
|
+
lambda do
|
21
|
+
q.reorder_answers(Reorderer.new(0, 7, 2, 3, 4, 5, 6, 1))
|
22
|
+
end.must_raise(MovingAnchoredAnswerError)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "reordering_vector" do
|
27
|
+
q = example_questions[2]
|
28
|
+
rv = q.reordering_vector { |a| a.reverse }
|
29
|
+
|
30
|
+
it "must be a ReorderingVector" do
|
31
|
+
rv.must_be_kind_of(ReorderingVector)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "must have the expected order" do
|
35
|
+
rv.to_a.must_equal([5, 4, 3, 2, 1, 0, 6, 7])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "render_answers" do
|
40
|
+
first_tag = "a"
|
41
|
+
q = example_questions.first.render_answers(first_tag: first_tag)
|
42
|
+
|
43
|
+
it "must have the correct first answer and tag" do
|
44
|
+
af = q.answers.first
|
45
|
+
af.tag.must_equal first_tag
|
46
|
+
af.text.must_equal "This is the first answer"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "must have the correct last answer and tag" do
|
50
|
+
al = q.answers.last
|
51
|
+
al.tag.must_equal "d"
|
52
|
+
al.text.must_equal "(a) and (c)"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/test/quiz_spec.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Quiz do
|
4
|
+
describe "answer_reordering_vectors" do
|
5
|
+
it "should generate expected answer reordering vectors" do
|
6
|
+
quiz_source = Quiz.new(example_questions)
|
7
|
+
vectors = quiz_source.answer_reordering_vectors { |a| a.reverse }
|
8
|
+
|
9
|
+
vectors.must_equal([
|
10
|
+
ReorderingVector([3, 2, 1, 0]),
|
11
|
+
ReorderingVector([3, 2, 1, 0]),
|
12
|
+
ReorderingVector([5, 4, 3, 2, 1, 0, 6, 7])
|
13
|
+
])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "question_reordering_vector" do
|
18
|
+
it "should generate expected reordering vector" do
|
19
|
+
quiz_source = Quiz.new(example_questions)
|
20
|
+
v = quiz_source.question_reordering_vector { |a| a.reverse }
|
21
|
+
|
22
|
+
v.must_equal(ReorderingVector([2, 1, 0]))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "reorder_answers" do
|
27
|
+
quiz_source = Quiz.new(example_questions)
|
28
|
+
vectors = quiz_source.answer_reordering_vectors { |a| a.reverse }
|
29
|
+
quiz = quiz_source.reorder_answers(vectors)
|
30
|
+
|
31
|
+
it "should *not* have shuffled the questions" do
|
32
|
+
quiz.questions.first.prompt.must_equal(
|
33
|
+
EXAMPLE_QUESTION_PARAMS.first[:prompt]
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should have shuffled the answers within the question" do
|
38
|
+
quiz.questions.first.answers.first.text.must_equal(
|
39
|
+
EXAMPLE_QUESTION_PARAMS.first[:answers].last.text
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "reorder_questions" do
|
45
|
+
it "should return a quiz with questions re-ordered" do
|
46
|
+
quiz_source = Quiz.new(example_questions)
|
47
|
+
vector = quiz_source.question_reordering_vector { |a| a.reverse }
|
48
|
+
quiz = quiz_source.reorder_questions(vector)
|
49
|
+
|
50
|
+
quiz.questions.first.prompt.must_equal(
|
51
|
+
EXAMPLE_QUESTION_PARAMS.last[:prompt]
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe QuizVariant do
|
4
|
+
describe "initialize" do
|
5
|
+
quiz_source = Quiz.new(example_questions)
|
6
|
+
|
7
|
+
answer_orderings = [
|
8
|
+
ReorderingVector([3, 1, 0, 2]),
|
9
|
+
ReorderingVector([1, 0, 2, 3]),
|
10
|
+
ReorderingVector([4, 0, 1, 3, 5, 2, 6, 7])
|
11
|
+
]
|
12
|
+
question_ordering = ReorderingVector([0, 2, 1])
|
13
|
+
|
14
|
+
variant = QuizVariant.new(quiz_source, answer_orderings, question_ordering)
|
15
|
+
|
16
|
+
it "has questions in the right order" do
|
17
|
+
variant.questions[0].prompt.must_equal "Example 1 prompt"
|
18
|
+
variant.questions[1].prompt.must_equal "What is the colour of a Banana?"
|
19
|
+
variant.questions[2].prompt.must_equal "Example 2 prompt"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "has answers in the right order" do
|
23
|
+
banana_question = variant.questions[1]
|
24
|
+
banana_question.answers[0].text.must_equal "Brown"
|
25
|
+
banana_question.answers[1].text.must_equal "Green"
|
26
|
+
banana_question.answers[6].text.must_equal "All of the colours above"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "has the tags assigned" do
|
30
|
+
banana_question = variant.questions[1]
|
31
|
+
banana_question.answers[0].tag.must_equal "a"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|