moodle2cc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/.gitignore +20 -0
  2. data/Gemfile +4 -0
  3. data/Guardfile +14 -0
  4. data/LICENSE +661 -0
  5. data/README.md +38 -0
  6. data/Rakefile +12 -0
  7. data/bin/moodle2cc +6 -0
  8. data/lib/moodle2cc.rb +60 -0
  9. data/lib/moodle2cc/canvas/.converter.rb.swo +0 -0
  10. data/lib/moodle2cc/canvas/.wiki.rb.swo +0 -0
  11. data/lib/moodle2cc/canvas/assessment.rb +109 -0
  12. data/lib/moodle2cc/canvas/assignment.rb +108 -0
  13. data/lib/moodle2cc/canvas/converter.rb +28 -0
  14. data/lib/moodle2cc/canvas/course.rb +145 -0
  15. data/lib/moodle2cc/canvas/discussion_topic.rb +64 -0
  16. data/lib/moodle2cc/canvas/label.rb +8 -0
  17. data/lib/moodle2cc/canvas/question.rb +468 -0
  18. data/lib/moodle2cc/canvas/question_bank.rb +54 -0
  19. data/lib/moodle2cc/canvas/resource.rb +16 -0
  20. data/lib/moodle2cc/canvas/templates/assignment.html.erb +9 -0
  21. data/lib/moodle2cc/canvas/templates/syllabus.html.erb +9 -0
  22. data/lib/moodle2cc/canvas/templates/wiki_content.html.erb +10 -0
  23. data/lib/moodle2cc/canvas/web_content.rb +10 -0
  24. data/lib/moodle2cc/canvas/web_link.rb +17 -0
  25. data/lib/moodle2cc/canvas/wiki.rb +25 -0
  26. data/lib/moodle2cc/cc/assessment.rb +25 -0
  27. data/lib/moodle2cc/cc/assignment.rb +66 -0
  28. data/lib/moodle2cc/cc/cc_helper.rb +282 -0
  29. data/lib/moodle2cc/cc/converter.rb +136 -0
  30. data/lib/moodle2cc/cc/course.rb +16 -0
  31. data/lib/moodle2cc/cc/discussion_topic.rb +53 -0
  32. data/lib/moodle2cc/cc/label.rb +6 -0
  33. data/lib/moodle2cc/cc/question.rb +23 -0
  34. data/lib/moodle2cc/cc/resource.rb +30 -0
  35. data/lib/moodle2cc/cc/templates/assignment.html.erb +9 -0
  36. data/lib/moodle2cc/cc/templates/syllabus.html.erb +9 -0
  37. data/lib/moodle2cc/cc/templates/wiki_content.html.erb +10 -0
  38. data/lib/moodle2cc/cc/web_content.rb +40 -0
  39. data/lib/moodle2cc/cc/web_link.rb +53 -0
  40. data/lib/moodle2cc/cc/wiki.rb +72 -0
  41. data/lib/moodle2cc/cli.rb +14 -0
  42. data/lib/moodle2cc/error.rb +3 -0
  43. data/lib/moodle2cc/migrator.rb +23 -0
  44. data/lib/moodle2cc/moodle/backup.rb +38 -0
  45. data/lib/moodle2cc/moodle/course.rb +24 -0
  46. data/lib/moodle2cc/moodle/grade_item.rb +24 -0
  47. data/lib/moodle2cc/moodle/info.rb +8 -0
  48. data/lib/moodle2cc/moodle/mod.rb +127 -0
  49. data/lib/moodle2cc/moodle/question.rb +98 -0
  50. data/lib/moodle2cc/moodle/question_category.rb +17 -0
  51. data/lib/moodle2cc/moodle/section.rb +37 -0
  52. data/lib/moodle2cc/resource_factory.rb +32 -0
  53. data/lib/moodle2cc/version.rb +3 -0
  54. data/moodle2cc.gemspec +29 -0
  55. data/test/acceptance/migrator_test.rb +23 -0
  56. data/test/fixtures/cc.imscc +0 -0
  57. data/test/fixtures/moodle_backup/course_files/folder/test.txt +1 -0
  58. data/test/fixtures/moodle_backup/course_files/test.txt +1 -0
  59. data/test/fixtures/moodle_backup/moodle.xml +916 -0
  60. data/test/test_helper.rb +43 -0
  61. data/test/test_question_helper.rb +107 -0
  62. data/test/test_wiki_helper.rb +21 -0
  63. data/test/unit/canvas/assessment_test.rb +218 -0
  64. data/test/unit/canvas/assignment_test.rb +220 -0
  65. data/test/unit/canvas/converter_test.rb +159 -0
  66. data/test/unit/canvas/course_test.rb +176 -0
  67. data/test/unit/canvas/discussion_topic_test.rb +129 -0
  68. data/test/unit/canvas/label_test.rb +32 -0
  69. data/test/unit/canvas/question_bank_test.rb +73 -0
  70. data/test/unit/canvas/question_test.rb +824 -0
  71. data/test/unit/canvas/web_content_test.rb +37 -0
  72. data/test/unit/canvas/web_link_test.rb +48 -0
  73. data/test/unit/canvas/wiki_test.rb +74 -0
  74. data/test/unit/cc/assessment_test.rb +48 -0
  75. data/test/unit/cc/assignment_test.rb +67 -0
  76. data/test/unit/cc/cc_helper_test.rb +19 -0
  77. data/test/unit/cc/converter_test.rb +159 -0
  78. data/test/unit/cc/course_test.rb +36 -0
  79. data/test/unit/cc/discussion_topic_test.rb +83 -0
  80. data/test/unit/cc/label_test.rb +29 -0
  81. data/test/unit/cc/question_test.rb +49 -0
  82. data/test/unit/cc/web_content_test.rb +76 -0
  83. data/test/unit/cc/web_link_test.rb +79 -0
  84. data/test/unit/cc/wiki_test.rb +139 -0
  85. data/test/unit/migrator_test.rb +56 -0
  86. data/test/unit/moodle/backup_test.rb +30 -0
  87. data/test/unit/moodle/course_test.rb +57 -0
  88. data/test/unit/moodle/grade_item_test.rb +72 -0
  89. data/test/unit/moodle/info_test.rb +21 -0
  90. data/test/unit/moodle/mod_test.rb +301 -0
  91. data/test/unit/moodle/question_category_test.rb +34 -0
  92. data/test/unit/moodle/question_test.rb +185 -0
  93. data/test/unit/moodle/section_test.rb +75 -0
  94. data/test/unit/resource_factory_test.rb +119 -0
  95. metadata +342 -0
