github-to-canvas-quiz 0.1.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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +48 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +21 -0
  7. data/NOTES.md +53 -0
  8. data/README.md +89 -0
  9. data/Rakefile +8 -0
  10. data/bin/console +29 -0
  11. data/bin/github-to-canvas-quiz +15 -0
  12. data/bin/setup +8 -0
  13. data/github-to-canvas-quiz.gemspec +50 -0
  14. data/lib/github_to_canvas_quiz/builder/quiz.rb +110 -0
  15. data/lib/github_to_canvas_quiz/canvas_api/client.rb +80 -0
  16. data/lib/github_to_canvas_quiz/canvas_api/endpoints/quiz_questions.rb +29 -0
  17. data/lib/github_to_canvas_quiz/canvas_api/endpoints/quizzes.rb +25 -0
  18. data/lib/github_to_canvas_quiz/canvas_api/endpoints.rb +10 -0
  19. data/lib/github_to_canvas_quiz/cli.rb +31 -0
  20. data/lib/github_to_canvas_quiz/markdown_builder.rb +90 -0
  21. data/lib/github_to_canvas_quiz/markdown_converter.rb +44 -0
  22. data/lib/github_to_canvas_quiz/model/answer/fill_in_multiple_blanks.rb +34 -0
  23. data/lib/github_to_canvas_quiz/model/answer/matching.rb +35 -0
  24. data/lib/github_to_canvas_quiz/model/answer/multiple_answers.rb +33 -0
  25. data/lib/github_to_canvas_quiz/model/answer/multiple_choice.rb +33 -0
  26. data/lib/github_to_canvas_quiz/model/answer/multiple_dropdowns.rb +34 -0
  27. data/lib/github_to_canvas_quiz/model/answer/short_answer.rb +33 -0
  28. data/lib/github_to_canvas_quiz/model/answer/true_false.rb +33 -0
  29. data/lib/github_to_canvas_quiz/model/question.rb +61 -0
  30. data/lib/github_to_canvas_quiz/model/quiz.rb +62 -0
  31. data/lib/github_to_canvas_quiz/parser/canvas/answer/base.rb +19 -0
  32. data/lib/github_to_canvas_quiz/parser/canvas/answer/fill_in_multiple_blanks.rb +30 -0
  33. data/lib/github_to_canvas_quiz/parser/canvas/answer/matching.rb +31 -0
  34. data/lib/github_to_canvas_quiz/parser/canvas/answer/multiple_answers.rb +33 -0
  35. data/lib/github_to_canvas_quiz/parser/canvas/answer/multiple_choice.rb +33 -0
  36. data/lib/github_to_canvas_quiz/parser/canvas/answer/multiple_dropdowns.rb +30 -0
  37. data/lib/github_to_canvas_quiz/parser/canvas/answer/short_answer.rb +29 -0
  38. data/lib/github_to_canvas_quiz/parser/canvas/answer/true_false.rb +29 -0
  39. data/lib/github_to_canvas_quiz/parser/canvas/answer.rb +34 -0
  40. data/lib/github_to_canvas_quiz/parser/canvas/helpers.rb +25 -0
  41. data/lib/github_to_canvas_quiz/parser/canvas/question.rb +55 -0
  42. data/lib/github_to_canvas_quiz/parser/canvas/quiz.rb +44 -0
  43. data/lib/github_to_canvas_quiz/parser/markdown/answer/base.rb +22 -0
  44. data/lib/github_to_canvas_quiz/parser/markdown/answer/fill_in_multiple_blanks.rb +21 -0
  45. data/lib/github_to_canvas_quiz/parser/markdown/answer/matching.rb +23 -0
  46. data/lib/github_to_canvas_quiz/parser/markdown/answer/multiple_answers.rb +21 -0
  47. data/lib/github_to_canvas_quiz/parser/markdown/answer/multiple_choice.rb +19 -0
  48. data/lib/github_to_canvas_quiz/parser/markdown/answer/multiple_dropdowns.rb +21 -0
  49. data/lib/github_to_canvas_quiz/parser/markdown/answer/short_answer.rb +21 -0
  50. data/lib/github_to_canvas_quiz/parser/markdown/answer/true_false.rb +21 -0
  51. data/lib/github_to_canvas_quiz/parser/markdown/answer.rb +34 -0
  52. data/lib/github_to_canvas_quiz/parser/markdown/base.rb +22 -0
  53. data/lib/github_to_canvas_quiz/parser/markdown/helpers/node_parser.rb +21 -0
  54. data/lib/github_to_canvas_quiz/parser/markdown/helpers/node_scanner.rb +208 -0
  55. data/lib/github_to_canvas_quiz/parser/markdown/question.rb +76 -0
  56. data/lib/github_to_canvas_quiz/parser/markdown/quiz.rb +42 -0
  57. data/lib/github_to_canvas_quiz/repository_interface.rb +42 -0
  58. data/lib/github_to_canvas_quiz/reverse_markdown/converters/p.rb +20 -0
  59. data/lib/github_to_canvas_quiz/reverse_markdown/converters/pre.rb +51 -0
  60. data/lib/github_to_canvas_quiz/reverse_markdown/register.rb +9 -0
  61. data/lib/github_to_canvas_quiz/synchronizer/quiz.rb +140 -0
  62. data/lib/github_to_canvas_quiz/version.rb +5 -0
  63. data/lib/github_to_canvas_quiz.rb +62 -0
  64. 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