github-to-canvas-quiz 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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