senkyoshi 1.0.3 → 1.0.4
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.
- 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
|