senkyoshi 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +13 -13
- data/lib/senkyoshi/collection.rb +16 -2
- data/lib/senkyoshi/configuration.rb +1 -1
- data/lib/senkyoshi/models/attachment.rb +2 -0
- data/lib/senkyoshi/models/content.rb +13 -25
- data/lib/senkyoshi/models/content_reviewed_criteria.rb +27 -0
- data/lib/senkyoshi/models/course.rb +4 -0
- data/lib/senkyoshi/models/course_toc.rb +21 -0
- data/lib/senkyoshi/models/file_resource.rb +14 -0
- data/lib/senkyoshi/models/forum.rb +14 -1
- data/lib/senkyoshi/models/grade_completed_criteria.rb +8 -0
- data/lib/senkyoshi/models/grade_criteria.rb +46 -0
- data/lib/senkyoshi/models/grade_range_criteria.rb +34 -0
- data/lib/senkyoshi/models/grade_range_percent_criteria.rb +28 -0
- data/lib/senkyoshi/models/gradebook.rb +6 -0
- data/lib/senkyoshi/models/heirarchy.rb +84 -0
- data/lib/senkyoshi/models/link.rb +15 -0
- data/lib/senkyoshi/models/module.rb +6 -0
- data/lib/senkyoshi/models/module_converter.rb +86 -0
- data/lib/senkyoshi/models/module_item.rb +8 -2
- data/lib/senkyoshi/models/outcome_definition.rb +10 -5
- data/lib/senkyoshi/models/qti.rb +1 -0
- data/lib/senkyoshi/models/question.rb +3 -2
- data/lib/senkyoshi/models/questions/matching.rb +5 -1
- data/lib/senkyoshi/models/questions/ordering.rb +5 -3
- data/lib/senkyoshi/models/rule.rb +46 -0
- data/lib/senkyoshi/models/rule_criteria.rb +146 -0
- data/lib/senkyoshi/models/wikipage.rb +12 -2
- data/lib/senkyoshi/version.rb +1 -1
- data/lib/senkyoshi/xml_parser.rb +35 -41
- data/lib/senkyoshi.rb +25 -5
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 26c7126fd95fa6cf136131c92014471f65e2089e
|
4
|
+
data.tar.gz: 6a6a5352177a859ddbb3ac1c83fe9ac465e927a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb156e023de570e457222738c42a34af1433c279ad9879501fcd7ed82d5a76538f2e3bfc191832bdcdcd03a54f4486a87abe2fbc9783d2eb9d54a7b415dff903
|
7
|
+
data.tar.gz: fb795ce77619b24d9b0ec6e55eeba40ece4e82b5f325b8b48a39be08d970349e96828395a1c29e34d63e4b8d60cd96d16b8fe11c57b844a9b140b11f780bb400
|
data/README.md
CHANGED
@@ -4,6 +4,10 @@ Senkyoshi converts exported Blackboard packages into Canvas .imscc packages. It
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
+
#### Canvas
|
8
|
+
See [Senkyoshi Canvas Plugin](https://github.com/atomicjolt/senkyoshi_canvas_plugin) if you want to enable this in your canvas app.
|
9
|
+
|
10
|
+
#### CLI
|
7
11
|
Add this line to your application's Gemfile:
|
8
12
|
|
9
13
|
```ruby
|
@@ -32,24 +36,24 @@ Create a `senkyoshi.yml` and add credentials:
|
|
32
36
|
# Generally looks like https://< mycanvas_instance >/api
|
33
37
|
:canvas_url: <canvas instance api url>
|
34
38
|
|
35
|
-
# Canvas tokens can be generated at <my_canvas_url>/profile/
|
36
|
-
# that the user has the required
|
39
|
+
# Canvas tokens can be generated at <my_canvas_url>/profile/settings provided
|
40
|
+
# that the user has the required privileges
|
37
41
|
:canvas_token: <canvas token>
|
38
42
|
|
39
|
-
#
|
43
|
+
# URL of SCORM manager. This could be the Adhesion app
|
40
44
|
# [https://github.com/atomicjolt/adhesion]
|
41
45
|
:scorm_url: <scorm manager url>
|
42
46
|
|
43
|
-
# This should be the endpoint to launch a given
|
44
|
-
#
|
47
|
+
# This should be the endpoint to launch a given SCORM course. In the case of
|
48
|
+
# Adhesion, this will look like https://<adhesion url>/scorm_course
|
45
49
|
:scorm_launch_url: <scorm launch url>
|
46
50
|
|
47
|
-
# This is the secret to authenticate requests to the
|
48
|
-
# of
|
51
|
+
# This is the secret to authenticate requests to the SCORM manager. In the case
|
52
|
+
# of Adhesion, you can generate a shared secret by logging into the server and
|
49
53
|
# running rake shared_auth, which will generate and save a token
|
50
54
|
:scorm_shared_auth: <scorm manager token>
|
51
55
|
|
52
|
-
# The account or sub-account
|
56
|
+
# The account or sub-account ID. This can be :self, :default, or an ID
|
53
57
|
:account_id: <id>
|
54
58
|
```
|
55
59
|
|
@@ -63,7 +67,7 @@ This will take all your files in your source folder and convert them to your out
|
|
63
67
|
|
64
68
|
Run converting files in parallel:
|
65
69
|
```sh
|
66
|
-
time rake senkyoshi:imscc -m
|
70
|
+
time rake senkyoshi:imscc -m # The `time` command is optional.
|
67
71
|
```
|
68
72
|
|
69
73
|
Delete entire outputs folder:
|
@@ -90,7 +94,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/atomic
|
|
90
94
|
## License
|
91
95
|
|
92
96
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
93
|
-
|
94
|
-
|
95
|
-
### Things Not Quite Implemented
|
96
|
-
People, Groups, Sets, Blogs
|
data/lib/senkyoshi/collection.rb
CHANGED
@@ -2,8 +2,8 @@ module Senkyoshi
|
|
2
2
|
class Collection
|
3
3
|
attr_reader :resources
|
4
4
|
|
5
|
-
def initialize
|
6
|
-
@resources =
|
5
|
+
def initialize(resources = [])
|
6
|
+
@resources = resources
|
7
7
|
end
|
8
8
|
|
9
9
|
def add(resources)
|
@@ -16,6 +16,20 @@ module Senkyoshi
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
def find_by_id(id)
|
20
|
+
@resources.detect { |item| item.respond_to?(:id) && item.id == id }
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_instances_of(class_name)
|
24
|
+
@resources.select { |res| res.class == class_name }
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_instances_not_of(types)
|
28
|
+
@resources.select do |res|
|
29
|
+
types.each { |type| res.class != type }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
19
33
|
def each
|
20
34
|
@resources.each do |resource|
|
21
35
|
yield resource
|
@@ -20,6 +20,7 @@ module Senkyoshi
|
|
20
20
|
"x-bb-module-page" => "WikiPage",
|
21
21
|
"x-bb-lesson-plan" => "WikiPage",
|
22
22
|
"x-bb-syllabus" => "WikiPage",
|
23
|
+
"x-bb-courselink" => "WikiPage",
|
23
24
|
}.freeze
|
24
25
|
|
25
26
|
MODULE_TYPES = {
|
@@ -55,6 +56,8 @@ module Senkyoshi
|
|
55
56
|
def iterate_xml(xml, pre_data)
|
56
57
|
@points = pre_data[:points] || 0
|
57
58
|
@parent_title = pre_data[:parent_title]
|
59
|
+
@indent = pre_data[:indent]
|
60
|
+
@file_name = pre_data[:file_name]
|
58
61
|
@title = xml.xpath("/CONTENT/TITLE/@value").first.text
|
59
62
|
@url = xml.at("URL")["value"]
|
60
63
|
@body = xml.xpath("/CONTENT/BODY/TEXT").first.text
|
@@ -65,6 +68,7 @@ module Senkyoshi
|
|
65
68
|
@type = xml.xpath("/CONTENT/RENDERTYPE/@value").first.text
|
66
69
|
@parent_id = pre_data[:parent_id]
|
67
70
|
@module_type = MODULE_TYPES[self.class.name]
|
71
|
+
@referred_to_title = pre_data[:referred_to_title]
|
68
72
|
|
69
73
|
if pre_data[:assignment_id] && !pre_data[:assignment_id].empty?
|
70
74
|
@id = pre_data[:assignment_id]
|
@@ -78,18 +82,14 @@ module Senkyoshi
|
|
78
82
|
end
|
79
83
|
|
80
84
|
def set_module
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
id: id,
|
90
|
-
parent_id: parent_id,
|
91
|
-
file_name: file_name,
|
92
|
-
}
|
85
|
+
ModuleItem.new(
|
86
|
+
@title,
|
87
|
+
@module_type,
|
88
|
+
@id,
|
89
|
+
@url,
|
90
|
+
@indent,
|
91
|
+
@file_name,
|
92
|
+
).canvas_conversion
|
93
93
|
end
|
94
94
|
|
95
95
|
def canvas_conversion(course, _resources = nil)
|
@@ -97,19 +97,7 @@ module Senkyoshi
|
|
97
97
|
end
|
98
98
|
|
99
99
|
def create_module(course)
|
100
|
-
course
|
101
|
-
cc_module = course.canvas_modules.
|
102
|
-
detect { |a| a.identifier == @parent_id }
|
103
|
-
if cc_module
|
104
|
-
cc_module.module_items << @module_item
|
105
|
-
else
|
106
|
-
title = @parent_title || @title
|
107
|
-
cc_module = Module.new(title, @parent_id)
|
108
|
-
cc_module = cc_module.canvas_conversion
|
109
|
-
cc_module.module_items << @module_item
|
110
|
-
course.canvas_modules << cc_module
|
111
|
-
end
|
112
|
-
course
|
100
|
+
super(course)
|
113
101
|
end
|
114
102
|
end
|
115
103
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "senkyoshi/models/rule_criteria"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class ContentReviewedCriteria < RuleCriteria
|
5
|
+
attr_reader(:reviewed_content_id)
|
6
|
+
|
7
|
+
def initialize(id, negated, reviewed_content_id)
|
8
|
+
super(id, negated)
|
9
|
+
@reviewed_content_id = reviewed_content_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_xml(xml)
|
13
|
+
id = RuleCriteria.get_id xml
|
14
|
+
negated = Senkyoshi.true? RuleCriteria.get_negated(xml)
|
15
|
+
reviewed_content_id = xml.xpath("./REVIEWED_CONTENT_ID/@value").text
|
16
|
+
ContentReviewedCriteria.new(id, negated, reviewed_content_id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_foreign_id
|
20
|
+
@reviewed_content_id
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_completion_type
|
24
|
+
COMPLETION_TYPES[:must_view]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
require "senkyoshi/models/content_file"
|
3
|
+
|
4
|
+
module Senkyoshi
|
5
|
+
class CourseToc
|
6
|
+
def self.get_pre_data(xml, file_name)
|
7
|
+
target_type = xml.xpath("/COURSETOC/TARGETTYPE/@value").first.text
|
8
|
+
if target_type != "MODULE" || target_type != "DIVIDER"
|
9
|
+
title = xml.xpath("/COURSETOC/LABEL/@value").first.text
|
10
|
+
internal_handle = xml.xpath("/COURSETOC/INTERNALHANDLE/@value").
|
11
|
+
first.text
|
12
|
+
{
|
13
|
+
title: title,
|
14
|
+
target_type: target_type,
|
15
|
+
original_file: file_name,
|
16
|
+
internal_handle: internal_handle,
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -19,5 +19,19 @@ module Senkyoshi
|
|
19
19
|
def iterate_xml(_xml, _pre_data)
|
20
20
|
self
|
21
21
|
end
|
22
|
+
|
23
|
+
def create_module(course)
|
24
|
+
course.canvas_modules ||= []
|
25
|
+
cc_module = Course.master_module(course)
|
26
|
+
if cc_module
|
27
|
+
cc_module.module_items << @module_item
|
28
|
+
else
|
29
|
+
cc_module = Module.new(MASTER_MODULE, MASTER_MODULE)
|
30
|
+
cc_module = cc_module.canvas_conversion
|
31
|
+
cc_module.module_items << @module_item
|
32
|
+
course.canvas_modules << cc_module
|
33
|
+
end
|
34
|
+
course
|
35
|
+
end
|
22
36
|
end
|
23
37
|
end
|
@@ -9,9 +9,19 @@ module Senkyoshi
|
|
9
9
|
@discussion_type = "threaded"
|
10
10
|
end
|
11
11
|
|
12
|
-
def iterate_xml(data,
|
12
|
+
def iterate_xml(data, pre_data)
|
13
13
|
@title = Senkyoshi.get_attribute_value(data, "TITLE")
|
14
14
|
@text = Senkyoshi.get_text(data, "TEXT")
|
15
|
+
if pre_data[:internal_handle]
|
16
|
+
@module_item = ModuleItem.new(
|
17
|
+
@title,
|
18
|
+
"DiscussionTopic",
|
19
|
+
@id,
|
20
|
+
nil,
|
21
|
+
pre_data[:indent],
|
22
|
+
@id,
|
23
|
+
).canvas_conversion
|
24
|
+
end
|
15
25
|
self
|
16
26
|
end
|
17
27
|
|
@@ -22,6 +32,9 @@ module Senkyoshi
|
|
22
32
|
discussion.identifier = @id
|
23
33
|
discussion.discussion_type = @discussion_type
|
24
34
|
course.discussions << discussion
|
35
|
+
if @module_item
|
36
|
+
course = create_module(course)
|
37
|
+
end
|
25
38
|
course
|
26
39
|
end
|
27
40
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "senkyoshi/models/rule_criteria"
|
2
|
+
module Senkyoshi
|
3
|
+
class GradeCriteria < RuleCriteria
|
4
|
+
attr_reader(:outcome_def_id)
|
5
|
+
|
6
|
+
def initialize(id, outcome_def_id, negated)
|
7
|
+
super(id, negated)
|
8
|
+
@outcome_def_id = outcome_def_id
|
9
|
+
@foreign_content_id = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_foreign_id
|
13
|
+
@foreign_asidata_id || @foreign_content_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.get_outcome_def_id(xml)
|
17
|
+
xml.xpath("./OUTCOME_DEFINITION_ID/@value").text
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.from_xml(xml)
|
21
|
+
id = RuleCriteria.get_id xml
|
22
|
+
negated = Senkyoshi.true? RuleCriteria.get_negated xml
|
23
|
+
outcome_def_id = GradeCriteria.get_outcome_def_id xml
|
24
|
+
new(id, outcome_def_id, negated)
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# use gradebook to match outcome_definition_id to the
|
29
|
+
# assignment content id
|
30
|
+
##
|
31
|
+
def set_foreign_ids(resources, outcome_def_id)
|
32
|
+
gradebook = resources.find_instances_of(Gradebook).first
|
33
|
+
|
34
|
+
outcome = gradebook.find_outcome_def(outcome_def_id)
|
35
|
+
if outcome
|
36
|
+
@foreign_content_id = outcome.content_id
|
37
|
+
@foreign_asidata_id = outcome.asidataid
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def canvas_conversion(course, content_id, resources)
|
42
|
+
set_foreign_ids(resources, @outcome_def_id)
|
43
|
+
super(course, content_id, resources)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "senkyoshi/models/grade_criteria"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class GradeRangeCriteria < GradeCriteria
|
5
|
+
attr_reader(:min_score)
|
6
|
+
|
7
|
+
def initialize(id, outcome_def_id, negated, min_score)
|
8
|
+
super(id, outcome_def_id, negated)
|
9
|
+
@min_score = min_score
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.get_min_score(xml)
|
13
|
+
xml.xpath("./MIN_SCORE/@value").text
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_xml(xml)
|
17
|
+
id = RuleCriteria.get_id xml
|
18
|
+
negated = Senkyoshi.true? RuleCriteria.get_negated xml
|
19
|
+
outcome_def_id = GradeCriteria.get_outcome_def_id xml
|
20
|
+
min_score = GradeRangePercentCriteria.get_min_score xml
|
21
|
+
new(id, outcome_def_id, negated, min_score)
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_completion_type
|
25
|
+
COMPLETION_TYPES[:min_score]
|
26
|
+
end
|
27
|
+
|
28
|
+
def make_completion(mod)
|
29
|
+
super(mod).tap do |completion_requirement|
|
30
|
+
completion_requirement.min_score = @min_score
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "senkyoshi/models/grade_range_criteria"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class GradeRangePercentCriteria < GradeRangeCriteria
|
5
|
+
def make_completion(mod)
|
6
|
+
super(mod).tap do |completion_requirement|
|
7
|
+
completion_requirement.min_score = GradeRangePercentCriteria.
|
8
|
+
get_percentage(@min_score, @points_possible)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_points_possible(resources, id)
|
13
|
+
resource = resources.find_by_id(id)
|
14
|
+
resource.points_possible if resource
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.get_percentage(min_score, points_possible)
|
18
|
+
(min_score.to_f / 100) * points_possible.to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def canvas_conversion(course, content_id, resources)
|
22
|
+
set_foreign_ids(resources, @outcome_def_id)
|
23
|
+
@points_possible = get_points_possible(resources, get_foreign_id)
|
24
|
+
|
25
|
+
super(course, content_id, resources)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -57,6 +57,12 @@ module Senkyoshi
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
+
def find_outcome_def(outcome_def_id)
|
61
|
+
@outcome_definitions.detect do |outcome_def|
|
62
|
+
outcome_def.id == outcome_def_id
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
60
66
|
def canvas_conversion(course, resources = nil)
|
61
67
|
convert_categories(course)
|
62
68
|
@outcome_definitions.
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "senkyoshi/models/resource"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class Heirarchy
|
5
|
+
def self.item_iterator(item, course_toc, discussion_boards)
|
6
|
+
if item.search("item").count.zero?
|
7
|
+
toc_item = setup_item(item, item.parent, course_toc)
|
8
|
+
toc_item[:indent] = 0
|
9
|
+
set_discussion_boards(discussion_boards, toc_item)
|
10
|
+
else
|
11
|
+
item.search("item").flat_map do |internal_item|
|
12
|
+
toc_item = setup_item(internal_item, item, course_toc)
|
13
|
+
toc_item[:indent] = get_indent(internal_item)
|
14
|
+
toc_item = set_discussion_boards(discussion_boards, toc_item)
|
15
|
+
toc_item
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get_indent(item, indent = -2)
|
21
|
+
return indent if item.parent.name == "organization"
|
22
|
+
indent += 1
|
23
|
+
get_indent(item.parent, indent)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.set_discussion_boards(discussion_boards, toc_item)
|
27
|
+
if toc_item[:internal_handle] == "discussion_board_entry"
|
28
|
+
resource = discussion_boards.select do |db|
|
29
|
+
title_attribute = db.attributes["title"] || db.attributes["bb:title"]
|
30
|
+
title_attribute.value == toc_item[:title]
|
31
|
+
end
|
32
|
+
if resource.count == 1
|
33
|
+
file_attribute = resource.first.attributes["file"] ||
|
34
|
+
resource.first.attributes["bb:file"]
|
35
|
+
toc_item[:file_name] = file_attribute.value.gsub(".dat", "")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
toc_item
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.setup_item(item, parent_item, course_toc)
|
42
|
+
if item.attributes["identifierref"]
|
43
|
+
title = item.at("title").text
|
44
|
+
if title == "--TOP--"
|
45
|
+
file_name = item.parent.attributes["identifierref"].value
|
46
|
+
title = item.parent.at("title").text
|
47
|
+
item_id = item.parent.attributes["identifierref"].
|
48
|
+
value.gsub("res", "")
|
49
|
+
else
|
50
|
+
file_name = item.attributes["identifierref"].value
|
51
|
+
if parent_item.attributes["identifierref"]
|
52
|
+
item_id = parent_item.attributes["identifierref"].
|
53
|
+
value.gsub("res", "")
|
54
|
+
else
|
55
|
+
item_id = item.attributes["identifierref"].value.gsub("res", "")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
toc_item = course_toc.
|
59
|
+
detect { |ct| ct[:original_file] == file_name } || {}
|
60
|
+
toc_item[:file_name] = file_name
|
61
|
+
toc_item[:title] = title
|
62
|
+
toc_item[:parent_id] = get_parent_id(course_toc, item_id)
|
63
|
+
toc_item
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.get_parent_id(course_toc, item_id)
|
68
|
+
header_ids = get_headers(course_toc, "SUBHEADER")
|
69
|
+
if header_ids.empty?
|
70
|
+
header_ids = get_headers(course_toc, "CONTENT")
|
71
|
+
end
|
72
|
+
header_id = header_ids.
|
73
|
+
reject { |x| x.to_i > item_id.to_i }.
|
74
|
+
min_by { |x| (x.to_i - item_id.to_i).abs }
|
75
|
+
|
76
|
+
header_id ? "res#{header_id}" : nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.get_headers(course_toc, target_type)
|
80
|
+
course_toc.select { |ct| ct[:target_type] == target_type }.
|
81
|
+
map { |sh| sh[:original_file].gsub("res", "") }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Senkyoshi
|
2
|
+
##
|
3
|
+
# Represents a link between Blackboard resources.
|
4
|
+
##
|
5
|
+
class Link < FileResource
|
6
|
+
def self.get_pre_data(xml_data, _file)
|
7
|
+
{
|
8
|
+
referrer: xml_data.children.at("REFERRER").attributes["id"].value,
|
9
|
+
referred_to: xml_data.children.at("REFERREDTO").attributes["id"].value,
|
10
|
+
referred_to_title: xml_data.children.at("TITLE").
|
11
|
+
attributes["value"].value,
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -10,6 +10,12 @@ module Senkyoshi
|
|
10
10
|
@module_items = []
|
11
11
|
end
|
12
12
|
|
13
|
+
def self.find_module_from_item_id(modules, id)
|
14
|
+
modules.detect do |mod|
|
15
|
+
mod.module_items.detect { |item| item.identifierref == id }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
13
19
|
def canvas_conversion(*)
|
14
20
|
CanvasCc::CanvasCC::Models::CanvasModule.new.tap do |cc_module|
|
15
21
|
cc_module.identifier = @identifier
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Senkyoshi
|
2
|
+
class ModuleConverter
|
3
|
+
def self.set_modules(course, pre_data)
|
4
|
+
master_module = Course.master_module(course)
|
5
|
+
subheaders = get_subheaders(pre_data)
|
6
|
+
pre_data.each do |data|
|
7
|
+
if check_module_header(data, subheaders)
|
8
|
+
course = add_canvas_module(course, data)
|
9
|
+
elsif data[:target_type] == "CONTENT"
|
10
|
+
module_item = create_module_subheader(data)
|
11
|
+
course = add_canvas_module_item(course, module_item, data)
|
12
|
+
elsif data[:target_type] == nil || data[:target_type] == "APPLICATION"
|
13
|
+
module_item = master_module.module_items.
|
14
|
+
detect { |i| i.identifier == data[:file_name] }
|
15
|
+
if module_item
|
16
|
+
course = add_canvas_module_item(course, module_item, data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
course.canvas_modules.delete(master_module)
|
21
|
+
course
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.check_module_header(data, subheaders)
|
25
|
+
data[:target_type] == "SUBHEADER" || data[:target_type] == "CONTENT" &&
|
26
|
+
(subheaders.empty? || !data[:parent_id])
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.get_subheaders(pre_data)
|
30
|
+
pre_data.select { |ct| ct[:target_type] == "SUBHEADER" }.
|
31
|
+
map { |sh| sh[:original_file].gsub("res", "") }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create_module_subheader(data)
|
35
|
+
ModuleItem.new(
|
36
|
+
data[:title],
|
37
|
+
"ContextModuleSubHeader",
|
38
|
+
data[:file_name],
|
39
|
+
nil,
|
40
|
+
data[:indent],
|
41
|
+
data[:file_name],
|
42
|
+
).canvas_conversion
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.add_canvas_module(course, data)
|
46
|
+
canvas_module = Module.new(data[:title], data[:file_name])
|
47
|
+
course.canvas_modules << canvas_module.canvas_conversion
|
48
|
+
course
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.add_canvas_module_item(course, module_item, data)
|
52
|
+
parent_module = get_subheader_parent(course, data)
|
53
|
+
if !parent_module
|
54
|
+
parent_module = Module.new(course.title, MAIN_CANVAS_MODULE).
|
55
|
+
canvas_conversion
|
56
|
+
course.canvas_modules << parent_module
|
57
|
+
end
|
58
|
+
parent_module.module_items ||= {}
|
59
|
+
parent_module.module_items << module_item
|
60
|
+
course
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.get_subheader_parent(course, data)
|
64
|
+
if !data[:parent_id]
|
65
|
+
parent_module = course.canvas_modules.
|
66
|
+
detect { |a| a.identifier == MAIN_CANVAS_MODULE }
|
67
|
+
else
|
68
|
+
parent_module = course.canvas_modules.
|
69
|
+
detect { |a| a.identifier == data[:parent_id] }
|
70
|
+
if !parent_module
|
71
|
+
course.canvas_modules.
|
72
|
+
reject { |a| a.title == MASTER_MODULE }.
|
73
|
+
each do |cc_module|
|
74
|
+
if cc_module.module_items
|
75
|
+
items = cc_module.module_items.flatten
|
76
|
+
item = items.
|
77
|
+
detect { |i| i.identifier == data[:parent_id] }
|
78
|
+
parent_module = cc_module if item
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
parent_module
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -2,15 +2,20 @@ require "senkyoshi/models/resource"
|
|
2
2
|
|
3
3
|
module Senkyoshi
|
4
4
|
class ModuleItem < Resource
|
5
|
-
def initialize(title, type, identifierref, url)
|
5
|
+
def initialize(title, type, identifierref, url, indent, id)
|
6
6
|
@title = title
|
7
|
-
@identifier = Senkyoshi.create_random_hex
|
7
|
+
@identifier = id || Senkyoshi.create_random_hex
|
8
8
|
@content_type = type
|
9
9
|
@identifierref = identifierref
|
10
|
+
@indent = indent
|
10
11
|
@workflow_state = "active"
|
11
12
|
@url = url
|
12
13
|
end
|
13
14
|
|
15
|
+
def self.find_item_from_id_ref(module_items, id_ref)
|
16
|
+
module_items.detect { |item| item.identifierref == id_ref }
|
17
|
+
end
|
18
|
+
|
14
19
|
def canvas_conversion(*)
|
15
20
|
CanvasCc::CanvasCC::Models::ModuleItem.new.tap do |item|
|
16
21
|
item.title = @title
|
@@ -18,6 +23,7 @@ module Senkyoshi
|
|
18
23
|
item.content_type = @content_type
|
19
24
|
item.identifierref = @identifierref
|
20
25
|
item.workflow_state = @workflow_state
|
26
|
+
item.indent = @indent
|
21
27
|
item.url = @url
|
22
28
|
end
|
23
29
|
end
|
@@ -5,12 +5,15 @@ module Senkyoshi
|
|
5
5
|
class OutcomeDefinition < Resource
|
6
6
|
include Senkyoshi
|
7
7
|
attr_reader :id, :content_id, :asidataid, :is_user_created
|
8
|
-
def self.from(xml, category)
|
9
|
-
outcome_definition = OutcomeDefinition.new(category)
|
8
|
+
def self.from(xml, category, id = nil)
|
9
|
+
outcome_definition = OutcomeDefinition.new(category, id)
|
10
10
|
outcome_definition.iterate_xml(xml)
|
11
11
|
end
|
12
12
|
|
13
|
-
def initialize(category)
|
13
|
+
def initialize(category, id, content_id = nil, asidataid = nil)
|
14
|
+
@id = id
|
15
|
+
@content_id = content_id
|
16
|
+
@asidataid = asidataid
|
14
17
|
@category = category
|
15
18
|
end
|
16
19
|
|
@@ -20,7 +23,9 @@ module Senkyoshi
|
|
20
23
|
@id = xml.xpath("./@id").text
|
21
24
|
@title = xml.xpath("./TITLE/@value").text
|
22
25
|
@points_possible = xml.xpath("./POINTSPOSSIBLE/@value").text
|
23
|
-
@is_user_created = true?
|
26
|
+
@is_user_created = Senkyoshi.true?(
|
27
|
+
xml.xpath("./ISUSERCREATED/@value").text,
|
28
|
+
)
|
24
29
|
self
|
25
30
|
end
|
26
31
|
|
@@ -37,7 +42,7 @@ module Senkyoshi
|
|
37
42
|
def canvas_conversion(course, _ = nil)
|
38
43
|
assignment_group = AssignmentGroup.find_or_create(course, @category)
|
39
44
|
assignment = CanvasCc::CanvasCC::Models::Assignment.new
|
40
|
-
assignment.identifier =
|
45
|
+
assignment.identifier = @id
|
41
46
|
assignment.assignment_group_identifier_ref = assignment_group.identifier
|
42
47
|
assignment.title = @title
|
43
48
|
assignment.position = 1
|
data/lib/senkyoshi/models/qti.rb
CHANGED
@@ -102,7 +102,7 @@ module Senkyoshi
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def get_fraction(answer_text)
|
105
|
-
if @correct_answers && answer_text == @correct_answers["name"]
|
105
|
+
if @correct_answers && answer_text.to_s == @correct_answers["name"].to_s
|
106
106
|
@correct_answers["fraction"].to_f
|
107
107
|
else
|
108
108
|
@incorrect_answers["fraction"].to_f
|
@@ -125,7 +125,8 @@ module Senkyoshi
|
|
125
125
|
if score_number > 0
|
126
126
|
@correct_answers["fraction"] = score_number.to_f / @max_score.to_f
|
127
127
|
else
|
128
|
-
|
128
|
+
# mark as correct when there is no score for the answer
|
129
|
+
@correct_answers["fraction"] = 1
|
129
130
|
end
|
130
131
|
end
|
131
132
|
end
|
@@ -51,7 +51,11 @@ module Senkyoshi
|
|
51
51
|
end
|
52
52
|
|
53
53
|
answers << answer
|
54
|
-
@matches << {
|
54
|
+
@matches << {
|
55
|
+
id: Senkyoshi.create_random_hex,
|
56
|
+
question_text: question,
|
57
|
+
answer_text: answer,
|
58
|
+
}
|
55
59
|
end
|
56
60
|
end
|
57
61
|
@distractors = matches_array.reject { |i| answers.include?(i) }
|
@@ -32,9 +32,11 @@ module Senkyoshi
|
|
32
32
|
def set_order_answers(resprocessing)
|
33
33
|
order_answers = {}
|
34
34
|
correct = resprocessing.at("respcondition[title=correct]")
|
35
|
-
correct
|
36
|
-
|
37
|
-
|
35
|
+
if correct
|
36
|
+
correct.at("and").children.each_with_index do |varequal, index|
|
37
|
+
id = varequal.text
|
38
|
+
order_answers[id] = index + 1
|
39
|
+
end
|
38
40
|
end
|
39
41
|
order_answers
|
40
42
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "senkyoshi/models/grade_completed_criteria"
|
2
|
+
require "senkyoshi/models/content_reviewed_criteria"
|
3
|
+
require "senkyoshi/models/grade_range_criteria"
|
4
|
+
require "senkyoshi/models/grade_range_percent_criteria"
|
5
|
+
require "senkyoshi/models/resource"
|
6
|
+
|
7
|
+
module Senkyoshi
|
8
|
+
class Rule < FileResource
|
9
|
+
attr_reader(:title, :content_id, :criteria_list, :id)
|
10
|
+
|
11
|
+
CRITERIA_MAP = {
|
12
|
+
grade_completed_criteria: GradeCompletedCriteria,
|
13
|
+
content_reviewed_criteria: ContentReviewedCriteria,
|
14
|
+
grade_range_criteria: GradeRangeCriteria,
|
15
|
+
grade_range_percent_criteria: GradeRangePercentCriteria,
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(resource_id)
|
19
|
+
super(resource_id)
|
20
|
+
@criteria_list = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_criteria_list(xml)
|
24
|
+
xml.children.select { |child_xml| !child_xml.blank? }.
|
25
|
+
map do |child_xml|
|
26
|
+
criteria = CRITERIA_MAP[child_xml.name.downcase.to_sym]
|
27
|
+
criteria.from_xml(child_xml) if !criteria.nil?
|
28
|
+
end.compact
|
29
|
+
end
|
30
|
+
|
31
|
+
def iterate_xml(xml, _pre_data = nil)
|
32
|
+
@title = xml.xpath("./TITLE/@value").text
|
33
|
+
@content_id = xml.xpath("./CONTENT_ID/@value").text
|
34
|
+
@id = xml.xpath("./@id").text
|
35
|
+
@criteria_list = get_criteria_list(xml.xpath("./CRITERIA_LIST"))
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def canvas_conversion(course, _resources)
|
40
|
+
@criteria_list.each do |criteria|
|
41
|
+
criteria.canvas_conversion(course, @content_id, _resources)
|
42
|
+
end
|
43
|
+
course
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require "senkyoshi"
|
2
|
+
|
3
|
+
module Senkyoshi
|
4
|
+
class RuleCriteria
|
5
|
+
include Senkyoshi
|
6
|
+
attr_reader(:id, :negated, :content_id, :asidata_id)
|
7
|
+
|
8
|
+
COMPLETION_TYPES = {
|
9
|
+
must_view: "must_view",
|
10
|
+
must_submit: "must_submit",
|
11
|
+
min_score: "min_score",
|
12
|
+
must_contribute: "must_contribute",
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(id, negated)
|
16
|
+
@id = id
|
17
|
+
@negated = negated
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_if_unique(collection, item)
|
21
|
+
if collection.detect do |collection_item|
|
22
|
+
yield(collection_item, item)
|
23
|
+
end.nil?
|
24
|
+
collection << item
|
25
|
+
end
|
26
|
+
collection
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_to_module_if_unique(items, item)
|
30
|
+
add_if_unique(
|
31
|
+
items, item
|
32
|
+
) { |a, b| a.identifierref == b.identifierref }
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.get_id(xml)
|
36
|
+
xml.xpath("./@id").text
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.get_negated(xml)
|
40
|
+
xml.xpath("./NEGATED/@value").text
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.from_xml(xml)
|
44
|
+
new(RuleCriteria.get_id(xml), RuleCriteria.get_negated(xml))
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Determine two identifierrefs are pointed to by items in the same module
|
49
|
+
def self.in_same_module?(modules, content_id, resource_id)
|
50
|
+
content_module = Module.find_module_from_item_id(modules, content_id)
|
51
|
+
resource_module = Module.find_module_from_item_id(modules, resource_id)
|
52
|
+
return nil if content_module.nil? || resource_module.nil?
|
53
|
+
content_module == resource_module
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.module_prerequisite?(modules, content_id, resource_id)
|
57
|
+
in_same_module?(modules, content_id, resource_id) == false
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.module_completion_requirement?(modules, content_id, resource_id)
|
61
|
+
in_same_module?(modules, content_id, resource_id) == true
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Returns the most applicable id. If we have an assignment id return that,
|
66
|
+
# otherwise return the content id.
|
67
|
+
##
|
68
|
+
def get_id
|
69
|
+
@asidata_id || @content_id
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Factory method to construct a completion requirement. Should be passed
|
74
|
+
# the module that the completion requirement should belong to
|
75
|
+
##
|
76
|
+
def make_completion(mod)
|
77
|
+
CanvasCc::CanvasCC::Models::ModuleCompletionRequirement.new.tap do |req|
|
78
|
+
mod_item = ModuleItem.find_item_from_id_ref(
|
79
|
+
mod.module_items, get_foreign_id
|
80
|
+
)
|
81
|
+
|
82
|
+
req.identifierref = mod_item.identifier if mod_item
|
83
|
+
req.type = get_completion_type
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Factory method to construct a module prerequisite. Should be passed the
|
89
|
+
# prerequisite module
|
90
|
+
##
|
91
|
+
def make_prereq(prereq_module)
|
92
|
+
CanvasCc::CanvasCC::Models::ModulePrerequisite.new.tap do |prereq|
|
93
|
+
prereq.identifierref = prereq_module.identifier
|
94
|
+
prereq.type = "context_module"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Use gradebook to find assignment id if applicable
|
100
|
+
##
|
101
|
+
def set_ids(content_id, resources)
|
102
|
+
@content_id = content_id
|
103
|
+
gradebook = resources.find_instances_of(Gradebook).first
|
104
|
+
if gradebook
|
105
|
+
outcome_def = gradebook.outcome_definitions.detect do |out_def|
|
106
|
+
out_def.content_id == content_id
|
107
|
+
end
|
108
|
+
@asidata_id = outcome_def.asidataid if outcome_def
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def canvas_conversion(course, content_id, resources)
|
113
|
+
set_ids(content_id, resources)
|
114
|
+
|
115
|
+
mod = Module.find_module_from_item_id course.canvas_modules, get_id
|
116
|
+
|
117
|
+
is_completion = RuleCriteria.module_completion_requirement?(
|
118
|
+
course.canvas_modules, get_id, get_foreign_id
|
119
|
+
)
|
120
|
+
|
121
|
+
is_prereq = RuleCriteria.module_prerequisite?(
|
122
|
+
course.canvas_modules, get_id, get_foreign_id
|
123
|
+
)
|
124
|
+
|
125
|
+
if is_completion
|
126
|
+
add_to_module_if_unique(
|
127
|
+
mod.completion_requirements, make_completion(mod)
|
128
|
+
)
|
129
|
+
elsif is_prereq
|
130
|
+
prereq_module = Module.find_module_from_item_id(
|
131
|
+
course.canvas_modules, get_foreign_id
|
132
|
+
)
|
133
|
+
|
134
|
+
add_to_module_if_unique(
|
135
|
+
mod.prerequisites, make_prereq(prereq_module)
|
136
|
+
)
|
137
|
+
|
138
|
+
add_to_module_if_unique(
|
139
|
+
prereq_module.completion_requirements, make_completion(prereq_module)
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
course
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -35,6 +35,7 @@ module Senkyoshi
|
|
35
35
|
|
36
36
|
def _set_body(original_body, url, extendeddata)
|
37
37
|
body = original_body.dup
|
38
|
+
|
38
39
|
if !url.empty?
|
39
40
|
body = %{
|
40
41
|
<a href="#{url}">
|
@@ -43,12 +44,21 @@ module Senkyoshi
|
|
43
44
|
#{body}
|
44
45
|
}
|
45
46
|
end
|
47
|
+
if @referred_to_title.present?
|
48
|
+
body = %{
|
49
|
+
<a href="$CANVAS_COURSE_REFERENCE$#{@referred_to_title}">
|
50
|
+
Course Link: #{@referred_to_title}
|
51
|
+
</a>
|
52
|
+
#{body}
|
53
|
+
}
|
54
|
+
end
|
46
55
|
if extendeddata
|
47
56
|
body = %{
|
48
57
|
#{body}
|
49
58
|
#{_extendeddata(extendeddata)}
|
50
59
|
}
|
51
60
|
end
|
61
|
+
|
52
62
|
body
|
53
63
|
end
|
54
64
|
|
@@ -63,10 +73,10 @@ module Senkyoshi
|
|
63
73
|
end
|
64
74
|
|
65
75
|
def _component_label(node)
|
66
|
-
visible = true?(node.search("vislableToStudents/@value").to_s)
|
76
|
+
visible = Senkyoshi.true?(node.search("vislableToStudents/@value").to_s)
|
67
77
|
if visible
|
68
78
|
component_label = node.search("componentLabel/@value").to_s
|
69
|
-
overridden = true?(node.search("labelOverridden/@value").to_s)
|
79
|
+
overridden = Senkyoshi.true?(node.search("labelOverridden/@value").to_s)
|
70
80
|
if overridden
|
71
81
|
component_label
|
72
82
|
else
|
data/lib/senkyoshi/version.rb
CHANGED
data/lib/senkyoshi/xml_parser.rb
CHANGED
@@ -20,21 +20,27 @@ require "senkyoshi/models/assessment"
|
|
20
20
|
require "senkyoshi/models/question_bank"
|
21
21
|
require "senkyoshi/models/survey"
|
22
22
|
|
23
|
+
require "senkyoshi/models/heirarchy"
|
24
|
+
|
23
25
|
require "senkyoshi/models/announcement"
|
24
26
|
require "senkyoshi/models/answer"
|
25
|
-
require "senkyoshi/models/qti"
|
26
27
|
require "senkyoshi/models/assignment"
|
27
28
|
require "senkyoshi/models/assignment_group"
|
29
|
+
require "senkyoshi/models/attachment"
|
28
30
|
require "senkyoshi/models/blog"
|
29
31
|
require "senkyoshi/models/content"
|
30
32
|
require "senkyoshi/models/content_file"
|
31
33
|
require "senkyoshi/models/course"
|
34
|
+
require "senkyoshi/models/course_toc"
|
35
|
+
require "senkyoshi/models/external_url"
|
32
36
|
require "senkyoshi/models/file"
|
33
37
|
require "senkyoshi/models/forum"
|
34
38
|
require "senkyoshi/models/gradebook"
|
35
39
|
require "senkyoshi/models/group"
|
40
|
+
require "senkyoshi/models/link"
|
36
41
|
require "senkyoshi/models/module"
|
37
42
|
require "senkyoshi/models/module_item"
|
43
|
+
require "senkyoshi/models/qti"
|
38
44
|
require "senkyoshi/models/question"
|
39
45
|
require "senkyoshi/models/quiz"
|
40
46
|
require "senkyoshi/models/resource"
|
@@ -43,7 +49,7 @@ require "senkyoshi/models/staff_info"
|
|
43
49
|
require "senkyoshi/models/wikipage"
|
44
50
|
require "senkyoshi/models/attachment"
|
45
51
|
require "senkyoshi/models/external_url"
|
46
|
-
|
52
|
+
require "senkyoshi/models/rule"
|
47
53
|
require "senkyoshi/exceptions"
|
48
54
|
|
49
55
|
module Senkyoshi
|
@@ -57,28 +63,21 @@ module Senkyoshi
|
|
57
63
|
content: "Content",
|
58
64
|
staffinfo: "StaffInfo",
|
59
65
|
gradebook: "Gradebook",
|
66
|
+
rule: "Rule",
|
60
67
|
}.freeze
|
61
68
|
|
62
69
|
PRE_RESOURCE_TYPE = {
|
63
|
-
|
70
|
+
coursetoc: "CourseToc",
|
64
71
|
gradebook: "Gradebook",
|
72
|
+
link: "Link",
|
65
73
|
courseassessment: "QTI",
|
66
74
|
}.freeze
|
67
75
|
|
68
|
-
def self.
|
69
|
-
doc = Nokogiri::XML.parse(manifest)
|
70
|
-
resources = doc.at("resources")
|
71
|
-
organizations = doc.at("organizations")
|
72
|
-
iterate_xml(organizations, resources, zip_file, resource_xids).
|
73
|
-
flatten - ["", nil]
|
74
|
-
end
|
75
|
-
|
76
|
-
def self.iterate_xml(organizations, resources, zip_file, resource_xids)
|
77
|
-
pre_data = pre_iterator(organizations, resources, zip_file)
|
76
|
+
def self.iterate_xml(resources, zip_file, resource_xids, pre_data)
|
78
77
|
staff_info = StaffInfo.new
|
79
78
|
iterator_master(resources, zip_file) do |xml_data, type, file|
|
80
79
|
if RESOURCE_TYPE[type.to_sym]
|
81
|
-
single_pre_data = get_single_pre_data(pre_data, file)
|
80
|
+
single_pre_data = get_single_pre_data(pre_data, file) || {}
|
82
81
|
res_class = Senkyoshi.const_get RESOURCE_TYPE[type.to_sym]
|
83
82
|
case type
|
84
83
|
when "staffinfo"
|
@@ -87,12 +86,12 @@ module Senkyoshi
|
|
87
86
|
res_class.from(xml_data, single_pre_data, resource_xids)
|
88
87
|
end
|
89
88
|
end
|
90
|
-
end
|
89
|
+
end.flatten - ["", nil]
|
91
90
|
end
|
92
91
|
|
93
92
|
def self.get_single_pre_data(pre_data, file)
|
94
|
-
pre_data.detect do |
|
95
|
-
|
93
|
+
pre_data.detect do |data_item|
|
94
|
+
data_item[:file_name] == file || data_item[:assignment_id] == file
|
96
95
|
end || { file_name: file }
|
97
96
|
end
|
98
97
|
|
@@ -119,8 +118,9 @@ module Senkyoshi
|
|
119
118
|
pre_data[type].push(data) if data
|
120
119
|
end
|
121
120
|
end
|
121
|
+
pre_data["content"] = build_heirarchy(organizations, resources,
|
122
|
+
pre_data["coursetoc"]) - ["", nil]
|
122
123
|
pre_data = connect_content(pre_data)
|
123
|
-
build_heirarchy(organizations, pre_data)
|
124
124
|
end
|
125
125
|
|
126
126
|
def self.connect_content(pre_data)
|
@@ -130,39 +130,33 @@ module Senkyoshi
|
|
130
130
|
detect { |g| g[:content_id] == content[:file_name] }
|
131
131
|
content.merge!(gradebook) if gradebook
|
132
132
|
end
|
133
|
+
|
133
134
|
if pre_data["courseassessment"]
|
134
135
|
course_assessment = pre_data["courseassessment"].
|
135
136
|
detect { |ca| ca[:original_file_name] == content[:assignment_id] }
|
136
137
|
content.merge!(course_assessment) if course_assessment
|
137
138
|
end
|
138
|
-
end
|
139
|
-
pre_data["content"]
|
140
|
-
end
|
141
139
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
if parent_id == unset_id
|
151
|
-
content[:title] = get_title(organizations, content)
|
152
|
-
elsif parent[:parent_id] == unset_id
|
153
|
-
content[:parent_title] = parent[:title]
|
140
|
+
if pre_data["link"]
|
141
|
+
matching_link = pre_data["link"].detect do |link|
|
142
|
+
link[:referrer] == content[:file_name]
|
143
|
+
end
|
144
|
+
|
145
|
+
if matching_link
|
146
|
+
content[:referred_to_title] = matching_link[:referred_to_title]
|
147
|
+
end
|
154
148
|
end
|
155
|
-
next if parents_ids.include?(content[:id])
|
156
|
-
next if parents_ids.include?(parent_id)
|
157
|
-
parents_ids << parent_id
|
158
|
-
parent[:parent_id] = parent[:id]
|
159
|
-
parent[:parent_title] = nil
|
160
149
|
end
|
150
|
+
|
151
|
+
pre_data["content"]
|
161
152
|
end
|
162
153
|
|
163
|
-
def self.
|
164
|
-
|
165
|
-
|
154
|
+
def self.build_heirarchy(organizations, resources, course_toc)
|
155
|
+
discussion_boards = resources.
|
156
|
+
search("resource[type=\"resource/x-bb-discussionboard\"]")
|
157
|
+
organizations.at("organization").children.flat_map do |item|
|
158
|
+
Heirarchy.item_iterator(item, course_toc, discussion_boards)
|
159
|
+
end
|
166
160
|
end
|
167
161
|
|
168
162
|
##
|
data/lib/senkyoshi.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "senkyoshi/version"
|
2
2
|
require "senkyoshi/xml_parser"
|
3
|
+
require "senkyoshi/models/module_converter"
|
3
4
|
require "senkyoshi/canvas_course"
|
4
5
|
require "senkyoshi/collection"
|
5
6
|
require "senkyoshi/configuration"
|
@@ -15,6 +16,8 @@ require "senkyoshi/exceptions"
|
|
15
16
|
module Senkyoshi
|
16
17
|
FILE_BASE = "$IMS-CC-FILEBASE$".freeze
|
17
18
|
DIR_BASE = "$CANVAS_COURSE_REFERENCE$/files/folder".freeze
|
19
|
+
MAIN_CANVAS_MODULE = "aj_main_module".freeze
|
20
|
+
MASTER_MODULE = "master_module".freeze
|
18
21
|
|
19
22
|
class << self
|
20
23
|
attr_writer :configuration
|
@@ -41,9 +44,17 @@ module Senkyoshi
|
|
41
44
|
resource_xids = resources.resources.
|
42
45
|
map(&:xid).
|
43
46
|
select { |r| r.include?("xid-") }
|
44
|
-
resources.add(Senkyoshi.parse_manifest(file, manifest, resource_xids))
|
45
47
|
|
46
|
-
|
48
|
+
xml = Nokogiri::XML.parse(manifest)
|
49
|
+
xml_resources = xml.at("resources")
|
50
|
+
xml_organizations = xml.at("organizations")
|
51
|
+
|
52
|
+
pre_data = Senkyoshi.pre_iterator(xml_organizations, xml_resources, file)
|
53
|
+
resources.add(
|
54
|
+
Senkyoshi.iterate_xml(xml_resources, file, resource_xids, pre_data),
|
55
|
+
)
|
56
|
+
|
57
|
+
course = create_canvas_course(resources, zip_path, pre_data)
|
47
58
|
build_file(course, imscc_path, resources)
|
48
59
|
end
|
49
60
|
end
|
@@ -73,12 +84,21 @@ module Senkyoshi
|
|
73
84
|
resources.each(&:cleanup)
|
74
85
|
end
|
75
86
|
|
76
|
-
def self.create_canvas_course(resources, zip_name)
|
87
|
+
def self.create_canvas_course(resources, zip_name, pre_data)
|
77
88
|
course = CanvasCc::CanvasCC::Models::Course.new
|
78
89
|
course.course_code = zip_name
|
79
|
-
|
90
|
+
|
91
|
+
# Wait until after we set modules to convert Rules
|
92
|
+
resources.find_instances_not_of([Rule]).each do |resource|
|
80
93
|
course = resource.canvas_conversion(course, resources)
|
81
94
|
end
|
95
|
+
|
96
|
+
course = ModuleConverter.set_modules(course, pre_data)
|
97
|
+
|
98
|
+
resources.find_instances_of(Rule).each do |rule|
|
99
|
+
course = rule.canvas_conversion(course, resources)
|
100
|
+
end
|
101
|
+
|
82
102
|
course
|
83
103
|
end
|
84
104
|
|
@@ -91,7 +111,7 @@ module Senkyoshi
|
|
91
111
|
end
|
92
112
|
end
|
93
113
|
|
94
|
-
def true?(obj)
|
114
|
+
def self.true?(obj)
|
95
115
|
obj.to_s == "true"
|
96
116
|
end
|
97
117
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: senkyoshi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Atomic Jolt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-01-
|
11
|
+
date: 2017-01-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry-byebug
|
@@ -177,14 +177,23 @@ files:
|
|
177
177
|
- lib/senkyoshi/models/blog.rb
|
178
178
|
- lib/senkyoshi/models/content.rb
|
179
179
|
- lib/senkyoshi/models/content_file.rb
|
180
|
+
- lib/senkyoshi/models/content_reviewed_criteria.rb
|
180
181
|
- lib/senkyoshi/models/course.rb
|
182
|
+
- lib/senkyoshi/models/course_toc.rb
|
181
183
|
- lib/senkyoshi/models/external_url.rb
|
182
184
|
- lib/senkyoshi/models/file.rb
|
183
185
|
- lib/senkyoshi/models/file_resource.rb
|
184
186
|
- lib/senkyoshi/models/forum.rb
|
187
|
+
- lib/senkyoshi/models/grade_completed_criteria.rb
|
188
|
+
- lib/senkyoshi/models/grade_criteria.rb
|
189
|
+
- lib/senkyoshi/models/grade_range_criteria.rb
|
190
|
+
- lib/senkyoshi/models/grade_range_percent_criteria.rb
|
185
191
|
- lib/senkyoshi/models/gradebook.rb
|
186
192
|
- lib/senkyoshi/models/group.rb
|
193
|
+
- lib/senkyoshi/models/heirarchy.rb
|
194
|
+
- lib/senkyoshi/models/link.rb
|
187
195
|
- lib/senkyoshi/models/module.rb
|
196
|
+
- lib/senkyoshi/models/module_converter.rb
|
188
197
|
- lib/senkyoshi/models/module_item.rb
|
189
198
|
- lib/senkyoshi/models/outcome_definition.rb
|
190
199
|
- lib/senkyoshi/models/qti.rb
|
@@ -209,6 +218,8 @@ files:
|
|
209
218
|
- lib/senkyoshi/models/questions/true_false.rb
|
210
219
|
- lib/senkyoshi/models/quiz.rb
|
211
220
|
- lib/senkyoshi/models/resource.rb
|
221
|
+
- lib/senkyoshi/models/rule.rb
|
222
|
+
- lib/senkyoshi/models/rule_criteria.rb
|
212
223
|
- lib/senkyoshi/models/scorm_package.rb
|
213
224
|
- lib/senkyoshi/models/staff_info.rb
|
214
225
|
- lib/senkyoshi/models/survey.rb
|
@@ -236,7 +247,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
236
247
|
version: '0'
|
237
248
|
requirements: []
|
238
249
|
rubyforge_project:
|
239
|
-
rubygems_version: 2.
|
250
|
+
rubygems_version: 2.5.1
|
240
251
|
signing_key:
|
241
252
|
specification_version: 4
|
242
253
|
summary: Converts Blackboard zip file to Canvas Common Cartridge
|