quiz_master 0.0.2 → 0.0.3
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 +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
|