@@ -0,0 +1,14 @@
1
+ require 'thor'
2
+
3
+ module Moodle2CC
4
+ class CLI < Thor
5
+ desc "migrate MOODLE_BACKUP_ZIP EXPORT_DIR", "Migrates Moodle backup ZIP to IMS Common Cartridge package"
6
+ method_option :format, :default => 'cc'
7
+ method_options :format => :string
8
+ def migrate(source, destination)
9
+ migrator = Moodle2CC::Migrator.new source, destination, options
10
+ migrator.migrate
11
+ puts "#{source} converted to #{migrator.imscc_path}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Moodle2CC
2
+ class Error < StandardError; end
3
+ end
@@ -0,0 +1,23 @@
1
+ module Moodle2CC
2
+ class Migrator
3
+ def initialize(source, destination, options={})
4
+ @source = source
5
+ @destination = destination
6
+ @format = options['format'] || 'cc'
7
+ raise(Moodle2CC::Error, "'#{@source}' does not exist") unless File.exists?(@source)
8
+ raise(Moodle2CC::Error, "'#{@destination}' is not a directory") unless File.directory?(@destination)
9
+ raise(Moodle2CC::Error, "'#{@format}' is not a valid format. Please use 'cc' or 'canvas'.") unless ['cc', 'canvas'].include?(@format)
10
+ @converter_class = @format == 'cc' ? Moodle2CC::CC::Converter : Moodle2CC::Canvas::Converter
11
+ end
12
+
13
+ def migrate
14
+ backup = Moodle2CC::Moodle::Backup.read @source
15
+ @converter = @converter_class.new backup, @destination
16
+ @converter.convert
17
+ end
18
+
19
+ def imscc_path
20
+ @converter.imscc_path
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ require 'zip/zipfilesystem'
2
+
3
+ module Moodle2CC::Moodle
4
+ class Backup
5
+ include HappyMapper
6
+
7
+ attr_accessor :backup_file, :files
8
+
9
+ tag 'MOODLE_BACKUP'
10
+ has_one :info, Info
11
+ has_one :course, Course
12
+
13
+ def self.read(backup_file)
14
+ Zip::ZipFile.open(backup_file) do |zipfile|
15
+ xml = zipfile.file.read("moodle.xml")
16
+ backup = parse xml
17
+ backup.backup_file = backup_file
18
+ backup.files = zipfile.entries.select { |e| e.name =~ /^course_files/ && !e.directory? }.
19
+ map { |e| e.name.sub('course_files/', '') }.sort
20
+ backup
21
+ end
22
+ end
23
+
24
+ def copy_files_to(dir)
25
+ Zip::ZipFile.open(@backup_file) do |zipfile|
26
+ @files.each do |file|
27
+ zipfile.file.open("course_files/#{file}") do |zip|
28
+ destination_file = File.join(dir, file)
29
+ FileUtils.mkdir_p(File.dirname(destination_file))
30
+ File.open(destination_file, 'w') do |f|
31
+ f.write zip.read
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ module Moodle2CC::Moodle
2
+ class Course
3
+ include HappyMapper
4
+
5
+ tag 'COURSE'
6
+ element :id, Integer, :tag => 'HEADER/ID'
7
+ element :fullname, String, :tag => 'HEADER/FULLNAME'
8
+ element :shortname, String, :tag => 'HEADER/SHORTNAME'
9
+ element :startdate, Integer, :tag => 'HEADER/STARTDATE'
10
+ element :format, String, :tag => 'HEADER/FORMAT'
11
+ element :visible, Boolean, :tag => 'HEADER/VISIBLE'
12
+ has_many :sections, Section
13
+ has_many :mods, Mod
14
+ has_many :grade_items, GradeItem
15
+ has_many :question_categories, QuestionCategory
16
+
17
+ after_parse do |course|
18
+ course.sections.each { |section| section.course = course }
19
+ course.mods.each { |mod| mod.course = course }
20
+ course.grade_items.each { |grade_item| grade_item.course = course }
21
+ course.question_categories.each { |question_category| question_category.course = course }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Moodle2CC::Moodle
2
+ class GradeItem
3
+ include HappyMapper
4
+
5
+ attr_accessor :course
6
+
7
+ tag 'GRADEBOOK/GRADE_ITEMS/GRADE_ITEM'
8
+ element :id, Integer, :tag => 'ID'
9
+ element :category_id, Integer, :tag => 'CATEGORYID'
10
+ element :item_name, String, :tag => 'ITEMNAME'
11
+ element :item_type, String, :tag => 'ITEMTYPE'
12
+ element :item_module, String, :tag => 'ITEMMODULE'
13
+ element :item_instance, Integer, :tag => 'ITEMINSTANCE'
14
+ element :item_number, Integer, :tag => 'ITEMNUMBER'
15
+ element :grade_type, Integer, :tag => 'GRADETYPE'
16
+ element :grade_max, Float, :tag => 'GRADEMAX'
17
+ element :grade_min, Float, :tag => 'GRADEMIN'
18
+ element :scale_id, Integer, :tag => 'SCALEID'
19
+
20
+ def instance
21
+ course.mods.find { |mod| mod.id == item_instance }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ module Moodle2CC::Moodle
2
+ class Info
3
+ include HappyMapper
4
+
5
+ tag 'INFO'
6
+ element :name, String, :tag => 'NAME'
7
+ end
8
+ end
@@ -0,0 +1,127 @@
1
+ module Moodle2CC::Moodle
2
+ class Mod
3
+ include HappyMapper
4
+
5
+ attr_accessor :course
6
+
7
+ class QuestionInstance
8
+ include HappyMapper
9
+
10
+ attr_accessor :mod
11
+
12
+ tag 'QUESTION_INSTANCES/QUESTION_INSTANCE'
13
+ element :id, Integer, :tag => 'ID'
14
+ element :question_id, Integer, :tag => 'QUESTION'
15
+ element :grade, Integer, :tag => 'GRADE'
16
+
17
+ def question
18
+ mod.course.question_categories.map do |qc|
19
+ qc.questions.find { |q| q.id == question_id }
20
+ end.compact.first
21
+ end
22
+ end
23
+
24
+ class Page
25
+ include HappyMapper
26
+
27
+ tag 'ENTRIES/ENTRY/PAGES/PAGE'
28
+ element :page_name, String, :tag => 'PAGENAME'
29
+ element :version, Integer, :tag => 'VERSION'
30
+ element :content, String, :tag => 'CONTENT'
31
+ end
32
+
33
+ class Option
34
+ include HappyMapper
35
+
36
+ tag 'OPTIONS/OPTION'
37
+ element :id, Integer, :tag => 'ID'
38
+ element :text, String, :tag => 'TEXT'
39
+ end
40
+
41
+ tag 'MODULES/MOD'
42
+ element :id, Integer, :tag => 'ID'
43
+ element :var1, Integer, :tag => 'VAR1'
44
+ element :var2, Integer, :tag => 'VAR2'
45
+ element :var3, Integer, :tag => 'VAR3'
46
+ element :var4, Integer, :tag => 'VAR4'
47
+ element :var5, Integer, :tag => 'VAR5'
48
+ element :mod_type, String, :tag => 'MODTYPE'
49
+ element :type, String, :tag => 'TYPE'
50
+ element :name, String, :tag => 'NAME'
51
+ element :description, String, :tag => 'DESCRIPTION'
52
+ element :summary, String, :tag => 'SUMMARY'
53
+ element :alltext, String, :tag => 'ALLTEXT'
54
+ element :text, String, :tag => 'TEXT'
55
+ element :content, String, :tag => 'CONTENT'
56
+ element :assignment_type, String, :tag => 'ASSIGNMENTTYPE'
57
+ element :reference, String, :tag => 'REFERENCE'
58
+ element :intro, String, :tag => 'INTRO'
59
+ element :resubmit, Boolean, :tag => 'RESUBMIT'
60
+ element :prevent_late, Boolean, :tag => 'PREVENTLATE'
61
+ element :grade, Integer, :tag => 'GRADE'
62
+ element :number_of_attachments, Integer, :tag => 'NATTACHMENTS'
63
+ element :number_of_student_assessments, Integer, :tag => 'NSASSESSMENTS'
64
+ element :anonymous, Boolean, :tag => 'ANONYMOUS'
65
+ element :submission_start, Integer, :tag => 'SUBMISSIONSTART'
66
+ element :submission_end, Integer, :tag => 'SUBMISSIONEND'
67
+ element :assessment_start, Integer, :tag => 'ASSESSMENTSTART'
68
+ element :assessment_end, Integer, :tag => 'ASSESSMENTEND'
69
+ element :time_due, Integer, :tag => 'TIMEDUE'
70
+ element :time_available, Integer, :tag => 'TIMEAVAILABLE'
71
+ element :time_open, Integer, :tag => 'TIMEOPEN'
72
+ element :time_close, Integer, :tag => 'TIMECLOSE'
73
+ element :time_limit, Integer, :tag => 'TIMELIMIT'
74
+ element :attempts_number, Integer, :tag => 'ATTEMPTS_NUMBER'
75
+ element :grade_method, Integer, :tag => 'GRADEMETHOD'
76
+ element :password, String, :tag => 'PASSWORD'
77
+ element :subnet, String, :tag => 'SUBNET'
78
+ element :shuffle_answers, Boolean, :tag => 'SHUFFLEANSWERS'
79
+ element :page_name, String, :tag => 'PAGENAME'
80
+ has_many :question_instances, QuestionInstance
81
+ has_many :questions, Question
82
+ has_many :pages, Page
83
+ has_many :options, Option
84
+
85
+ after_parse do |mod|
86
+ mod.question_instances.each { |question_instance| question_instance.mod = mod }
87
+ end
88
+
89
+ def section_mod
90
+ course.sections.map { |section| section.mods.find { |mod| mod.instance_id == id && mod.mod_type == mod_type } }.compact.first
91
+ end
92
+
93
+ def grade_item
94
+ course.grade_items.find { |grade_item| grade_item.item_instance == id }
95
+ end
96
+
97
+ def questions
98
+ if mod_type == 'questionnaire'
99
+ @questions.sort! { |a,b| a.position <=> b.position }
100
+ return @questions
101
+ end
102
+ if mod_type == 'choice'
103
+ question = Question.new
104
+ question.id = "choice_question_#{@id}"
105
+ question.name = @name
106
+ question.text = @text
107
+ question.type = @mod_type
108
+ question.grade = 1
109
+ question.answers = []
110
+ @options.each do |option|
111
+ answer = Question::Answer.new
112
+ answer.id = option.id
113
+ answer.text = option.text
114
+ question.answers << answer
115
+ end
116
+ @questions = [question]
117
+ else
118
+ @questions = question_instances.map do |qi|
119
+ question = qi.question
120
+ question.grade = qi.grade
121
+ question.instance_id = qi.id
122
+ question
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,98 @@
1
+ module Moodle2CC::Moodle
2
+ class Question
3
+ include HappyMapper
4
+
5
+ attr_accessor :question_category, :grade, :instance_id
6
+
7
+ class Answer
8
+ include HappyMapper
9
+
10
+ tag 'ANSWERS/ANSWER'
11
+ element :id, Integer, :tag => 'ID'
12
+ element :text, String, :tag => 'ANSWER_TEXT'
13
+ element :fraction, Float, :tag => 'FRACTION'
14
+ element :feedback, String, :tag => 'FEEDBACK'
15
+ end
16
+
17
+ class Calculation
18
+ include HappyMapper
19
+
20
+ class DatasetDefinition
21
+ include HappyMapper
22
+
23
+ class DatasetItem
24
+ include HappyMapper
25
+
26
+ tag 'DATASET_ITEMS/DATASET_ITEM'
27
+ element :number, Integer, :tag => 'NUMBER'
28
+ element :value, Float, :tag => 'VALUE'
29
+ end
30
+
31
+ tag 'DATASET_DEFINITIONS/DATASET_DEFINITION'
32
+ element :name, String, :tag => 'NAME'
33
+ element :options, String, :tag => 'OPTIONS'
34
+ has_many :dataset_items, DatasetItem
35
+ end
36
+
37
+ tag 'CALCULATED'
38
+ element :answer_id, Integer, :tag => 'ANSWER'
39
+ element :tolerance, Float, :tag => 'TOLERANCE'
40
+ element :correct_answer_length, Integer, :tag => 'CORRECTANSWERLENGTH'
41
+ element :correct_answer_format, Integer, :tag => 'CORRECTANSWERFORMAT'
42
+ has_many :dataset_definitions, DatasetDefinition
43
+ end
44
+
45
+ class Match
46
+ include HappyMapper
47
+
48
+ tag 'MATCHS/MATCH'
49
+ element :id, Integer, :tag => 'ID'
50
+ element :code, Integer, :tag => 'CODE'
51
+ element :question_text, String, :tag => 'QUESTIONTEXT'
52
+ element :answer_text, String, :tag => 'ANSWERTEXT'
53
+ end
54
+
55
+ class Numerical
56
+ include HappyMapper
57
+
58
+ tag 'NUMERICAL'
59
+ element :answer_id, Integer, :tag => 'ANSWER'
60
+ element :tolerance, Integer, :tag => 'TOLERANCE'
61
+ end
62
+
63
+ class Choice
64
+ include HappyMapper
65
+
66
+ tag 'QUESTION_CHOICE'
67
+ element :id, Integer, :tag => 'ID'
68
+ element :content, String, :tag => 'CONTENT'
69
+ end
70
+
71
+ tag 'QUESTION'
72
+ element :id, Integer, :tag => 'ID'
73
+ element :name, String, :tag => 'NAME'
74
+ element :text, String, :tag => 'QUESTIONTEXT'
75
+ element :content, String, :tag => 'CONTENT'
76
+ element :length, Integer, :tag => 'LENGTH'
77
+ element :general_feedback, String, :tag => 'GENERALFEEDBACK'
78
+ element :default_grade, Integer, :tag => 'DEFAULTGRADE'
79
+ element :position, Integer, :tag => 'POSITION'
80
+ element :type, String, :tag => 'QTYPE'
81
+ element :type_id, Integer, :tag => 'TYPE_ID'
82
+ has_many :numericals, Numerical
83
+ has_many :answers, Answer
84
+ has_many :calculations, Calculation
85
+ has_many :matches, Match
86
+ has_many :choices, Choice
87
+
88
+ def instance
89
+ question_category.course.mods.select do |mod|
90
+ mod.mod_type == 'quiz'
91
+ end.map do |mod|
92
+ mod.question_instances.find do |qi|
93
+ qi.question_id == id
94
+ end
95
+ end.compact.first
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,17 @@
1
+ module Moodle2CC::Moodle
2
+ class QuestionCategory
3
+ include HappyMapper
4
+
5
+ attr_accessor :course
6
+
7
+ tag 'QUESTION_CATEGORIES/QUESTION_CATEGORY'
8
+ element :id, Integer, :tag => 'ID'
9
+ element :name, String, :tag => 'NAME'
10
+ element :info, String, :tag => 'INFO'
11
+ has_many :questions, Question
12
+
13
+ after_parse do |qc|
14
+ qc.questions.each { |q| q.question_category = qc }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ module Moodle2CC::Moodle
2
+ class Section
3
+ include HappyMapper
4
+
5
+ attr_accessor :course
6
+
7
+ class Mod
8
+ include HappyMapper
9
+
10
+ attr_accessor :section
11
+
12
+ tag 'MODS/MOD'
13
+
14
+ element :id, Integer, :tag => 'ID'
15
+ element :mod_type, String, :tag => 'TYPE'
16
+ element :instance_id, Integer, :tag => 'INSTANCE'
17
+ element :added, Integer, :tag => 'ADDED'
18
+ element :indent, Integer, :tag => 'INDENT'
19
+ element :visible, Boolean, :tag => 'VISIBLE'
20
+
21
+ def instance
22
+ section.course.mods.find { |mod| mod.id == instance_id && mod.mod_type == mod_type }
23
+ end
24
+ end
25
+
26
+ tag 'SECTIONS/SECTION'
27
+ element :id, Integer, :tag => 'ID'
28
+ element :number, Integer, :tag => 'NUMBER'
29
+ element :summary, String, :tag => 'SUMMARY'
30
+ element :visible, Boolean, :tag => 'VISIBLE'
31
+ has_many :mods, Mod
32
+
33
+ after_parse do |section|
34
+ section.mods.each { |mod| mod.section = section }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ class Moodle2CC::ResourceFactory
2
+
3
+ def initialize(namespace)
4
+ @namespace = namespace
5
+ end
6
+
7
+ def get_resource_from_mod(mod, position=0)
8
+ case mod.mod_type
9
+ when 'assignment', 'workshop'
10
+ @namespace.const_get(:Assignment).new(mod, position)
11
+ when 'resource'
12
+ if mod.type == 'file'
13
+ @namespace.const_get(:WebLink).new(mod)
14
+ elsif mod.type == 'html'
15
+ @namespace.const_get(:WebContent).new(mod)
16
+ end
17
+ when 'forum'
18
+ @namespace.const_get(:DiscussionTopic).new(mod, position)
19
+ when 'quiz', 'questionnaire', 'choice'
20
+ @namespace.const_get(:Assessment).new(mod, position)
21
+ when 'wiki'
22
+ @namespace.const_get(:Wiki).new(mod)
23
+ when 'label'
24
+ html = Nokogiri::HTML(mod.content)
25
+ if html.text == mod.content && mod.content.length < 50 # label doesn't contain HTML
26
+ @namespace.const_get(:Label).new(mod)
27
+ else
28
+ @namespace.const_get(:WebContent).new(mod)
29
+ end
30
+ end
31
+ end
32
+ end