word_2_quiz 1.0.0
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 +7 -0
- data/.gitignore +16 -0
- data/.hound.yml +7 -0
- data/.rubocop.yml +637 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/Gemfile +4 -0
- data/README.md +49 -0
- data/Rakefile +12 -0
- data/bin/console +14 -0
- data/bin/convert_quiz +13 -0
- data/bin/setup +8 -0
- data/lib/word_2_quiz.rb +5 -0
- data/lib/word_2_quiz/answer.rb +52 -0
- data/lib/word_2_quiz/helpers.rb +104 -0
- data/lib/word_2_quiz/question.rb +65 -0
- data/lib/word_2_quiz/quiz.rb +86 -0
- data/lib/word_2_quiz/quiz_parser.rb +15 -0
- data/lib/word_2_quiz/quiz_solutions_parser.rb +20 -0
- data/lib/word_2_quiz/version.rb +3 -0
- data/word_2_quiz.gemspec +37 -0
- metadata +191 -0
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
word2quiz
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.9
|
data/.travis.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
---
|
2
|
+
language: ruby
|
3
|
+
cache: bundler
|
4
|
+
|
5
|
+
rvm:
|
6
|
+
- 2.1.9
|
7
|
+
- 2.2.2
|
8
|
+
- 2.3.3
|
9
|
+
|
10
|
+
before_install:
|
11
|
+
- gem update bundler
|
12
|
+
|
13
|
+
install:
|
14
|
+
- bundle install --path vendor/bundle
|
15
|
+
|
16
|
+
branches:
|
17
|
+
only:
|
18
|
+
- master
|
19
|
+
|
20
|
+
script: bundle exec rake
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Word2Quiz
|
2
|
+
|
3
|
+
Converts word document quizzes to a Word2Quiz::Quiz, which can be converted to a hash or to an Instructure canvas hash format.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'word_2_quiz'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install word_2_quiz
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Expects a docx quiz where every question is on a new line started with a number
|
24
|
+
followed by a period, and every answer is below each question. Every answer is
|
25
|
+
on a new line, and begins with a letter followed by a period. The title is
|
26
|
+
assumed to be on lines 3 & 4, and the description is generated from lines 0-5 of
|
27
|
+
the quiz.
|
28
|
+
|
29
|
+
The answer key should be in a doc file, where each solution consists of a
|
30
|
+
number and an answer - e.g `1. C 2.B`
|
31
|
+
|
32
|
+
To get the parse the quiz, run
|
33
|
+
`quiz = Word2Quiz.parse_quiz("path/to/quiz.docx", "path/to/solutions.doc")`
|
34
|
+
|
35
|
+
To get the quiz as a hash, run `quiz.to_h`
|
36
|
+
|
37
|
+
To get the quiz for creating on canvas run `quiz.to_canvas`
|
38
|
+
|
39
|
+
To get the quiz questions for creating on canvas run `quiz.questions_as_canvas`
|
40
|
+
|
41
|
+
## Development
|
42
|
+
|
43
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
44
|
+
|
45
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
46
|
+
|
47
|
+
## Contributing
|
48
|
+
|
49
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/word_2_quiz.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "word_2_quiz"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/convert_quiz
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
LIB_DIR = File.join(__dir__, "..", "lib")
|
4
|
+
$LOAD_PATH << LIB_DIR
|
5
|
+
require "bundler/setup"
|
6
|
+
require "word_2_quiz/quiz_parser"
|
7
|
+
require "word_2_quiz/quiz_solutions_parser"
|
8
|
+
|
9
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
10
|
+
# with your gem easier. You can also use a different console, if you like.
|
11
|
+
#Switch this out with parse solutions
|
12
|
+
#Word2Quiz.parse_quiz(ARGV[0])
|
13
|
+
Word2Quiz.parse_quiz(ARGV[0], ARGV[1])
|
data/bin/setup
ADDED
data/lib/word_2_quiz.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require "word_2_quiz/helpers"
|
2
|
+
|
3
|
+
module Word2Quiz
|
4
|
+
##
|
5
|
+
# Answer contains all data for a single answer. An answer has text and whether
|
6
|
+
# it is correct.
|
7
|
+
##
|
8
|
+
class Answer
|
9
|
+
attr_accessor :text, :correct
|
10
|
+
|
11
|
+
# index of the very first answer paragraph
|
12
|
+
ANSWER_START = 0
|
13
|
+
|
14
|
+
# Canvas expects a weight greather than zero for correct, 0 for incorrect.
|
15
|
+
CORRECT_WEIGHT = 100
|
16
|
+
INCORRECT_WEIGHT = 0
|
17
|
+
|
18
|
+
def initialize(text = "", correct = false)
|
19
|
+
@text = text
|
20
|
+
@correct = correct
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Expects an array of paragraphs that are a single answer, and
|
25
|
+
# The correct answer for that question, e.g. answer="a"
|
26
|
+
##
|
27
|
+
def self.from_paragraphs(paragraphs, solution)
|
28
|
+
non_empty_paragraphs = Helpers.strip_blanks(paragraphs)
|
29
|
+
text = non_empty_paragraphs.map(&:to_html).join("\n")
|
30
|
+
start_paragraph = non_empty_paragraphs[ANSWER_START]
|
31
|
+
is_correct = start_paragraph.text.downcase.start_with?(solution.downcase)
|
32
|
+
|
33
|
+
Answer.new(text, is_correct)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
{
|
38
|
+
text: @text,
|
39
|
+
correct: @correct,
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Canvas has an undocumented answer_html field for answers that we have to
|
44
|
+
# use because we are sending html, not plain text.
|
45
|
+
def to_canvas
|
46
|
+
{
|
47
|
+
answer_html: @text,
|
48
|
+
answer_weight: @correct ? CORRECT_WEIGHT : INCORRECT_WEIGHT,
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require "numbers_in_words"
|
2
|
+
module Word2Quiz
|
3
|
+
module Helpers
|
4
|
+
TIME_TO_MINUTES_MAP = {
|
5
|
+
"hours" => 60,
|
6
|
+
"minutes" => 1,
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
# indexes of paragraphs where title and description start and end
|
10
|
+
TITLE_START = 3
|
11
|
+
TITLE_END = 4
|
12
|
+
DESCRIPTION_START = 0
|
13
|
+
DESCRIPTION_END = 5
|
14
|
+
QUESTION_START = 0
|
15
|
+
|
16
|
+
##
|
17
|
+
# Takes in an array of indexes, and returns out an array of arrays of
|
18
|
+
# paragraphs bounded by the indexes, e.g. if indexes is [2, 5] then it
|
19
|
+
# returns:
|
20
|
+
# [paragraphs[0...2], paragraphs[2...5], paragraphs[5...paragraphs.count]]
|
21
|
+
##
|
22
|
+
def self.map_to_boundaries(indexes:, paragraphs:)
|
23
|
+
indexes.map.with_index do |start_index, i|
|
24
|
+
first_index = start_index
|
25
|
+
last_index = indexes[i + 1] || paragraphs.count
|
26
|
+
|
27
|
+
paragraphs[first_index...last_index]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.get_quiz_duration(paragraphs)
|
32
|
+
time_paragraph = paragraphs.find do |p|
|
33
|
+
p.text.include?("one session not to exceed ")
|
34
|
+
end
|
35
|
+
|
36
|
+
time = time_paragraph.text.sub(
|
37
|
+
"This examination consists of one session not to exceed ",
|
38
|
+
"",
|
39
|
+
).chomp(".")
|
40
|
+
|
41
|
+
time_number = time.split(" ").first
|
42
|
+
time_unit = time.split(" ").last
|
43
|
+
unit_conversion = TIME_TO_MINUTES_MAP[time_unit]
|
44
|
+
|
45
|
+
NumbersInWords.in_numbers(time_number) * unit_conversion
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Returns the quiz title. The 3rd and 4th lines contain the best title for
|
50
|
+
# the quiz.
|
51
|
+
##
|
52
|
+
def self.get_quiz_title(paragraphs)
|
53
|
+
paragraphs[TITLE_START..TITLE_END].map(&:text).join(" ")
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Returns the quiz description. Lines 0-5 are the best description for the
|
58
|
+
# quiz.
|
59
|
+
##
|
60
|
+
def self.get_quiz_description(paragraphs)
|
61
|
+
paragraphs[DESCRIPTION_START..DESCRIPTION_END].map(&:to_html).join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Returns the question number
|
66
|
+
#
|
67
|
+
##
|
68
|
+
def self.get_question_number(paragraphs)
|
69
|
+
match = paragraphs[QUESTION_START].text.match(/^(\d+)\./)
|
70
|
+
match ? match.captures.first : nil
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Finds the index of the paragraph where the questions start
|
75
|
+
##
|
76
|
+
def self.get_question_start_index(paragraphs)
|
77
|
+
paragraphs.find_index { |p| p.text.include?("Multiple Choice") }
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Finds the index of the paragraph where the questions end
|
82
|
+
##
|
83
|
+
def self.get_question_end_index(paragraphs)
|
84
|
+
paragraphs.find_index { |p| p.text.include?("End of examination") }
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Returns the indexes of the start of each question.
|
89
|
+
##
|
90
|
+
def self.get_question_indexes(paragraphs)
|
91
|
+
paragraphs.each_index.select { |i| paragraphs[i].text.match(/^\d*\./) }
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Returns an array of paragraphs with leading and trailing blank paragraphs
|
96
|
+
# removed.
|
97
|
+
##
|
98
|
+
def self.strip_blanks(paragraphs)
|
99
|
+
t = paragraphs.drop_while { |p| p.text.chomp.empty? }
|
100
|
+
t.pop while t.last.text.chomp.empty?
|
101
|
+
t
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "word_2_quiz/helpers"
|
2
|
+
|
3
|
+
module Word2Quiz
|
4
|
+
##
|
5
|
+
# Question contains all data for a single question. A question has text, and
|
6
|
+
# answers.
|
7
|
+
##
|
8
|
+
class Question
|
9
|
+
attr_accessor :text, :answers
|
10
|
+
|
11
|
+
POINTS_POSSIBLE = 5
|
12
|
+
|
13
|
+
def initialize(text = "", answers = [])
|
14
|
+
@answers = answers
|
15
|
+
@text = text
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Creates a question from an array of strings
|
20
|
+
# paragraphs: an array of nokogiri nodes containing each line for the
|
21
|
+
# question text and answers
|
22
|
+
# solution: a string containing which answer(a,b,c,d) is correct
|
23
|
+
##
|
24
|
+
def self.from_paragraphs(paragraphs, solution)
|
25
|
+
answers = []
|
26
|
+
|
27
|
+
answer_start_indexes = paragraphs.each_index.select do |i|
|
28
|
+
# an answer starts with a letter then a dot
|
29
|
+
paragraphs[i].text.match(/^[a-z]\./)
|
30
|
+
end
|
31
|
+
|
32
|
+
all_answer_paragraphs = Helpers.map_to_boundaries(
|
33
|
+
indexes: answer_start_indexes,
|
34
|
+
paragraphs: paragraphs,
|
35
|
+
)
|
36
|
+
|
37
|
+
question_paragraphs = paragraphs.take(answer_start_indexes.first)
|
38
|
+
question_paragraphs = Helpers.strip_blanks(question_paragraphs)
|
39
|
+
question_text = question_paragraphs.map(&:to_html).join("\n")
|
40
|
+
|
41
|
+
all_answer_paragraphs.each do |answer_paragraphs|
|
42
|
+
answer = Answer.from_paragraphs(answer_paragraphs, solution)
|
43
|
+
answers.push(answer)
|
44
|
+
end
|
45
|
+
|
46
|
+
Question.new(question_text, answers)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_h
|
50
|
+
{
|
51
|
+
text: @text,
|
52
|
+
answers: @answers.map(&:to_h),
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_canvas
|
57
|
+
{
|
58
|
+
question_type: "multiple_choice_question",
|
59
|
+
question_text: @text,
|
60
|
+
points_possible: POINTS_POSSIBLE,
|
61
|
+
answers: @answers.map(&:to_canvas),
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "word_2_quiz/question"
|
2
|
+
require "word_2_quiz/answer"
|
3
|
+
require "word_2_quiz/helpers"
|
4
|
+
|
5
|
+
module Word2Quiz
|
6
|
+
##
|
7
|
+
# Quiz contains all quiz data. A quiz has a description,
|
8
|
+
# a title, a time limit, and questions.
|
9
|
+
##
|
10
|
+
class Quiz
|
11
|
+
attr_accessor :questions, :description, :title, :time_limit
|
12
|
+
|
13
|
+
ALLOWED_ATTEMPTS = 1
|
14
|
+
|
15
|
+
def initialize(title:, time_limit:, description: "")
|
16
|
+
@questions = []
|
17
|
+
@description = description
|
18
|
+
@title = title
|
19
|
+
@time_limit = time_limit
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_paragraphs(paragraphs, answers)
|
23
|
+
all_question_paragraphs = Quiz.get_question_paragraphs(paragraphs)
|
24
|
+
|
25
|
+
quiz_title = Helpers.get_quiz_title(paragraphs)
|
26
|
+
quiz_description = Helpers.get_quiz_description(paragraphs)
|
27
|
+
|
28
|
+
quiz_duration = Helpers.get_quiz_duration(paragraphs)
|
29
|
+
|
30
|
+
quiz = Quiz.new(
|
31
|
+
title: quiz_title,
|
32
|
+
time_limit: quiz_duration,
|
33
|
+
description: quiz_description,
|
34
|
+
)
|
35
|
+
|
36
|
+
all_question_paragraphs.each do |question_paragraphs|
|
37
|
+
question_number = Helpers.get_question_number(question_paragraphs)
|
38
|
+
question = Question.from_paragraphs(
|
39
|
+
question_paragraphs,
|
40
|
+
answers[question_number],
|
41
|
+
)
|
42
|
+
|
43
|
+
quiz.questions.push(question)
|
44
|
+
end
|
45
|
+
quiz
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.get_question_paragraphs(paragraphs)
|
49
|
+
question_start_index = Helpers.get_question_start_index(paragraphs)
|
50
|
+
question_end_index = Helpers.get_question_end_index(paragraphs)
|
51
|
+
|
52
|
+
question_range = paragraphs[question_start_index...question_end_index]
|
53
|
+
|
54
|
+
question_indexes = Helpers.get_question_indexes(question_range)
|
55
|
+
|
56
|
+
Helpers.map_to_boundaries(
|
57
|
+
indexes: question_indexes,
|
58
|
+
paragraphs: question_range,
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_h
|
63
|
+
{
|
64
|
+
title: @title,
|
65
|
+
questions: @questions.map(&:to_h),
|
66
|
+
description: @description,
|
67
|
+
time_limit: @time_limit,
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_canvas
|
72
|
+
{
|
73
|
+
title: @title,
|
74
|
+
question_count: @questions.count,
|
75
|
+
quiz_type: "assignment",
|
76
|
+
description: @description,
|
77
|
+
allowed_attempts: ALLOWED_ATTEMPTS,
|
78
|
+
time_limit: @time_limit,
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def questions_as_canvas
|
83
|
+
@questions.map(&:to_canvas)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|