github-to-canvas-quiz 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +48 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/NOTES.md +53 -0
- data/README.md +89 -0
- data/Rakefile +8 -0
- data/bin/console +29 -0
- data/bin/github-to-canvas-quiz +15 -0
- data/bin/setup +8 -0
- data/github-to-canvas-quiz.gemspec +50 -0
- data/lib/github_to_canvas_quiz/builder/quiz.rb +110 -0
- data/lib/github_to_canvas_quiz/canvas_api/client.rb +80 -0
- data/lib/github_to_canvas_quiz/canvas_api/endpoints/quiz_questions.rb +29 -0
- data/lib/github_to_canvas_quiz/canvas_api/endpoints/quizzes.rb +25 -0
- data/lib/github_to_canvas_quiz/canvas_api/endpoints.rb +10 -0
- data/lib/github_to_canvas_quiz/cli.rb +31 -0
- data/lib/github_to_canvas_quiz/markdown_builder.rb +90 -0
- data/lib/github_to_canvas_quiz/markdown_converter.rb +44 -0
- data/lib/github_to_canvas_quiz/model/answer/fill_in_multiple_blanks.rb +34 -0
- data/lib/github_to_canvas_quiz/model/answer/matching.rb +35 -0
- data/lib/github_to_canvas_quiz/model/answer/multiple_answers.rb +33 -0
- data/lib/github_to_canvas_quiz/model/answer/multiple_choice.rb +33 -0
- data/lib/github_to_canvas_quiz/model/answer/multiple_dropdowns.rb +34 -0
- data/lib/github_to_canvas_quiz/model/answer/short_answer.rb +33 -0
- data/lib/github_to_canvas_quiz/model/answer/true_false.rb +33 -0
- data/lib/github_to_canvas_quiz/model/question.rb +61 -0
- data/lib/github_to_canvas_quiz/model/quiz.rb +62 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/base.rb +19 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/fill_in_multiple_blanks.rb +30 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/matching.rb +31 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/multiple_answers.rb +33 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/multiple_choice.rb +33 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/multiple_dropdowns.rb +30 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/short_answer.rb +29 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer/true_false.rb +29 -0
- data/lib/github_to_canvas_quiz/parser/canvas/answer.rb +34 -0
- data/lib/github_to_canvas_quiz/parser/canvas/helpers.rb +25 -0
- data/lib/github_to_canvas_quiz/parser/canvas/question.rb +55 -0
- data/lib/github_to_canvas_quiz/parser/canvas/quiz.rb +44 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/base.rb +22 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/fill_in_multiple_blanks.rb +21 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/matching.rb +23 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/multiple_answers.rb +21 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/multiple_choice.rb +19 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/multiple_dropdowns.rb +21 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/short_answer.rb +21 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer/true_false.rb +21 -0
- data/lib/github_to_canvas_quiz/parser/markdown/answer.rb +34 -0
- data/lib/github_to_canvas_quiz/parser/markdown/base.rb +22 -0
- data/lib/github_to_canvas_quiz/parser/markdown/helpers/node_parser.rb +21 -0
- data/lib/github_to_canvas_quiz/parser/markdown/helpers/node_scanner.rb +208 -0
- data/lib/github_to_canvas_quiz/parser/markdown/question.rb +76 -0
- data/lib/github_to_canvas_quiz/parser/markdown/quiz.rb +42 -0
- data/lib/github_to_canvas_quiz/repository_interface.rb +42 -0
- data/lib/github_to_canvas_quiz/reverse_markdown/converters/p.rb +20 -0
- data/lib/github_to_canvas_quiz/reverse_markdown/converters/pre.rb +51 -0
- data/lib/github_to_canvas_quiz/reverse_markdown/register.rb +9 -0
- data/lib/github_to_canvas_quiz/synchronizer/quiz.rb +140 -0
- data/lib/github_to_canvas_quiz/version.rb +5 -0
- data/lib/github_to_canvas_quiz.rb +62 -0
- metadata +348 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Canvas
|
6
|
+
module Answer
|
7
|
+
class MultipleDropdowns < Base
|
8
|
+
def parse
|
9
|
+
Model::Answer::MultipleDropdowns.new(
|
10
|
+
title: title,
|
11
|
+
text: data['text'],
|
12
|
+
comments: comments,
|
13
|
+
blank_id: data['blank_id']
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def title
|
20
|
+
data.fetch('weight', 0).positive? ? 'Correct' : 'Incorrect'
|
21
|
+
end
|
22
|
+
|
23
|
+
def comments
|
24
|
+
choose_text(data.fetch('comments', ''), data.fetch('comments_html', ''))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Canvas
|
6
|
+
module Answer
|
7
|
+
class ShortAnswer < Base
|
8
|
+
def parse
|
9
|
+
Model::Answer::ShortAnswer.new(
|
10
|
+
title: title,
|
11
|
+
text: data['text'],
|
12
|
+
comments: comments
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def title
|
19
|
+
data.fetch('weight', 0).positive? ? 'Correct' : 'Incorrect'
|
20
|
+
end
|
21
|
+
|
22
|
+
def comments
|
23
|
+
choose_text(data.fetch('comments', ''), data.fetch('comments_html', ''))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Canvas
|
6
|
+
module Answer
|
7
|
+
class TrueFalse < Base
|
8
|
+
def parse
|
9
|
+
Model::Answer::TrueFalse.new(
|
10
|
+
title: title,
|
11
|
+
text: data['text'],
|
12
|
+
comments: comments
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def title
|
19
|
+
data.fetch('weight', 0).positive? ? 'Correct' : 'Incorrect'
|
20
|
+
end
|
21
|
+
|
22
|
+
def comments
|
23
|
+
choose_text(data.fetch('comments', ''), data.fetch('comments_html', ''))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'answer/base'
|
4
|
+
require_relative 'answer/fill_in_multiple_blanks'
|
5
|
+
require_relative 'answer/matching'
|
6
|
+
require_relative 'answer/multiple_answers'
|
7
|
+
require_relative 'answer/multiple_choice'
|
8
|
+
require_relative 'answer/multiple_dropdowns'
|
9
|
+
require_relative 'answer/short_answer'
|
10
|
+
require_relative 'answer/true_false'
|
11
|
+
|
12
|
+
module GithubToCanvasQuiz
|
13
|
+
module Parser
|
14
|
+
module Canvas
|
15
|
+
module Answer
|
16
|
+
CLASSES = {
|
17
|
+
'fill_in_multiple_blanks_question' => FillInMultipleBlanks,
|
18
|
+
'matching_question' => Matching,
|
19
|
+
'multiple_answers_question' => MultipleAnswers,
|
20
|
+
'multiple_choice_question' => MultipleChoice,
|
21
|
+
'multiple_dropdowns_question' => MultipleDropdowns,
|
22
|
+
'short_answer_question' => ShortAnswer,
|
23
|
+
'true_false_question' => TrueFalse
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def self.for(type, data)
|
27
|
+
raise UnknownQuestionType, type unless CLASSES.key?(type)
|
28
|
+
|
29
|
+
CLASSES[type].new(data).parse
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Canvas
|
6
|
+
module Helpers
|
7
|
+
def choose_text(text, html)
|
8
|
+
html.empty? ? text : clean_html(html)
|
9
|
+
end
|
10
|
+
|
11
|
+
def clean_html(html)
|
12
|
+
html = remove_canvas_cruft(html)
|
13
|
+
HTMLEntities.new.decode(html)
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove_canvas_cruft(html)
|
17
|
+
nodes = Nokogiri::HTML5.fragment(html)
|
18
|
+
nodes.css('.screenreader-only').remove
|
19
|
+
cleaned_html = nodes.to_html
|
20
|
+
cleaned_html.gsub(/\(?Links to an external site.\)?/, '')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Canvas
|
6
|
+
class Question
|
7
|
+
include Helpers
|
8
|
+
|
9
|
+
attr_reader :data
|
10
|
+
|
11
|
+
def initialize(data)
|
12
|
+
@data = data
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse
|
16
|
+
Model::Question.new(
|
17
|
+
course_id: data['course_id'],
|
18
|
+
quiz_id: data['quiz_id'],
|
19
|
+
id: data['id'],
|
20
|
+
type: data['question_type'],
|
21
|
+
name: data['question_name'],
|
22
|
+
description: clean_html(data['question_text']),
|
23
|
+
sources: sources,
|
24
|
+
answers: answers,
|
25
|
+
distractors: distractors
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def sources
|
32
|
+
html = data['neutral_comments_html']
|
33
|
+
return unless html
|
34
|
+
|
35
|
+
Nokogiri::HTML5.fragment(html).css('a').map do |node|
|
36
|
+
{
|
37
|
+
'name' => clean_html(node.content),
|
38
|
+
'url' => node['href']
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def answers
|
44
|
+
data['answers'].map do |answer|
|
45
|
+
Answer.for(data['question_type'], answer)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def distractors
|
50
|
+
(data['matching_answer_incorrect_matches'] || '').split("\n")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Canvas
|
6
|
+
# Parses a quiz from the Canvas API and returns a Quiz
|
7
|
+
class Quiz
|
8
|
+
include Helpers
|
9
|
+
|
10
|
+
attr_reader :data
|
11
|
+
|
12
|
+
def initialize(data)
|
13
|
+
@data = data
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
Model::Quiz.new(
|
18
|
+
course_id: data['course_id'],
|
19
|
+
id: data['id'],
|
20
|
+
repo: repo,
|
21
|
+
title: data['title'],
|
22
|
+
description: description
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Remove header elements
|
29
|
+
def description
|
30
|
+
nodes = Nokogiri::HTML5.fragment(data['description'])
|
31
|
+
nodes.css('#git-data-element').remove
|
32
|
+
nodes.css('.fis-header').remove
|
33
|
+
nodes.to_html.strip
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parse the repo from the #git-data-element
|
37
|
+
def repo
|
38
|
+
data_element = Nokogiri::HTML5.fragment(data['description']).css('#git-data-element').first
|
39
|
+
data_element ? data_element['data-repo'] : nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class Base
|
8
|
+
include Helpers::NodeParser
|
9
|
+
|
10
|
+
attr_reader :answer_nodes, :comment, :title
|
11
|
+
|
12
|
+
def initialize(title, nodes)
|
13
|
+
@title = title
|
14
|
+
scanner = Helpers::NodeScanner.new(nodes)
|
15
|
+
@answer_nodes = scanner.scan_before('blockquote') || scanner.scan_rest
|
16
|
+
@comment = scanner.eof? ? '' : scanner.scan_rest.first.inner_html.strip
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class FillInMultipleBlanks < Base
|
8
|
+
def parse
|
9
|
+
text, blank_id = parse_text_from_nodes(answer_nodes, 'li')
|
10
|
+
Model::Answer::FillInMultipleBlanks.new(
|
11
|
+
title: title,
|
12
|
+
text: text,
|
13
|
+
comments: comment,
|
14
|
+
blank_id: blank_id
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class Matching < Base
|
8
|
+
def parse
|
9
|
+
left, right = parse_text_from_nodes(answer_nodes, 'li')
|
10
|
+
|
11
|
+
Model::Answer::Matching.new(
|
12
|
+
title: title,
|
13
|
+
left: left,
|
14
|
+
right: right,
|
15
|
+
text: left,
|
16
|
+
comments: comment
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class MultipleAnswers < Base
|
8
|
+
def parse
|
9
|
+
text = answer_nodes.to_html.strip
|
10
|
+
|
11
|
+
Model::Answer::MultipleAnswers.new(
|
12
|
+
title: title,
|
13
|
+
text: text,
|
14
|
+
comments: comment
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class MultipleChoice < Base
|
8
|
+
def parse
|
9
|
+
Model::Answer::MultipleChoice.new(
|
10
|
+
title: title,
|
11
|
+
text: answer_nodes.to_html.strip,
|
12
|
+
comments: comment
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class MultipleDropdowns < Base
|
8
|
+
def parse
|
9
|
+
text, blank_id = parse_text_from_nodes(answer_nodes, 'li')
|
10
|
+
Model::Answer::MultipleDropdowns.new(
|
11
|
+
title: title,
|
12
|
+
text: text,
|
13
|
+
comments: comment,
|
14
|
+
blank_id: blank_id
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class ShortAnswer < Base
|
8
|
+
def parse
|
9
|
+
text = parse_text_from_nodes(answer_nodes, 'p').first
|
10
|
+
|
11
|
+
Model::Answer::ShortAnswer.new(
|
12
|
+
title: title,
|
13
|
+
text: text,
|
14
|
+
comments: comment
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Answer
|
7
|
+
class TrueFalse < Base
|
8
|
+
def parse
|
9
|
+
text = parse_text_from_nodes(answer_nodes, 'p').first
|
10
|
+
|
11
|
+
Model::Answer::TrueFalse.new(
|
12
|
+
title: title,
|
13
|
+
text: text,
|
14
|
+
comments: comment
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'answer/base'
|
4
|
+
require_relative 'answer/fill_in_multiple_blanks'
|
5
|
+
require_relative 'answer/matching'
|
6
|
+
require_relative 'answer/multiple_answers'
|
7
|
+
require_relative 'answer/multiple_choice'
|
8
|
+
require_relative 'answer/multiple_dropdowns'
|
9
|
+
require_relative 'answer/short_answer'
|
10
|
+
require_relative 'answer/true_false'
|
11
|
+
|
12
|
+
module GithubToCanvasQuiz
|
13
|
+
module Parser
|
14
|
+
module Markdown
|
15
|
+
module Answer
|
16
|
+
CLASSES = {
|
17
|
+
'fill_in_multiple_blanks_question' => FillInMultipleBlanks,
|
18
|
+
'matching_question' => Matching,
|
19
|
+
'multiple_answers_question' => MultipleAnswers,
|
20
|
+
'multiple_choice_question' => MultipleChoice,
|
21
|
+
'multiple_dropdowns_question' => MultipleDropdowns,
|
22
|
+
'short_answer_question' => ShortAnswer,
|
23
|
+
'true_false_question' => TrueFalse
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def self.for(type, title, nodes)
|
27
|
+
raise UnknownQuestionType, type unless CLASSES.key?(type)
|
28
|
+
|
29
|
+
CLASSES[type].new(title, nodes).parse
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
class Base
|
7
|
+
attr_reader :frontmatter, :markdown
|
8
|
+
|
9
|
+
def initialize(markdown)
|
10
|
+
# Separate the frontmatter and the rest of the markdown content
|
11
|
+
parsed = if Pathname(markdown).exist?
|
12
|
+
FrontMatterParser::Parser.parse_file(markdown)
|
13
|
+
else
|
14
|
+
FrontMatterParser::Parser.new(:md).call(markdown)
|
15
|
+
end
|
16
|
+
@frontmatter = parsed.front_matter
|
17
|
+
@markdown = parsed.content
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubToCanvasQuiz
|
4
|
+
module Parser
|
5
|
+
module Markdown
|
6
|
+
module Helpers
|
7
|
+
module NodeParser
|
8
|
+
def parse_text_from_nodes(nodes, selector)
|
9
|
+
nodes.css(selector).map do |node|
|
10
|
+
parse_text_from_node(node)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse_text_from_node(node)
|
15
|
+
CGI.unescapeHTML(node.content).strip
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|